🔴

JPEGさんは赤がお嫌い?

2024/03/15に公開

概要

JPEGさんのせいで赤毛がノイズまみれになって悲しそうな女の子のイラスト
generated by ChromaXL_v1b[1]

「JPEGさんって、私みたいな赤毛の子がお嫌いなのかしら? だからブロックノイズでいっぱいにしたのね」

「JPEGは赤が苦手」の謎

みなさんは「JPEG」という画像フォーマットについて、どのような知識あるいはイメージを持っていますか?

  • 非可逆圧縮を行うフォーマットである
  • 圧縮率を調整する「品質」というパラメータがある
  • 写真の圧縮が得意なフォーマットである
  • アニメ調のイラストやドット絵にJPEGを使うとノイズが多くなる
  • 容量が小さいのでインターネット上でよく使われる
  • ここにあなたのJPEGに対するイメージを書く

「JPEGは赤が苦手」のウワサ

この他によく聞くものとして「赤色を表現するのが苦手」というイメージを持っている人もいるかもしれません。Wikipediaにも「特に赤には弱く、赤の部分でノイズが発生しやすい」と記されており、画像をよく扱う人にとってはもはや常識に近い知識といえます。

Googleで「JPEG 赤」「JPEG 赤 劣化」などと調べても同様に「JPEGは赤色が劣化しやすく、ノイズが発生しやすいのだ」という記述がいくらでも見つかります。

JPG形式は圧縮によって赤の色域が劣化しやすい
JPGは赤が苦手?画像拡張子の種類や違い、劣化を防ぐ選び方とは? | Beginner's Design Noteより

イラストを変換しててランドセル部分の赤色の滲みが目立つ。
WebPは赤色が滲んだり、画質にちょっと不満に思うところがある | BABELより

特に赤には弱く、小さくすると赤の部分でノイズがよく発生します。
小川技研サイト: jpeg/gifより

JPGなら赤が劣化するのは避けられません。
メディバンで書き出しをすると赤色が劣化してしまうのですがどうしたらいいですか? - Yahoo!知恵袋より

jpgは赤系の色が特に劣化しやすいよ。
Twitterにアップする画像は自動的にjpgに変換されるから、赤の多い画像は画質が悲惨になりがちだよ。
ものろーぐ(@HitorinoNight):1018512947583176704より

まあただ、jpgだと赤がめちゃくちゃノイズ入ることすら知らないトピ主だからな…そんなこと知らないか。
ツイッターで画像を投稿すると本当に画質が悪くなって困っています。 | cremu(クレム)より

ここでいう「赤色が劣化する」「赤色の部分でノイズが発生する」とは、おおよそ以下のような現象を指していそうです。

赤色が劣化する画像例の比較
赤色が劣化する画像例の比較

鮮やかな赤色の線でできた左の原画像と比較して、右のJPEG画像は赤色がくすんでいて、周囲にもやもやとしたノイズが出ています。

しかし、なぜこの現象が起こるのか、その理由について深く掘り下げている人はあまり見つかりません。多くの人が経験的に常識だと思っているのに、誰もその仕組みを知らないなんてなんだか不思議ですね。少なくともJPEGは現代の人間が理解できるアルゴリズムであり、神秘的な力を借りたり、未来の科学技術を先取りした 魔法圧縮 ではないはずです。

仮説

ここで、なぜJPEGで赤色が劣化するのか、いくつか仮説を考えてみます。

まず考えられるのは、JPEGというフォーマット自体が赤色の情報を捨てて圧縮している、という理由です。なぜ赤色を捨てるのか……写真によく登場するのが植物の緑や空の青で、赤色が入るケースは少ないので設計段階で赤色を捨てることにした、というのはどうでしょう? インターネットで広く使われるにはあまりに出来が悪い気がしますが、そこは業界のゴリ押しで普及させて――いや、仮説にしても無茶ですね。

あるいは、JPEG自身は特定の色を捨てるよう設計されているわけではないが、人間の目には赤色の差がよく分かる傾向がある、というのもあるかもしれません。色相に関係なく同じような劣化やノイズが発生していても、赤色に近い部分だけを見分けやすいなら「JPEGのせいで赤色が劣化する」と表現したくなるでしょう。

