JavaFX ImageView without any smoothing
Asked Answered
A

4

23

Is it possible to render a scaled image in an ImageView in JavaFX 2.2 without any smoothing applied? I'm rendering a 50x50 image into a 200x200 ImageView, with setSmooth(false), so each pixel in the source image should map to a 4x4 square on the screen.

However, the resulting render still smooths the source pixel across all 16 destination pixels. Does anyone know of a way to do this without manually copying over each pixel into a new image?

Alesandrini answered 18/4, 2013 at 17:17 Comment(3)
You could try Canvas to get maximum control.Armure
Unfortunately I don't think there's a way to do it in a canvas, either. What they really need to do is allow us to make our own 2D filters (I guess they're called "Effects" in JavaFX).Alesandrini
Updated my answer with ideas on implementing your own image filter.Doggone
D
38

In JavaFX versions up to at least 21, ImageView will always do some smoothing regardless of the smooth hint you provide to the ImageView (I don't know why the implementation works this way).

Tested on JavaFX 21 on OS X 14, but reports show that the functionality works similarly for some other platforms, such as Windows.

Perhaps it is a bug that ImageView will always smooth the Image, but the documentation doesn't specify exactly what smoothing does or doesn't do, so it's hard to say what its real intent is. You may want to post a reference to this question to the openjfx-dev mailing list or log an issue in the JavaFX issue tracker to get a more expert opinion from a developer.


I tried a few different methods for scaling the Image:

  1. Scale in the Image constructor.
  2. Scale in ImageView with fitWidth/fitHeight.
  3. Scale by using the scaleX/scaleY properties on an ImageView.
  4. Scale by sampling the Image with a PixelReader and creating a new Image with a PixelWriter.

I found that methods 1 & 4 resulted in a sharp pixelated image as you wish and 2 & 3 resulted in a blurry aliased image.

robot-sampling

Sample code to generate the above output.

import javafx.application.Application;
import javafx.geometry.HPos;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.image.*;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class ImageScaler extends Application {
    private static final String IMAGE_LOC =
            "http://icons.iconarchive.com/icons/martin-berube/character/32/Robot-icon.png";

    private static final int SCALE_FACTOR = 6;

    private Image image;
    private int scaledImageSize;

    @Override
    public void init() {
        image = new Image(
                IMAGE_LOC
        );

        scaledImageSize = (int) image.getWidth() * SCALE_FACTOR;
    }

    @Override
    public void start(Stage stage) {
        GridPane layout = new GridPane();
        layout.setHgap(10);
        layout.setVgap(10);

        ImageView originalImageView = new ImageView(image);
        StackPane originalImageViewStack = new StackPane();
        originalImageViewStack.getChildren().add(originalImageView);
        originalImageViewStack.setMinWidth(scaledImageSize);

        ImageView sizedImageInView = new ImageView(
                new Image(
                        IMAGE_LOC,
                        scaledImageSize,
                        scaledImageSize,
                        false,
                        false
                )
        );

        ImageView fittedImageView = new ImageView(image);
        fittedImageView.setSmooth(false);
        fittedImageView.setFitWidth(scaledImageSize);
        fittedImageView.setFitHeight(scaledImageSize);

        ImageView scaledImageView = new ImageView(image);
        scaledImageView.setSmooth(false);
        scaledImageView.setScaleX(SCALE_FACTOR);
        scaledImageView.setScaleY(SCALE_FACTOR);
        Group scaledImageViewGroup = new Group(scaledImageView);

        ImageView resampledImageView = new ImageView(
                resample(
                        image,
                        SCALE_FACTOR
                )
        );

        layout.addRow(
                0,
                withTooltip(
                        originalImageViewStack,
                        "Unmodified image"
                ),
                withTooltip(
                        sizedImageInView,
                        "Image sized in Image constructor - Image smoothing false"
                ),
                withTooltip(
                        fittedImageView,
                        "Image fitted using ImageView fitWidth/fitHeight - ImageView smoothing false"
                ),
                withTooltip(
                        scaledImageViewGroup,
                        "ImageView scaled with Node scaleX/scaleY - ImageView smoothing false"
                ),
                withTooltip(
                        resampledImageView,
                        "Image manually recreated as a new WritableImage using a PixelWriter"
                )
        );

        layout.addRow(
                1,
                centeredLabel("Original"),
                centeredLabel("Sized"),
                centeredLabel("Fitted"),
                centeredLabel("Scaled"),
                centeredLabel("Resampled")
        );
        layout.setAlignment(Pos.CENTER);

        layout.setStyle("-fx-background-color: cornsilk; -fx-padding: 10;");
        stage.setScene(
                new Scene(layout)
        );
        stage.show();
    }

    private Node withTooltip(Node node, String text) {
        Tooltip.install(node, new Tooltip(text));
        return node;
    }

    private Label centeredLabel(String text) {
        Label label = new Label(text);
        GridPane.setHalignment(label, HPos.CENTER);

        return label;
    }

    private Image resample(Image input, int scaleFactor) {
        final int W = (int) input.getWidth();
        final int H = (int) input.getHeight();
        final int S = scaleFactor;

        WritableImage output = new WritableImage(
                W * S,
                H * S
        );

        PixelReader reader = input.getPixelReader();
        PixelWriter writer = output.getPixelWriter();

        for (int y = 0; y < H; y++) {
            for (int x = 0; x < W; x++) {
                final int argb = reader.getArgb(x, y);
                for (int dy = 0; dy < S; dy++) {
                    for (int dx = 0; dx < S; dx++) {
                        writer.setArgb(x * S + dx, y * S + dy, argb);
                    }
                }
            }
        }

        return output;
    }

    public static void main(String[] args) {
        Application.launch(args);
    }
}

Update with ideas on implementing your own image filter

A JavaFX Effect is not the same as the Filter used for the Image loading routines, though an Effect to filter an image could be created. In JavaFX versions up to at least 21, there is no publicly documented API to support the creation of custom effect or image filter, so creating a custom effect or image filter may prove difficult.

The native code for image support is open source as part of the openjfx project, so you could look at that to see how the filtering is currently implemented.

You may also want to file a feature request against the JavaFX runtime project to "allow us to make our own 2D filters".

Doggone answered 18/4, 2013 at 20:38 Comment(10)
You always give such impressive answers jewelsea :DArmure
Yes, thanks for the detailed answer. I've come to the same conclusion, and ended up implementing #4. The implementation itself is trivial, but I fear that it's much slower than #2 or #3 (although I haven't verified this yet).Alesandrini
It's been two years since this answer. Does anyone know if this has been fixed in JavaFX?Mauri
I'm not sure what you mean by fixed Edu. My guess is that the system is behaving as the developers intend. If you wish more information on filtering and smoothing images in JavaFX, you can post to the open-jfx developers mailing list as indicated in the answer.Doggone
It is strange, the N° 1. solution just doesn't work on my BMP file, it does not load the image, it works well for the 3 other solutions, I will use the N° 4. solution, thanks.Overstay
I need sharp pixels in a canvas node (after scaleXY). How can I do that?Petrochemistry
Fabricio, please ask your canvas question as a new question. You can link back to this one to aid potential answerers.Doggone
A shame this will probably never get fixed. It's a major stain in an otherwise great SDK and makes certain requirements require absolutely ridiculous workarounds.Deprived
@Deprived there have been reasonably recent discussion on similar topics on the openjfx-dev mailing lists with proposals to add nearest neighbor sampling rather than linear sampling to some portions of the rendering pipeline. I think those discussions were focused on the 3D mesh rendering pipeline rather than the 2D image rendering pipeline, but perhaps there is some commonality there.Doggone
I don't think that's a directly related problem, but it's good to know this stuff hasn't been entirely forgotten. I wish that stuff was more accessible, I might be willing to help contribute to improvements if it were a more modern communications medium like a web forum where graphics can be attached and so on. I can't stand mailing lists.Deprived
C
6

I know this is a bit older, but I recently had a need for such ImageView, and the following little hack does exactly what I want on my (Windows) machine. No guarantees that it works everywhere.

import com.sun.javafx.sg.prism.NGImageView;
import com.sun.javafx.sg.prism.NGNode;
import com.sun.prism.Graphics;
import com.sun.prism.Texture;
import com.sun.prism.impl.BaseResourceFactory;

import com.sun.prism.Image;
import javafx.scene.image.ImageView;

@SuppressWarnings("restriction")
public class PixelatedImageView extends ImageView {
    @Override protected NGNode impl_createPeer() {
        return new NGImageView() {
            private Image image;

            @Override public void setImage(Object img) {
                super.setImage(img);
                image = (Image) img;
            }

            @Override protected void renderContent(Graphics g) {
                BaseResourceFactory factory = (BaseResourceFactory) g.getResourceFactory();
                Texture tex = factory.getCachedTexture(image, Texture.WrapMode.CLAMP_TO_EDGE);
                tex.setLinearFiltering(false);
                tex.unlock();
                super.renderContent(g);
            }
        };
    }
}

The trick here is that the texture gets re-used, so the linear filtering setting remains "sticky". Why NGImageView couldn't simply pass the "smooth" flag to the texture's linear filtering setting is beyond me however.

Casern answered 2/9, 2016 at 14:13 Comment(3)
Looks like they don't want you to do this.Adornment
This worked for Java 8, but looks like the functions mentioned here have been renamed and set to private functions in Java 9. Doesn't look like it can be overridden in the same way without significant changes to ImageView and ImageViewHelper. Anyone figured out how to work around the new restrictions in Java 9?Venality
This answer saved me hours of work ... Why is there an option for smooth=false if JavaFX simply does not give a damn about it.Gonfanon
P
2

No fix for ImageView, but it helped me a lot. After searching for ages, I stumbled upon this post: How can I disable antialiasing on a JavaFX Canvas?

For drawing the image on a canvas, the smoothing can be disabled since JavaFX 12

canvas.getGraphicsContext2D().setImageSmoothing(false);
Parlin answered 8/4, 2021 at 18:23 Comment(2)
There is still some level of smoothing sadly, but it is MUCH less aggressive than regular smoothing. Its relatively easy to make your own "ImageCanvas' class which wraps this and offers the same methods of 'ImageView".Luna
Hmm, I don't see any amount of smoothing with this turned on (Java 17). The infuriating thing is that we have to switch to Canvas when ImageViews would be better.Deprived
B
0

When you add the following constructor to Martin Sojka's answer you can simply pass the javafx Image to the constructor. Also despite the warnings about deprecated functions his answer still works fine (on JDK 1.8_121).

public PixelatedImageView (javafx.scene.image.Image image) {
    super(image);
}
Backcourt answered 3/4, 2017 at 18:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.