第3章 ラムダ式を使ったプログラミング : 問題 14 : PixelReader による画像変換の拡張
問題
ピクセル単位の遅延評価を扱うため、今までの画像変換を変更して、画像内の任意のピクセルを読み込むことができるPixelReader
を渡すようにせよ
色だけを参照する単純な変換であるグレイスケール変換は
(x, y, reader) -> reader.getColor(x, y).grayscale()
と表せる
この画像変換は、畳み込みフィルターを表すことができるし、いままで表せなかった鏡像変換もサポートする
(x, y, reader) -> reader.getColor((width - 1) - x, y)
解答
“ピクセル単位の遅延評価” ですか
これって練習問題のレベルじゃないよね
この本に次のようなヒントが書いてあったけど
PixelReader は、操作のパイプラインの特定のレベルにある。
パイプラインの個々のレベルで最近読み込まれたピクセルはキャッシュに保持せよ。
ピクセルを求められたら、リーダーはキャッシュ(あるいは、レベル 0 なら元画像)を調べる。
ピクセルがそこになければ、リーダーを構築し、そのリーダーはピクセルを前段階で求める。
意味わかります?
翻訳者の方は大変だったでしょうね
まず、PixelReader
を渡す画像変換の関数型インターフェースImageTransformer
を定義する
■ ImageTransformer
@FunctionalInterface public interface ImageTransformer { /** * 変換 * @param x カレント位置のX座標 * @param y カレント位置のY座標 * @param reader Pixel Reader * @return 変換後の色 */ Color apply(int x, int y, PixelReader reader); /** 色のみの変換から画像変換を生成 */ static ImageTransformer onlyCurrentColor(UnaryOperator<Color> f) { return (x, y, reader) -> f.apply(reader.getColor(x, y)); } /** カレント位置の色のみの変換から画像変換を生成 */ static ImageTransformer onlyCurrent(ColorTransformer f) { return (x, y, reader) -> f.apply(x, y, reader.getColor(x, y)); } /** 畳み込みフィルターから画像変換を生成 */ static ImageTransformer onlyNeighbor(ImageFilter f, int width, int height) { return (x, y, reader) -> { Color[][] matrix = new Color[3][3]; for (int dx = -1; dx <= 1; dx++) { for (int dy = -1; dy <= 1; dy++) { matrix[dx + 1][dy + 1] = Color.BLACK; // 範囲外のとき PixelReader#getColor は IndexOutOfBoundsException をスロー if (0 <= x + dx && x + dx < width && 0 <= y + dy && y + dy < height) { matrix[dx + 1][dy + 1] = reader.getColor(x + dx, y + dy); } } } }; } }
■ PixelReader
について一言
Image
とPixelReader
の関係は、まさにCollection
とIterator
のような関係である
PixelReader
≒Image
と見なしてかまわない
実際にはPixelReader
は、画像ファイルの内部フォーマットの情報(パレットを持つか?、色の三原色の並び順)まで公開する
PixelReader
⊇Image
である
■ キャッシュ機能を持った変換済み画像のPixelReader
LatentImage
の Nested Class として “画像変換ImageTransformer
によって変換された画像を表すPixelReader
” を導入する
このクラスは
- 変換元画像と画像変換をメンバーに持つ
- 一度変換したピクセルはキャッシュに記憶
getPixelFormat
,getPixels
メソッドは、画像の内部フォーマット情報なので無視
画像変換ImageTransformer
では呼ばれないと仮定する
/** * 画像変換(ImageTransformer)によって変換された画像に対応する Pixel Reader */ private static class TransformedPixelReader implements PixelReader { /** 画像変換 */ private final ImageTransformer transformer; /** 変換元画像に対応する Pixel Reader */ private final PixelReader original; /** 変換済みの色のキャッシュ */ private final Color[][] cache; /** * コンストラクタ * @param transformer 画像変換 * @param original 変換元画像に対応する Pixel Reader * @param width 変換元画像の幅 * @param height 変換元画像の高さ */ public TransformedPixelReader(ImageTransformer transformer, PixelReader original, int width, int height ) { this.transformer = transformer; this.original = original; cache = new Color[width][height]; } /** * 指定された座標の色 * @param x X座標 * @param y Y座標 * @return 色 */ public Color getColor(int x, int y) { if (cache[x][y] == null) { cache[x][y] = transformer.apply(x, y, original); } return cache[x][y]; } /** * 指定された座標の色を ARGB フォーマットで * @param x X座標 * @param y Y座標 * @return ARGB フォーマットの色 */ public int getArgb(int x, int y) { Color color = getColor(x, y); int alpha = (int)Math.round(255.0 * color.getOpacity()); int red = (int)Math.round(255.0 * color.getRed()); int green = (int)Math.round(255.0 * color.getGreen()); int blue = (int)Math.round(255.0 * color.getBlue()); return alpha << 24 | red << 16 | green << 8 | blue; } // getPixelFormat, getPixels メソッドは省略 }
じゃLatentImage
クラスを実装してみよう
遅延評価自体はTransformedPixelReader
が受け持ってくれるので、LatentImage
のコードはすっきりする
■ LatentImage
public class LatentImage { /** 変換対象の画像 */ private final Image in; /** 画像の幅 */ private final int width; /** 画像の高さ */ private final int height; /** 変換後の画像の Pixel Reader */ private PixelReader transformed; /** コンストラクタ */ private LatentImage(Image in) { this.in = in; width = (int)in.getWidth(); height = (int)in.getHeight(); transformed = in.getPixelReader(); } /** オブジェクトの生成 */ public static LatentImage from(Image in) { return new LatentImage(in); } /** 座標を無視した画像変換の適用 */ public LatentImage transform(UnaryOperator<Color> f) { return transform(ImageTransformer.onlyCurrentColor(f)); } /** カレントのみの画像変換の適用 */ public LatentImage transform(ColorTransformer f) { return transform(ImageTransformer.onlyCurrent(f)); } /** 畳み込みフィルターの適用 */ public LatentImage transform(ImageFilter f) { return transform(ImageTransformer.onlyNeighbor(f, width, height)); } /** 画像変換の適用 */ public LatentImage transform(ImageTransformer f) { transformed = new TransformedPixelReader(f, transformed, width, height); return this; } /** 全ての画像変換を適用した画像の取得 */ public Image toImage() { WritableImage out = new WritableImage(width, height); for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { out.getPixelWriter().setColor(x, y, transformed.getColor(x, y)); } } } }
それでは、ぼかし(ピクセルとその隣接する8個のピクセルの平均で置き換え)と鏡像と枠線の変換を適用してみよう
■ ぼかしと鏡像と枠線
public void start(Stage stage) throws Exception { Image image = new Image("queen-mary.png"); int width = (int)image.getWidth(); int height = (int)image.getHeight(); Image image2; ColorTransformer frame = (x, y, c) -> // 枠線を付ける x < 15 || y < 15 || x >= width - 15 || y >= height - 15 ? Color.AQUAMARINE : c; ImageFilter blur = (x, y, m) -> { // ぼかしフィルター double red = 0.0; double green = 0.0; double blue = 0.0; for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { red += m[i][j].getRed(); green += m[i][j].getGreen(); blue += m[i][j].getBlue(); } } return ImageFilter.color(red/9.0, green/9.0, blue/9.0); }; ImageTransformer mirror = (x, y, reader) -> reader.getColor((width - 1) - x, y); image2 = LatentImage.from(image) .transform(blur) .transform(mirror) .transform(frame) .toImage(); stage.setScene(new Scene(new HBox(new ImageView(image), new ImageView(image2)))); stage.show(); }
■ 実行結果