もう一つあるとすれば、JPEGが苦手なパターンと赤色が一緒に出てくる傾向が強い、という理由でしょうか。JPEGは細かいパターンの圧縮が苦手なはずで、そのような細かい表現に赤色が多ければ「赤色はノイズが出やすい」という印象に繋がりそうです。赤が劣化したという悪い記憶ばかり残っているマーフィーの法則っぽい主張で、これは合理的な検証が難しいかもしれません。

JPEGの処理方法から見た「赤」の扱い

さて、まずはJPEGというフォーマット自体について掘り下げてみましょう。JPEGは可逆圧縮に対応していたり、表色系としてCMYKを使ったりできますが、それらの解説は他サイトやコメントでの指摘に譲ります。ここでは、RGBで表現された原画像を、YCbCrを表色系とした不可逆圧縮なJPEGに変換する過程について考えていきます。

JPEGの処理の流れ

JPEGは画像を以下の手順で圧縮します。

  1. 原画像のRGB表現をYCbCr表現に変換する
  2. 必要に応じてCb成分とCr成分を間引く(クロマサブサンプリング)
  3. 各成分の2次元空間表現を8x8ピクセルのブロックに分割する
  4. 離散コサイン変換で8x8ブロックを周波数領域に変換する
  5. 得られた周波数領域表現の低周波部分を優先して量子化する
  6. 量子化されたデータをハフマン符号で圧縮する

気になるのは、RGBからYCbCrに変換して間引く部分(1と2)と、それを離散コサイン変換・量子化して情報量を削減する部分(4と5)ですね。それぞれ表色系が変わったり、色の成分が削減されている部分です。3はただのデータ分割、6は可逆圧縮なので、画像の品質とは関係ありません。

RGBとYCbCr

そもそもYCbCrという表色系は、明るさを示すY、青色っぽさを示すCb、赤色っぽさを示すCrを組み合わせて色を表現するものです。RGBからYCbCrへの変換でビット数を減らすわけではないので、直ちに情報量が変わるわけではありません。しかし、人間の視覚が持つ「明るさの変化には敏感で、色の変化には鈍感」という特徴を踏まえると、RGBでは全てのビットが均等に重要である[2]一方、YCbCrではYに重要な情報が集まっていると考えることができます。逆に、CbとCrは人間の視覚にとっては薄い情報といえます。

そのため、CbとCrを周辺のピクセルと合わせて間引いても、人間の視覚から見るとあまり影響がありません。これがクロマサブサンプリングと呼ばれる操作で、水平方向に対して2ピクセル→1ピクセルに間引いたり(4:2:2)、水平・垂直方向の両方に4ピクセル→1ピクセルに間引いたり(4:2:0)することができます。

ここで、一定のビット数を保ってRGB→YCbCrに変換し、さらにRGBに戻すことで、どのような色が失われやすいかを考えてみましょう。これは、原画像をJPEGにエンコードして、変換後のJPEGファイルを表示する処理に相当します。RGBとYCbCrの変換にはITU-R BT.601を使います。

RGB→YCbCr→RGB変換の概略図
RGB→YCbCr→RGB変換の概略図

#!/usr/bin/env python3

from collections import Counter

import numpy as np

# RGB空間の定義(24bit)
RGB = np.array(
    np.meshgrid(
        np.arange(256),
        np.arange(256),
        np.arange(256)
    )
)

# RGB -> YCbCr変換(24bit)
Y = np.array(
    RGB[0] * 0.299 + RGB[1] * 0.587 + RGB[2] * 0.114,
    dtype="i8"
).flatten()
Cb = np.array(
    -0.1687 * RGB[0] - 0.3313 * RGB[1] + 0.5 * RGB[2] + 128,
    dtype="i8"
).flatten()
Cr = np.array(
    0.5 * RGB[0] - 0.4187 * RGB[1] - 0.0813 * RGB[2] + 128,
    dtype="i8"
).flatten()

# YCbCr -> RGB変換(24bit)
R = np.array(
    (Y + 1.402 * (Cr - 128)),
    dtype="i8"
)
G = np.array(
    (Y - 0.34414 * (Cb - 128) - 0.71414 * (Cr - 128)),
    dtype="i8"
)
B = np.array(
    (Y + 1.772 * (Cb - 128)),
    dtype="i8"
)

