10 イメージOps APIの使用
この章では、JavaFXアプリケーション内でロー・ピクセルを読み書きできるようにするAPIである、イメージOpsについて説明します。
イメージからピクセルを読み取る方法、イメージにピクセルを書き込む方法およびスナップショットを作成する方法を学習します。
イメージOps APIの概要
イメージOps APIは、javafx.scene.image
パッケージの次のクラスおよびインタフェースで構成されています。
-
Image
: グラフィック・イメージを表します。このクラスによって、イメージからピクセルを直接読み取るためのPixelReader
が提供されます。 -
WritableImage
:Image
のサブクラス。このクラスによって、イメージにピクセルを直接書き込むためのPixelWriter
が提供されます。WritableImage
は、そこにピクセルを書き込むまでは、最初は空で(透過的に)作成されます。 -
PixelReader
: ピクセルを含むイメージまたはその他の面からピクセル・データを取得するための方法を定義するインタフェース。 -
PixelWriter
:WritableImage
または書込み可能なピクセルを含むその他の面にピクセル・データを書き込むための方法を定義するインタフェース。 -
PixelFormat
: 特定のフォーマットのピクセルについてデータのレイアウトを定義します。 -
WritablePixelFormat
:PixelFormat
のサブクラスであり、フル・カラーを格納できるピクセル・フォーマットを表します。これは、任意のイメージからピクセル・データを書き込むための宛先フォーマットとして使用できます。
次の項では、コンパイルして実行できるコード例を使用して、このAPIについて説明します。
イメージからのピクセルの読取り
イメージを表示するJavaFXアプリケーションで(ImageView
とともに)使用されるjavafx.scene.image.Image
クラスについては、すでに理解している場合があります。次の例では、JavaFXのロゴをoracle.comからロードしてJavaFX Sceneグラフに追加することによって、イメージを表示する方法を示します。
例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) { // Create Image and ImageView objects Image image = new Image("http://docs.oracle.com/javafx/" + "javafx/images/javafx-documentation.png"); ImageView imageView = new ImageView(); imageView.setImage(image); // Display image on screen StackPane root = new StackPane(); root.getChildren().add(imageView); Scene scene = new Scene(root, 300, 250); primaryStage.setTitle("Image Read Test"); 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) { // Create Image and ImageView objects Image image = new Image("http://docs.oracle.com/javafx/" + "javafx/images/javafx-documentation.png"); ImageView imageView = new ImageView(); imageView.setImage(image); // Obtain PixelReader PixelReader pixelReader = image.getPixelReader(); System.out.println("Image Width: "+image.getWidth()); System.out.println("Image Height: "+image.getHeight()); System.out.println("Pixel Format: "+pixelReader.getPixelFormat()); // Determine the color of each pixel in the image 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("\nPixel color at coordinates (" + 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("Opacity = " + color.getOpacity()); System.out.println("Saturation = " + color.getSaturation()); } } // Display image on screen StackPane root = new StackPane(); root.getChildren().add(imageView); Scene scene = new Scene(root, 300, 250); primaryStage.setTitle("Image Read Test"); primaryStage.setScene(scene); primaryStage.show(); } public static void main(String[] args) { launch(args); } }
このバージョンでは、イメージのすべてのピクセルから色情報を取得するために、(getColor
メソッドを呼び出す)ネストされたforループが使用されます。ピクセルは、左上隅(0,0)から始まって、イメージを左から右へと1つずつ読み取られます。Y座標は、行全体が読み取られた後にのみ増分されます。各ピクセルに関する情報(色の値、不透明度、彩度の値など)が標準出力に出力され、読取り操作が正しく機能していることが示されます。
.
.. // beginning of output omitted
Pixel color at
coordinates (117,27) 0x95a7b4ff
R = 0.5843137502670288
G = 0.6549019813537598
B = 0.7058823704719543
Opacity = 1.0
Saturation = 0.17222220767979304
Pixel color at coordinates (118,27) 0x2d5169ff
R = 0.1764705926179886
G = 0.3176470696926117
B = 0.4117647111415863
Opacity = 1.0
Saturation = 0.5714285662587809
... // remainder of output omitted
各ピクセルの色を変更し、それを画面に書き込むことを試したい場合があります。しかし、イメージ・オブジェクトは読取り専用です。この場合は、かわりに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) { // Create Image and ImageView objects Image image = new Image("http://docs.oracle.com/javafx/" + "javafx/images/javafx-documentation.png"); ImageView imageView = new ImageView(); imageView.setImage(image); // Obtain PixelReader PixelReader pixelReader = image.getPixelReader(); System.out.println("Image Width: "+image.getWidth()); System.out.println("Image Height: "+image.getHeight()); System.out.println("Pixel Format: "+pixelReader.getPixelFormat()); // Create WritableImage WritableImage wImage = new WritableImage( (int)image.getWidth(), (int)image.getHeight()); PixelWriter pixelWriter = wImage.getPixelWriter(); // Determine the color of each pixel in a specified row 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("\nPixel color at coordinates ("+ 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("Opacity = "+color.getOpacity()); System.out.println("Saturation = "+color.getSaturation()); // Now write a brighter color to the PixelWriter. color = color.brighter(); pixelWriter.setColor(readX,readY,color); } } // Display image on screen imageView.setImage(wImage); StackPane root = new StackPane(); root.getChildren().add(imageView); Scene scene = new Scene(root, 300, 250); primaryStage.setTitle("Image Write Test"); primaryStage.setScene(scene); primaryStage.show(); } public static void main(String[] args) { launch(args); } }
このバージョンでは、JavaFXのロゴと同じ幅および高さに初期化されたWritableImage
が作成されます。(新しいイメージにピクセル・データを書き込むための) PixelWriter
が取得された後、コードでは(現在のピクセルの色の陰影を明るくするために) brighter()
メソッドが呼び出され、pixelWriter.setColor(readX,readY,Color)
を呼び出すことで新しいイメージにデータが書き込まれます。
図10-2に、このプロセスの結果を示します。
バイト配列およびPixelFormatによるイメージの書込み
ここまでのデモでは、ピクセルの色を正常に取得して変更しましたが、APIで実行できることと比べて、コードは比較的単純でした(また、必ずしも最適ではありませんでした)。例10-4では、ピクセル・データの格納方法を指定するためにPixelFormat
を使用して、ピクセルを四角形に一度に書き込む新しいデモを作成します。また、このバージョンでは、ImageView
ではなくCanvas
にイメージ・データを表示します。(Canvas
クラスの詳細は、「Canvas APIの使用」を参照してください。)
例10-4 キャンパスへの四角形の書込み
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 { // Image Data private static final int IMAGE_WIDTH = 10; private static final int IMAGE_HEIGHT = 10; private byte imageData[] = new byte[IMAGE_WIDTH * IMAGE_HEIGHT * 3]; // Drawing Surface (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; } // Add drop shadow effect gc.applyEffect(new DropShadow(20, 20, 20, Color.GRAY)); root.getChildren().add(canvas); } }
このデモでは、データは既存のイメージから読み取られず、新しいWritableImage
オブジェクトが完全に最初から作成されます。多色の10x10の四角形の行が複数描画され、その色のデータは各ピクセルのRGB値を表すバイトの配列に格納されます。
特に興味深いのは、privateメソッド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; } }
ここでは、PixelWriter
がCanvas
から取得され、新しい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 { // Image Data private static final int IMAGE_WIDTH = 10; private static final int IMAGE_HEIGHT = 10; private byte imageData[] = new byte[IMAGE_WIDTH * IMAGE_HEIGHT * 3]; // Drawing Surface (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(); //Take snapshot of the scene WritableImage writableImage = scene.snapshot(null); // Write snapshot to file system as a .png image 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; } // Add drop shadow effect gc.applyEffect(new DropShadow(20, 20, 20, Color.GRAY)); root.getChildren().add(canvas); } }
例10-8に示すように、注意する必要がある変更はstartメソッドに対する次の変更です。
例10-8 変更されたstartメソッド
... Scene scene = new Scene(root, 400, 400); primaryStage.setScene(scene); primaryStage.show(); //Take snapshot of the scene WritableImage writableImage = scene.snapshot(null); // Write snapshot to file system as a .png image 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
ファイルとして書き込まれます。