戻る

Kotlinで左ビットシフト

概要

Kotlinで画像を出力するコードを作成していた時に、左ビットシフトを使ったコードに出会うことになりました。 これまで直接ビットシフトを扱ったことがなかったので、メモと考察を書いています。 22進数のnn桁左ビットシフトとは、1010進数において2n2^n倍することと等価です。

本編

全体像のコード

package io.github.inuverse.jpeg.infrastructure

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

data class Rgb (
    val r: UByte,
    val g: UByte, 
    val b: UByte
)

data class ImageRgb (
    val width: Int, 
    val height: Int, 
    val pixels: List<Rgb>
)

class ImageWriter {
    fun write(imageRgb: ImageRgb, filePath: String, format: String = "png") {

        val image = BufferedImage(imageRgb.width, imageRgb.height, BufferedImage.TYPE_INT_RGB)

        for (y in 0 until imageRgb.height) {
            for (x in 0 until imageRgb.width) {
                val pixel = imageRgb.pixels[y * imageRgb.width + x]
                val r = pixel.r.toInt()
                val g = pixel.g.toInt()
                val b = pixel.b.toInt()

                val rgbInt = (r shl 16) or (g shl 8) or b
                image.setRGB(x, y, rgbInt)
            }
        }

        val outputFile = File(filePath)
        ImageIO.write(image, format, outputFile)

    }
}

データクラスRgbの要素は型は符号なし8bit整数(UByte)としているのは、RGBが0~255までの256階調で色の濃度を表すからです。

BufferedImage.TYPE_INT_RGBについて

ここでBufferedImage.TYPE_INT_RGBについて確認しておきましょう。 クラスBufferedImageによると まず

Represents an image with 8-bit RGB color components packed into integer pixels.

とあります。8ビットRGBカラー成分を整数ピクセルにパックした画像を表すとのことです。詳しくはClass DirectColorModelも参考にしてください。

0xAARRGGBB

のように、RGBの値がパックされています。Aは透明度を表していますが、TYPE_INT_RGBはアルファを持たないので、AA=00AA = 00です。 RGBはそれぞれを8 bit整数で表されており、それらを連結させて1つの数字とします。つまり、1つの数字でRGBを表すことができるようになります。

左ビットシフト

RRGGBBのように1つの数字で表されないといけないけれど、数字の配置に意味があるとき、通常の四則演算で操作するのは困難です。 ビットシフトはこれを実現することが簡単にできます。

該当の部分は

val rgbInt = (r shl 16) or (g shl 8) or b

であり、shl左ビットシフトを表しています。 CやJavaでは左ビットシフトは<<演算子で表され、Javaとの互換性のあるKotlinもまた<<で表現することもできます。

具体的な例を考えていみましょう。 r = 255, g = 100, b = 50であるとします。 コンピュータの内部ではこれらは2進数で表現されるので、それぞれr = 11111111, g = 01100100, b = 00110010となります。 shl 16は左に16桁ずらし、空けた16桁を0で埋める操作です。shl 8は同様に8桁ずらします。

    11111111    00000000    00000000
                01100100    00000000
                            00110010

この時点では16桁ずらされた8bit整数、8桁ずらされた8bit整数、そしてただの8bit整数が存在しているだけです。

これらを連結させるためにはor演算子を用います。

    11111111 00000000 00000000
or           01100100 00000000
or                    00110010
---------------------------------
=   11111111 01100100 00110010

これは論理演算を思い出せば理解できて、論理和であるor

ABResult
111
101
011
000

であることを思い出せばOKです。8bit整数を16桁や8桁ずらすので、比較に必ず0が含まれます。 RGBを表す数字は重なることはなく、それぞれの値が最終的にバッキングされ、1つの整数をなします。

左ビットシフトは何をしているのか?

2進数の桁を増やす左ビットシフトですが、10進数で慣れ親しんでいる我々にとって、結局のところどんな操作に対応しているのか不明瞭です。 結論から言えば、nn桁の左ビットシフトは、元の数の2n2^n乗倍を計算することに対応しています。例えば、

val b1 = 0b0001
val b2 = b1 shl 1
val b3 = b1 shl 2
val b4 = b1 shl 3
println("b1: ${b1} ${b1.toString(2)}")
println("b2: ${b2} ${b2.toString(2)}")
println("b3: ${b3} ${b3.toString(2)}")
println("b4: ${b4} ${b4.toString(2)}")

の出力結果を考えてみましょう。これはb1をを1(2)1_{(2)}としており、その1から3桁の左ビットシフトを表示するコードです。 printlnでは10進数と2進数の両方を与えますが、Kotlinの表示のデフォルトは10進数です。なのでtoString()メソッドで2進数を 表示させています。結果は次になります:

b1: 1 1
b2: 2 10
b3: 4 100
b4: 8 1000

21,22,232^1, 2^2, 2^310,100,100010, 100, 1000が対応していることが分かります。

2乗ずつ増えることは10進数の場合を考えるとすごく自然に思えます。 というのも10進数における「左ビットシフト」を考えてみると10,100,1000,10, 100, 1000, \ldotsのようになります。 これは101,102,103,10^1, 10^2, 10^3, \ldotsです。 10進数の左ビットシフトが単純に10をかけることに対応するように、2進数のビットシフトは2をかけるのです。

戻る