# 結果出力
rgb_unique_set = set(zip(R, G, B))
rgb_all_count = 256 ** 3
rgb_count = len(rgb_unique_set)
rgb_part_count = Counter([
    max((("R", c[0]), ("G", c[1]), ("B", c[2])), key=lambda x: x[1])[0]
    for c in zip(RGB[0].flatten(), RGB[1].flatten(), RGB[2].flatten())
])["R"]
r_count = Counter([
    max((("R", c[0]), ("G", c[1]), ("B", c[2])), key=lambda x: x[1])[0]
    for c in rgb_unique_set
])["R"]
g_count = Counter([
    max((("G", c[0]), ("B", c[1]), ("R", c[2])), key=lambda x: x[1])[0]
    for c in rgb_unique_set
])["G"]
b_count = Counter([
    max((("B", c[0]), ("R", c[1]), ("G", c[2])), key=lambda x: x[1])[0]
    for c in rgb_unique_set
])["B"]
rgb_rate = 100 * rgb_count / 256 ** 3
r_rate = 100 * r_count / (256 ** 3 / 3)
g_rate = 100 * g_count / (256 ** 3 / 3)
b_rate = 100 * b_count / (256 ** 3 / 3)
print(f"""RGBの復元率: {rgb_rate:.2f}% ({rgb_all_count:>8} -> {rgb_count:>7})
R  の復元率: {r_rate:.2f}% ({rgb_part_count:>8} -> {r_count:>7})
 G の復元率: {g_rate:.2f}% ({rgb_part_count:>8} -> {g_count:>7})
  Bの復元率: {b_rate:.2f}% ({rgb_part_count:>8} -> {b_count:>7})""")
対象 復元率 元の色数 変換後の色数
色空間全体 23.86% 16,777,216色 4,002,405色
赤系統色 23.88% 5,625,216色 1,335,423色
緑系統色 23.88% 5,625,216色 1,335,423色
青系統色 23.88% 5,625,216色 1,335,423色

結果を確認すると、どうやらRGB空間よりもYCbCr空間の方が4倍ほど広いことが分かりました。復元率を計算したところ、色の系統[3]にかかわらず24%程度しか復元できていません。これは、YCbCr空間はその広い領域を網羅するために24ビットを使っており、RGB空間からYCbCr空間へ射影しうる狭い領域は22ビットほどの情報量しか持たないことを示しています。

この結果を見ると、YCbCr自体は赤色を軽視しているわけではなさそうです。しかし、もしかしたら彩度や明度によって失いやすい色があるのかもしれません。先に示した赤色の劣化では、鮮やかな赤色がくすんだ見た目になってしまうのが問題になっていました。仮に鮮やかな色の75%がくすんだ色に押し込められるなら、見た目は大きく変わってしまいます。検証のために、赤・緑・青のそれぞれに対して彩度と明度のグラデーション(色相を固定)に対して同様の変換を行いました。

#!/usr/bin/env python3

import numpy as np
from PIL import Image

mesh_x, mesh_y = np.array(np.meshgrid(np.arange(256), np.arange(256)))

# メインカラーのグラデーション
maincolor_mesh = 255.0 - mesh_y
# サブカラーのグラデーション
subcolor_mesh_unclip = np.sqrt(mesh_x * mesh_x + mesh_y * mesh_y)
subcolor_mesh = (255.0 - subcolor_mesh_unclip) * (subcolor_mesh_unclip <= 255)

