CUBは子供の白熊

Java SE 8 実践プログラミングの練習問題を解く

第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について一言

ImagePixelReaderの関係は、まさにCollectionIteratorのような関係である
  PixelReaderImage
と見なしてかまわない
実際にはPixelReaderは、画像ファイルの内部フォーマットの情報(パレットを持つか?、色の三原色の並び順)まで公開する
  PixelReaderImage
である

■ キャッシュ機能を持った変換済み画像の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();
}

■ 実行結果 f:id:ClosedUnBounded:20150624202853p:plain