Skip to content
Permalink
Browse files

[FEATURE] Improved color vision deficiency sim

This modifies the previous support for grayscale
and LMS-based simulation for protanopia and
deuteranopia, and brings it in line with the
methodology currently used in Chromium and Firefox
(https://bugs.chromium.org/p/chromium/issues/detail?id=1003700,
https://bugzilla.mozilla.org/show_bug.cgi?id=1655053).

QGIS now uses updated grayscale luminance
calculations (renamed to achromatopsia), a
precomputed protanopia matrix (renamed from
protanope), a precomputed deuteranopia matrix
(renamed from deuteranope), and an additional mode
for tritanopia using a similarly precomputed matrix.

This commit addresses issue #29760.
  • Loading branch information
willcohen authored and nyalldawson committed Oct 27, 2020
1 parent 195fe7e commit 7bd81dfc6049deeee1420b458b0085e532cdb3f2
@@ -23,10 +23,11 @@ color blindness modes.
public:
enum PreviewMode
{
PreviewGrayscale,
PreviewMono,
PreviewProtanope,
PreviewDeuteranope
PreviewAchromatopsia,
PreviewProtanopia,
PreviewDeuteranopia,
PreviewTritanopia
};

QgsPreviewEffect( QObject *parent /TransferThis/ );
@@ -552,33 +552,40 @@ QgsLayoutDesignerDialog::QgsLayoutDesignerDialog( QWidget *parent, Qt::WindowFla
{
mView->setPreviewModeEnabled( false );
} );
connect( mActionPreviewModeGrayscale, &QAction::triggered, this, [ = ]
connect( mActionPreviewModeMono, &QAction::triggered, this, [ = ]
{
mView->setPreviewMode( QgsPreviewEffect::PreviewGrayscale );
mView->setPreviewMode( QgsPreviewEffect::PreviewMono );
mView->setPreviewModeEnabled( true );
} );
connect( mActionPreviewModeMono, &QAction::triggered, this, [ = ]
connect( mActionPreviewAchromatopsia, &QAction::triggered, this, [ = ]
{
mView->setPreviewMode( QgsPreviewEffect::PreviewMono );
mView->setPreviewMode( QgsPreviewEffect::PreviewAchromatopsia );
mView->setPreviewModeEnabled( true );
} );
connect( mActionPreviewProtanopia, &QAction::triggered, this, [ = ]
{
mView->setPreviewMode( QgsPreviewEffect::PreviewProtanopia );
mView->setPreviewModeEnabled( true );
} );
connect( mActionPreviewProtanope, &QAction::triggered, this, [ = ]
connect( mActionPreviewDeuteranopia, &QAction::triggered, this, [ = ]
{
mView->setPreviewMode( QgsPreviewEffect::PreviewProtanope );
mView->setPreviewMode( QgsPreviewEffect::PreviewDeuteranopia );
mView->setPreviewModeEnabled( true );
} );
connect( mActionPreviewDeuteranope, &QAction::triggered, this, [ = ]
connect( mActionPreviewTritanopia, &QAction::triggered, this, [ = ]
{
mView->setPreviewMode( QgsPreviewEffect::PreviewDeuteranope );
mView->setPreviewMode( QgsPreviewEffect::PreviewTritanopia );
mView->setPreviewModeEnabled( true );
} );
QActionGroup *previewGroup = new QActionGroup( this );
previewGroup->setExclusive( true );
mActionPreviewModeOff->setActionGroup( previewGroup );
mActionPreviewModeGrayscale->setActionGroup( previewGroup );
mActionPreviewModeMono->setActionGroup( previewGroup );
mActionPreviewProtanope->setActionGroup( previewGroup );
mActionPreviewDeuteranope->setActionGroup( previewGroup );
mActionPreviewAchromatopsia->setActionGroup( previewGroup );
mActionPreviewProtanopia->setActionGroup( previewGroup );
mActionPreviewDeuteranopia->setActionGroup( previewGroup );
mActionPreviewTritanopia->setActionGroup( previewGroup );

connect( mActionSaveAsTemplate, &QAction::triggered, this, &QgsLayoutDesignerDialog::saveAsTemplate );
connect( mActionLoadFromTemplate, &QAction::triggered, this, &QgsLayoutDesignerDialog::addItemsFromTemplate );
@@ -3061,10 +3061,11 @@ void QgisApp::createActionGroups()
QActionGroup *mPreviewGroup = new QActionGroup( this );
mPreviewGroup->setExclusive( true );
mActionPreviewModeOff->setActionGroup( mPreviewGroup );
mActionPreviewModeGrayscale->setActionGroup( mPreviewGroup );
mActionPreviewModeMono->setActionGroup( mPreviewGroup );
mActionPreviewProtanope->setActionGroup( mPreviewGroup );
mActionPreviewDeuteranope->setActionGroup( mPreviewGroup );
mActionPreviewAchromatopsia->setActionGroup( mPreviewGroup );
mActionPreviewProtanopia->setActionGroup( mPreviewGroup );
mActionPreviewDeuteranopia->setActionGroup( mPreviewGroup );
mActionPreviewTritanopia->setActionGroup( mPreviewGroup );
}

void QgisApp::setAppStyleSheet( const QString &stylesheet )
@@ -4302,10 +4303,11 @@ void QgisApp::setupConnections()

// connect preview modes actions
connect( mActionPreviewModeOff, &QAction::triggered, this, &QgisApp::disablePreviewMode );
connect( mActionPreviewModeGrayscale, &QAction::triggered, this, &QgisApp::activateGrayscalePreview );
connect( mActionPreviewModeMono, &QAction::triggered, this, &QgisApp::activateMonoPreview );
connect( mActionPreviewProtanope, &QAction::triggered, this, &QgisApp::activateProtanopePreview );
connect( mActionPreviewDeuteranope, &QAction::triggered, this, &QgisApp::activateDeuteranopePreview );
connect( mActionPreviewAchromatopsia, &QAction::triggered, this, &QgisApp::activateAchromatopsiaPreview );
connect( mActionPreviewProtanopia, &QAction::triggered, this, &QgisApp::activateProtanopiaPreview );
connect( mActionPreviewDeuteranopia, &QAction::triggered, this, &QgisApp::activateDeuteranopiaPreview );
connect( mActionPreviewTritanopia, &QAction::triggered, this, &QgisApp::activateTritanopiaPreview );

// setup undo/redo actions
connect( mUndoWidget, &QgsUndoWidget::undoStackChanged, this, &QgisApp::updateUndoActions );
@@ -7690,28 +7692,34 @@ void QgisApp::disablePreviewMode()
mMapCanvas->setPreviewModeEnabled( false );
}

void QgisApp::activateGrayscalePreview()
void QgisApp::activateMonoPreview()
{
mMapCanvas->setPreviewModeEnabled( true );
mMapCanvas->setPreviewMode( QgsPreviewEffect::PreviewGrayscale );
mMapCanvas->setPreviewMode( QgsPreviewEffect::PreviewMono );
}

void QgisApp::activateMonoPreview()
void QgisApp::activateAchromatopsiaPreview()
{
mMapCanvas->setPreviewModeEnabled( true );
mMapCanvas->setPreviewMode( QgsPreviewEffect::PreviewMono );
mMapCanvas->setPreviewMode( QgsPreviewEffect::PreviewAchromatopsia );
}

void QgisApp::activateProtanopiaPreview()
{
mMapCanvas->setPreviewModeEnabled( true );
mMapCanvas->setPreviewMode( QgsPreviewEffect::PreviewProtanopia );
}

void QgisApp::activateProtanopePreview()
void QgisApp::activateDeuteranopiaPreview()
{
mMapCanvas->setPreviewModeEnabled( true );
mMapCanvas->setPreviewMode( QgsPreviewEffect::PreviewProtanope );
mMapCanvas->setPreviewMode( QgsPreviewEffect::PreviewDeuteranopia );
}

void QgisApp::activateDeuteranopePreview()
void QgisApp::activateTritanopiaPreview()
{
mMapCanvas->setPreviewModeEnabled( true );
mMapCanvas->setPreviewMode( QgsPreviewEffect::PreviewDeuteranope );
mMapCanvas->setPreviewMode( QgsPreviewEffect::PreviewTritanopia );
}

void QgisApp::toggleFilterLegendByExpression( bool checked )
@@ -1889,28 +1889,35 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow
void disablePreviewMode();

/**
* Enable a grayscale preview mode on the map canvas
* Enable a monochrome preview mode on the map canvas
* \since QGIS 2.3
*/
void activateGrayscalePreview();
void activateMonoPreview();

/**
* Enable a monochrome preview mode on the map canvas
* \since QGIS 2.3
* Enable a color blindness (achromatopsia) preview mode on the map canvas
* Replaces the grayscale preview mode
* \since QGIS 3.17
*/
void activateMonoPreview();
void activateAchromatopsiaPreview();

/**
* Enable a color blindness (protanope) preview mode on the map canvas
* Enable a color blindness (protanopia) preview mode on the map canvas
* \since QGIS 2.3
*/
void activateProtanopePreview();
void activateProtanopiaPreview();

/**
* Enable a color blindness (deuteranope) preview mode on the map canvas
* Enable a color blindness (deuteranopia) preview mode on the map canvas
* \since QGIS 2.3
*/
void activateDeuteranopePreview();
void activateDeuteranopiaPreview();

/**
* Enable a color blindness (tritanopia) preview mode on the map canvas
* \since QGIS 3.17
*/
void activateTritanopiaPreview();

void toggleFilterLegendByExpression( bool );
void updateFilterLegend();
@@ -2410,7 +2410,7 @@ QgsPreviewEffect::PreviewMode QgsMapCanvas::previewMode() const
{
if ( !mPreviewEffect )
{
return QgsPreviewEffect::PreviewGrayscale;
return QgsPreviewEffect::PreviewAchromatopsia;
}

return mPreviewEffect->mode();
@@ -21,7 +21,7 @@

QgsPreviewEffect::QgsPreviewEffect( QObject *parent )
: QGraphicsEffect( parent )
, mMode( PreviewGrayscale )
, mMode( PreviewAchromatopsia )
{
//effect is disabled by default
setEnabled( false );
@@ -54,31 +54,16 @@ void QgsPreviewEffect::draw( QPainter *painter )

switch ( mMode )
{
case QgsPreviewEffect::PreviewGrayscale:
{
QRgb *line = nullptr;

for ( int y = 0; y < image.height(); y++ )
{
line = ( QRgb * )image.scanLine( y );
for ( int x = 0; x < image.width(); x++ )
{
int gray = 0.21 * qRed( line[x] ) + 0.72 * qGreen( line[x] ) + 0.07 * qBlue( line[x] );
line[x] = qRgb( gray, gray, gray );
}
}

painter->drawImage( offset, image );
break;
}
case QgsPreviewEffect::PreviewMono:
{
QImage bwImage = image.convertToFormat( QImage::Format_Mono );
painter->drawImage( offset, bwImage );
break;
}
case QgsPreviewEffect::PreviewProtanope:
case QgsPreviewEffect::PreviewDeuteranope:
case QgsPreviewEffect::PreviewAchromatopsia:
case QgsPreviewEffect::PreviewProtanopia:
case QgsPreviewEffect::PreviewDeuteranopia:
case QgsPreviewEffect::PreviewTritanopia:
{
QRgb *line = nullptr;

@@ -104,49 +89,63 @@ QRgb QgsPreviewEffect::simulateColorBlindness( QRgb &originalColor, QgsPreviewEf
int green = qGreen( originalColor );
int blue = qBlue( originalColor );

//convert RGB to LMS color space
// (http://vision.psychol.cam.ac.uk/jdmollon/papers/colourmaps.pdf p245, equation 4) #spellok
double L = ( 17.8824 * red ) + ( 43.5161 * green ) + ( 4.11935 * blue );
double M = ( 3.45565 * red ) + ( 27.1554 * green ) + ( 3.86714 * blue );
double S = ( 0.0299566 * red ) + ( 0.184309 * green ) + ( 1.46709 * blue );
int r = red;
int g = green;
int b = blue;

//simulate color blindness
//matrix values taken from Machado et al. (2009), https://doi.org/10.1109/TVCG.2009.113:
//https://www.inf.ufrgs.br/~oliveira/pubs_files/CVD_Simulation/CVD_Simulation.html
switch ( mode )
{
case PreviewProtanope:
simulateProtanopeLMS( L, M, S );
case PreviewAchromatopsia:
simulateAchromatopsia( r, g, b, red, green, blue );
break;
case PreviewProtanopia:
simulateProtanopia( r, g, b, red, green, blue );
break;
case PreviewDeuteranope:
simulateDeuteranopeLMS( L, M, S );
case PreviewDeuteranopia:
simulateDeuteranopia( r, g, b, red, green, blue );
break;
case PreviewTritanopia:
simulateTritanopia( r, g, b, red, green, blue );
break;
default:
break;
}

//convert LMS back to RGB color space
//(http://vision.psychol.cam.ac.uk/jdmollon/papers/colourmaps.pdf p248, equation 6) #spellok
red = ( 0.080944 * L ) + ( -0.130504 * M ) + ( 0.116721 * S );
green = ( -0.0102485 * L ) + ( 0.0540194 * M ) + ( -0.113615 * S );
blue = ( -0.000365294 * L ) + ( -0.00412163 * M ) + ( 0.693513 * S );

//restrict values to 0-255
red = std::max( std::min( 255, red ), 0 );
green = std::max( std::min( 255, green ), 0 );
blue = std::max( std::min( 255, blue ), 0 );
r = std::max( std::min( 255, r ), 0 );
g = std::max( std::min( 255, g ), 0 );
b = std::max( std::min( 255, b ), 0 );

return qRgb( red, green, blue );
return qRgb( r, g, b );
}

void QgsPreviewEffect::simulateAchromatopsia( int &r, int &g, int &b, int &red, int &green, int &blue )
{
r = ( 0.299 * red ) + ( 0.587 * green ) + ( 0.114 * blue );
g = r;
b = r;
}

void QgsPreviewEffect::simulateProtanopia( int &r, int &g, int &b, int &red, int &green, int &blue )
{
r = ( 0.152286 * red ) + ( 1.052583 * green ) + ( -0.204868 * blue );
g = ( 0.114503 * red ) + ( 0.786281 * green ) + ( 0.099216 * blue );
b = ( -0.003882 * red ) + ( -0.048116 * green ) + ( 1.051998 * blue );
}

void QgsPreviewEffect::simulateProtanopeLMS( double &L, double &M, double &S )
void QgsPreviewEffect::simulateDeuteranopia( int &r, int &g, int &b, int &red, int &green, int &blue )
{
//adjust L component to simulate vision of Protanope
//(http://vision.psychol.cam.ac.uk/jdmollon/papers/colourmaps.pdf p248, equation 5) #spellok
L = ( 2.02344 * M ) + ( -2.52581 * S );
r = ( 0.367322 * red ) + ( 0.860646 * green ) + ( -0.227968 * blue );
g = ( 0.280085 * red ) + ( 0.672501 * green ) + ( 0.047413 * blue );
b = ( -0.011820 * red ) + ( 0.042940 * green ) + ( 0.968881 * blue );
}

void QgsPreviewEffect::simulateDeuteranopeLMS( double &L, double &M, double &S )
void QgsPreviewEffect::simulateTritanopia( int &r, int &g, int &b, int &red, int &green, int &blue )
{
//adjust M component to simulate vision of Deuteranope
//(http://vision.psychol.cam.ac.uk/jdmollon/papers/colourmaps.pdf p248, equation 5) #spellok
M = ( 0.494207 * L ) + ( 1.24827 * S );
r = ( 1.255528 * red ) + ( -0.076749 * green ) + ( -0.178779 * blue );
g = ( -0.078411 * red ) + ( 0.930809 * green ) + ( 0.147602 * blue );
b = ( 0.004733 * red ) + ( 0.691367 * green ) + ( 0.303900 * blue );
}
@@ -35,10 +35,11 @@ class GUI_EXPORT QgsPreviewEffect: public QGraphicsEffect
public:
enum PreviewMode
{
PreviewGrayscale,
PreviewMono,
PreviewProtanope,
PreviewDeuteranope
PreviewAchromatopsia,
PreviewProtanopia,
PreviewDeuteranopia,
PreviewTritanopia
};

QgsPreviewEffect( QObject *parent SIP_TRANSFERTHIS );
@@ -67,8 +68,10 @@ class GUI_EXPORT QgsPreviewEffect: public QGraphicsEffect
PreviewMode mMode;

QRgb simulateColorBlindness( QRgb &originalColor, PreviewMode type );
void simulateProtanopeLMS( double &L, double &M, double &S );
void simulateDeuteranopeLMS( double &L, double &M, double &S );
void simulateAchromatopsia( int &r, int &g, int &b, int &red, int &green, int &blue );
void simulateProtanopia( int &r, int &g, int &b, int &red, int &green, int &blue );
void simulateDeuteranopia( int &r, int &g, int &b, int &red, int &green, int &blue );
void simulateTritanopia( int &r, int &g, int &b, int &red, int &green, int &blue );
};

#endif // QGSPREVIEWEFFECT_H

0 comments on commit 7bd81df

Please sign in to comment.
You can’t perform that action at this time.