for x in zip(
    ["R", "G", "B"],
    [
        np.array([maincolor_mesh, subcolor_mesh, subcolor_mesh]),
        np.array([subcolor_mesh, maincolor_mesh, subcolor_mesh]),
        np.array([subcolor_mesh, subcolor_mesh, maincolor_mesh]),
    ]
):
    # RGB空間の定義(24bit)
    name, RGB = x

    # RGB -> YCbCr変換(24bit)
    Y = np.array(
        RGB[0] * 0.299 + RGB[1] * 0.587 + RGB[2] * 0.114,
        dtype="i8"
    ).flatten()
    Cb = np.array(
        -0.1687 * RGB[0] - 0.3313 * RGB[1] + 0.5 * RGB[2] + 128,
        dtype="i8"
    ).flatten()
    Cr = np.array(
        0.5 * RGB[0] - 0.4187 * RGB[1] - 0.0813 * RGB[2] + 128,
        dtype="i8"
    ).flatten()

    # YCbCr -> RGB変換(24bit)
    R = np.array(
        (Y + 1.402 * (Cr - 128)),
        dtype="i8"
    )
    G = np.array(
        (Y - 0.34414 * (Cb - 128) - 0.71414 * (Cr - 128)),
        dtype="i8"
    )
    B = np.array(
        (Y + 1.772 * (Cb - 128)),
        dtype="i8"
    )

    # 結果出力
    converted_array = np.dstack([
        (R * (R >= 0)).reshape((256, 256)),
        (G * (G >= 0)).reshape((256, 256)),
        (B * (B >= 0)).reshape((256, 256))
    ])
    base_array = np.dstack(RGB)
    MSE = np.mean((converted_array.astype(float) - base_array.astype(float)) ** 2)
    PSNR = 10 * np.log10((255 ** 2) / MSE)

    print(f"{name}のPSNR: {PSNR:.4f}")
    Image.fromarray(converted_array.astype("uint8")).convert("RGB").save(f"{name}_ycbcr.png")
    Image.fromarray(base_array.astype("uint8")).convert("RGB").save(f"{name}_rgb.png")

RGB→YCbCr→RGB変換を行ったグラデーション画像の比較
RGB→YCbCr→RGB変換を行ったグラデーション画像の比較

左側が原画像で、右側がRGB→YCbCr→RGB変換を行ったものです[4]。こちらの結果も確認すると、グラデーションの一部にバンディングのようなパターンがわずかに生じただけで、鮮やかな色だけが特に劣化するような現象は起きませんでした。また、色の変化は各成分で256階調(8ビット)中最大でも3階調程度であり、RGB空間がYCbCr空間の4分の1程度の広さであることから、色相によらずこの傾向が高いと予想できます。

以上より、YCbCrは赤色はもちろん、鮮やかな色を選択的に捨てるわけではないことが分かりました。

クロマサブサンプリング

では、クロマサブサンプリングはどうでしょうか? クロマサブサンプリングは周辺のピクセルを合わせて赤色成分を間引きますが、同じように青色成分も間引きます。仮にこれが赤色を劣化させる原因であれば、同じように「青色が劣化してる!」「青色のノイズがやばい!」と騒ぐ青髪キャラ好きが出てくるはずです。赤髪キャラ好きの方が声が大きく、主張が激しいわけではありませんよね。

また、クロマサブサンプリングの影響を受けるのは赤と青だけではありません。緑色もCb成分とCr成分を少しずつ利用しているので、これらの情報が削減されれば緑色も影響を受けます。クロマサブサンプリングが保証するのは明るさを保持した圧縮であり、緑色の情報は他の色と同様に削られます。

このことが直感的に分かるよう、赤・青・緑で同じパターンの画像を用意し、クロマサブサンプリングの有無でどのように出力結果が変化するかを示します。

# クロマサブサンプリングを行って画像を生成
convert input.png -sampling-factor 4:2:0 -quality 50 output_420.jpg

# クロマサブサンプリングを行わずに画像を生成
convert input.png -sampling-factor 4:4:4 -quality 50 output_444.jpg

赤・青・緑の各色におけるクロマサブサンプリングの影響比較
赤・青・緑の各色におけるクロマサブサンプリングの影響比較

左側が原画像で、中央がクロマサブサンプリングあり、右側がクロマサブサンプリングを行っていないものです。色によってノイズにどれほど差があるのか、さらにPSNRで定量的に確認してみましょう。

compare -metric PSNR input.png output_420.jpg NULL:
compare -metric PSNR input.png output_444.jpg NULL:
クロマサブサンプリングあり(4:2:0) クロマサブサンプリングなし(4:4:4)
23.7649 27.2341
23.5496 27.7646
21.9544 25.9038

