mirror of
https://github.com/badvision/lawless-legends.git
synced 2025-01-13 03:30:28 +00:00
Image conversion tests and tweaks to get image dithering looking correctly for HGR and DHGR again (finally!!)
This commit is contained in:
parent
b11da29c56
commit
559e20e645
@ -86,7 +86,7 @@
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.8.1</version>
|
||||
<version>4.11</version>
|
||||
<scope>test</scope>
|
||||
<type>jar</type>
|
||||
</dependency>
|
||||
|
@ -47,23 +47,30 @@ import static org.badvision.outlaweditor.apple.AppleNTSCGraphics.hgrToDhgr;
|
||||
public class ImageDitherEngine {
|
||||
|
||||
int byteRenderWidth;
|
||||
int errorWindow = 7;
|
||||
int overlap = 3;
|
||||
int pixelRenderWidth;
|
||||
final int errorWindow = 6;
|
||||
final int overlap = 3;
|
||||
final int pixelShiftHgr = -2;
|
||||
final int pixelShiftDhgr = -2;
|
||||
WritableImage source;
|
||||
byte[] screen;
|
||||
Platform platform;
|
||||
int bufferWidth;
|
||||
int height;
|
||||
int divisor;
|
||||
int[][] coefficients;
|
||||
public int[][] coefficients;
|
||||
|
||||
public ImageDitherEngine(Platform platform) {
|
||||
this.platform = platform;
|
||||
byteRenderWidth = platform == Platform.AppleII_DHGR ? 7 : 14;
|
||||
}
|
||||
|
||||
public Platform getPlatform() {
|
||||
return platform;
|
||||
}
|
||||
|
||||
public void setSourceImage(Image img) {
|
||||
source = getScaledImage(img, bufferWidth * byteRenderWidth, height);
|
||||
source = getScaledImage(img, pixelRenderWidth, height);
|
||||
}
|
||||
|
||||
private static WritableImage getScaledImage(Image img, int width, int height) {
|
||||
@ -76,11 +83,14 @@ public class ImageDitherEngine {
|
||||
}
|
||||
|
||||
public WritableImage getPreviewImage() {
|
||||
return new WritableImage(bufferWidth * byteRenderWidth, height * 2);
|
||||
WritableImage out = new WritableImage(pixelRenderWidth, height * 2);
|
||||
platform.imageRenderer.renderImage(out, screen, bufferWidth, height);
|
||||
return out;
|
||||
}
|
||||
|
||||
public void setOutputDimensions(int width, int height) {
|
||||
this.bufferWidth = width;
|
||||
this.pixelRenderWidth = width * byteRenderWidth;
|
||||
this.height = height;
|
||||
screen = platform.imageRenderer.createImageBuffer(width, height);
|
||||
}
|
||||
@ -106,15 +116,15 @@ public class ImageDitherEngine {
|
||||
WritableImage tmpScaled2;
|
||||
int[] scanline;
|
||||
List<Integer> pixels;
|
||||
|
||||
|
||||
public Image getScratchBuffer() {
|
||||
return keepScaled;
|
||||
}
|
||||
|
||||
public byte[] dither(boolean propagateError) {
|
||||
keepScaled = new WritableImage(source.getPixelReader(), 560, 192);
|
||||
tmpScaled1 = new WritableImage(source.getPixelReader(), 560, 192);
|
||||
tmpScaled2 = new WritableImage(source.getPixelReader(), 560, 192);
|
||||
keepScaled = new WritableImage(source.getPixelReader(), pixelRenderWidth, height);
|
||||
tmpScaled1 = new WritableImage(source.getPixelReader(), pixelRenderWidth, height);
|
||||
tmpScaled2 = new WritableImage(source.getPixelReader(), pixelRenderWidth, height);
|
||||
for (int i = 0; i < screen.length; i++) {
|
||||
screen[i] = (byte) 0;
|
||||
}
|
||||
@ -136,6 +146,7 @@ public class ImageDitherEngine {
|
||||
}
|
||||
|
||||
void hiresDither(int y, int x, boolean propagateError) {
|
||||
int ditherVerticalRange = Math.min(3, height - y);
|
||||
int bb1 = screen[(y + startY) * bufferWidth + startX + x] & 255;
|
||||
int bb2 = screen[(y + startY) * bufferWidth + startX + x + 1] & 255;
|
||||
int next = bb2 & 127; // Preserve hi-bit so last pixel stays solid, it is a very minor detail
|
||||
@ -143,17 +154,16 @@ public class ImageDitherEngine {
|
||||
if ((x + startX) > 0) {
|
||||
prev = screen[(y + startY) * bufferWidth + startX + x - 1] & 255;
|
||||
}
|
||||
if ((x + startX) < (bufferWidth-2)) {
|
||||
if ((x + startX) < (bufferWidth - 2)) {
|
||||
next = screen[(y + startY) * bufferWidth + startX + x + 2] & 255;
|
||||
}
|
||||
// First byte, compared with a sliding window encompassing the previous byte, if any.
|
||||
long leastError = Long.MAX_VALUE;
|
||||
for (int hi = 0; hi < 2; hi++) {
|
||||
tmpScaled2.getPixelWriter().setPixels(0, y, 560, (y < 190) ? 3 : (y < 191) ? 2 : 1, keepScaled.getPixelReader(), 0, y);
|
||||
tmpScaled2.getPixelWriter().setPixels(0, y, pixelRenderWidth, ditherVerticalRange, keepScaled.getPixelReader(), 0, y);
|
||||
int b1 = (hi << 7);
|
||||
long totalError = 0;
|
||||
for (int c = 0; c < 7; c++) {
|
||||
// for (int c = 6; c >= 0; c--) {
|
||||
int on = b1 | (1 << c);
|
||||
int off = on ^ (1 << c);
|
||||
// get values for "off"
|
||||
@ -161,19 +171,17 @@ public class ImageDitherEngine {
|
||||
scanline[0] = i;
|
||||
i = hgrToDhgr[(i & 0x010000000) >> 20 | off][bb2];
|
||||
scanline[1] = i;
|
||||
// scanline[2] = hgrToDhgr[(i & 0x10000000) != 0 ? next | 0x0100 : next][0] & 0x0fffffff;
|
||||
long errorOff = getError(x * 14 - overlap + c * 2, y, 28 + c * 2 - overlap, errorWindow, pixels, tmpScaled2.getPixelReader(), scanline);
|
||||
int off1 = pixels.get(c * 2 + 28);
|
||||
int off2 = pixels.get(c * 2 + 29);
|
||||
double errorOff = getError(x * 14 - overlap + c * 2, y, 28 + c * 2 - overlap + pixelShiftHgr, errorWindow, tmpScaled2.getPixelReader(), scanline);
|
||||
int off1 = pixels.get(c * 2 + 28 + pixelShiftHgr);
|
||||
int off2 = pixels.get(c * 2 + 29 + pixelShiftHgr);
|
||||
// get values for "on"
|
||||
i = hgrToDhgr[0][prev];
|
||||
scanline[0] = i;
|
||||
i = hgrToDhgr[(i & 0x010000000) >> 20 | on][bb2];
|
||||
scanline[1] = i;
|
||||
// scanline[2] = hgrToDhgr[(i & 0x10000000) != 0 ? next | 0x0100 : next][0] & 0x0fffffff;
|
||||
long errorOn = getError(x * 14 - overlap + c * 2, y, 28 + c * 2 - overlap, errorWindow, pixels, tmpScaled2.getPixelReader(), scanline);
|
||||
int on1 = pixels.get(c * 2 + 28);
|
||||
int on2 = pixels.get(c * 2 + 29);
|
||||
double errorOn = getError(x * 14 - overlap + c * 2, y, 28 + c * 2 - overlap + pixelShiftHgr, errorWindow, tmpScaled2.getPixelReader(), scanline);
|
||||
int on1 = pixels.get(c * 2 + 28 + pixelShiftHgr);
|
||||
int on2 = pixels.get(c * 2 + 29 + pixelShiftHgr);
|
||||
int[] col1;
|
||||
int[] col2;
|
||||
if (errorOff < errorOn) {
|
||||
@ -188,42 +196,40 @@ public class ImageDitherEngine {
|
||||
col2 = Palette.parseIntColor(on2);
|
||||
}
|
||||
if (propagateError) {
|
||||
propagateError(x * 14 + c * 2, y, tmpScaled2, col1);
|
||||
propagateError(x * 14 + c * 2 + 1, y, tmpScaled2, col2);
|
||||
propagateError(x * 14 + c * 2 + pixelShiftHgr, y, tmpScaled2, col1);
|
||||
propagateError(x * 14 + c * 2 + 1 + pixelShiftHgr, y, tmpScaled2, col2);
|
||||
}
|
||||
}
|
||||
if (totalError < leastError) {
|
||||
tmpScaled1.getPixelWriter().setPixels(0, y, 560, (y < 190) ? 3 : (y < 191) ? 2 : 1, tmpScaled2.getPixelReader(), 0, y);
|
||||
tmpScaled1.getPixelWriter().setPixels(0, y, pixelRenderWidth, ditherVerticalRange, tmpScaled2.getPixelReader(), 0, y);
|
||||
leastError = totalError;
|
||||
bb1 = b1;
|
||||
}
|
||||
}
|
||||
}
|
||||
keepScaled.getPixelWriter().setPixels(0, y, 560, (y < 190) ? 3 : (y < 191) ? 2 : 1, tmpScaled1.getPixelReader(), 0, y);
|
||||
keepScaled.getPixelWriter().setPixels(0, y, pixelRenderWidth, ditherVerticalRange, tmpScaled1.getPixelReader(), 0, y);
|
||||
// Second byte, compared with a sliding window encompassing the next byte, if any.
|
||||
leastError = Long.MAX_VALUE;
|
||||
for (int hi = 0; hi < 2; hi++) {
|
||||
tmpScaled2.getPixelWriter().setPixels(0, y, 560, (y < 190) ? 3 : (y < 191) ? 2 : 1, keepScaled.getPixelReader(), 0, y);
|
||||
tmpScaled2.getPixelWriter().setPixels(0, y, pixelRenderWidth, ditherVerticalRange, keepScaled.getPixelReader(), 0, y);
|
||||
int b2 = (hi << 7);
|
||||
long totalError = 0;
|
||||
for (int c = 0; c < 7; c++) {
|
||||
// for (int c = 6; c >= 0; c--) {
|
||||
int on = b2 | (1 << c);
|
||||
int off = on ^ (1 << c);
|
||||
// get values for "off"
|
||||
int i = hgrToDhgr[bb1][off];
|
||||
scanline[0] = i;
|
||||
scanline[1] = hgrToDhgr[(i & 0x010000000) >> 20 | next][0];
|
||||
// int errorOff = getError(x * 14 - overlap + c * 2, y, 28 + c * 2 - overlap, errorWindow, pixels, tmpScaled2.getPixelReader(), scanline);
|
||||
long errorOff = getError(x * 14 + 14 - overlap + c * 2, y, 14 + c * 2 - overlap, errorWindow, pixels, tmpScaled2.getPixelReader(), scanline);
|
||||
int off1 = pixels.get(c * 2 + 14);
|
||||
int off2 = pixels.get(c * 2 + 15);
|
||||
double errorOff = getError(x * 14 + 14 - overlap + c * 2, y, 14 + c * 2 - overlap + pixelShiftHgr, errorWindow, tmpScaled2.getPixelReader(), scanline);
|
||||
int off1 = pixels.get(c * 2 + 14 + pixelShiftHgr);
|
||||
int off2 = pixels.get(c * 2 + 15 + pixelShiftHgr);
|
||||
// get values for "on"
|
||||
i = hgrToDhgr[bb1][on];
|
||||
scanline[0] = i;
|
||||
scanline[1] = hgrToDhgr[(i & 0x010000000) >> 20 | next][0];
|
||||
long errorOn = getError(x * 14 + 14 - overlap + c * 2, y, 14 + c * 2 - overlap, errorWindow, pixels, tmpScaled2.getPixelReader(), scanline);
|
||||
int on1 = pixels.get(c * 2 + 14);
|
||||
int on2 = pixels.get(c * 2 + 15);
|
||||
double errorOn = getError(x * 14 + 14 - overlap + c * 2, y, 14 + c * 2 - overlap + pixelShiftHgr, errorWindow, tmpScaled2.getPixelReader(), scanline);
|
||||
int on1 = pixels.get(c * 2 + 14 + pixelShiftHgr);
|
||||
int on2 = pixels.get(c * 2 + 15 + pixelShiftHgr);
|
||||
int[] col1;
|
||||
int[] col2;
|
||||
if (errorOff < errorOn) {
|
||||
@ -238,17 +244,17 @@ public class ImageDitherEngine {
|
||||
col2 = Palette.parseIntColor(on2);
|
||||
}
|
||||
if (propagateError) {
|
||||
propagateError(x * 14 + c * 2 + 14, y, tmpScaled2, col1);
|
||||
propagateError(x * 14 + c * 2 + 15, y, tmpScaled2, col2);
|
||||
propagateError(x * 14 + c * 2 + 14 + pixelShiftHgr, y, tmpScaled2, col1);
|
||||
propagateError(x * 14 + c * 2 + 15 + pixelShiftHgr, y, tmpScaled2, col2);
|
||||
}
|
||||
}
|
||||
if (totalError < leastError) {
|
||||
tmpScaled1.getPixelWriter().setPixels(0, y, 560, (y < 190) ? 3 : (y < 191) ? 2 : 1, tmpScaled2.getPixelReader(), 0, y);
|
||||
tmpScaled1.getPixelWriter().setPixels(0, y, pixelRenderWidth, ditherVerticalRange, tmpScaled2.getPixelReader(), 0, y);
|
||||
leastError = totalError;
|
||||
bb2 = b2;
|
||||
}
|
||||
}
|
||||
keepScaled.getPixelWriter().setPixels(0, y, 560, (y < 190) ? 3 : (y < 191) ? 2 : 1, tmpScaled1.getPixelReader(), 0, y);
|
||||
keepScaled.getPixelWriter().setPixels(0, y, pixelRenderWidth, ditherVerticalRange, tmpScaled1.getPixelReader(), 0, y);
|
||||
screen[(y + startY) * bufferWidth + startX + x] = (byte) bb1;
|
||||
screen[(y + startY) * bufferWidth + startX + x + 1] = (byte) bb2;
|
||||
}
|
||||
@ -259,22 +265,22 @@ public class ImageDitherEngine {
|
||||
}
|
||||
scanline[0] = 0;
|
||||
if (x >= 4) {
|
||||
scanline[0] = screen[y * 80 + x - 1] << 21;
|
||||
scanline[0] = screen[y * bufferWidth + x - 1] << 21;
|
||||
}
|
||||
scanline[1] = 0;
|
||||
if (x < 76) {
|
||||
scanline[2] = screen[y * 80 + x + 4];
|
||||
scanline[2] = 0;
|
||||
if (x + 4 < bufferWidth) {
|
||||
scanline[2] = screen[y * bufferWidth + x + 4];
|
||||
}
|
||||
int bytes[] = new int[]{
|
||||
screen[y * 80 + x] & 255,
|
||||
screen[y * 80 + x + 1] & 255,
|
||||
screen[y * 80 + x + 2] & 255,
|
||||
screen[y * 80 + x + 3] & 255
|
||||
screen[y * bufferWidth + x] & 255,
|
||||
screen[y * bufferWidth + x + 1] & 255,
|
||||
screen[y * bufferWidth + x + 2] & 255,
|
||||
screen[y * bufferWidth + x + 3] & 255
|
||||
};
|
||||
|
||||
for (int byteOffset = 0; byteOffset < 4; byteOffset++) {
|
||||
// First byte, compared with a sliding window encompassing the previous byte, if any.
|
||||
int leastError = Integer.MAX_VALUE;
|
||||
int b1 = (bytes[byteOffset] & 0x07f);
|
||||
for (int bit = 0; bit < 7; bit++) {
|
||||
int on = b1 | (1 << bit);
|
||||
@ -288,8 +294,8 @@ public class ImageDitherEngine {
|
||||
i <<= 7;
|
||||
i |= (byteOffset == 0) ? off : bytes[0] & 255;
|
||||
scanline[1] = i;
|
||||
int errorOff = getError((x + byteOffset) * 7 - overlap + bit, y, 28 + (byteOffset * 7) + bit - overlap, errorWindow, pixels, tmpScaled1.getPixelReader(), scanline);
|
||||
int off1 = pixels.get(byteOffset * 7 + bit + 28);
|
||||
double errorOff = getError((x + byteOffset) * 7 - overlap + bit, y, 28 + (byteOffset * 7) + bit - overlap + pixelShiftDhgr, errorWindow, keepScaled.getPixelReader(), scanline);
|
||||
int offColor = pixels.get(byteOffset * 7 + bit + 28+ pixelShiftDhgr);
|
||||
// get values for "on"
|
||||
i = (byteOffset == 3) ? on : bytes[3] & 255;
|
||||
i <<= 7;
|
||||
@ -299,22 +305,21 @@ public class ImageDitherEngine {
|
||||
i <<= 7;
|
||||
i |= (byteOffset == 0) ? on : bytes[0] & 255;
|
||||
scanline[1] = i;
|
||||
int errorOn = getError((x + byteOffset) * 7 - overlap + bit, y, 28 + (byteOffset * 7) + bit - overlap, errorWindow, pixels, tmpScaled1.getPixelReader(), scanline);
|
||||
int on1 = pixels.get(byteOffset * 7 + bit + 28);
|
||||
double errorOn = getError((x + byteOffset) * 7 - overlap + bit, y, 28 + (byteOffset * 7) + bit - overlap + pixelShiftDhgr, errorWindow, keepScaled.getPixelReader(), scanline);
|
||||
int onColor = pixels.get(byteOffset * 7 + bit + 28+ pixelShiftDhgr);
|
||||
|
||||
int[] col1;
|
||||
if (errorOff < errorOn) {
|
||||
b1 = off;
|
||||
col1 = Palette.parseIntColor(off1);
|
||||
col1 = Palette.parseIntColor(offColor);
|
||||
} else {
|
||||
b1 = on;
|
||||
col1 = Palette.parseIntColor(on1);
|
||||
col1 = Palette.parseIntColor(onColor);
|
||||
}
|
||||
if (propagateError) {
|
||||
propagateError((x + byteOffset) * 7 + bit, y, tmpScaled1, col1);
|
||||
propagateError((x + byteOffset) * 7 + bit, y, keepScaled, col1);
|
||||
}
|
||||
}
|
||||
keepScaled.getPixelWriter().setPixels(0, y, 560, (y < 190) ? 3 : (y < 191) ? 2 : 1, tmpScaled1.getPixelReader(), 0, y);
|
||||
bytes[byteOffset] = b1;
|
||||
screen[(y + startY) * bufferWidth + startX + x] = (byte) bytes[0];
|
||||
screen[(y + startY) * bufferWidth + startX + x + 1] = (byte) bytes[1];
|
||||
@ -322,11 +327,13 @@ public class ImageDitherEngine {
|
||||
screen[(y + startY) * bufferWidth + startX + x + 3] = (byte) bytes[3];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static int ALPHA_SOLID = 255 << 24;
|
||||
|
||||
private void propagateError(int x, int y, WritableImage img, int[] newColor) {
|
||||
if (x < 0 || y < 0) return;
|
||||
if (x < 0 || y < 0) {
|
||||
return;
|
||||
}
|
||||
int pixel = img.getPixelReader().getArgb(x, y);
|
||||
for (int i = 0; i < 3; i++) {
|
||||
int error = Palette.getComponent(pixel, i) - newColor[i];
|
||||
@ -343,94 +350,51 @@ public class ImageDitherEngine {
|
||||
}
|
||||
}
|
||||
|
||||
private static int getError(int imageXStart, int y, int scanlineXStart, int window, final List<Integer> pixels, PixelReader source, int[] scanline) {
|
||||
PixelWriter fakeWriter = new PixelWriter() {
|
||||
@Override
|
||||
public PixelFormat getPixelFormat() {
|
||||
throw new UnsupportedOperationException("Not supported yet.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setArgb(int x, int y, int c) {
|
||||
pixels.add(c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColor(int i, int i1, Color color) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends Buffer> void setPixels(int i, int i1, int i2, int i3, PixelFormat<T> pf, T t, int i4) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPixels(int i, int i1, int i2, int i3, PixelFormat<ByteBuffer> pf, byte[] bytes, int i4, int i5) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPixels(int i, int i1, int i2, int i3, PixelFormat<IntBuffer> pf, int[] ints, int i4, int i5) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPixels(int i, int i1, int i2, int i3, PixelReader reader, int i4, int i5) {
|
||||
}
|
||||
};
|
||||
|
||||
private double getError(int imageXStart, int y, int scanlineXStart, int window, PixelReader source, int[] scanline) {
|
||||
pixels.clear();
|
||||
PixelWriter fakeWriter = new PixelWriter() {
|
||||
@Override
|
||||
public PixelFormat getPixelFormat() {
|
||||
throw new UnsupportedOperationException("Not supported yet.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setArgb(int x, int y, int c) {
|
||||
pixels.add(c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColor(int i, int i1, Color color) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends Buffer> void setPixels(int i, int i1, int i2, int i3, PixelFormat<T> pf, T t, int i4) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPixels(int i, int i1, int i2, int i3, PixelFormat<ByteBuffer> pf, byte[] bytes, int i4, int i5) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPixels(int i, int i1, int i2, int i3, PixelFormat<IntBuffer> pf, int[] ints, int i4, int i5) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPixels(int i, int i1, int i2, int i3, PixelReader reader, int i4, int i5) {
|
||||
}
|
||||
};
|
||||
AppleImageRenderer.renderScanline(fakeWriter, 0, scanline, true, false, 20);
|
||||
// double max = 0;
|
||||
// double min = Double.MAX_VALUE;
|
||||
double total = 0;
|
||||
// List<Double> err = new ArrayList<>();
|
||||
for (int p = 0; p < window; p++) {
|
||||
if ((imageXStart + p) < 0 || (imageXStart + p) >= 560 || scanlineXStart + p < 0) {
|
||||
if ((imageXStart + p) < 0 || (imageXStart + p) >= pixelRenderWidth || scanlineXStart + p < 0) {
|
||||
continue;
|
||||
}
|
||||
int[] c1 = Palette.parseIntColor(pixels.get(scanlineXStart + p));
|
||||
int[] c2 = Palette.parseIntColor(source.getArgb(imageXStart + p, y));
|
||||
double dist = Palette.distance(c1, c2);
|
||||
total += dist;
|
||||
// max = Math.max(dist, max);
|
||||
// min = Math.min(dist, min);
|
||||
// err.add(dist);
|
||||
}
|
||||
// double avg = total/((double) window);
|
||||
// double range = max-min;
|
||||
// double totalDev = 0.0;
|
||||
// for (Double d : err) {
|
||||
// totalDev = Math.pow(d-avg, 2);
|
||||
//// errorTotal += (d-min)/range;
|
||||
// }
|
||||
// totalDev /= ((double) window);
|
||||
// double stdDev = Math.sqrt(totalDev);
|
||||
// return (int) (min+(avg*(stdDev/range)));
|
||||
return (int) total;
|
||||
}
|
||||
|
||||
// int currentPixel = source.getRGB(x, y);
|
||||
//
|
||||
// for (int i = 0; i < 3; i++) {
|
||||
// int error = Palette.getComponent(currentPixel, i) - Palette.getComponent(closestColor, i);
|
||||
// if (x + 1 < source.getWidth()) {
|
||||
// int c = source.getRGB(x + 1, y);
|
||||
// source.setRGB(x + 1, y, Palette.addError(c, i, (error * 7) >> 4));
|
||||
// }
|
||||
// if (y + 1 < source.getHeight()) {
|
||||
// if (x - 1 > 0) {
|
||||
// int c = source.getRGB(x - 1, y + 1);
|
||||
// source.setRGB(x - 1, y + 1, Palette.addError(c, i, (error * 3) >> 4));
|
||||
// }
|
||||
// {
|
||||
// int c = source.getRGB(x, y + 1);
|
||||
// source.setRGB(x, y + 1, Palette.addError(c, i, (error * 5) >> 4));
|
||||
// }
|
||||
// if (x + 1 < source.getWidth()) {
|
||||
// int c = source.getRGB(x + 1, y + 1);
|
||||
// source.setRGB(x + 1, y + 1, Palette.addError(c, i, error >> 4));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return dest;
|
||||
// }
|
||||
}
|
||||
|
@ -52,11 +52,11 @@ public abstract class Palette {
|
||||
}
|
||||
|
||||
public int findColor(int color[]) {
|
||||
int lastDiff = COLOR_DISTANCE_MAX;
|
||||
double lastDiff = COLOR_DISTANCE_MAX;
|
||||
int bestFit = 0;
|
||||
for (int i = 0; i < colors.size(); i++) {
|
||||
int test[] = (int[]) colors.get(i);
|
||||
int diff = (int) distance(color, test);
|
||||
double diff = distance(color, test);
|
||||
if (diff < lastDiff) {
|
||||
lastDiff = diff;
|
||||
bestFit = i;
|
||||
@ -66,8 +66,19 @@ public abstract class Palette {
|
||||
return bestFit;
|
||||
}
|
||||
|
||||
public static double distance(int color[], int test[]) {
|
||||
return Math.pow(Math.abs(color[0] - test[0]), 3D) + Math.pow(Math.abs(color[1] - test[1]), 3D) + Math.pow(Math.abs(color[2] - test[2]), 3D);
|
||||
public static double distance(int c1[], int c2[]) {
|
||||
double rmean = ( c1[0] + c2[1] ) / 2.0;
|
||||
double r = c1[0] - c2[0];
|
||||
double g = c1[1] - c2[1];
|
||||
double b = c1[2] - c2[2];
|
||||
double weightR = 2.0 + rmean/256.0;
|
||||
double weightG = 4.0;
|
||||
double weightB = 2.0 + (255.0-rmean)/256.0;
|
||||
return Math.sqrt(weightR*(r*r) + weightG*(g*g) + weightB*(b*b)) / 1.73167;
|
||||
}
|
||||
|
||||
public static double distance_linear(int color[], int test[]) {
|
||||
return Math.sqrt(Math.pow(color[0] - test[0], 2D) + Math.pow(color[1] - test[1], 2D) + Math.pow(color[2] - test[2], 2D));
|
||||
}
|
||||
|
||||
public static int getR(int color) {
|
||||
@ -96,8 +107,19 @@ public abstract class Palette {
|
||||
}
|
||||
|
||||
public static int addError(int color, int component, int error) {
|
||||
int[] sourceColor = parseIntColor(color);
|
||||
sourceColor[component] = Math.max(0, Math.min(255, sourceColor[component] + error));
|
||||
return toRGBInt(sourceColor);
|
||||
int level = getComponent(color, component);
|
||||
level = Math.max(0, Math.min(255, level + error));
|
||||
switch (component) {
|
||||
case 0:
|
||||
color = color & 0x0FFFF | (level << 16);
|
||||
break;
|
||||
case 1:
|
||||
color = color & 0x0FF00FF | (level << 8);
|
||||
break;
|
||||
case 2:
|
||||
color = color & 0x0FFFF00 | level;
|
||||
break;
|
||||
}
|
||||
return color;
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,6 @@ import javafx.scene.image.ImageView;
|
||||
import javafx.scene.image.PixelReader;
|
||||
import javafx.scene.image.WritableImage;
|
||||
import javafx.stage.Stage;
|
||||
import org.badvision.outlaweditor.Application;
|
||||
import org.badvision.outlaweditor.apple.ImageDitherEngine;
|
||||
import org.badvision.outlaweditor.ui.ImageConversionPostAction;
|
||||
|
||||
@ -124,7 +123,7 @@ public class ImageConversionWizardController implements Initializable {
|
||||
saturationValue.textProperty().addListener((ObservableValue<? extends String> observable, String oldValue, String newValue)
|
||||
-> javafx.application.Platform.runLater(this::updateImageAdjustments));
|
||||
|
||||
configureFastFloydSteinbergPreset(null);
|
||||
configureAtkinsonPreset(null);
|
||||
}
|
||||
|
||||
private Stage stage;
|
||||
@ -221,7 +220,8 @@ public class ImageConversionWizardController implements Initializable {
|
||||
byte[] lastOutput;
|
||||
private void updateConvertedImageWithData(byte[] data) {
|
||||
lastOutput = data;
|
||||
convertedImageView.setImage(Application.currentPlatform.imageRenderer.renderImage(outputPreviewImage, data, getOutputWidth(), getOutputHeight()));
|
||||
convertedImageView.setImage(ditherEngine.getPreviewImage());
|
||||
// convertedImageView.setImage(ditherEngine.getScratchBuffer());
|
||||
}
|
||||
|
||||
@FXML
|
||||
|
@ -0,0 +1,214 @@
|
||||
package org.badvision.outlaweditor.test;
|
||||
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.WritableImage;
|
||||
import javafx.scene.paint.Color;
|
||||
import org.badvision.outlaweditor.apple.ImageDitherEngine;
|
||||
import org.badvision.outlaweditor.apple.Palette;
|
||||
import org.junit.After;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import static org.junit.Assert.*;
|
||||
import org.junit.Rule;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author blurry
|
||||
*/
|
||||
public class ImageDitheringTest {
|
||||
|
||||
public ImageDitheringTest() {
|
||||
}
|
||||
|
||||
ImageDitherEngine hgrDither, dhgrDither;
|
||||
|
||||
@Rule
|
||||
public JavaFXThreadingRule javafxRule = new JavaFXThreadingRule();
|
||||
|
||||
@BeforeClass
|
||||
public static void setUpClass() {
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void tearDownClass() {
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
hgrDither = new ImageDitherEngine(org.badvision.outlaweditor.Platform.AppleII);
|
||||
hgrDither.setOutputDimensions(560, 192);
|
||||
dhgrDither = new ImageDitherEngine(org.badvision.outlaweditor.Platform.AppleII_DHGR);
|
||||
dhgrDither.setOutputDimensions(560, 192);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void colorDiffTest() {
|
||||
int[] white = {255, 255, 255};
|
||||
int[] black = {0, 0, 0};
|
||||
int[] gray = {128, 128, 128};
|
||||
assertEquals(0, Palette.distance(white, white), 0);
|
||||
assertEquals(0, Palette.distance(black, black), 0);
|
||||
assertEquals(0, Palette.distance(gray, gray), 0);
|
||||
double midDist1 = Palette.distance(white, gray);
|
||||
double midDist2 = Palette.distance(black, gray);
|
||||
assertEquals(midDist1, midDist2, 3.0);
|
||||
double maxDist = Palette.distance(white, black);
|
||||
assertEquals(maxDist, midDist1 + midDist2, 0.01);
|
||||
assertEquals(255.0 * Math.sqrt(3.0), maxDist, 0.01);
|
||||
}
|
||||
|
||||
/*
|
||||
@Test
|
||||
public void blackTest() {
|
||||
WritableImage blackSource = new WritableImage(560, 192);
|
||||
fillColor(blackSource, Color.BLACK);
|
||||
WritableImage blackConverted = getTestConversion(hgrDither, blackSource);
|
||||
assertExactImage(blackSource, blackConverted);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whiteTest() {
|
||||
WritableImage whiteSource = new WritableImage(560, 192);
|
||||
fillColor(whiteSource, Color.WHITE);
|
||||
WritableImage whiteConverted = getTestConversion(hgrDither, whiteSource);
|
||||
assertLowError(whiteSource, whiteConverted, 16, 1.0);
|
||||
}
|
||||
*/
|
||||
@Test
|
||||
public void grayHGRTest() {
|
||||
testSolidColor(new Color(0.5f, 0.5f, 0.5f, 1.0f), hgrDither, 3.0);
|
||||
}
|
||||
@Test
|
||||
public void grayDHGRTest() {
|
||||
testSolidColor(new Color(0.5f, 0.5f, 0.5f, 1.0f), dhgrDither, 3.0);
|
||||
}
|
||||
@Test
|
||||
public void redHGRTest() {
|
||||
testSolidColor(new Color(1f, 0f, 0f, 1.0f), hgrDither, 3.0);
|
||||
}
|
||||
@Test
|
||||
public void redDHGRTest() {
|
||||
testSolidColor(new Color(1f, 0f, 0f, 1.0f), dhgrDither, 3.0);
|
||||
}
|
||||
@Test
|
||||
public void greenHGRTest() {
|
||||
testSolidColor(new Color(0f, 1f, 0f, 1.0f), hgrDither, 3.0);
|
||||
}
|
||||
@Test
|
||||
public void greenDHGRTest() {
|
||||
testSolidColor(new Color(0f, 1f, 0f, 1.0f), dhgrDither, 3.0);
|
||||
}
|
||||
@Test
|
||||
public void blueHGRTest() {
|
||||
testSolidColor(new Color(0f, 0f, 1f, 1.0f), hgrDither, 3.0);
|
||||
}
|
||||
@Test
|
||||
public void blueDHGRTest() {
|
||||
testSolidColor(new Color(0f, 0f, 1f, 1.0f), dhgrDither, 3.0);
|
||||
}
|
||||
|
||||
|
||||
private void testSolidColor(Color color, ImageDitherEngine engine, Double maxDelta) {
|
||||
WritableImage source = new WritableImage(560, 192);
|
||||
fillColor(source, color);
|
||||
WritableImage converted = getTestConversion(engine, source);
|
||||
assertLowError(source, converted, 16, maxDelta);
|
||||
}
|
||||
|
||||
public void assertExactImage(Image img1, Image img2) throws AssertionError {
|
||||
for (int x = 0; x < img1.getWidth(); x++) {
|
||||
for (int y = 0; y < img1.getHeight(); y++) {
|
||||
int col1 = img1.getPixelReader().getArgb(x, y) & 0x0FFFFFF;
|
||||
int col2 = img2.getPixelReader().getArgb(x, y) & 0x0FFFFFF;
|
||||
if (col1 != col2) {
|
||||
throw new AssertionError("Pixels are not the same color at " + x + "," + y + "; (" + Integer.toHexString(col1) + " vs " + Integer.toHexString(col2));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void assertLowError(Image img1, Image img2, int gridSize, double maxError) throws AssertionError {
|
||||
double error = getAverageImageError(img1, img2, gridSize);
|
||||
if (error > maxError) {
|
||||
throw new AssertionError("Average error is greater than threshold: " + error + " (max is " + maxError + ")");
|
||||
}
|
||||
}
|
||||
|
||||
public void assertHighError(Image img1, Image img2, int gridSize, double minError) throws AssertionError {
|
||||
double error = getAverageImageError(img1, img2, gridSize);
|
||||
if (error < minError) {
|
||||
throw new AssertionError("Average error is lower than threshold: " + error + " (min is " + minError + ")");
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluates a pair of images as a grid of square regions
|
||||
// Returns the average error value of all regions
|
||||
public double getAverageImageError(Image img1, Image img2, int gridSize) {
|
||||
double totalError = 0;
|
||||
double regionCount = 0;
|
||||
for (int x = 0; x < img1.getWidth(); x += gridSize) {
|
||||
int x2 = (int) Math.min(img1.getWidth(), x + gridSize);
|
||||
for (int y = 0; y < img1.getHeight(); y += gridSize) {
|
||||
int y2 = (int) Math.min(img1.getHeight(), y + gridSize);
|
||||
totalError += averageErrorForRegion(img1, img2, x, x2, y, y2);
|
||||
regionCount++;
|
||||
}
|
||||
}
|
||||
return totalError / regionCount;
|
||||
}
|
||||
|
||||
public double averageErrorForRegion(Image img1, Image img2, int x1, int x2, int y1, int y2) {
|
||||
int[] col1 = getAverageColor(img1, x1, x2, y1, y2);
|
||||
int[] col2 = getAverageColor(img2, x1, x2, y1, y2);
|
||||
System.out.printf("Col1 %d %d %d, Col2 %d %d %d\n", col1[0], col1[1], col1[2], col2[0], col2[1], col2[2]);
|
||||
return Palette.distance_linear(col1, col2);
|
||||
}
|
||||
|
||||
public int[] getAverageColor(Image img, int x1, int x2, int y1, int y2) {
|
||||
long[] colors = new long[3];
|
||||
long pixelCount = 0;
|
||||
for (int x = x1; x < x2 && x < img.getWidth(); x++) {
|
||||
for (int y = y1; y < y2 && y < img.getHeight(); y++) {
|
||||
int color = img.getPixelReader().getArgb(x, y) & 0x0ffffff;
|
||||
int[] col = Palette.parseIntColor(color);
|
||||
colors[0] += col[0];
|
||||
colors[1] += col[1];
|
||||
colors[2] += col[2];
|
||||
pixelCount++;
|
||||
}
|
||||
}
|
||||
return new int[]{
|
||||
(int) (colors[0] / pixelCount),
|
||||
(int) (colors[1] / pixelCount),
|
||||
(int) (colors[2] / pixelCount)
|
||||
};
|
||||
}
|
||||
|
||||
private void configureAtkinsonDither(ImageDitherEngine ditherEngine) {
|
||||
int[][] coefficients = new int[][]{
|
||||
{0, 0, 0}, {0, 1, 0}, {0, 1, 1}, {0, 1, 0}, {1, 0, 0}};
|
||||
ditherEngine.setCoefficients(coefficients);
|
||||
ditherEngine.setDivisor(8);
|
||||
}
|
||||
|
||||
private void fillColor(WritableImage img, Color color) {
|
||||
for (int x = 0; x < img.getWidth(); x++) {
|
||||
for (int y = 0; y < img.getHeight(); y++) {
|
||||
img.getPixelWriter().setColor(x, y, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private WritableImage getTestConversion(ImageDitherEngine dither, WritableImage source) {
|
||||
dither.setSourceImage(source);
|
||||
configureAtkinsonDither(dither);
|
||||
dither.dither(true);
|
||||
return dither.getPreviewImage();
|
||||
}
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
package org.badvision.outlaweditor.test;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
import javax.swing.SwingUtilities;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.embed.swing.JFXPanel;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.rules.TestRule;
|
||||
import org.junit.runner.Description;
|
||||
import org.junit.runners.model.Statement;
|
||||
|
||||
/**
|
||||
* A JUnit {@link Rule} for running tests on the JavaFX thread and performing
|
||||
* JavaFX initialization. To include in your test case, add the following code:
|
||||
*
|
||||
* <pre>
|
||||
* {@literal @}Rule
|
||||
* public JavaFXThreadingRule jfxRule = new JavaFXThreadingRule();
|
||||
* </pre>
|
||||
*
|
||||
* @author Andy Till
|
||||
*
|
||||
*/
|
||||
public class JavaFXThreadingRule implements TestRule {
|
||||
|
||||
/**
|
||||
* Flag for setting up the JavaFX, we only need to do this once for all tests.
|
||||
*/
|
||||
private static boolean jfxIsSetup;
|
||||
|
||||
@Override
|
||||
public Statement apply(Statement statement, Description description) {
|
||||
|
||||
return new OnJFXThreadStatement(statement);
|
||||
}
|
||||
|
||||
private static class OnJFXThreadStatement extends Statement {
|
||||
|
||||
private final Statement statement;
|
||||
|
||||
public OnJFXThreadStatement(Statement aStatement) {
|
||||
statement = aStatement;
|
||||
}
|
||||
|
||||
private Throwable rethrownException = null;
|
||||
|
||||
@Override
|
||||
public void evaluate() throws Throwable {
|
||||
|
||||
if(!jfxIsSetup) {
|
||||
setupJavaFX();
|
||||
|
||||
jfxIsSetup = true;
|
||||
}
|
||||
|
||||
final CountDownLatch countDownLatch = new CountDownLatch(1);
|
||||
|
||||
Platform.runLater(() -> {
|
||||
try {
|
||||
statement.evaluate();
|
||||
} catch (Throwable e) {
|
||||
rethrownException = e;
|
||||
}
|
||||
countDownLatch.countDown();
|
||||
});
|
||||
|
||||
countDownLatch.await();
|
||||
|
||||
// if an exception was thrown by the statement during evaluation,
|
||||
// then re-throw it to fail the test
|
||||
if(rethrownException != null) {
|
||||
throw rethrownException;
|
||||
}
|
||||
}
|
||||
|
||||
protected void setupJavaFX() throws InterruptedException {
|
||||
|
||||
long timeMillis = System.currentTimeMillis();
|
||||
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
// initializes JavaFX environment
|
||||
new JFXPanel();
|
||||
|
||||
latch.countDown();
|
||||
});
|
||||
|
||||
System.out.println("javafx initialising...");
|
||||
latch.await();
|
||||
System.out.println("javafx is initialised in " + (System.currentTimeMillis() - timeMillis) + "ms");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user