2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

色の計算をする際は2乗した値で計算するといい感じになる

Posted at

こんな動画を見た

以下がこの動画の主張である。

  • 人間の目は、同じ絶対量の明るさの変化を暗いときのほうが明るいときより強く感じるぜ!
  • カメラは、単純に明るさをそのまま測るぜ!
  • 人間が敏感な暗い部分のサンプリングを細かくするため、0~1で表した明るさの平方根を画像ファイルに記録し、表示するときに2乗するぜ!
  • この形式の画像ファイルは表示するだけならいいけど、何も考えずに編集すると狂うぜ!
  • たとえばぼかしを行う際、記録された値の平均をそのまま取ると、無駄に暗くなってしまうぜ!
  • 世の中の多くの場面でこの頭の悪い処理が行われてるぜ!
  • 記録された値を2乗してから平均を取り、その結果の平方根を取るといい感じになるぜ!

やってみた

動画で例に出ている、赤から緑へ変化する画像を生成してみた。
うまくいけば、中間は黄色になるはずである。

今回は、Pillow (PIL Fork) を用いて生成してみた。

生成を行うコード
from PIL import Image
from math import sqrt

width = 512
one_height = 64

left_color = (255, 0, 0)
right_color = (0, 255, 0)

out_file = "red_green_%dx%d.png" % (width, one_height)

img = Image.new("RGB", (width, one_height * 2))

def apply1(func, t):
    return tuple([func(v) for v in t])

def apply2(func, t1, t2):
    return tuple([func(v1, v2) for v1, v2 in zip(t1, t2)])

def to_max1(v):
    return v / 255.0

def from_max1(v):
    return round(v * 255)

def simple(ratio_of_2, v1, v2):
    return v1 * (1 - ratio_of_2) + v2 * ratio_of_2

def use_sqrt(ratio_of_2, v1, v2):
    return sqrt(simple(ratio_of_2, v1 * v1, v2 * v2))

lc1 = apply1(to_max1, left_color)
rc1 = apply1(to_max1, right_color)

for y in range(one_height):
    for x in range(width):
        ratio = x / (width - 1)
        blend_simple = apply2(lambda v1, v2 : simple(ratio, v1, v2), lc1, rc1)
        blend_sqrt = apply2(lambda v1, v2 : use_sqrt(ratio, v1, v2), lc1, rc1)
        img.putpixel((x, y), apply1(from_max1, blend_simple))
        img.putpixel((x, y + one_height), apply1(from_max1, blend_sqrt))

img.save(out_file)

生成結果は以下の画像になった。

色の計算方法の比較

上半分が何も工夫せずに計算を行った結果、下半分が2乗した値で計算を行った結果である。
比べてみると、上半分 (工夫なし) の中央付近が暗くなっていることがわかる。
今回の例では、両端の明るさは 0 および 1 なので、2乗しても変わらない。
真ん中の明るさは単純計算だと 0.5 となるが、この平方根をとると約 0.707 となり、約 1.41 倍の値を書き込むことになる。
一方、単純計算の 0.5 を2乗すると 0.25 となり、動画の主張 (画像ファイルに記録された値の2乗の明るさで表示される) が正しいとすれば、単純計算だと本来の半分の明るさになることがわかる。

RGB 色空間との関係

そういえば、リサイズを含む画像処理を行う際は JPEG 形式のデフォルトなどとして用いられる sRGB 空間だと暗くなるので、RGB 空間に変換してから処理しろ、という主張があった。

sRGB空間で画像処理するべからず - 豪鬼メモ

RGB と sRGB の変換は「単に2乗/平方根を取る」という単純なものではないはずである。

sRGB値を各種パラメータに変換あるいは逆変換する変換式一覧
によれば、0~1 の sRGB 値から 0~1 の RGB 値への変換は、以下の関数で行えるらしい。