出力された画像およびPSNRによる評価を見ると、クロマサブサンプリングは全ての色を同様に間引く傾向があることを確認できました。色が劣化してくすんで見える現象は、どうやらクロマサブサンプリングのせいだったようです。PSNRによれば、赤と青はほぼ同じレベルで、緑色に至っては他の色よりも品質が低くなっています。

クロマサブサンプリングでは赤と青が劣化してしまうという指摘は、インターネットでもいくつか見つけることができます。

青と赤の劣化が激しい。
完全に違う色になっている。
4:2:0 は、色差成分が 1/4 サイズになるので、どうしても青と赤が劣化する。
青と赤成分の画像サイズが 1/4 になっているのに近い。
楓 software: なぜ JPEG XR ?より

こちらのサイトには、先に示したような細い線の色が劣化する現象を示す画像が掲載されています。しかし、なぜか赤と青でしか確認しておらず、そのせいで「赤と青が特に劣化する(緑色は関係ない?)」という微妙に正しくない結論に至っているようでした。Cb成分とCr成分が間引かれているのは正しいですが、全ての色が影響を受けています。

クロマサブサンプリングを行わなかった場合は、色の鮮やかさもPSNRも向上しました。1ピクセルの線は4:2:0では周囲の白色に引きずられて彩度を失ってしまいますが、4:4:4であれば自らの色だけを使って彩度をほとんど変えずに変換できます。

以上より、クロマサブサンプリングには鮮やかな色の細かい表現をくすんだ色に変えてしまう性質があるものの、これは赤色に限った傾向ではないことが分かりました。

離散コサイン変換と量子化

離散コサイン変換は、RGB空間からYCbCr空間に変換するのと同じように重要な情報を集中させる効果があります。画像から切り出されたばかりの8x8ピクセルのブロックは、場所によらず全てのピクセルが重要である可能性が高いです。これを離散コサイン変換で周波数領域に射影すれば、ブロックの内容にかかわらず左上のパラメータほど重要な情報を集めた表現に変換することができます。詳細については他サイトなどを参照してください。

離散コサイン変換自体はYCbCrと同様に情報量を減らすためのものではなく、64バイトの空間ブロックを同じく64バイトの周波数領域ブロックに変換するという処理です。しかし、左上のパラメータが重要であるという特徴を持たせることで、後続の量子化処理の品質を高く保ち、そして制御可能なものにします。

量子化とは、一般的には連続的なアナログ値を離散的なデジタル値で近似的に表現する処理です。そもそも連続的な色の強さを8ビットの256階調に割り当てるのも量子化でした。JPEGにおいて離散コサイン変換のあとに行う量子化では、右下のパラメータの情報量を削減するために、周波数領域ブロックを位置に応じた値で割っていきます。大まかには、量子化テーブルの値が16なら256階調(8ビット)が16階調(4ビット)になるのです。

JPEGの不可逆圧縮の本質はここにあり、さらにハフマン符号化を施して実際のバイナリも情報量に見合ったサイズになるよう圧縮します。

<levels width="8" height="8" divisor="1">
  16,  11,  10,  16,  24,  40,  51,  61,
  12,  12,  14,  19,  26,  58,  60,  55,
  14,  13,  16,  24,  40,  57,  69,  56,
  14,  17,  22,  29,  51,  87,  80,  62,
  18,  22,  37,  56,  68, 109, 103,  77,
  24,  35,  55,  64,  81, 104, 113,  92,
  49,  64,  78,  87, 103, 121, 120, 101,
  72,  92,  95,  98, 112, 100, 103,  99
</levels>

上記のような量子化テーブル自体は各成分ごとに個別に指定できるため、Cr成分のみ大きく削るテーブルを割り当てれば「赤色を軽視するJPEGファイル」を作ることができるかもしれません。しかし、ほとんどの場合はY成分の量子化テーブルと、Cb成分とCr成分で共用の量子化テーブルの2つを使うという設定が多いです。仮にこのようなJPEGエンコーダを使っているせいで赤色の劣化が気になるなら、PNGを使うより先に画像処理ソフトを新調した方がよいでしょう。

参考のために、Cr成分を大きく削った量子化テーブルを指定したJPEGファイルを作ってみます。

convert dqt.png -sampling-factor 4:2:0 -quality 50 -define jpeg:q-table=break-red-quantization-table.xml dqt.jpg

