JavaFX - 如何在对Canvas进行缩放时防止模糊,以实现像素化缩放?

5
有没有办法在缩放Canvas时停止模糊?我认为这与GPU插值有关。但我需要的是像素完美的“像素化”缩放。只使用最近的“真实”相邻像素的颜色。
我已经看到了这里的解决方案,但两个建议中工作的(#1 /#4),#4明显是CPU缩放,我猜想#1也是。
这种缩放需要快速操作 - 我希望能够支持多达20-25个图层(可能是StackPane中的Canvases,但只要它们不会损坏CPU,我对其他想法持开放态度)。我怀疑没有GPU支持不可能完成此操作,虽然JFX提供了这种支持,但也许API不够灵活。像链接答案中依赖于CPU重新缩放的#4之类的策略可能行不通。
如果您使用此代码缩放到最高缩放级别,则可以明显看到模糊。
我们需要更新JFX API以支持此功能吗?这应该是可以做到的。
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Paint;
import javafx.stage.Stage;

public class ScaleTest extends Application {
    
    private static final int width = 1200;
    private static final int height = 800;
    private static final int topMargin = 32;
    private StackPane stackPane;
    private IntegerProperty zoomLevel = new SimpleIntegerProperty(100);
    
    @Override
    public void start(Stage stage) {
        stage.setWidth(width);
        stage.setMinHeight(height);
        
        stackPane = new StackPane();
        stackPane.setLayoutY(topMargin);
        Canvas canvas = new Canvas(width, height - topMargin);
        
        Label label = new Label();
        label.setLayoutY(2);
        label.setLayoutX(2);
        label.setStyle("-fx-text-fill: #FFFFFF");
        label.textProperty().bind(zoomLevel.asString());
        Button zoomInBtn = new Button("Zoom In");
        zoomInBtn.setLayoutY(2);
        zoomInBtn.setLayoutX(50);
        zoomInBtn.onActionProperty().set((e) -> {
            if (zoomLevel.get() < 3200) {
                zoomLevel.set(zoomLevel.get() * 2);
                stackPane.setScaleX(zoomLevel.get() / 100.0);
                stackPane.setScaleY(zoomLevel.get() / 100.0);
            }
        });
        Button zoomOutBtn = new Button("Zoom Out");
        zoomOutBtn.setLayoutY(2);
        zoomOutBtn.setLayoutX(140);
        zoomOutBtn.onActionProperty().set((e) -> {
            if (zoomLevel.get() > 25) {
                zoomLevel.set(zoomLevel.get() / 2);
                stackPane.setScaleX(zoomLevel.get() / 100.0);
                stackPane.setScaleY(zoomLevel.get() / 100.0);
            }
        });
        
        Pane mainPane = new Pane(stackPane, label, zoomInBtn, zoomOutBtn);
        mainPane.setStyle("-fx-background-color: #000000");
        Scene scene = new Scene(mainPane);
        stage.setScene(scene);
        
        drawGrid(canvas, 0, 0, width, height - topMargin, 16);
        stackPane.getChildren().add(canvas);
        
        stage.show();
    }
    
    private void drawGrid(Canvas canvas, int xPos, int yPos, int width, int height, int gridSize) {
        boolean darkX = true;
        String darkCol = "#111111";
        String lightCol = "#222266";
        
        for (int x = xPos; x < canvas.getWidth(); x += gridSize) {
            boolean dark = darkX;
            darkX = !darkX;
            if (x > width) {
                break;
            }
            
            for (int y = yPos; y < canvas.getHeight(); y += gridSize) {
                if (y > height) {
                    break;
                }
                
                dark = !dark;
                String color;
                if (dark) {
                    color = darkCol;
                } else {
                    color = lightCol;
                }
                canvas.getGraphicsContext2D().setFill(Paint.valueOf(color));
                canvas.getGraphicsContext2D().fillRect(x, y, gridSize, gridSize);
            }
        }
    }
}

blurry image

2个回答

3
你的示例调整节点的比例属性来重采样一个固定大小的图像,这不可避免地会导致这样的伪影。只有矢量表示可以以任意精度缩放。您可能需要决定您的应用程序如何支持矢量和/或位图。例如,如果您的应用程序真正关注于缩放矩形,则应使用缩放后的坐标调用fillRect(),而不是缩小较小矩形的图片。
你引用了一个很好的调整大小的摘要,所以我将专注于矢量机会:
  • Shape的具体子类实际上是几何元素的矢量表示,可以在任何比例尺下呈现;调整此处此处所示的示例大小以查看效果;滚动缩放并单击此处拖动圆形,注意其边框在所有比例尺下保持清晰。

  • SVGPath是可以按比例缩放的Shape,如此处所示。

  • 在内部,Font存储单个字形的矢量表示。当实例化时,字形会以某个点大小光栅化

  • 如果已知合适的矢量表示,则可以将绘图绑定到Canvas的大小,如此处所示。


我猜举例使用网格可能会有点误导。通过CPU缩放图像没有问题,但对于大型图像或特别是分层图像,这显然无法“扩展”。我也会做一些与矢量形状相关的工作,但正如你所暗示的那样,那更容易。我需要的基本上就是像“绘画程序”一样精确到像素级别的放大。为了使这更容易,我决定将缩放级别限制为2的幂,就像我发布的演示一样-因此,每个像素只需进行清晰的调整大小即可。这种方法对于光栅图形不好用吗? - Manius
抱歉,我不太清楚您的意图,许多编辑器支持矢量和位图;我添加了一个示例,提供了有关矢量缩放的不同视角。 - trashgod
没问题,我不想在问题中加入太多细节,因为人们可能会认为它太具体而关闭它 - 我并不期望别人能够心灵感应。:) 我相信一旦我进入后期阶段,这些链接中的一些将会很有帮助,谢谢。 - Manius

