Added grayscale simulation

This commit is contained in:
Bernie Jenny 2018-04-02 21:18:25 +10:00
parent 0fb067d504
commit 70b83101d4
3 changed files with 156 additions and 86 deletions

View File

@ -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);

View File

@ -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;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB