如何裁剪和调整JavaFX图像大小?

3
我正在尝试在JavaFX画布上显示非常大的图像。每个图像都有相应的世界文件,我可以使用它来正确定位图像。我的画布大小为800x600。有时我需要将整个图像写入画布,有时只需要其中的一部分。
到目前为止,我已经完成了以下工作:
  • 从文件中加载完整大小的图像到Image对象中。
  • 计算要显示的Image的哪一部分,并计算缩放参数以正确适应800x600画布。
所以基本上我想使用GraphicsContext.drawImage(...) - 将给定图像的当前源矩形绘制到Canvas的给定目标矩形中。 对于这种方法,我正确地计算了所有参数。问题是有时候Image大于2048x2048,由于某种原因JavaFX会尝试直接使用GPU将该图像绘制到画布上。这就是我得到异常的情况:
java.lang.NullPointerException
    at com.sun.prism.sw.SWGraphics.drawTexture(SWGraphics.java:686) at com.sun.prism.sw.SWGraphics.drawTexture(SWGraphics.java:686)
    at com.sun.prism.sw.SWGraphics.drawTexture(SWGraphics.java:665)
    at com.sun.prism.sw.SWGraphics.drawTexture(SWGraphics.java:648)
    at com.sun.javafx.sg.prism.NGCanvas.handleRenderOp(NGCanvas.java:1228)
    at com.sun.javafx.sg.prism.NGCanvas.renderStream(NGCanvas.java:997)
    at com.sun.javafx.sg.prism.NGCanvas.renderContent(NGCanvas.java:578)
    at com.sun.javafx.sg.prism.NGNode.doRender(NGNode.java:2043)
    at com.sun.javafx.sg.prism.NGNode.render(NGNode.java:1951)
    at com.sun.javafx.sg.prism.NGGroup.renderContent(NGGroup.java:225)
    at com.sun.javafx.sg.prism.NGRegion.renderContent(NGRegion.java:575)
    at com.sun.javafx.sg.prism.NGNode.doRender(NGNode.java:2043)
    at com.sun.javafx.sg.prism.NGNode.render(NGNode.java:1951)
    at com.sun.javafx.sg.prism.NGGroup.renderContent(NGGroup.java:225)
    at com.sun.javafx.sg.prism.NGRegion.renderContent(NGRegion.java:575)
    at com.sun.javafx.sg.prism.NGNode.renderForClip(NGNode.java:2282)
    at com.sun.javafx.sg.prism.NGNode.renderRectClip(NGNode.java:2176)
    at com.sun.javafx.sg.prism.NGNode.renderClip(NGNode.java:2202)
    at com.sun.javafx.sg.prism.CacheFilter.impl_renderNodeToCache(CacheFilter.java:655)
    at com.sun.javafx.sg.prism.CacheFilter.render(CacheFilter.java:561)
    at com.sun.javafx.sg.prism.NGNode.renderCached(NGNode.java:2346)
    at com.sun.javafx.sg.prism.NGNode.doRender(NGNode.java:2034)
    at com.sun.javafx.sg.prism.NGNode.render(NGNode.java:1951)
    at com.sun.javafx.sg.prism.NGGroup.renderContent(NGGroup.java:225)
    at com.sun.javafx.sg.prism.NGRegion.renderContent(NGRegion.java:575)
    at com.sun.javafx.sg.prism.NGNode.doRender(NGNode.java:2043)
    at com.sun.javafx.sg.prism.NGNode.render(NGNode.java:1951)
    at com.sun.javafx.sg.prism.NGGroup.renderContent(NGGroup.java:225)
    at com.sun.javafx.sg.prism.NGRegion.renderContent(NGRegion.java:575)
    at com.sun.javafx.sg.prism.NGNode.doRender(NGNode.java:2043)
    at com.sun.javafx.sg.prism.NGNode.render(NGNode.java:1951)
    at com.sun.javafx.sg.prism.NGGroup.renderContent(NGGroup.java:225)
    at com.sun.javafx.sg.prism.NGRegion.renderContent(NGRegion.java:575)
    at com.sun.javafx.sg.prism.NGNode.renderForClip(NGNode.java:2282)
    at com.sun.javafx.sg.prism.NGNode.renderRectClip(NGNode.java:2176)
    at com.sun.javafx.sg.prism.NGNode.renderClip(NGNode.java:2202)
    at com.sun.javafx.sg.prism.NGNode.doRender(NGNode.java:2037)
    at com.sun.javafx.sg.prism.NGNode.render(NGNode.java:1951)
    at com.sun.javafx.sg.prism.NGGroup.renderContent(NGGroup.java:225)
    at com.sun.javafx.sg.prism.NGRegion.renderContent(NGRegion.java:575)
    at com.sun.javafx.sg.prism.NGNode.doRender(NGNode.java:2043)
    at com.sun.javafx.sg.prism.NGNode.render(NGNode.java:1951)
    at com.sun.javafx.sg.prism.NGGroup.renderContent(NGGroup.java:225)
    at com.sun.javafx.sg.prism.NGRegion.renderContent(NGRegion.java:575)
    at com.sun.javafx.sg.prism.NGNode.doRender(NGNode.java:2043)
    at com.sun.javafx.sg.prism.NGNode.render(NGNode.java:1951)
    at com.sun.javafx.sg.prism.NGGroup.renderContent(NGGroup.java:225)
    at com.sun.javafx.sg.prism.NGRegion.renderContent(NGRegion.java:575)
    at com.sun.javafx.sg.prism.NGNode.renderForClip(NGNode.java:2282)
    at com.sun.javafx.sg.prism.NGNode.renderRectClip(NGNode.java:2176)
    at com.sun.javafx.sg.prism.NGNode.renderClip(NGNode.java:2202)
    at com.sun.javafx.sg.prism.NGNode.doRender(NGNode.java:2037)
    at com.sun.javafx.sg.prism.NGNode.render(NGNode.java:1951)
    at com.sun.javafx.sg.prism.NGGroup.renderContent(NGGroup.java:225)
    at com.sun.javafx.sg.prism.NGRegion.renderContent(NGRegion.java:575)
    at com.sun.javafx.sg.prism.NGNode.doRender(NGNode.java:2043)
    at com.sun.javafx.sg.prism.NGNode.render(NGNode.java:1951)
    at com.sun.javafx.sg.prism.NGGroup.renderContent(NGGroup.java:225)
    at com.sun.javafx.sg.prism.NGRegion.renderContent(NGRegion.java:575)
    at com.sun.javafx.sg.prism.NGNode.doRender(NGNode.java:2043)
    at com.sun.javafx.sg.prism.NGNode.render(NGNode.java:1951)
    at com.sun.javafx.tk.quantum.ViewPainter.doPaint(ViewPainter.java:469)
    at com.sun.javafx.tk.quantum.ViewPainter.paintImpl(ViewPainter.java:317)
    at com.sun.javafx.tk.quantum.UploadingPainter.run(UploadingPainter.java:132)
    at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
    at java.util.concurrent.FutureTask.runAndReset(FutureTask.java:308)
    at com.sun.javafx.tk.RenderJob.run(RenderJob.java:58)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at com.sun.javafx.tk.quantum.QuantumRenderer$PipelineRunnable.run(QuantumRenderer.java:129)
    at java.lang.Thread.run(Thread.java:744)

