From b11da29c560c745bf1e59d7a61a02f16177a2d3a Mon Sep 17 00:00:00 2001 From: Brendan Robert Date: Thu, 2 Oct 2014 01:25:54 -0500 Subject: [PATCH] Added image adjustments and tweaked HGR conversion some more (and fixed another bug...) --- .../outlaweditor/apple/ImageDitherEngine.java | 50 +++----- .../impl/ImageConversionWizardController.java | 110 +++++++++++------- .../ui/impl/ImageEditorTabControllerImpl.java | 24 ++-- .../resources/fxml/imageConversionWizard.fxml | 8 +- 4 files changed, 97 insertions(+), 95 deletions(-) diff --git a/OutlawEditor/src/main/java/org/badvision/outlaweditor/apple/ImageDitherEngine.java b/OutlawEditor/src/main/java/org/badvision/outlaweditor/apple/ImageDitherEngine.java index 085f216a..b738192b 100644 --- a/OutlawEditor/src/main/java/org/badvision/outlaweditor/apple/ImageDitherEngine.java +++ b/OutlawEditor/src/main/java/org/badvision/outlaweditor/apple/ImageDitherEngine.java @@ -47,8 +47,8 @@ import static org.badvision.outlaweditor.apple.AppleNTSCGraphics.hgrToDhgr; public class ImageDitherEngine { int byteRenderWidth; - final int errorWindow = 6; - final int overlap = 2; + int errorWindow = 7; + int overlap = 3; WritableImage source; byte[] screen; Platform platform; @@ -56,7 +56,6 @@ public class ImageDitherEngine { int height; int divisor; int[][] coefficients; - boolean resetOutput = true; public ImageDitherEngine(Platform platform) { this.platform = platform; @@ -65,7 +64,6 @@ public class ImageDitherEngine { public void setSourceImage(Image img) { source = getScaledImage(img, bufferWidth * byteRenderWidth, height); - resetOutput = true; } private static WritableImage getScaledImage(Image img, int width, int height) { @@ -85,7 +83,6 @@ public class ImageDitherEngine { this.bufferWidth = width; this.height = height; screen = platform.imageRenderer.createImageBuffer(width, height); - resetOutput = true; } public void setDivisor(int divisor) { @@ -109,31 +106,20 @@ public class ImageDitherEngine { WritableImage tmpScaled2; int[] scanline; List pixels; - - public byte[] restartDither(int value) { - keepScaled = new WritableImage(source.getPixelReader(), 560, 192); - tmpScaled1 = new WritableImage(source.getPixelReader(), 560, 192); - tmpScaled2 = new WritableImage(source.getPixelReader(), 560, 192); - for (int i = 0; i < screen.length; i++) { - screen[i] = (byte) (value >= 0 ? value : (int) Math.floor(Math.random() * 256.0)); - } - scanline = new int[3]; - pixels = new ArrayList<>(); - return screen; - } public Image getScratchBuffer() { return keepScaled; } public byte[] dither(boolean propagateError) { - if (resetOutput) { - restartDither(0); - resetOutput = false; + keepScaled = new WritableImage(source.getPixelReader(), 560, 192); + tmpScaled1 = new WritableImage(source.getPixelReader(), 560, 192); + tmpScaled2 = new WritableImage(source.getPixelReader(), 560, 192); + for (int i = 0; i < screen.length; i++) { + screen[i] = (byte) 0; } - keepScaled.getPixelWriter().setPixels(0, 0, 560, 192, source.getPixelReader(), 0, 0); - tmpScaled1.getPixelWriter().setPixels(0, 0, 560, 192, source.getPixelReader(), 0, 0); - tmpScaled2.getPixelWriter().setPixels(0, 0, 560, 192, source.getPixelReader(), 0, 0); + scanline = new int[3]; + pixels = new ArrayList<>(); for (int y = 0; y < height; y++) { for (int x = 0; x < bufferWidth; x += 2) { switch (platform) { @@ -161,11 +147,11 @@ public class ImageDitherEngine { next = screen[(y + startY) * bufferWidth + startX + x + 2] & 255; } // First byte, compared with a sliding window encompassing the previous byte, if any. - int leastError = Integer.MAX_VALUE; + 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); int b1 = (hi << 7); - int totalError = 0; + long totalError = 0; for (int c = 0; c < 7; c++) { // for (int c = 6; c >= 0; c--) { int on = b1 | (1 << c); @@ -176,7 +162,7 @@ public class ImageDitherEngine { i = hgrToDhgr[(i & 0x010000000) >> 20 | off][bb2]; scanline[1] = i; // scanline[2] = hgrToDhgr[(i & 0x10000000) != 0 ? next | 0x0100 : next][0] & 0x0fffffff; - int errorOff = getError(x * 14 - overlap + c * 2, y, 28 + c * 2 - overlap, errorWindow, pixels, tmpScaled2.getPixelReader(), scanline); + 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); // get values for "on" @@ -185,7 +171,7 @@ public class ImageDitherEngine { i = hgrToDhgr[(i & 0x010000000) >> 20 | on][bb2]; scanline[1] = i; // scanline[2] = hgrToDhgr[(i & 0x10000000) != 0 ? next | 0x0100 : next][0] & 0x0fffffff; - int errorOn = getError(x * 14 - overlap + c * 2, y, 28 + c * 2 - overlap, errorWindow, pixels, tmpScaled2.getPixelReader(), scanline); + 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); int[] col1; @@ -203,7 +189,7 @@ public class ImageDitherEngine { } if (propagateError) { propagateError(x * 14 + c * 2, y, tmpScaled2, col1); - propagateError(x * 14 + c * 2, y, tmpScaled2, col2); + propagateError(x * 14 + c * 2 + 1, y, tmpScaled2, col2); } } if (totalError < leastError) { @@ -214,11 +200,11 @@ public class ImageDitherEngine { } keepScaled.getPixelWriter().setPixels(0, y, 560, (y < 190) ? 3 : (y < 191) ? 2 : 1, tmpScaled1.getPixelReader(), 0, y); // Second byte, compared with a sliding window encompassing the next byte, if any. - leastError = Integer.MAX_VALUE; + 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); int b2 = (hi << 7); - int totalError = 0; + long totalError = 0; for (int c = 0; c < 7; c++) { // for (int c = 6; c >= 0; c--) { int on = b2 | (1 << c); @@ -228,14 +214,14 @@ public class ImageDitherEngine { 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); - int errorOff = getError(x * 14 + 14 - overlap + c * 2, y, 14 + 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); // get values for "on" i = hgrToDhgr[bb1][on]; scanline[0] = i; scanline[1] = hgrToDhgr[(i & 0x010000000) >> 20 | next][0]; - int errorOn = getError(x * 14 + 14 - overlap + c * 2, y, 14 + c * 2 - overlap, errorWindow, pixels, tmpScaled2.getPixelReader(), scanline); + 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); int[] col1; diff --git a/OutlawEditor/src/main/java/org/badvision/outlaweditor/ui/impl/ImageConversionWizardController.java b/OutlawEditor/src/main/java/org/badvision/outlaweditor/ui/impl/ImageConversionWizardController.java index 6e7845e0..c84e1e2b 100644 --- a/OutlawEditor/src/main/java/org/badvision/outlaweditor/ui/impl/ImageConversionWizardController.java +++ b/OutlawEditor/src/main/java/org/badvision/outlaweditor/ui/impl/ImageConversionWizardController.java @@ -5,6 +5,9 @@ import java.text.NumberFormat; import java.util.HashMap; import java.util.Map; import java.util.ResourceBundle; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.event.ActionEvent; @@ -12,8 +15,10 @@ import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.control.Slider; import javafx.scene.control.TextField; +import javafx.scene.effect.ColorAdjust; import javafx.scene.image.Image; import javafx.scene.image.ImageView; +import javafx.scene.image.PixelReader; import javafx.scene.image.WritableImage; import javafx.stage.Stage; import org.badvision.outlaweditor.Application; @@ -26,6 +31,7 @@ import org.badvision.outlaweditor.ui.ImageConversionPostAction; * @author blurry */ public class ImageConversionWizardController implements Initializable { + @FXML private TextField brightnessValue; @FXML @@ -85,8 +91,9 @@ public class ImageConversionWizardController implements Initializable { private ImageView sourceImageView; @FXML private ImageView convertedImageView; - @FXML - private TextField fillValue; + + private ColorAdjust imageAdjustments = new ColorAdjust(); + /** * Initializes the controller class. */ @@ -94,19 +101,29 @@ public class ImageConversionWizardController implements Initializable { public void initialize(URL url, ResourceBundle rb) { for (TextField field : new TextField[]{ brightnessValue, contrastValue, hueValue, saturationValue, - cropBottomValue, cropLeftValue, cropRightValue, cropTopValue, - coefficientValue01, coefficientValue02, coefficientValue11, coefficientValue12, - coefficientValue21, coefficientValue22, coefficientValue30, coefficientValue31, - coefficientValue32, coefficientValue40, coefficientValue41, coefficientValue41, - coefficientValue42, divisorValue, outputHeightValue, outputWidthValue, fillValue + cropBottomValue, cropLeftValue, cropRightValue, cropTopValue, + coefficientValue01, coefficientValue02, coefficientValue11, coefficientValue12, + coefficientValue21, coefficientValue22, coefficientValue30, coefficientValue31, + coefficientValue32, coefficientValue40, coefficientValue41, coefficientValue41, + coefficientValue42, divisorValue, outputHeightValue, outputWidthValue }) { configureNumberValidation(field, "0"); } - + brightnessValue.textProperty().bindBidirectional(brightnessSlider.valueProperty(), NumberFormat.getNumberInstance()); contrastValue.textProperty().bindBidirectional(contrastSlider.valueProperty(), NumberFormat.getNumberInstance()); hueValue.textProperty().bindBidirectional(hueSlider.valueProperty(), NumberFormat.getNumberInstance()); saturationValue.textProperty().bindBidirectional(saturationSlider.valueProperty(), NumberFormat.getNumberInstance()); + + brightnessValue.textProperty().addListener((ObservableValue observable, String oldValue, String newValue) + -> javafx.application.Platform.runLater(this::updateImageAdjustments)); + contrastValue.textProperty().addListener((ObservableValue observable, String oldValue, String newValue) + -> javafx.application.Platform.runLater(this::updateImageAdjustments)); + hueValue.textProperty().addListener((ObservableValue observable, String oldValue, String newValue) + -> javafx.application.Platform.runLater(this::updateImageAdjustments)); + saturationValue.textProperty().addListener((ObservableValue observable, String oldValue, String newValue) + -> javafx.application.Platform.runLater(this::updateImageAdjustments)); + configureFastFloydSteinbergPreset(null); } @@ -128,34 +145,48 @@ public class ImageConversionWizardController implements Initializable { public void setDitherEngine(ImageDitherEngine engine) { this.ditherEngine = engine; } - + public void setSourceImage(Image image) { sourceImage = image; preprocessImage(); } + private void updateImageAdjustments() { + double hue = Double.parseDouble(hueValue.getText()); + double saturation = Double.parseDouble(saturationValue.getText()); + double brightness = Double.parseDouble(brightnessValue.getText()); + double contrast = Double.parseDouble(contrastValue.getText()); + + imageAdjustments = new ColorAdjust(); + imageAdjustments.setContrast(contrast); + imageAdjustments.setBrightness(brightness); + imageAdjustments.setHue(hue); + imageAdjustments.setSaturation(saturation); + sourceImageView.setEffect(imageAdjustments); + } + private void preprocessImage() { - preprocessedImage = new WritableImage(sourceImage.getPixelReader(), (int) sourceImage.getWidth(), (int) sourceImage.getHeight()); - ditherEngine.setSourceImage(preprocessedImage); + PixelReader pixelReader = sourceImage.getPixelReader(); + preprocessedImage = new WritableImage(pixelReader, (int) sourceImage.getWidth(), (int) sourceImage.getHeight()); updateSourceView(preprocessedImage); } - + public void setOutputDimensions(int targetWidth, int targetHeight) { ditherEngine.setOutputDimensions(targetWidth, targetHeight); outputWidthValue.setText(String.valueOf(targetWidth)); outputHeightValue.setText(String.valueOf(targetHeight)); outputPreviewImage = ditherEngine.getPreviewImage(); } - + public int getOutputWidth() { return Integer.parseInt(outputWidthValue.getText()); } - + public int getOutputHeight() { - return Integer.parseInt(outputHeightValue.getText()); + return Integer.parseInt(outputHeightValue.getText()); } - private void updateSourceView(WritableImage image) { + private void updateSourceView(Image image) { sourceImageView.setImage(image); sourceImageView.setFitWidth(0); sourceImageView.setFitHeight(0); @@ -165,35 +196,27 @@ public class ImageConversionWizardController implements Initializable { defaultTextFieldValues.put(cropBottomValue, String.valueOf(height)); cropRightValue.setText(String.valueOf(width)); cropBottomValue.setText(String.valueOf(height)); - } - - @FXML - private void fillOutput(ActionEvent event) { - int fill = Integer.parseInt(fillValue.getText()); - updateConvertedImageWithData(ditherEngine.restartDither(fill)); } - @FXML - private void randomizeOutput(ActionEvent event) { - updateConvertedImageWithData(ditherEngine.restartDither(-1)); - } - @FXML private void performQuantizePass(ActionEvent event) { - ditherEngine.setCoefficients(getCoefficients()); - ditherEngine.setDivisor(getDivisor()); + prepareForConversion(); byte[] out = ditherEngine.dither(false); updateConvertedImageWithData(out); } - + @FXML private void performDiffusionPass(ActionEvent event) { - ditherEngine.setCoefficients(getCoefficients()); - ditherEngine.setDivisor(getDivisor()); + prepareForConversion(); byte[] out = ditherEngine.dither(true); - sourceImageView.setImage(ditherEngine.getScratchBuffer()); updateConvertedImageWithData(out); } + + private void prepareForConversion() { + ditherEngine.setCoefficients(getCoefficients()); + ditherEngine.setDivisor(getDivisor()); + ditherEngine.setSourceImage(sourceImageView.snapshot(null, null)); + } byte[] lastOutput; private void updateConvertedImageWithData(byte[] data) { @@ -211,13 +234,18 @@ public class ImageConversionWizardController implements Initializable { private void performCancel(ActionEvent event) { stage.close(); } - + private final Map defaultTextFieldValues = new HashMap<>(); + private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(3); private void configureNumberValidation(TextField field, String defaultValue) { defaultTextFieldValues.put(field, defaultValue); field.textProperty().addListener((ChangeListener) (ObservableValue observable, Object oldValue, Object newValue) -> { if (newValue == null || "".equals(newValue)) { - field.textProperty().setValue(defaultTextFieldValues.get(field)); + scheduler.schedule(() -> { + if (null == field.textProperty().getValue() || field.textProperty().getValue().isEmpty()) { + field.textProperty().setValue(defaultTextFieldValues.get(field)); + } + }, 250, TimeUnit.MILLISECONDS); } else { try { Double.parseDouble(newValue.toString()); @@ -227,7 +255,7 @@ public class ImageConversionWizardController implements Initializable { } }); } - + private void setCoefficients(int... coeff) { coefficientValue30.setText(String.valueOf(coeff[3])); coefficientValue40.setText(String.valueOf(coeff[4])); @@ -242,7 +270,7 @@ public class ImageConversionWizardController implements Initializable { coefficientValue32.setText(String.valueOf(coeff[13])); coefficientValue42.setText(String.valueOf(coeff[14])); } - + private int[][] getCoefficients() { diffusionCoeffficients[0][0] = 0; diffusionCoeffficients[1][0] = 0; @@ -261,11 +289,11 @@ public class ImageConversionWizardController implements Initializable { diffusionCoeffficients[4][2] = Integer.parseInt(coefficientValue42.getText()); return diffusionCoeffficients; } - + private void setDivisor(int div) { divisorValue.setText(String.valueOf(div)); } - + private int getDivisor() { return Integer.valueOf(divisorValue.getText()); } @@ -298,8 +326,9 @@ public class ImageConversionWizardController implements Initializable { 3, 5, 7, 5, 3, 1, 3, 5, 3, 1 ); - setDivisor(48); + setDivisor(48); } + @FXML private void configureStuckiPreset(ActionEvent event) { setCoefficients( @@ -359,4 +388,5 @@ public class ImageConversionWizardController implements Initializable { ); setDivisor(4); } + } diff --git a/OutlawEditor/src/main/java/org/badvision/outlaweditor/ui/impl/ImageEditorTabControllerImpl.java b/OutlawEditor/src/main/java/org/badvision/outlaweditor/ui/impl/ImageEditorTabControllerImpl.java index abd8a8a2..9b84fcbb 100644 --- a/OutlawEditor/src/main/java/org/badvision/outlaweditor/ui/impl/ImageEditorTabControllerImpl.java +++ b/OutlawEditor/src/main/java/org/badvision/outlaweditor/ui/impl/ImageEditorTabControllerImpl.java @@ -4,10 +4,8 @@ import org.badvision.outlaweditor.ui.EntitySelectorCell; import java.util.logging.Level; import java.util.logging.Logger; import javafx.event.ActionEvent; -import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.scene.control.cell.ComboBoxListCell; -import javafx.util.Callback; import org.badvision.outlaweditor.Application; import org.badvision.outlaweditor.Editor; import org.badvision.outlaweditor.ImageEditor; @@ -50,14 +48,9 @@ public class ImageEditorTabControllerImpl extends ImageEditorTabController { } } }); - imageSelector.setCellFactory(new Callback, ListCell>() { + imageSelector.setCellFactory((ListView param) -> new EntitySelectorCell(imageNameField) { @Override - public ListCell call(ListView param) { - return new EntitySelectorCell(imageNameField) { - @Override - public void finishUpdate(Image item) { - } - }; + public void finishUpdate(Image item) { } }); } @@ -149,14 +142,11 @@ public class ImageEditorTabControllerImpl extends ImageEditorTabController { if (currentImage == null) { return; } - confirm("Delete image '" + currentImage.getName() + "'. Are you sure?", new Runnable() { - @Override - public void run() { - Image del = currentImage; - setCurrentImage(null); - Application.gameData.getImage().remove(del); - rebuildImageSelector(); - } + confirm("Delete image '" + currentImage.getName() + "'. Are you sure?", () -> { + Image del = currentImage; + setCurrentImage(null); + Application.gameData.getImage().remove(del); + rebuildImageSelector(); }, null); } diff --git a/OutlawEditor/src/main/resources/fxml/imageConversionWizard.fxml b/OutlawEditor/src/main/resources/fxml/imageConversionWizard.fxml index 33a5a9e1..4379767d 100644 --- a/OutlawEditor/src/main/resources/fxml/imageConversionWizard.fxml +++ b/OutlawEditor/src/main/resources/fxml/imageConversionWizard.fxml @@ -205,18 +205,14 @@