add mutable filters into internal color blindness test

This commit is contained in:
Sergey Malenkov
2016-03-30 22:36:56 +03:00
parent e8308d5c79
commit e1bd4f5e4b
5 changed files with 312 additions and 122 deletions
@@ -0,0 +1,109 @@
package com.intellij.ide.ui;
import com.intellij.util.Matrix;
import com.intellij.util.Vector;
/**
* @author Sergey.Malenkov
*/
final class ColorBlindnessMatrix {
private static final Matrix CORRECTION = Matrix.createIdentity(3);
private static final Matrix RGB_LMS; // a matrix to convert a RGB color to the LMS space
private static final Matrix LMS_RGB; // a matrix to convert a LMS color to the RGB space
private static final Vector WHITE_BLUE;
private static final Vector WHITE_RED;
static {
// a matrix to convert a RGB color to the XYZ space
Matrix RGB_XYZ = Matrix.create(3,
0.4124, 0.2126, 0.0193,
0.3576, 0.7152, 0.1192,
0.1805, 0.0722, 0.9505);
// a matrix to convert an XYZ color to the LMS space
Matrix XYZ_LMS = Matrix.create(3,
0.7328, -0.7036, 0.0030,
0.4296, 1.6975, 0.0136,
-0.1624, 0.0061, 0.9834);
// create direct conversion from RGB to LMS and vice versa
RGB_LMS = RGB_XYZ.multiply(XYZ_LMS);
LMS_RGB = RGB_LMS.inverse();
// To simulate color blindness we remove the data lost by the absence of a cone.
// This cannot be done by just zeroing out the corresponding LMS component,
// because it would create a color outside of the RGB gammut. Instead,
// we project the color along the axis of the missing component
// onto a plane within the RGB gammut:
// - since the projection happens along the axis of the missing component,
// a color blind viewer perceives the projected color the same.
// - We use the plane defined by 3 points in LMS space: black, white and
// blue and red for protanopia/deuteranopia and tritanopia respectively.
Vector red = RGB_LMS.getRow(0); // LMS space red
Vector blue = RGB_LMS.getRow(2); // LMS space blue
Vector white = Vector.create(1, 1, 1).multiply(RGB_LMS); // LMS space white
// To find the planes we solve the a*L + b*M + c*S = 0 equation
// for the LMS values of the three known points. This equation is trivially solved,
// and has for solution the following cross-products:
WHITE_BLUE = cross(white, blue); // protanopia/deuteranopia
WHITE_RED = cross(white, red); // tritanopia
}
private static Vector cross(Vector left, Vector right) {
return Vector.create(
left.get(1) * right.get(2) - left.get(2) * right.get(1),
left.get(2) * right.get(0) - left.get(0) * right.get(2),
left.get(0) * right.get(1) - left.get(1) * right.get(0));
}
private static Matrix calculate(Matrix simulation, Matrix correction) {
// We will calculate the error between the color and the color
// viewed by a color blind user and "spread" this error onto the healthy cones.
// The correction matrix perform this last step and have been chosen arbitrarily.
Matrix matrix = simulation.multiply(RGB_LMS);
if (correction == null) correction = CORRECTION;
return LMS_RGB.multiply(matrix.plus(correction.multiply(RGB_LMS.minus(matrix))));
}
static final class Protanopia {
private static final double V1 = -WHITE_BLUE.get(1) / WHITE_BLUE.get(0);
private static final double V2 = -WHITE_BLUE.get(2) / WHITE_BLUE.get(0);
private static final Matrix SIMULATION = Matrix.create(3, 0, 0, 0, V1, 1, 0, V2, 0, 1);
private static final Matrix CORRECTION = Matrix.create(3, 1, .7, .7, 0, 1, 0, 0, 0, 1);
static final Matrix MATRIX = calculate(CORRECTION);
static Matrix calculate(Matrix correction) {
return ColorBlindnessMatrix.calculate(SIMULATION, correction);
}
}
static final class Deuteranopia {
private static final double V1 = -WHITE_BLUE.get(0) / WHITE_BLUE.get(1);
private static final double V2 = -WHITE_BLUE.get(2) / WHITE_BLUE.get(1);
private static final Matrix SIMULATION = Matrix.create(3, 1, V1, 0, 0, 0, 0, 0, V2, 1);
private static final Matrix CORRECTION = Matrix.create(3, 1, 0, 0, .7, 1, .7, 0, 0, 1);
static final Matrix MATRIX = calculate(CORRECTION);
static Matrix calculate(Matrix correction) {
return ColorBlindnessMatrix.calculate(SIMULATION, correction);
}
}
static final class Tritanopia {
private static final double V1 = -WHITE_RED.get(0) / WHITE_RED.get(2);
private static final double V2 = -WHITE_RED.get(1) / WHITE_RED.get(2);
private static final Matrix SIMULATION = Matrix.create(3, 1, 0, V1, 0, 1, V2, 0, 0, 0);
private static final Matrix CORRECTION = Matrix.create(3, 1, 0, 0, 0, 1, 0, .7, .7, 1);
static final Matrix MATRIX = calculate(CORRECTION);
static Matrix calculate(Matrix correction) {
return ColorBlindnessMatrix.calculate(SIMULATION, correction);
}
}
}
@@ -0,0 +1,18 @@
package com.intellij.ide.ui;
/**
* @author Sergey.Malenkov
*/
class ColorConverter {
public int convert(int red, int green, int blue, int alpha) {
return (fix(red) << 16) | (fix(green) << 8) | fix(blue) | (fix(alpha) << 24);
}
public int convert(int argb) {
return convert(0xFF & (argb >> 16), 0xFF & (argb >> 8), 0xFF & argb, 0xFF & (argb >> 24));
}
private static int fix(int value) {
return value < 0 ? 0 : value > 255 ? 255 : value;
}
}
@@ -0,0 +1,51 @@
package com.intellij.ide.ui;
import com.intellij.util.Matrix;
import com.intellij.util.Vector;
/**
* @author Sergey.Malenkov
*/
final class MatrixConverter extends ColorConverter {
private final Double myWeight;
private final Matrix myMatrix;
public MatrixConverter(Matrix matrix) {
this(null, matrix);
}
public MatrixConverter(Double weight, Matrix matrix) {
if (weight != null && !(0 < weight && weight < 1)) throw new IllegalArgumentException("unsupported weight");
int rows = matrix.getRows();
if (rows != 3 && rows != 4) throw new IllegalArgumentException("unsupported rows");
int columns = matrix.getColumns();
if (columns != 3 && columns != 4) throw new IllegalArgumentException("unsupported columns");
myWeight = weight;
myMatrix = matrix;
}
@Override
public int convert(int red, int green, int blue, int alpha) {
Vector vector = myMatrix.getRows() == 4
? Vector.create(red, green, blue, alpha)
: Vector.create(red, green, blue);
Vector result = vector.multiply(myMatrix);
if (myWeight != null) {
if (vector.getSize() != result.getSize()) {
vector = result.getSize() == 4
? Vector.create(red, green, blue, alpha)
: Vector.create(red, green, blue);
}
vector = vector.multiply(1 - myWeight);
result = result.multiply(myWeight).plus(vector);
}
red = (int)result.get(0);
green = (int)result.get(1);
blue = (int)result.get(2);
if (result.getSize() == 4) {
alpha = (int)result.get(3);
}
return super.convert(red, green, blue, alpha);
}
}
@@ -15,86 +15,10 @@
*/
package com.intellij.ide.ui;
import com.intellij.util.Matrix;
import com.intellij.util.Vector;
import java.awt.image.ImageFilter;
import java.awt.image.RGBImageFilter;
final class MatrixFilter extends WeightFilter {
private static final Matrix RGB_LMS; // a matrix to convert a RGB color to the LMS space
private static final Matrix LMS_RGB; // a matrix to convert a LMS color to the RGB space
private static final Matrix PROTANOPIA_SIMULATION;
private static final Matrix PROTANOPIA_CORRECTION;
private static final Matrix DEUTERANOPIA_SIMULATION;
private static final Matrix DEUTERANOPIA_CORRECTION;
private static final Matrix TRITANOPIA_SIMULATION;
private static final Matrix TRITANOPIA_CORRECTION;
static {
Matrix RGB_XYZ = Matrix.create(3, // a matrix to convert a RGB color to the XYZ space
0.4124, 0.2126, 0.0193,
0.3576, 0.7152, 0.1192,
0.1805, 0.0722, 0.9505);
Matrix XYZ_LMS = Matrix.create(3, // a matrix to convert an XYZ color to the LMS space
0.7328, -0.7036, 0.0030,
0.4296, 1.6975, 0.0136,
-0.1624, 0.0061, 0.9834);
// create direct conversion from RGB to LMS and vice versa
RGB_LMS = RGB_XYZ.multiply(XYZ_LMS);
LMS_RGB = RGB_LMS.inverse();
// To simulate color blindness we remove the data lost by the absence of a cone.
// This cannot be done by just zeroing out the corresponding LMS component,
// because it would create a color outside of the RGB gammut. Instead,
// we project the color along the axis of the missing component
// onto a plane within the RGB gammut:
// - since the projection happens along the axis of the missing component,
// a color blind viewer perceives the projected color the same.
// - We use the plane defined by 3 points in LMS space: black, white and
// blue and red for protanopia/deuteranopia and tritanopia respectively.
Vector red = RGB_LMS.getRow(0); // LMS space red
Vector blue = RGB_LMS.getRow(2); // LMS space blue
Vector white = Vector.create(1, 1, 1).multiply(RGB_LMS); // LMS space white
// To find the planes we solve the a*L + b*M + c*S = 0 equation
// for the LMS values of the three known points. This equation is trivially solved,
// and has for solution the following cross-products:
Vector p0 = cross(white, blue); // protanopia/deuteranopia
Vector p1 = cross(white, red); // tritanopia
// The following 3 matrices perform the projection of a LMS color
// onto the given plane along the selected axis
PROTANOPIA_SIMULATION = Matrix.create(3,
0, 0, 0,
-p0.get(1) / p0.get(0), 1, 0,
-p0.get(2) / p0.get(0), 0, 1);
DEUTERANOPIA_SIMULATION = Matrix.create(3,
1, -p0.get(0) / p0.get(1), 0,
0, 0, 0,
0, -p0.get(2) / p0.get(1), 1);
TRITANOPIA_SIMULATION = Matrix.create(3,
1, 0, -p1.get(0) / p1.get(2),
0, 1, -p1.get(1) / p1.get(2),
0, 0, 0);
// We will calculate the error between the color and the color
// viewed by a color blind user and "spread" this error onto the healthy cones.
// The matrices below perform this last step and have been chosen arbitrarily.
// The amount of correction can be adjusted here.
PROTANOPIA_CORRECTION = Matrix.create(3, 1, .7, .7, 0, 1, 0, 0, 0, 1);
DEUTERANOPIA_CORRECTION = Matrix.create(3, 1, 0, 0, .7, 1, .7, 0, 0, 1);
TRITANOPIA_CORRECTION = Matrix.create(3, 1, 0, 0, 0, 1, 0, .7, .7, 1);
}
final class MatrixFilter extends RGBImageFilter {
public static ImageFilter get(ColorBlindness blindness) {
if (blindness == ColorBlindness.protanopia) return protanopia;
if (blindness == ColorBlindness.deuteranopia) return deuteranopia;
@@ -102,45 +26,37 @@ final class MatrixFilter extends WeightFilter {
return null;
}
public static final ImageFilter protanopia = forProtanopia(null, true);
public static final ImageFilter deuteranopia = forDeuteranopia(null, true);
public static final ImageFilter tritanopia = forTritanopia(null, true);
public static final ImageFilter protanopia = forProtanopia(null);
public static final ImageFilter deuteranopia = forDeuteranopia(null);
public static final ImageFilter tritanopia = forTritanopia(null);
public static ImageFilter forProtanopia(Double weight, boolean fix) {
return new MatrixFilter("Protanopia", weight, PROTANOPIA_SIMULATION, fix ? PROTANOPIA_CORRECTION : null);
public static ImageFilter forProtanopia(Double weight) {
return new MatrixFilter("Protanopia (correction matrix)", new MatrixConverter(weight, ColorBlindnessMatrix.Protanopia.MATRIX));
}
public static ImageFilter forDeuteranopia(Double weight, boolean fix) {
return new MatrixFilter("Deuteranopia", weight, DEUTERANOPIA_SIMULATION, fix ? DEUTERANOPIA_CORRECTION : null);
public static ImageFilter forDeuteranopia(Double weight) {
return new MatrixFilter("Deuteranopia (correction matrix)", new MatrixConverter(weight, ColorBlindnessMatrix.Deuteranopia.MATRIX));
}
public static ImageFilter forTritanopia(Double weight, boolean fix) {
return new MatrixFilter("Tritanopia", weight, TRITANOPIA_SIMULATION, fix ? TRITANOPIA_CORRECTION : null);
public static ImageFilter forTritanopia(Double weight) {
return new MatrixFilter("Tritanopia (correction matrix)", new MatrixConverter(weight, ColorBlindnessMatrix.Tritanopia.MATRIX));
}
private final Matrix myMatrix;
private final String myName;
private final ColorConverter myConverter;
private MatrixFilter(String name, Double weight, Matrix simulation, Matrix correction) {
super(name + (correction == null ? " (simulation matrix)" : " (correction matrix)"), weight);
Matrix matrix = simulation.multiply(RGB_LMS);
if (correction != null) {
Matrix minus = RGB_LMS.minus(matrix);
Matrix multiply = correction.multiply(minus);
matrix = matrix.plus(multiply);
}
myMatrix = LMS_RGB.multiply(matrix);
private MatrixFilter(String name, ColorConverter converter) {
myName = name;
myConverter = converter;
}
@Override
int toRGB(int srcR, int srcG, int srcB) {
Vector vector = Vector.create(srcR, srcG, srcB).multiply(myMatrix);
return toRGB(srcR, srcG, srcB, fix(vector.get(0)), fix(vector.get(1)), fix(vector.get(2)));
public String toString() {
return myName;
}
private static Vector cross(Vector left, Vector right) {
return Vector.create(
left.get(1) * right.get(2) - left.get(2) * right.get(1),
left.get(2) * right.get(0) - left.get(0) * right.get(2),
left.get(0) * right.get(1) - left.get(1) * right.get(0));
@Override
public int filterRGB(int x, int y, int rgb) {
return myConverter.convert(rgb);
}
}
@@ -19,6 +19,8 @@ import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.project.DumbAwareAction;
import com.intellij.openapi.ui.ComboBox;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.ui.components.panels.VerticalLayout;
import com.intellij.util.Matrix;
import com.intellij.util.ui.ImageUtil;
import com.intellij.util.ui.JBDimension;
import com.intellij.util.ui.JBInsets;
@@ -27,11 +29,12 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.event.ChangeListener;
import java.awt.*;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.image.BufferedImage;
import java.awt.image.ImageFilter;
import java.awt.image.RGBImageFilter;
/**
* @author Sergey.Malenkov
@@ -45,6 +48,35 @@ public class ColorBlindnessInternalAction extends DumbAwareAction {
private static final class ColorDialog extends DialogWrapper {
private final ColorView myView = new ColorView();
private final JComboBox myCombo = new ComboBox<>(FilterItem.ALL);
private final JSlider myFirstSlider = createSlider();
private final JSlider mySecondSlider = createSlider();
private static JSlider createSlider() {
JSlider slider = new JSlider(0, 100);
slider.setBorder(BorderFactory.createEmptyBorder(10, 0, 0, 0));
slider.setMajorTickSpacing(10);
slider.setMinorTickSpacing(1);
slider.setPaintTicks(true);
slider.setPaintLabels(true);
slider.setVisible(false);
return slider;
}
private static void showSlider(JSlider slider, ChangeListener listener) {
slider.setValue(70);
slider.setVisible(true);
slider.addChangeListener(listener);
}
private static void hideSlider(JSlider slider, ChangeListener listener) {
slider.removeChangeListener(listener);
slider.setVisible(false);
}
private void updateFilter(MutableFilter filter) {
filter.update(myFirstSlider.getValue() / 100.0, mySecondSlider.getValue() / 100.0);
myView.set(filter);
}
private ColorDialog(AnActionEvent event) {
super(event.getProject());
@@ -64,11 +96,38 @@ public class ColorBlindnessInternalAction extends DumbAwareAction {
myView.setMinimumSize(new JBDimension(360, 200));
myView.setPreferredSize(new JBDimension(720, 400));
myCombo.addItemListener(myView);
ChangeListener listener = event -> {
if (myView.myFilter instanceof MutableFilter) {
updateFilter((MutableFilter)myView.myFilter);
}
};
myCombo.addItemListener(event -> {
if (ItemEvent.SELECTED == event.getStateChange()) {
Object object = event.getItem();
if (object instanceof FilterItem) {
FilterItem item = (FilterItem)object;
if (item.myFilter instanceof MutableFilter) {
showSlider(myFirstSlider, listener);
showSlider(mySecondSlider, listener);
updateFilter((MutableFilter)item.myFilter);
}
else {
hideSlider(myFirstSlider, listener);
hideSlider(mySecondSlider, listener);
myView.set(item.myFilter);
}
}
}
});
JPanel control = new JPanel(new VerticalLayout(10));
control.add(VerticalLayout.TOP, myCombo);
control.add(VerticalLayout.TOP, myFirstSlider);
control.add(VerticalLayout.TOP, mySecondSlider);
JPanel panel = new JPanel(new BorderLayout(10, 10));
panel.add(BorderLayout.CENTER, myView);
panel.add(BorderLayout.SOUTH, myCombo);
panel.add(BorderLayout.SOUTH, control);
return panel;
}
@@ -95,21 +154,39 @@ public class ColorBlindnessInternalAction extends DumbAwareAction {
new FilterItem(null),
new FilterItem(DaltonizationFilter.protanopia),
new FilterItem(MatrixFilter.protanopia),
new FilterItem(new MutableFilter("Protanopia (mutable)") {
@Override
ColorConverter getConverter(double first, double second) {
Matrix matrix = Matrix.create(3, 1, first, second, 0, 1, 0, 0, 0, 1);
return new MatrixConverter(ColorBlindnessMatrix.Protanopia.calculate(matrix));
}
}),
new FilterItem(DaltonizationFilter.deuteranopia),
new FilterItem(MatrixFilter.deuteranopia),
new FilterItem(new MutableFilter("Deuteranopia (mutable)") {
@Override
ColorConverter getConverter(double first, double second) {
Matrix matrix = Matrix.create(3, 1, 0, 0, first, 1, second, 0, 0, 1);
return new MatrixConverter(ColorBlindnessMatrix.Deuteranopia.calculate(matrix));
}
}),
new FilterItem(DaltonizationFilter.tritanopia),
new FilterItem(MatrixFilter.tritanopia),
new FilterItem(new MutableFilter("Tritanopia (mutable)") {
@Override
ColorConverter getConverter(double first, double second) {
Matrix matrix = Matrix.create(3, 1, 0, 0, 0, 1, 0, first, second, 1);
return new MatrixConverter(ColorBlindnessMatrix.Tritanopia.calculate(matrix));
}
}),
new FilterItem(SimulationFilter.protanopia),
new FilterItem(MatrixFilter.forProtanopia(null, false)),
new FilterItem(SimulationFilter.deuteranopia),
new FilterItem(MatrixFilter.forDeuteranopia(null, false)),
new FilterItem(SimulationFilter.tritanopia),
new FilterItem(MatrixFilter.forTritanopia(null, false)),
new FilterItem(SimulationFilter.achromatopsia),
};
}
private static final class ColorView extends JComponent implements ItemListener {
private static final class ColorView extends JComponent {
private ImageFilter myFilter;
private Image myImage;
@@ -138,17 +215,36 @@ public class ColorBlindnessInternalAction extends DumbAwareAction {
g.drawImage(myImage, bounds.x, bounds.y, bounds.width, bounds.height, this);
}
void set(ImageFilter filter) {
myImage = null;
myFilter = filter;
repaint();
}
}
private abstract static class MutableFilter extends RGBImageFilter {
private final String myName;
private ColorConverter myConverter;
private MutableFilter(String name) {
myName = name;
update(.7, .7);
}
private void update(double first, double second) {
myConverter = getConverter(first, second);
}
abstract ColorConverter getConverter(double first, double second);
@Override
public void itemStateChanged(ItemEvent event) {
if (ItemEvent.SELECTED == event.getStateChange()) {
Object object = event.getItem();
if (object instanceof FilterItem) {
FilterItem item = (FilterItem)object;
myFilter = item.myFilter;
myImage = null;
repaint();
}
}
public String toString() {
return myName;
}
@Override
public int filterRGB(int x, int y, int rgb) {
return myConverter.convert(rgb);
}
}
}