diff --git a/src/ika/colororacle/ColorOracle.java b/src/ika/colororacle/ColorOracle.java index 51b0b56..eaa8ff5 100755 --- a/src/ika/colororacle/ColorOracle.java +++ b/src/ika/colororacle/ColorOracle.java @@ -44,19 +44,12 @@ public class ColorOracle extends WindowAdapter implements KeyListener, FocusList private static final String MENUICON = "menuIcon.gif"; /** - * The information panel image for deuteranopia. + * The information panels for the different types of simulation. */ private final Image deutanPanel = loadImage("deutanpanel.png"); - - /** - * The information panel image for protanopia. - */ private final Image protanPanel = loadImage("protanpanel.png"); - - /** - * The information panel image for tritanopia. - */ private final Image tritanPanel = loadImage("tritanpanel.png"); + private final Image grayscalePanel = loadImage("grayscalepanel.png"); /** * Wait a few milliseconds before taking a screenshot until the menu has @@ -67,9 +60,9 @@ public class ColorOracle extends WindowAdapter implements KeyListener, FocusList /** * Enumerate the four possible states of the current simulation. */ - enum Simulation { + protected enum Simulation { - normal, deutan, protan, tritan + normal, deutan, protan, tritan, grayscale } /** @@ -77,11 +70,6 @@ public class ColorOracle extends WindowAdapter implements KeyListener, FocusList */ private Simulation currentSimulation = Simulation.normal; - /** - * A flag that is true when a save-as dialog is open and false otherwise. - */ - private boolean currentlySavingImage = false; - /** * The about dialog. */ @@ -93,24 +81,14 @@ public class ColorOracle extends WindowAdapter implements KeyListener, FocusList private final Simulator simulator = new Simulator(); /** - * A menu item for normal vision that will be added to the tray menu. + * Menu items for different types of vision that will be added to the tray + * menu. */ private final CheckboxMenuItem normalMenuItem = new CheckboxMenuItem(); - - /** - * A menu item for deuteranopia that will be added to the tray menu. - */ private final CheckboxMenuItem deutanMenuItem = new CheckboxMenuItem(); - - /** - * A menu item for protanopia that will be added to the tray menu. - */ private final CheckboxMenuItem protanMenuItem = new CheckboxMenuItem(); - - /** - * A menu item for tritanopia that will be added to the tray menu. - */ private final CheckboxMenuItem tritanMenuItem = new CheckboxMenuItem(); + private final CheckboxMenuItem grayscaleMenuItem = new CheckboxMenuItem(); /** * The About menu item that will be added to the tray menu. @@ -408,6 +386,21 @@ public class ColorOracle extends WindowAdapter implements KeyListener, FocusList }); menu.add(tritanMenuItem); + // grayscale vision + grayscaleMenuItem.setLabel("Grayscale"); + grayscaleMenuItem.addItemListener(new java.awt.event.ItemListener() { + + @Override + public void itemStateChanged(ItemEvent evt) { + if (evt.getStateChange() == ItemEvent.SELECTED) { + simulate(ColorOracle.Simulation.grayscale); + } else if (currentSimulation == Simulation.grayscale) { + grayscaleMenuItem.setState(true); // this will not trigger another event + } + } + }); + menu.add(grayscaleMenuItem); + menu.addSeparator(); // about @@ -438,22 +431,6 @@ public class ColorOracle extends WindowAdapter implements KeyListener, FocusList } - /** - * Returns the name of the current simulation. - */ - private String currentSimulationName() { - switch (currentSimulation) { - case deutan: - return "Deuteranopia"; - case protan: - return "Protanopia"; - case tritan: - return "Tritanopia"; - default: - return ""; - } - } - /** * Takes a screenshot, simulates color-impaired vision on the screenshot and * shows the simulation. @@ -484,9 +461,7 @@ public class ColorOracle extends WindowAdapter implements KeyListener, FocusList // apply a simulation filter to the screenshot BufferedImage img = simulator.filter(screen.screenshotImage); - //img = computeDifference(img, screen.screenshotImage); - // ImageIO.write(img, "png", new File("screen" + Screen.getScreens().indexOf(screen) + ".png")); // show the result of the simulation in a window screen.showSimulationImage(img, this, panel); } @@ -519,23 +494,15 @@ public class ColorOracle extends WindowAdapter implements KeyListener, FocusList } /** - * Updates the menu: makes sure only one item has a check mark, enables or - * disables the menu items, and changes the title of the Save menu item. + * Updates the menu: makes sure only one item has a check mark. */ private void updateMenuState() { - // make sure only one menu item is checked normalMenuItem.setState(currentSimulation == Simulation.normal); deutanMenuItem.setState(currentSimulation == Simulation.deutan); protanMenuItem.setState(currentSimulation == Simulation.protan); tritanMenuItem.setState(currentSimulation == Simulation.tritan); - - // disable menu items if the Save dialog is in the foreground. - normalMenuItem.setEnabled(!currentlySavingImage); - deutanMenuItem.setEnabled(!currentlySavingImage); - protanMenuItem.setEnabled(!currentlySavingImage); - tritanMenuItem.setEnabled(!currentlySavingImage); - aboutMenuItem.setEnabled(!currentlySavingImage); + grayscaleMenuItem.setState(currentSimulation == Simulation.grayscale); } /** @@ -587,6 +554,9 @@ public class ColorOracle extends WindowAdapter implements KeyListener, FocusList case tritan: simulateAndShow(tritanPanel); break; + case grayscale: + simulateAndShow(grayscalePanel); + break; } } catch (Exception ex) { Logger.getLogger(ColorOracle.class.getName()).log(Level.SEVERE, null, ex); diff --git a/src/ika/colororacle/Simulator.java b/src/ika/colororacle/Simulator.java index 225849b..9b4537d 100755 --- a/src/ika/colororacle/Simulator.java +++ b/src/ika/colororacle/Simulator.java @@ -27,47 +27,48 @@ public class Simulator { /** * Default screen gamma on Windows is 2.2. */ - static final double GAMMA = 2.2; - static final double GAMMA_INV = 1. / GAMMA; + private static final double GAMMA = 2.2; + private static final double GAMMA_INV = 1. / GAMMA; + /** * A lookup table for the conversion from gamma-corrected sRGB values * [0..255] to linear RGB values [0..32767]. */ - static final short[] rgb2lin_red_LUT; + private static final short[] SRGB_TO_LINRGB; static { - // initialize rgb2lin_red_LUT - rgb2lin_red_LUT = new short[256]; + // initialize SRGB_TO_LINRGB + SRGB_TO_LINRGB = new short[256]; for (int i = 0; i < 256; i++) { // compute linear rgb between 0 and 1 final double lin = (0.992052 * Math.pow(i / 255., GAMMA) + 0.003974); // scale linear rgb to 0..32767 - rgb2lin_red_LUT[i] = (short) (lin * 32767.); + SRGB_TO_LINRGB[i] = (short) (lin * 32767.); } } /** * A lookup table for the conversion of linear RGB values [0..255] to * gamma-corrected sRGB values [0..255]. */ - static final byte[] lin2rgb_LUT; + private static final byte[] LINRGB_TO_SRGB; static { - // initialize lin2rgb_LUT - lin2rgb_LUT = new byte[256]; + // initialize LINRGB_TO_SRGB + LINRGB_TO_SRGB = new byte[256]; for (int i = 0; i < 256; i++) { - lin2rgb_LUT[i] = (byte) (255. * Math.pow(i / 255., GAMMA_INV)); + LINRGB_TO_SRGB[i] = (byte) (255. * Math.pow(i / 255., GAMMA_INV)); } } /** * Use this BufferedImageOp for the simulation. */ - BufferedImageOp op; + private BufferedImageOp op; /** * Creates a new instance of Simulator */ - public Simulator() { + protected Simulator() { } /** @@ -76,7 +77,7 @@ public class Simulator { * @normal The image with normal vision. * @return The image with simulated color vision impairment. */ - public BufferedImage filter(BufferedImage normal) { + protected BufferedImage filter(BufferedImage normal) { return op.filter(normal, null); } @@ -85,7 +86,7 @@ public class Simulator { * * @param simulationType The type of impairment to simulate. */ - void simulate(Simulation simulationType) { + protected void simulate(Simulation simulationType) { switch (simulationType) { case deutan: op = new RedGreenFilter(9591, 23173, -730); @@ -96,6 +97,9 @@ public class Simulator { case tritan: op = new TritanFilter(); break; + case grayscale: + op = new GrayscaleFilter(); + break; } } @@ -141,9 +145,9 @@ public class Simulator { final int b = 0xff & in; // get linear rgb values in the range 0..2^15-1 - final int r_lin = rgb2lin_red_LUT[r]; - final int g_lin = rgb2lin_red_LUT[g]; - final int b_lin = rgb2lin_red_LUT[b]; + final int r_lin = SRGB_TO_LINRGB[r]; + final int g_lin = SRGB_TO_LINRGB[g]; + final int b_lin = SRGB_TO_LINRGB[b]; // simulated red and green are identical // scale the matrix values to 0..2^15 for integer computations @@ -168,10 +172,10 @@ public class Simulator { } // convert reduced linear rgb to gamma corrected rgb - int red = lin2rgb_LUT[r_blind]; + int red = LINRGB_TO_SRGB[r_blind]; red = red >= 0 ? red : 256 + red; // from unsigned to signed - int blue = lin2rgb_LUT[b_blind]; - blue = blue >= 0 ? blue : 256 + blue; // from unsigned to signed + int blue = LINRGB_TO_SRGB[b_blind]; + blue = blue >= 0 ? blue : 256 + blue; // from unsigned to signed final int out = 0xff000000 | red << 16 | red << 8 | blue; @@ -223,9 +227,6 @@ public class Simulator { */ private class TritanFilter implements BufferedImageOp { - public TritanFilter() { - } - @Override public BufferedImage filter(BufferedImage src, BufferedImage dst) { @@ -278,9 +279,9 @@ public class Simulator { int b = 0xff & rgb; // get linear rgb values in the range 0..2^15-1 - r = rgb2lin_red_LUT[r]; - g = rgb2lin_red_LUT[g]; - b = rgb2lin_red_LUT[b]; + r = SRGB_TO_LINRGB[r]; + g = SRGB_TO_LINRGB[g]; + b = SRGB_TO_LINRGB[b]; /* Convert to LMS (dot product with transform matrix) */ final float L = (r * 0.05059983f + g * 0.08585369f + b * 0.00952420f) / 32767.f; @@ -310,7 +311,7 @@ public class Simulator { } else if (ired > 255) { ired = 255; } else { - ired = lin2rgb_LUT[ired]; + ired = LINRGB_TO_SRGB[ired]; ired = ired >= 0 ? ired : 256 + ired; // from unsigned to signed } if (igreen < 0) { @@ -318,7 +319,7 @@ public class Simulator { } else if (igreen > 255) { igreen = 255; } else { - igreen = lin2rgb_LUT[igreen]; + igreen = LINRGB_TO_SRGB[igreen]; igreen = igreen >= 0 ? igreen : 256 + igreen; // from unsigned to signed } if (iblue < 0) { @@ -326,7 +327,7 @@ public class Simulator { } else if (iblue > 255) { iblue = 255; } else { - iblue = lin2rgb_LUT[iblue]; + iblue = LINRGB_TO_SRGB[iblue]; iblue = iblue >= 0 ? iblue : 256 + iblue; // from unsigned to signed } @@ -374,4 +375,103 @@ public class Simulator { return null; } } + + /** + * A filter for grayscale conversion: perceptual luminance-preserving + * conversion to grayscale. + * https://en.wikipedia.org/wiki/Grayscale#Colorimetric_(perceptual_luminance-preserving)_conversion_to_grayscale + */ + private class GrayscaleFilter implements BufferedImageOp { + + @Override + public BufferedImage filter(BufferedImage src, BufferedImage dst) { + if (dst == null) { + dst = createCompatibleDestImage(src, null); + } + + // make sure the two images have the same size, color space, etc. + // MISSING FIXME + DataBufferInt inBuffer = (DataBufferInt) src.getRaster().getDataBuffer(); + DataBufferInt outBuffer = (DataBufferInt) dst.getRaster().getDataBuffer(); + int[] inData = inBuffer.getData(); + int[] outData = outBuffer.getData(); + + int prevIn = 0; + int prevOut = 0; + final int length = inData.length; + for (int i = 0; i < length; i++) { + final int in = inData[i]; + if (in == prevIn) { + outData[i] = prevOut; + } else { + final int rgb = inData[i]; + + final int r = (0xff0000 & rgb) >> 16; + final int g = (0xff00 & rgb) >> 8; + final int b = 0xff & rgb; + + // get linear rgb values in the range 0..2^15-1 + final int r_lin = SRGB_TO_LINRGB[r]; + final int g_lin = SRGB_TO_LINRGB[g]; + final int b_lin = SRGB_TO_LINRGB[b]; + + // perceptual luminance-preserving conversion to grayscale + // https://en.wikipedia.org/wiki/Grayscale#Colorimetric_(perceptual_luminance-preserving)_conversion_to_grayscale + double luminance = 0.2126 * r_lin + 0.7152 * g_lin + 0.0722 * b_lin; + int linRGB = ((int) (luminance)) >> 8; // divide by 2^8 to rescale + + // convert linear rgb to gamma corrected sRGB + if (linRGB < 0) { + linRGB = 0; + } else if (linRGB > 255) { + linRGB = 255; + } else { + linRGB = LINRGB_TO_SRGB[linRGB]; + linRGB = linRGB >= 0 ? linRGB : 256 + linRGB; // from unsigned to signed + } + + final int out = (int) (linRGB << 16 | linRGB << 8 | linRGB | 0xff000000); + + outData[i] = out; + prevIn = in; + prevOut = out; + } + } + + return dst; + } + + @Override + public Rectangle2D getBounds2D(BufferedImage src) { + return src.getRaster().getBounds(); + } + + @Override + public BufferedImage createCompatibleDestImage(BufferedImage src, + ColorModel destCM) { + if (destCM == null) { + destCM = src.getColorModel(); + } + int width = src.getWidth(); + int height = src.getHeight(); + BufferedImage image = new BufferedImage(destCM, + destCM.createCompatibleWritableRaster(width, height), + destCM.isAlphaPremultiplied(), null); + return image; + } + + @Override + public Point2D getPoint2D(Point2D srcPt, Point2D dstPt) { + if (dstPt == null) { + dstPt = new Point2D.Float(); + } + dstPt.setLocation(srcPt.getX(), srcPt.getY()); + return dstPt; + } + + @Override + public RenderingHints getRenderingHints() { + return null; + } + } } diff --git a/src/ika/icons/grayscalepanel.png b/src/ika/icons/grayscalepanel.png new file mode 100644 index 0000000..3783452 Binary files /dev/null and b/src/ika/icons/grayscalepanel.png differ