f(x) = \left\{
\begin{array}{ll}
\frac{x}{12.92} & (x \leq 0.04045) \\
{\left(\frac{x + 0.055}{1.055}\right)}^{2.4} & (\textrm{otherwise})
\end{array}
\right.

この関数の逆関数を考えると、0~1 の RGB 値から 0~1 の sRGB 値への変換は、以下の関数で行えるはずである。

\begin{array}{l}
\\
f^{-1}(x) = \left\{
\begin{array}{ll}
12.92\,x & \left(x \leq \frac{0.04045}{12.92}\right) \\
1.055\,x^{5/12} - 0.055 & (\textrm{otherwise})
\end{array}
\right.
\end{array}

先ほどのプログラムに、これを用いた処理を追加し、再び画像を生成してみた。
上から、そのまま計算、2乗して計算、RGB に変換して計算、である。

生成を行うコード
from PIL import Image
from math import sqrt

width = 512
one_height = 64

left_color = (255, 0, 0)
right_color = (0, 255, 0)

out_file = "red_green_%dx%d_2.png" % (width, one_height)

img = Image.new("RGB", (width, one_height * 3))

def apply1(func, t):
    return tuple([func(v) for v in t])

def apply2(func, t1, t2):
    return tuple([func(v1, v2) for v1, v2 in zip(t1, t2)])

def to_max1(v):
    return v / 255.0

def from_max1(v):
    return round(v * 255)

def to_rgb(v):
    if v <= 0.04045:
        return v / 12.92
    return pow((v + 0.055) / 1.055, 2.4)

def from_rgb(v):
    if v <= 0.04045 / 12.92:
        return v * 12.92
    return 1.055 * pow(v, 5.0 / 12.0) - 0.055

def simple(ratio_of_2, v1, v2):
    return v1 * (1 - ratio_of_2) + v2 * ratio_of_2

def use_sqrt(ratio_of_2, v1, v2):
    return sqrt(simple(ratio_of_2, v1 * v1, v2 * v2))

def use_rgb(ratio_of_2, v1, v2):
    return from_rgb(simple(ratio_of_2, to_rgb(v1), to_rgb(v2)))

lc1 = apply1(to_max1, left_color)
rc1 = apply1(to_max1, right_color)

for y in range(one_height):
    for x in range(width):
        ratio = x / (width - 1)
        blend_simple = apply2(lambda v1, v2 : simple(ratio, v1, v2), lc1, rc1)
        blend_sqrt = apply2(lambda v1, v2 : use_sqrt(ratio, v1, v2), lc1, rc1)
        blend_rgb = apply2(lambda v1, v2 : use_rgb(ratio, v1, v2), lc1, rc1)
        img.putpixel((x, y), apply1(from_max1, blend_simple))
        img.putpixel((x, y + one_height), apply1(from_max1, blend_sqrt))
        img.putpixel((x, y + one_height * 2), apply1(from_max1, blend_rgb))

img.save(out_file)

生成結果は以下の画像になった。

色の計算方法の比較 (RGB追加)

2乗した値で計算を行った場合より、RGB に変換して計算を行った場合のほうが若干明るくなっている。
先ほどの式に当てはめると、RGB 値 (計算結果) が 0.5 のとき、sRGB 値 (書き込む値) は約 0.735 となり、平方根の約 0.707 より大きくなっていることがわかる。
とはいえ、工夫をしない場合に書き込む値の 0.5 に比べると、差は小さい。

さらに、それぞれの計算方法における、データ上の値と計算に使う値の関係をグラフにしてみた。

データ上の値と計算に使う値の関係

生成方法 (gnuplot)

グラフの領域が正方形になるように、画像のサイズを調整した。

set terminal pngcairo size 531, 512
set output "color_plot.png"

set grid
set grid mxtics
set grid mytics
set xrange [0:1]
set yrange [0:1]
set xtics 0.2
set mxtics 2
set ytics 0.2
set mytics 2
set key top left
set xlabel "データ上の値"
set ylabel "計算に使う値"

plot x title "そのまま" linecolor "forest-green", \
	x*x title "2乗" linecolor "red", \
	x <= 0.04045 ? x/12.92 : ((x + 0.055) / 1.055) ** 2.4 title "RGB" linecolor "blue"

値を2乗することにより、そのまま計算するよりも RGB に変換したときに近い値が得られることがわかる。
すなわち、正確さより速さを重視したいときなどに、「2乗」は「RGB に変換」の近似として十分役立つ可能性がある。

各種ソフトウェアでの処理結果

画像の拡大

まず、左側が赤、右側が緑の、幅2ピクセルの画像を用意した。

red_green_24.png

この画像を各種ソフトウェアで幅256ピクセルに拡大し、処理結果を比較した。

ソフトウェア 処理結果 処理結果中央の色
ペイント red_green_24_mspaint_256.png (127, 128, 0)
GIMP (線形) red_green_24_gimp_senkei_256.png (187, 188, 0)
GIMP (キュービック) red_green_24_gimp_cubic_256.png (187, 188, 0)
JTrim (Hermite) red_green_24_jtrim_hermite_256.png (127, 128, 0)
JTrim (Lanczos3) red_green_24_jtrim_lanczos3_256.png (161, 87, 0)
AzPainter2 (バイリニア) red_green_24_azpainter2_bairinia_256.png (0, 255, 0)
AzPainter2 (Lanczos2) red_green_24_azpainter2_lanczos2_256.png (126, 129, 0)
PictBear red_green_24_pictbear_256.png (0, 255, 0)
Photopea (バイリニア法) red_green_24_photopea_bairinia_256.png (127, 128, 0)
Photopea
(バイキュービック法)
red_green_24_photopea_bicubic_256.png (127, 128, 0)
paint.net (バイリニア法)
「ガンマ補正を使用する」オン
red_green_24_paintnet_bairinia_256.png (187, 188, 0)
paint.net
(バイキュービック法)
「ガンマ補正を使用する」オン
red_green_24_paintnet_bicubic_256.png (187, 188, 0)
paint.net
(バイキュービック法)
「ガンマ補正を使用する」オフ
red_green_24_paintnet_bicubic_nogamma_256.png (126, 129, 0)
Krita (バイリニア) red_green_24_krita_bairinia_256.png (127, 128, 0)
Krita (Lanczos3) red_green_24_krita_lanczos3_256.png (126, 129, 0)
FireAlpaca (画像解像度) red_green_24_filealpaca_256.png (0, 255, 0)
FireAlpaca (変形)
バイキュービック (シャープ)
red_green_24_filealpaca_henkei_bicubic_256.png (125, 129, 0)
ImageMagick
(colorspace 指定なし)
red_green_24_magick_nocolorspace_256.png (126, 129, 0)
ImageMagick
(colorspace 指定あり)
red_green_24_magick_withcolorspace_256.png (187, 188, 0)
LibreOffice Draw red_green_24_libreoffice_draw_256.png (127, 128, 0)
Firefox (img タグ) red_green_24_firefox_256.png (125, 129, 0)
Firefox (canvas) red_green_24_firefox_canvas_256.png (127, 127, 0)
Google Chrome
(img タグ)
red_green_24_googlechrome_256.png (127, 128, 0)
Google Chrome
(canvas 品質指定なし)
red_green_24_googlechrome_canvas_256.png (127, 128, 0)
Google Chrome
(canvas 品質 low)
red_green_24_googlechrome_canvas_low_256.png (127, 128, 0)
Google Chrome
(canvas 品質 middle)
red_green_24_googlechrome_canvas_medium_256.png (127, 128, 0)
Google Chrome
(canvas 品質 high)
red_green_24_googlechrome_canvas_high_256.png (126, 129, 0)
補足

各ソフトウェアのバージョンは、以下の通りである。

ソフトウェア バージョン
ペイント 11.2311.30.0
GIMP 2.10.36
JTrim 1.53c
AzPainter2 2.12
PictBear 2.04
Photopea 不明 (2024年2月27日に実験)
paint.net V5.0.12 (Stable 5.12.8735.38135)
Krita 5.2.2
FireAlpaca 2.11.17
ImageMagick 7.1.1-12
LibreOffice Draw 7.5.7.1
Firefox 123.0
Google Chrome 122.0.6261.70

FireAlpaca で普通の補間ありの拡大を行う方法はわからなかった。
「変形」は、以下の手順で行った。

  1. 「編集 → キャンバスサイズ」で幅を 256 に設定する (「中央」を選択する)
  2. Ctrl + T を押し、レイヤーの変形モードに入る
  3. ドラッグでサイズを合わせる
  4. 「Ok」を押す

その結果、補間をかけることはできたが、左右の端がなぜか半透明になってしまった。

ImageMagick (colorspace 指定なし) は、以下のコマンドで変換を行った。

magick convert red_green_24.png -resize !256x24 red_green_24_magick_nocolorspace_256.png

ImageMagick (colorspace 指定あり) は、以下のコマンドで変換を行った。

magick convert red_green_24.png -colorspace rgb -resize !256x24 -colorspace srgb red_green_24_magick_withcolorspace_256.png

Firefox / Google Chrome (imgタグ) は、以下のように img タグの width 属性の指定による拡大を行った。

<img src="red_green_24.png" width="256" height="24" alt="">

Firefox / Google Chrome (canvas) は、CanvasRenderingContext2DdrawImage() メソッドを用いて描画を行った。
Google Chrome における品質の指定は、imageSmoothingQuality プロパティにより行った。

今回実験を行った多くのソフトウェアにおいて、処理結果中央の色は 127 付近の値となり、データ上の値をそのまま用いて計算しているらしいことがわかった。
「JTrim (Lanczos3)」「AzPainter2 (バイリニア)」「PictBear」は、色が切り替わる場所が中央からずれているが、色の中間地点ではやはり 127 付近の値となった。
そんな中、colorspace の指定を行った ImageMagick に加え、GIMP および「ガンマ補正を使用する」をオンにした paint.net でも、処理結果中央の色が 187 付近となり、RGB に変換して処理を行っていそうであることがわかった。

ぼかし

左半分が赤、右半分が緑の、幅256ピクセルの画像を用意した。

red_green_256x24_bin.png

生成コード
from PIL import Image

width = 256
height = 24

left_color = (255, 0, 0)
right_color = (0, 255, 0)

out_file = "red_green_%dx%d_bin.png" % (width, height)

img = Image.new("RGB", (width, height))

for y in range(height):
    for x in range(width):
        img.putpixel((x, y), left_color if x < width // 2 else right_color)

img.save(out_file)

この画像に対し、各種ソフトウェアでぼかしを行い、結果を比較してみた。

ソフトウェア 設定 処理結果
GIMP ガウスぼかし
サイズ: 16
red_green_256x24_bin_gimp_bokasi16.png
GIMP ガウスぼかし
サイズ: 32
red_green_256x24_bin_gimp_bokasi32.png
GIMP ガウスぼかし
サイズ: 64
red_green_256x24_bin_gimp_bokasi64.png
GIMP ガウスぼかし
サイズ: 128
red_green_256x24_bin_gimp_bokasi128.png
JTrim ガウスぼかし
ぼかしのレベル: 10
red_green_256x24_bin_jtrim_bokasi10.png
AzPainter2 ガウスぼかし
強さ: 128
red_green_256x24_bin_azpainter_bokasi128.png
PictBear ぼかし強 red_green_256x24_bin_pictbear_bokasi_kyou.png
Photopea ぼかし(ガウス)
半径: 64 px
red_green_256x24_bin_photopea_bokasi64.png
Photopea ぼかし(ガウス)
半径: 128 px
red_green_256x24_bin_photopea_bokasi128.png
paint.net ぼかし(ガウス)
半径: 64
red_green_256x24_bin_paintnet_bokasi64.png
paint.net ぼかし(ガウス)
半径: 128
red_green_256x24_bin_paintnet_bokasi128.png
Krita ガウシアンぼかし
半径: 64 px
red_green_256x24_bin_krita_bokasi64.png
Krita ガウシアンぼかし
半径: 128 px
red_green_256x24_bin_krita_bokasi128.png
FireAlpaca ガウスぼかし
値: 32
red_green_256x24_bin_firealpaca_bokasi32.png
FireAlpaca ガウスぼかし
値: 64
red_green_256x24_bin_firealpaca_bokasi64.png
FireAlpaca ガウスぼかし
値: 128
red_green_256x24_bin_firealpaca_bokasi128.png
補足

ソフトウェアのバージョンは、拡大の実験で使用したものと同じである。

GIMP の設定における「サイズ」とは Size X および Size Y (共通) のことであり、サイズ以外の設定は以下の通りである。

GIMP のガウスぼかし設定

paint.net における半径以外の設定は以下の通りである。

paint.net のぼかし(ガウス)設定

GIMP および paint.net では処理結果の中央付近が黄色になっているが、今回実験を行った他のソフトウェアでは中央付近が暗くなってしまっている。
また、ソフトウェアによってはぼかしの強さに限界があること、および同じような値を設定してもソフトウェアによってぼかしの強さに差があることもわかった。

まとめ

  • sRGB 色空間の画素値をそのまま計算に用いると結果が暗くなりやすいので、RGB 色空間に変換してから計算するべきである
  • 「RGB 色空間に変換する」かわりに、0~1 の画素値を2乗することで近似できる
  • GIMP、paint.net、ImageMagick は RGB 色空間での画像操作に対応しているようだが、対応せず暗い処理結果を生成してしまうソフトウェアも多い
2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?