赤と緑が強い部分のノイズが強い画像例の比較
赤と緑が強い部分のノイズが強い画像例の比較

原画像出力された画像を比較すると、赤と緑が強い場所で大きくブロックノイズが出ており、Cr成分に問題があることを確認できました。赤だけではなく緑でも品質が低下するのは、緑色の計算にCr成分が使われているという事実とも一致します。Cb成分を削る量子化テーブルなら、青と緑が影響を受けた結果を得られるでしょう。

以上より、離散コサイン変換と量子化には赤色を無視するよう設定する余地があるものの、ふつうの利用ケースではほぼ問題にならないことが分かりました。

まとめ

この節では、JPEGの処理方法を掘り下げることで赤色を劣化させる原因を探しました。

  • RGB空間からYCbCr空間に変換する処理は、色の変化にはほとんど影響がありません。
  • クロマサブサンプリングでは、赤色に限らず全ての色を劣化させる場合があります。
  • 離散コサイン変換は、明るさや色差の成分に関係なく同じ処理で変換されます。
  • 量子化ではCr成分のみ無視するよう割り当てる余地がありますが、通常は問題になりません。

以上から、JPEGというフォーマット自体には赤色だけを選択的に捨てる処理も意図も存在しないことが分かりました。そのため、冒頭の「JPEGは赤が劣化しやすく、ノイズが発生しやすいのだ」という主張はやはり正確ではなく、そう感じてしまう理由は別の部分にありそうです。

人間の目から見た「赤」の扱い

JPEGに問題がないことが分かったので、2つめの仮説について検討します。

引き続き「JPEG 赤」「JPEG 赤 劣化」などと調べたところ、JPEGのフォーマットではなく人間の視覚に原因がある旨の記載を見つけられました。色覚異常のない人間の目においては、赤色の違いを識別しやすいのでは?ということですね。

赤は波長が長いので、劣化が見つけやすい。
人間の目玉の構造の問題だと思います。
jpg形式で保存の際の赤の劣化について | OKWAVEより

人間の視覚の働きとして、赤に対して敏感なので、赤のほうが気が付きやすいせいだと思います。
jpgで赤色を保存すると画質が荒れるのはなぜでしょうか? - Yahoo!知恵袋より

まず、波長の長い光を見分けやすいという点について考えてみます。色覚異常のない人間(標準観測者)の視覚では、光の波長に対してどれほど明るく感じられるかという(明所視)標準比視感度曲線の最大値は555nm(黄緑色)です。赤(650nm)や青(450nm)はそれよりも感度が低く、緑 >>> 赤 > 青の順で強く感じます。波長が長ければ長いほど感じやすくなるわけではありません。

標準比視感度曲線
〈光とあかりの基礎知識〉標準比視感度(明所視) - Panasonic[1:1]

つまり、比視感度の差では、仮に青よりも赤の劣化を感じやすいことは説明できても、緑よりも赤の劣化を感じやすい説明がつきません。では、色による識別しやすさの違いの感覚はどこから生じるのでしょうか?

ここで、色彩に関する心理的効果を考慮してみます。赤や黄などの暖色は人間の注意を引きやすい色で、プレゼンや広告などで目立たせたい部分によく使われています。反対に、青や緑などの寒色は目立たず後退して見える色で、気持ちを落ち着ける効果があるといわれます。

この心理的効果を踏まえた上で、激しいブロックノイズが乗った赤い画像について、色相を変化させながら赤・橙・黄・緑・青・藍・紫の劣化がどれほど目立つかについて確認してみましょう。使用している画像は、冒頭で掲載したJPEGさんのご学友です。

同じノイズが乗った七色の画像例
generated by ChromaXL_v1b[1:2]

少なくとも私からは、赤・黄・緑あたりのノイズが強く見えています。みなさんはいかがでしょうか? 少し違う見え方に感じた人もいるかもしれませんが、概ね以下のような傾向を確認できました。

色の系統 誘目性の高さ 画像の明るさ ノイズの強さ
赤🔴 ⭐⭐⭐ ⭐⭐ 🔴🔴🔴
橙🟠 ⭐⭐ ⭐⭐ 🔴🔴
黄🟡 ⭐⭐⭐ ⭐⭐ 🔴🔴🔴
緑🟢 ⭐⭐⭐ 🔴🔴
青🔵 - 🔴
藍👖 - - -
紫🟣 ⭐⭐ 🔴🔴

