文档



JavaFX:使用JavaFX图形
10 使用Image Ops API(发布版8)
10 使用Image Ops API(8版)

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。

10 使用Image Ops API(Release 8)

从图像中读取像素

您可能已经熟悉了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所示的图像。

图10-1 显示图像

图10-1的描述
"图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显示了此过程的结果。

图10-2 一个更亮的标志,存储在一个WritableImage对象中

图10-2的描述如下
"图10-2 一个更亮的标志,存储在一个WritableImage对象中"的描述

使用字节数组和像素格式编写图像

到目前为止,演示已经成功获取并修改了像素颜色,但是与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);
    }
}
10 使用Image Ops API(Release 8)

图10-3 将像素写入画布

图10-3的描述如下
"图10-3 将像素写入画布"的描述

此演示不从现有图像中读取数据;它完全从头开始创建一个新的WritableImage对象。它绘制了几行多彩的10x10矩形,每个像素的颜色数据存储在一个字节数组中,表示每个像素的RGB值。

特别值得注意的是私有方法createImageDatadrawImageDatacreateImageData方法设置了每个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值。然后,通过将这些数据传递给PixelWritersetPixels方法,一次写入整个矩形的像素。

创建快照

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。然后(借助ImageIOSwingFXUtils),将该图像写入文件系统作为.png文件。

关闭窗口

目录

JavaFX: 使用 JavaFX 图形

展开 折叠