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文件。

