概要
Kotlinで画像を出力するコードを作成していた時に、左ビットシフトを使ったコードに出会うことになりました。 これまで直接ビットシフトを扱ったことがなかったので、メモと考察を書いています。 進数の桁左ビットシフトとは、進数において倍することと等価です。
本編
全体像のコード
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はアルファを持たないので、です。
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は
| A | B | Result |
|---|---|---|
| 1 | 1 | 1 |
| 1 | 0 | 1 |
| 0 | 1 | 1 |
| 0 | 0 | 0 |
であることを思い出せばOKです。8bit整数を16桁や8桁ずらすので、比較に必ず0が含まれます。 RGBを表す数字は重なることはなく、それぞれの値が最終的にバッキングされ、1つの整数をなします。
左ビットシフトは何をしているのか?
2進数の桁を増やす左ビットシフトですが、10進数で慣れ親しんでいる我々にとって、結局のところどんな操作に対応しているのか不明瞭です。 結論から言えば、桁の左ビットシフトは、元の数の乗倍を計算することに対応しています。例えば、
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から3桁の左ビットシフトを表示するコードです。
printlnでは10進数と2進数の両方を与えますが、Kotlinの表示のデフォルトは10進数です。なのでtoString()メソッドで2進数を
表示させています。結果は次になります:
b1: 1 1
b2: 2 10
b3: 4 100
b4: 8 1000
とが対応していることが分かります。
2乗ずつ増えることは10進数の場合を考えるとすごく自然に思えます。 というのも10進数における「左ビットシフト」を考えてみるとのようになります。 これはです。 10進数の左ビットシフトが単純に10をかけることに対応するように、2進数のビットシフトは2をかけるのです。

