"""回帰テスト採取用の単色 PNG を生成する(PLAN_02 ステップ 5).
旧実装 exe は «画像を読み込んでクリックした ROI の平均色» しか入力にできず,
RGB / L*a*b* を直接入力する手段が無い.また,クリック位置は手作業では固定できない
(PLAN_02 の注記).そこで **画像全面を単一色で塗った PNG** を読み込ませることで,
クリック位置によらず ROI 平均色 = 画像の色(既知の RGB)となるようにする.
出力サイズは既定で 1024×1280(旧実装のクロップ後サイズ)とし,
旧 exe のクロップ(中央基準の切り出し)が入っても色が変わらないようにする.
使い方(詳細は docs/06_TEST/TEST_01_回帰テスト採取手順.md):
```bash
uv run python scripts/generate_regression_images.py --out-dir out/regression_images
```
色を指定する場合(`R,G,B` を空白区切りで複数指定できる):
```bash
uv run python scripts/generate_regression_images.py --out-dir out/images \
--color 156,102,98 --color 0,0,0
```
"""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
import cv2
import numpy as np
from numpy.typing import NDArray
#: 旧実装がクロップ後に扱う画像サイズ(SPEC_01「画像選択」)
DEFAULT_WIDTH = 1024
DEFAULT_HEIGHT = 1280
#: 既定の採取ケース(R, G, B, 意図).
#: 「基準色」は旧実装のコメントに残る平均 BGR {98.4, 102.27, 156.4}(= RGB 156,102,98)に由来する.
#: 代表的な舌色をその周辺に散らし,加えて境界値・学習データ域外(診断失敗が出うる)色を含める
DEFAULT_COLORS: tuple[tuple[int, int, int, str], ...] = (
(156, 102, 98, "基準色(旧実装コメントの平均 BGR 由来)"),
(180, 120, 115, "やや明るい舌色"),
(205, 145, 140, "淡い舌色(淡紅・淡白寄り)"),
(140, 85, 85, "やや暗い舌色"),
(115, 62, 68, "暗い舌色(暗赤・紫寄り)"),
(170, 92, 112, "赤紫寄り"),
(196, 74, 74, "鮮やかな紅寄り"),
(150, 110, 130, "青み寄り"),
(255, 255, 255, "境界値: 白(明るさの上限)"),
(0, 0, 0, "境界値: 黒(XYZ が 0 にクリップされる想定)"),
(255, 0, 0, "域外: 純赤"),
(0, 0, 255, "域外: 純青"),
)
def build_solid_image(color: tuple[int, int, int], width: int, height: int) -> NDArray[np.uint8]:
"""指定 RGB の単色画像(OpenCV の BGR 配列)を作る."""
red, green, blue = color
image = np.empty((height, width, 3), dtype=np.uint8)
image[:, :] = (blue, green, red)
return image
def parse_color(text: str) -> tuple[int, int, int]:
"""`R,G,B` 形式の文字列を (R, G, B) に変換する."""
parts = [part.strip() for part in text.split(",")]
if len(parts) != 3:
raise argparse.ArgumentTypeError(f"色は R,G,B の 3 要素で指定してください: {text!r}")
try:
values = tuple(int(part) for part in parts)
except ValueError as error:
raise argparse.ArgumentTypeError(f"色は整数で指定してください: {text!r}") from error
if any(not 0 <= value <= 255 for value in values):
raise argparse.ArgumentTypeError(f"色は 0〜255 の範囲で指定してください: {text!r}")
return values[0], values[1], values[2]
def image_name(color: tuple[int, int, int]) -> str:
"""RGB からファイル名を作る(例: `rgb_156_102_098.png`)."""
red, green, blue = color
return f"rgb_{red:03d}_{green:03d}_{blue:03d}.png"
def write_images(
colors: list[tuple[int, int, int]], out_dir: Path, width: int, height: int
) -> list[Path]:
"""単色 PNG を書き出し,書き出したパスの一覧を返す."""
out_dir.mkdir(parents=True, exist_ok=True)
written: list[Path] = []
for color in colors:
path = out_dir / image_name(color)
if not cv2.imwrite(str(path), build_solid_image(color, width, height)):
raise OSError(f"PNG を書き出せませんでした: {path}")
written.append(path)
return written
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
"""コマンドライン引数を解釈する."""
parser = argparse.ArgumentParser(description="回帰テスト採取用の単色 PNG を生成する")
parser.add_argument(
"--out-dir",
type=Path,
required=True,
help="出力先ディレクトリ(リポジトリ外・作業用フォルダを推奨)",
)
parser.add_argument(
"--color",
type=parse_color,
action="append",
dest="colors",
metavar="R,G,B",
help="生成する色(複数指定可).省略時は既定の採取ケースを生成する",
)
parser.add_argument("--width", type=int, default=DEFAULT_WIDTH, help="画像の幅(px)")
parser.add_argument("--height", type=int, default=DEFAULT_HEIGHT, help="画像の高さ(px)")
return parser.parse_args(argv)
def main(argv: list[str] | None = None) -> int:
"""単色 PNG を生成し,採取表の雛形(CSV の行)を標準出力に出す."""
args = parse_args(argv)
colors = args.colors or [(red, green, blue) for red, green, blue, _ in DEFAULT_COLORS]
notes = {(red, green, blue): note for red, green, blue, note in DEFAULT_COLORS}
written = write_images(colors, args.out_dir, args.width, args.height)
print(f"{len(written)} 枚を生成しました: {args.out_dir}")
print("")
print("# 採取表の雛形(tests/fixtures/regression/rgb_to_lab.csv 用)")
print("matrix,r,g,b,l_star,a_star,b_star,note")
for color in colors:
red, green, blue = color
print(f"<変換行列ファイル名>,{red},{green},{blue},,,,{notes.get(color, '')}")
print("")
print("# 採取表の雛形(tests/fixtures/regression/rgb_to_scores.csv 用)")
print("matrix,r,g,b,purple,dark_red,crimson,pink,pale,failed,note")
for color in colors:
red, green, blue = color
print(f"<変換行列ファイル名>,{red},{green},{blue},,,,,,,{notes.get(color, '')}")
return 0
if __name__ == "__main__":
sys.exit(main())