10 使用Image Ops API
本章介绍了Image Ops,这是一个API,可以在JavaFX应用程序中读取和写入原始像素。
您将学习如何从图像中读取像素,将像素写入图像以及创建快照。
Image Ops API概述
Image Ops API包括以下类/接口在javafx.scene.image
包中:
-
Image
:表示图形图像。该类提供了一个PixelReader
,用于直接从图像中读取像素。 -
WritableImage
:是Image
的子类。该类提供了一个PixelWriter
,用于直接向图像中写入像素。一个WritableImage
最初是空的(透明的),直到您向其写入像素。 -
PixelReader
:定义了从图像或其他包含像素的表面检索像素数据的方法的接口。 -
PixelWriter
:定义了将像素数据写入WritableImage
或其他包含可写像素的表面的方法的接口。 -
PixelFormat
:定义了给定格式的像素数据的布局。 -
WritablePixelFormat
:是PixelFormat
的子类,表示可以存储完整颜色的像素格式。它可以用作从任意图像写入像素数据的目标格式。
以下部分通过可以编译和运行的示例演示了此API。
从图像中读取像素
您可能已经熟悉了javafx.scene.image.Image
类,它(以及ImageView
)用于显示图像的JavaFX应用程序。以下示例演示了如何通过从oracle.com加载JavaFX徽标并将其添加到JavaFX场景图中来显示图像。
示例10-1 加载和显示图像
package imageopstest; import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.layout.StackPane; import javafx.stage.Stage; import javafx.scene.image.Image; import javafx.scene.image.ImageView; public class ImageOpsTest extends Application { @Override public void start(Stage primaryStage) { // 创建Image和ImageView对象 Image image = new Image("http://docs.oracle.com/javafx/" + "javafx/images/javafx-documentation.png"); ImageView imageView = new ImageView(); imageView.setImage(image); // 在屏幕上显示图像 StackPane root = new StackPane(); root.getChildren().add(imageView); Scene scene = new Scene(root, 300, 250); primaryStage.setTitle("图像读取测试"); primaryStage.setScene(scene); primaryStage.show(); } public static void main(String[] args) { launch(args); } }
运行此程序将产生如图10-1所示的图像。
现在,让我们修改这段代码,直接从像素中读取Color
信息。您可以通过调用getPixelReader()
方法,然后使用返回的PixelReader
对象的getColor(x,y)
方法来获取指定坐标处像素的颜色。
示例 10-2 从像素中读取颜色信息
package imageopstest; import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.layout.StackPane; import javafx.stage.Stage; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.image.PixelReader; import javafx.scene.paint.Color; public class ImageOpsTest extends Application { @Override public void start(Stage primaryStage) { // 创建 Image 和 ImageView 对象 Image image = new Image("http://docs.oracle.com/javafx/" + "javafx/images/javafx-documentation.png"); ImageView imageView = new ImageView(); imageView.setImage(image); // 获取 PixelReader PixelReader pixelReader = image.getPixelReader(); System.out.println("图像宽度: "+image.getWidth()); System.out.println("图像高度: "+image.getHeight()); System.out.println("像素格式: "+pixelReader.getPixelFormat()); // 确定图像中每个像素的颜色 for (int readY = 0; readY < image.getHeight(); readY++) { for (int readX = 0; readX < image.getWidth(); readX++) { Color color = pixelReader.getColor(readX, readY); System.out.println("\n像素坐标为 (" + readX + "," + readY + ") 的颜色为 " + color.toString()); System.out.println("红色值 = " + color.getRed()); System.out.println("绿色值 = " + color.getGreen()); System.out.println("蓝色值 = " + color.getBlue()); System.out.println("不透明度 = " + color.getOpacity()); System.out.println("饱和度 = " + color.getSaturation()); } } // 在屏幕上显示图像 StackPane root = new StackPane(); root.getChildren().add(imageView); Scene scene = new Scene(root, 300, 250); primaryStage.setTitle("图像读取测试"); primaryStage.setScene(scene); primaryStage.show(); } public static void main(String[] args) { launch(args); } }
这个版本使用嵌套的 for 循环(调用 getColor
方法)从图像的每个像素中获取颜色信息。它逐个像素地读取,从左上角(0,0)开始,从左到右逐行进行。只有在读取完整行后,Y 坐标才会递增。然后,将每个像素的信息(颜色值、不透明度和饱和度等)打印到标准输出,证明读取操作正常工作。
.
.. // 输出的开头部分被省略
像素坐标为
(117,27) 的颜色为 0x95a7b4ff
R = 0.5843137502670288
G = 0.6549019813537598
B = 0.7058823704719543
不透明度 = 1.0
饱和度 = 0.17222220767979304
坐标(118,27)处的像素颜色为 0x2d5169ff
R = 0.1764705926179886
G = 0.3176470696926117
B = 0.4117647111415863
不透明度 = 1.0
饱和度 = 0.5714285662587809
... // 其余输出省略
你可能会尝试修改每个像素的颜色并将其写入屏幕。但请记住,Image 对象是只读的;要写入新数据,你需要一个 WritableImage 的实例。
写入像素到图像
现在让我们修改这个示例,将每个像素点变亮,然后将修改后的结果写入一个WritableImage
对象。
示例 10-3 写入到 WritableImage
package imageopstest; import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.layout.StackPane; import javafx.stage.Stage; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.image.PixelReader; import javafx.scene.image.PixelWriter; import javafx.scene.paint.Color; import javafx.scene.image.WritableImage; public class ImageOpsTest extends Application { @Override public void start(Stage primaryStage) { // 创建 Image 和 ImageView 对象 Image image = new Image("http://docs.oracle.com/javafx/" + "javafx/images/javafx-documentation.png"); ImageView imageView = new ImageView(); imageView.setImage(image); // 获取 PixelReader PixelReader pixelReader = image.getPixelReader(); System.out.println("图像宽度: "+image.getWidth()); System.out.println("图像高度: "+image.getHeight()); System.out.println("像素格式: "+pixelReader.getPixelFormat()); // 创建 WritableImage WritableImage wImage = new WritableImage( (int)image.getWidth(), (int)image.getHeight()); PixelWriter pixelWriter = wImage.getPixelWriter(); // 确定指定行中每个像素的颜色 for(int readY=0;readY<image.getHeight();readY++){ for(int readX=0; readX<image.getWidth();readX++){ Color color = pixelReader.getColor(readX,readY); System.out.println("\n像素坐标为 ("+ readX+","+readY+") 的颜色为 " +color.toString()); System.out.println("R = "+color.getRed()); System.out.println("G = "+color.getGreen()); System.out.println("B = "+color.getBlue()); System.out.println("不透明度 = "+color.getOpacity()); System.out.println("饱和度 = "+color.getSaturation()); // 现在将更亮的颜色写入 PixelWriter。 color = color.brighter(); pixelWriter.setColor(readX,readY,color); } } // 在屏幕上显示图像 imageView.setImage(wImage); StackPane root = new StackPane(); root.getChildren().add(imageView); Scene scene = new Scene(root, 300, 250); primaryStage.setTitle("图像写入测试"); primaryStage.setScene(scene); primaryStage.show(); } public static void main(String[] args) { launch(args); } }
这个版本创建了一个WritableImage
,其宽度和高度与JavaFX标志相同。在获取PixelWriter
(用于向新图像写入像素数据)之后,代码调用brighter()
方法(用于将当前像素颜色的阴影变亮),然后通过调用pixelWriter.setColor(readX,readY,Color)
将数据写入新图像。
图10-2显示了此过程的结果。
使用字节数组和像素格式编写图像
到目前为止,演示已经成功获取并修改了像素颜色,但是与API的能力相比,代码仍然相对简单(并不一定是最优的)。示例10-4创建了一个新的演示,它以矩形为单位写入像素,使用PixelFormat
来指定像素数据的存储方式。这个版本还将图像数据显示在Canvas
上,而不是ImageView
上。(有关Canvas
类的更多信息,请参见使用Canvas API)。
示例10-4 将矩形写入Canvas
package imageopstest; import java.nio.ByteBuffer; import javafx.application.Application; import javafx.scene.Group; import javafx.scene.Scene; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; import javafx.scene.effect.DropShadow; import javafx.scene.image.PixelFormat; import javafx.scene.image.PixelWriter; import javafx.scene.paint.Color; import javafx.stage.Stage; public class ImageOpsTest extends Application { // 图像数据 private static final int IMAGE_WIDTH = 10; private static final int IMAGE_HEIGHT = 10; private byte imageData[] = new byte[IMAGE_WIDTH * IMAGE_HEIGHT * 3]; // 绘图表面(Canvas) private GraphicsContext gc; private Canvas canvas; private Group root; public static void main(String[] args) { launch(args); } @Override public void start(Stage primaryStage) { primaryStage.setTitle("PixelWriter Test"); root = new Group(); canvas = new Canvas(200, 200); canvas.setTranslateX(100); canvas.setTranslateY(100); gc = canvas.getGraphicsContext2D(); createImageData(); drawImageData(); primaryStage.setScene(new Scene(root, 400, 400)); primaryStage.show(); } private void createImageData() { int i = 0; for (int y = 0; y < IMAGE_HEIGHT; y++) { int r = y * 255 / IMAGE_HEIGHT; for (int x = 0; x < IMAGE_WIDTH; x++) { int g = x * 255 / IMAGE_WIDTH; imageData[i] = (byte) r; imageData[i + 1] = (byte) g; i += 3; } } } private void drawImageData() { boolean on = true; PixelWriter pixelWriter = gc.getPixelWriter(); PixelFormat<ByteBuffer> pixelFormat = PixelFormat.getByteRgbInstance(); for (int y = 50; y < 150; y += IMAGE_HEIGHT) { for (int x = 50; x < 150; x += IMAGE_WIDTH) { if (on) { pixelWriter.setPixels(x, y, IMAGE_WIDTH, IMAGE_HEIGHT, pixelFormat, imageData, 0, IMAGE_WIDTH * 3); } on = !on; } on = !on; } // 添加阴影效果 gc.applyEffect(new DropShadow(20, 20, 20, Color.GRAY)); root.getChildren().add(canvas); } }
此演示不从现有图像中读取数据;它完全从头开始创建一个新的WritableImage
对象。它绘制了几行多彩的10x10矩形,每个像素的颜色数据存储在一个字节数组中,表示每个像素的RGB值。
特别值得注意的是私有方法createImageData
和drawImageData
。 createImageData
方法设置了每个10x10矩形中出现的颜色的RGB值:
例子10-5 设置像素的RGB值
... private void createImageData() { int i = 0; for (int y = 0; y < IMAGE_HEIGHT; y++) { System.out.println("y: "+y); int r = y * 255 / IMAGE_HEIGHT; for (int x = 0; x < IMAGE_WIDTH; x++) { System.out.println("\tx: "+x); int g = x * 255 / IMAGE_WIDTH; imageData[i] = (byte) r; imageData[i + 1] = (byte) g; System.out.println("\t\tR: "+(byte)r); System.out.println("\t\tG: "+(byte)g); i += 3; } } } ...
此方法设置了矩形的每个像素的R和G值(B始终为0)。这些值存储在imageData
字节数组中,总共有300个字节。(每个10x10矩形中有100个像素,每个像素都有R、G和B值,总共300个字节)。
有了这些数据,drawImageData
方法将每个矩形的像素渲染到屏幕上:
例子10-6 渲染像素
private void drawImageData() { boolean on = true; PixelWriter pixelWriter = gc.getPixelWriter(); PixelFormat<ByteBuffer> pixelFormat = PixelFormat.getByteRgbInstance(); for (int y = 50; y < 150; y += IMAGE_HEIGHT) { for (int x = 50; x < 150; x += IMAGE_WIDTH) { if (on) { pixelWriter.setPixels(x, y, IMAGE_WIDTH, IMAGE_HEIGHT, pixelFormat, imageData, 0, IMAGE_WIDTH * 3); } on = !on; } on = !on; } }
在这里,从Canvas
获取PixelWriter
,并实例化一个新的PixelFormat
,指定字节数组表示RGB值。然后,通过将这些数据传递给PixelWriter
的setPixels
方法,一次写入整个矩形的像素。
创建快照
javafx.scene.Scene
类还提供了一个快照方法,该方法返回当前应用程序场景中显示的所有内容的WritableImage
。当与Java的ImageIO
类一起使用时,您可以将快照保存到文件系统中。
示例 10-7 创建并保存快照
package imageopstest; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; import javafx.application.Application; import javafx.embed.swing.SwingFXUtils; import javafx.scene.Group; import javafx.scene.Scene; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; import javafx.scene.effect.DropShadow; import javafx.scene.image.PixelFormat; import javafx.scene.image.PixelWriter; import javafx.scene.image.WritableImage; import javafx.scene.paint.Color; import javafx.stage.Stage; import javax.imageio.ImageIO; public class ImageOpsTest extends Application { // 图像数据 private static final int IMAGE_WIDTH = 10; private static final int IMAGE_HEIGHT = 10; private byte imageData[] = new byte[IMAGE_WIDTH * IMAGE_HEIGHT * 3]; // 绘图表面(Canvas) private GraphicsContext gc; private Canvas canvas; private Group root; public static void main(String[] args) { launch(args); } @Override public void start(Stage primaryStage) { primaryStage.setTitle("PixelWriter Test"); root = new Group(); canvas = new Canvas(200, 200); canvas.setTranslateX(100); canvas.setTranslateY(100); gc = canvas.getGraphicsContext2D(); createImageData(); drawImageData(); Scene scene = new Scene(root, 400, 400); primaryStage.setScene(scene); primaryStage.show(); // 获取场景的快照 WritableImage writableImage = scene.snapshot(null); // 将快照写入文件系统作为 .png 图像 File outFile = new File("imageops-snapshot.png"); try { ImageIO.write(SwingFXUtils.fromFXImage(writableImage, null), "png", outFile); } catch (IOException ex) { System.out.println(ex.getMessage()); } } private void createImageData() { int i = 0; for (int y = 0; y < IMAGE_HEIGHT; y++) { System.out.println("y: " + y); int r = y * 255 / IMAGE_HEIGHT; for (int x = 0; x < IMAGE_WIDTH; x++) { System.out.println("\tx: " + x); int g = x * 255 / IMAGE_WIDTH; imageData[i] = (byte) r; imageData[i + 1] = (byte) g; System.out.println("\t\tR: " + (byte) r); System.out.println("\t\tG: " + (byte) g); i += 3; } } System.out.println("imageData.lengthdrawImageData: " + imageData.length); } private void drawImageData() { boolean on = true; PixelWriter pixelWriter = gc.getPixelWriter(); PixelFormat<ByteBuffer> pixelFormat = PixelFormat.getByteRgbInstance(); for (int y = 50; y < 150; y += IMAGE_HEIGHT) { for (int x = 50; x < 150; x += IMAGE_WIDTH) { if (on) { pixelWriter.setPixels(x, y, IMAGE_WIDTH, IMAGE_HEIGHT, pixelFormat, imageData, 0, IMAGE_WIDTH * 3); } on = !on; } on = !on; } // 添加阴影效果 gc.applyEffect(new DropShadow(20, 20, 20, Color.GRAY)); root.getChildren().add(canvas); } }
需要注意的变化是在start方法中进行了以下修改,如示例10-8所示:
示例10-8 修改后的start方法
... Scene scene = new Scene(root, 400, 400); primaryStage.setScene(scene); primaryStage.show(); //对场景进行快照 WritableImage writableImage = scene.snapshot(null); // 将快照写入文件系统作为.png图像 File outFile = new File("imageops-snapshot.png"); try { ImageIO.write(SwingFXUtils.fromFXImage(writableImage, null), "png", outFile); } catch (IOException ex) { System.out.println(ex.getMessage()); } ...
如你所见,调用scene.snapshot(null)
创建了一个新的快照,并将其赋值给新构造的WritableImage
。然后(借助ImageIO
和SwingFXUtils
),将该图像写入文件系统作为.png
文件。