目を引く色や、明るく鮮やかな色はノイズが目につきやすいです。特に誘目性が高い色は、人間の注意を引いて記憶に残りやすいと考えられます。実際には他の色でも明度や彩度が高い領域はノイズが目立ちやすいものの、画像全体を見たときには目が向かない可能性が高く、見過ごされるケースも多いのでしょう。仮に、3番目の仮説のように複雑なパターンの赤い領域でノイズを見かける頻度が高ければ、JPEGは赤が劣化しやすいという勘違いの原因になりそうです。

以上から、人間の視覚は色相に関係なく明度や彩度の差を大きく感じる性質があるものの、人間に与える心理的効果の差で赤色の部分に注意が集まりやすく、その結果として「JPEGでは赤色だけが特に劣化する」という誤認に繋がってしまうと結論づけることができそうです。

おまけ: 「赤」を劣化させない処理の検討

ここまで見てきたところ、どうもJPEG自体に赤色を劣化させる働きはないようです。インターネットでよく見かける「赤が劣化する」「赤の部分でノイズが発生する」という現象は、人間の視覚が引き起こすある種の錯覚のようなものだと分かりました。

最後におまけとして、JPEGが赤色だけを劣化させるわけではないという事実を 完全に無視 して「絶対に赤が劣化しないJPEG画像の作り方」を提案します。もちろんこの手法は全く意味がありませんが、今後「JPEGだと赤がめちゃくちゃノイズ入ることすら知らないのかよ」なんて言われたときに、誰でも直感的に分かる説明として持ち出せるでしょう。

  1. 赤色が多い画像(R)の色を全て反転して、赤色が少ない画像(B)に変換します。
  2. (B)をJPEGファイルに変換して送信します。JPEGは赤色だけを劣化させるので、赤色が少ない(B)をJPEGに変換することで劣化を抑えられます。
  3. (B)を受け取った人は、JPEGファイルをデコードして得られた画像を反転して(R')を得ます。
  4. (R')は、(R)を直接JPEGファイルに変換した場合よりも劣化が少ない画像のはずです。
##### そのままJPEGに変換してからPNGに戻す #####
# 赤い部分にノイズが出ちゃって困る
convert original.png -sampling-factor 4:2:0 -quality 50 original.jpg
convert original.jpg result_original.png

##### 反転+JPEGに変換してから反転+PNGに戻す #####
convert original.png -negate nega.png
# 赤い部分は水色に反転しているので全く影響なし!
convert nega.png -sampling-factor 4:2:0 -quality 50 nega.jpg
# 本当に?
convert nega.jpg -negate result_nega.png

「絶対に赤が劣化しないJPEG画像の作り方」の結果比較
初日の出と富士山のイラスト(2024年) by みふねたかし[1:3]

通常の変換ネガ経由の変換を重ねたり並べたりして直接見比べてみてください。うーん、最後までノイズたっぷり! 残念でした。

まとめ

PNGさんの目から見た悲しそうな赤毛の女の子のイラスト
generated by ChromaXL_v1b[1:4]

「私、赤毛ってとっても好きよ。きっと、周りの方の目がおかしいに決まっているわ」

転載元の「JPEGさんは赤がお嫌い?」はCC-BY 4.0(https://creativecommons.org/licenses/by/4.0/)でライセンスされているため、この記事についても同じライセンスが適用されます。

脚注
  1. 原画像およびその派生物はCC BY 4.0でライセンスされていません。 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  2. RGBの各成分が明るさに寄与する割合を考えると、厳密にはB→R→Gの順に重要さが高いというのが正しいです。 ↩︎

  3. ここでは、最も強い成分の系統に重複を許して属しているものとします。(r, g, b) = (255, 0, 0)は赤系統色、(0, 255, 255)は緑系統色と青系統色です。 ↩︎

  4. 他の色の結果はこちら: 赤:原画像, 赤:変換後, 緑:原画像, 緑:変換後, 青:原画像, 青:変換後 ↩︎

Discussion