saba1024のブログ

どうでも良い思いついた事とかプログラミング関係のメモとか書いていきます。

Apache Groovyで画像処理の基礎

本記事はG* Advent Calendar 2017の21日目の記事です。

ピクセル

Apache Groovyでも、Javaが用意しているBufferedImageImageIOを利用することで、ピクセル単位で画像を取り扱うことが出来ます。
ピクセルの正体は単なるInteger(32bit)です。
そしてその32bitの数値を8bitずつの合計4つに区切って、先頭ビットからそれぞれAlphaRedGreenBlueの値として扱います。

つまり, 11111111 00000000 10101010 11001100
というIntegerは、
11111111(Alpha)
00000000(Red)
10101010(Green)
11001100(Blue)

となります。 ただし、単純にInteger(10進数)の計算では上記の値をそれぞれ取得するのは難しいので、ビット演算(右シフト)で求めてあげます。
ついでに、読み込みと書き込みに必要なリソースを提供してくれるメソッドと保存に利用するメソッドを追加して、以下のようなクラスを用意しました。

import java.awt.image.BufferedImage
import javax.imageio.ImageIO

class ImageUtil {
    static Integer alpha(Integer v) {v >>> 24}
    static Integer red(Integer v) {v >> 16&0xff}
    static Integer green(Integer v) {v >> 8&0xff}
    static Integer blue(Integer v) {v&0xff}
    static Integer rgb(Integer red, Integer green, Integer blue) {
        0xff000000 | red << 16 | green << 8 | blue
    }
    static Integer rgb(Integer alpha, Integer red, Integer green, Integer blue) {
        alpha << 24| red << 16 | green << 8 | blue
    }

    static withStreams(File rawFile, Closure clj) {
        BufferedImage bufReader = ImageIO.read(rawFile)
        BufferedImage bufWriter = new BufferedImage(bufReader.width, bufReader.height, BufferedImage.TYPE_INT_RGB)
        clj(bufReader, bufWriter)
    }

    static save(BufferedImage buf, String fileName) {
        File file = new File("${fileName}")
        ImageIO.write(buf, "jpg", file)
    }
}

上記のコードを利用しつつ、以下の画像を加工してみます。

f:id:saba1024:20170523213245j:plain

ネガポジ変換

String dir = "/home/koji/work/groovy/image-processing"
String fileName = "test.jpg"

File file = new File("${dir}/${fileName}")

// ネガポジ変換
ImageUtil.withStreams(file) { BufferedImage bufReader, BufferedImage bufWriter ->
    Integer width = bufReader.width
    Integer height = bufReader.height

    // for each line(y)
    (0..<height).each {Integer y ->
        // for each columns(x)
        (0..<width).each {Integer x ->
            Integer colorInfo = bufReader.getRGB(x, y)
            Integer red = 255 - ImageUtil.red(colorInfo)
            Integer green = 255 - ImageUtil.green(colorInfo)
            Integer blue = 255 - ImageUtil.blue(colorInfo)
            Integer rgb = ImageUtil.rgb(red, green, blue)
            bufWriter.setRGB(x, y, rgb)
        }
    }
    ImageUtil.save(bufWriter, "${dir}/negaposi.jpg")
}

f:id:saba1024:20171220192320j:plain

左右反転

String dir = "/home/koji/work/groovy/image-processing"
String fileName = "test.jpg"

File file = new File("${dir}/${fileName}")
// 左右反転
ImageUtil.withStreams(file) { BufferedImage bufReader, BufferedImage bufWriter ->
    Integer width = bufReader.width
    Integer height = bufReader.height
    Integer maxIndexX = width - 1
    // for each line(y)
    (0..<height).each {Integer y ->
        // for each columns(x)
        (maxIndexX..0).each {Integer x ->
            Integer colorInfo = bufReader.getRGB(x, y)
            bufWriter.setRGB(maxIndexX - x, y, colorInfo)
        }
    }
    ImageUtil.save(bufWriter, "${dir}/reversal.jpg")
}

f:id:saba1024:20171220192331j:plain

縦に分割

画像を縦に5つに分割して、最後の分割分から順番に左から描画します。

String dir = "/home/koji/work/groovy/image-processing"
String fileName = "test.jpg"

File file = new File("${dir}/${fileName}")

ImageUtil.withStreams(file) { BufferedImage bufReader, BufferedImage bufWriter ->
    Integer width = bufReader.width
    Integer height = bufReader.height
    Integer splitCount = 5
    List <List<Integer>>splited = []
    Integer splitedWidth = width / splitCount
    Integer remainingPixels = width % splitCount

    Closure getIndexes = {Integer rowIndex ->
        Integer startIndex = (splitedWidth - 1) * rowIndex + rowIndex
        Integer endIndex = startIndex + (splitedWidth - 1)
        [startIndex, endIndex]
    }

    splitCount.times {Integer rowIndex ->
        List<Integer> rowPixcels = []
        def (startIndex, endIndex) = getIndexes(rowIndex)
        (0..<height).each {Integer y ->
            (startIndex..endIndex).each {Integer x ->
                rowPixcels << bufReader.getRGB(x, y)
            }
        }
        splited << rowPixcels
    }

    // 分割した時に、余っている部分があれば救済
    // 今回はとりあえず最後の分割画像に余りを単純に追加
    if (remainingPixels > 0) {
        Integer maxIndexX = width - 1
        Integer remainIndexX = remainingPixels -1
       (0..<height).each {Integer y ->
            (0..remainIndexX).each {Integer x ->
                splited.last() << bufReader.getRGB(width -1 - x, y)
            }
        }
    }

    // リスト内容を反転させて描画
    splited.reverse().eachWithIndex {List<Integer> column, rowIndex ->
        def (startIndex, endIndex) = getIndexes(rowIndex)
        (0..<height).each {Integer y ->
            (startIndex..endIndex).eachWithIndex {Integer x, Integer index ->
                def path = index + ((splitedWidth) * y)
                bufWriter.setRGB(x, y, column[path])
            }
        }
     }
    ImageUtil.save(bufWriter, "${dir}/split.jpg")
}

f:id:saba1024:20171220200733j:plain

以上です。
画像を扱うコード自体はなんだかオールドスクールな感じになりました。
AIだ機械学習だという流れの昨今、画像処理に関する知識も求められるようになっていますので、最も基礎的なピクセル単位での画像の取り扱いをまとめてみました。