所以接下来我想尝试的是在将图像发送到画布之前,在某个临时对象中裁剪和缩放图像。我找不到任何关于如何做到这一点的示例。我只找到了一个使用WritableImage裁剪图像的示例,但我不知道如何在裁剪后缩放它并将其转换为图像。
1个回答

8

根据您在流程的各个阶段可用的信息,有多种方法可以实现这一点。

如果您在加载前已知文件中图像的大小,并且可以计算出比例因子,则可以在加载时对其进行缩放:

double requiredWidth = ... ;
double requiredHeight = ... ;
String imageURL = ... ;

Image image = new Image(imageURL, requiredWidth, requiredHeight, false, true);

最后两个参数是preserveRatiosmooth。后者会强制使用较慢但更高质量的重新调整大小算法。
现在,您可以像链接的帖子中所示,将其裁剪到新的WritableImage中。
double x = ... ;
double y = ... ;
double width = ...;
double height = ... ;
WritableImage croppedImage = new WritableImage(image.getPixelReader(), x, y, width, height);

其中xywidthheight定义了裁剪区域(以缩放坐标表示)。

然后,您可以将裁剪后的图像直接绘制到画布上:

graphicsContent.drawImage(croppedImage, canvasX, canvasY);

另一种方法是加载整张图片,然后使用 ImageView 创建一个裁剪和缩放的视图:
Image fullImage = new Image(imageURL);

// define crop in image coordinates:
Rectangle2D croppedPortion = new Rectangle2D(x, y, width, height);

// target width and height:
double scaledWidth = ... ;
double scaledHeight = ... ;

ImageView imageView = new ImageView(fullImage);
imageView.setViewport(croppedPortion);
imageView.setFitWidth(scaledWidth);
imageView.setFitHeight(scaledHeight);
imageView.setSmooth(true);

现在你可以通过对原始图像进行裁剪来创建一个新的图像,并通过对 ImageView 进行快照来实现。为此,您需要将 ImageView 放置到一个离屏场景中:

Pane pane = new Pane(imageView);
Scene offScreenScene = new Scene(pane);
WritableImage croppedImage = imageView.snapshot(null, null);

然后,您可以像以前一样将剪裁后的图像绘制到画布中。


谢谢你的回答。不幸的是,我无法使其工作。如果我使用您的第二个建议,则在加载单个50 MB图像时会出现“java.lang.OutOfMemoryError:Java堆空间”异常,即使将堆空间设置为512m。只有当我能够先裁剪图像然后再调整大小时,您的第一个建议才可能有效。据我所知,这是不可能的。我不知道在Java中做到这一点会如此困难。我不知道该怎么解决这个问题。 - zoran
适用于高效编程访问的内存中图像表示将使用比磁盘上表示更多的内存。您的图像有1亿像素。我不知道JavaFX在内部如何表示它们,但我猜测每个像素至少需要一个int,因此单个图像需要约400MB,当然您的其余代码也需要一些堆空间。您不能分配更多内存吗?我建议至少使用2GB堆空间。 - James_D

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