1
这似乎可以解决问题:

package com.analogideas.scratch;
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Paint;
import javafx.stage.Stage;


public class ImagePixelator extends Application {

    private static final int width = 1200;
    private static final int height = 800;
    private static final int topMargin = 32;
    private IntegerProperty zoomLevel = new SimpleIntegerProperty(1);

    public static void main(String[] args) {
        launch(args);
    }
    
    @Override
    public void start(Stage stage) {
        stage.setWidth(width);
        stage.setMinHeight(height);

        Canvas canvasOut = new Canvas(width, height - topMargin);
        Canvas canvas = new Canvas(width, height - topMargin);
        drawGrid(canvas, 0, 0, width, height - topMargin, 16);
        WritableImage img = canvas.snapshot(null, null);
        drawImage(canvasOut, img, 0, 0, 1);
        StackPane pane = new StackPane(canvasOut);

        Label label = new Label();
        label.setLayoutY(2);
        label.setLayoutX(2);
        label.setStyle("-fx-text-fill: #FFFFFF");
        label.textProperty().bind(zoomLevel.asString());
        Button zoomInBtn = new Button("Zoom In");
        zoomInBtn.setLayoutY(2);
        zoomInBtn.setLayoutX(50);
        zoomInBtn.onActionProperty().set((e) -> {
            if (zoomLevel.get() < 3200) {
                zoomLevel.set(zoomLevel.get() * 2);
                int z = zoomLevel.get();
                drawImage(canvasOut, img, 0, 0, z);
            }
        });
        Button zoomOutBtn = new Button("Zoom Out");
        zoomOutBtn.setLayoutY(2);
        zoomOutBtn.setLayoutX(140);
        zoomOutBtn.onActionProperty().set((e) -> {
            if (zoomLevel.get() > 1) {
                zoomLevel.set(zoomLevel.get() /2);
                int z = zoomLevel.get();
                drawImage(canvasOut, img, 0, 0, z);
            }
        });

        Pane mainPane = new Pane(pane, label, zoomInBtn, zoomOutBtn);
        mainPane.setStyle("-fx-background-color: #000000");
        Scene scene = new Scene(mainPane);
        stage.setScene(scene);
        stage.show();
    }

    private void drawImage(Canvas canvas, Image image, int x, int y, float zoom) {
        GraphicsContext g = canvas.getGraphicsContext2D();
        g.setImageSmoothing(false);
        int w = (int) (image.getWidth() * zoom);
        int h = (int) (image.getHeight() * zoom);
        g.drawImage(image, x, y, w, h);

    }

    private void drawGrid(Canvas canvas, int xPos, int yPos, int width, int height, int gridSize) {
        Paint darkPaint = Paint.valueOf("#111111");
        Paint lightPaint = Paint.valueOf("#222266");
        GraphicsContext g = canvas.getGraphicsContext2D();
        g.setImageSmoothing(false);
        int xLimit = width / gridSize;
        int yLimit = height / gridSize;
        for (int x = 0; x <= xLimit; x++) {
            for (int y = 0; y <= yLimit; y++) {
                boolean dark = (((x ^ y) & 1) == 0);
                g.setFill(dark ? darkPaint : lightPaint);
                g.fillRect(xPos + x * gridSize, yPos + y * gridSize, gridSize, gridSize);
            }
        }
    }
}


1
@Manius 如果你已经在11上了,那就不用太担心。你已经度过了所有真正困难的变化。我发现11之后的一切都非常顺利。我现在使用的是17,我的大型项目是基于Java 8设计和构建的,在适应Java 11的变化后没有出现任何问题。 - swpalmer
1
可能/希望 - 这就是我现在正在研究的,当然还需要升级Gradle才能到达Java 17...所以,是的,有一些工作要做。 :) - Manius
1
@Manius 我也使用Gradle,如果你对这方面有问题,请告诉我。我已经将我的项目升级到Gradle 7.3.1。 - swpalmer
1
搞定了,谢谢。 :) 另外我使用的sdkman工具告诉我Gradle 7.3.2现在可用!跟不上啊! - Manius
1
@Manius请参考MaterialFX。该文档说明其“目的是为了提供一个JFoenix库的继任者,因为JFoenix有些过时而且有很多问题”。使用MaterialFX比我们“自己重新实现所需部分”更好的选项。 - jewelsea
显示剩余13条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接