"""セグメンテーションの評価データ(`<set>/sample/*` と `<set>/label/*`)の列挙と読み込み.
入力画像と正解マスクはファイル名の stem で対応づける.OpenCV の `imread` は Windows で日本語を
含むパスを読めないため,`np.fromfile` → `cv2.imdecode` で読む.例外メッセージにはファイル名を
出さず,評価セット内の何件目か(stem の昇順で 1 始まり)と件数で示す.
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
import cv2
import numpy as np
# 評価データとして読む画像の拡張子(小文字で比べる).
IMAGE_SUFFIXES: frozenset[str] = frozenset({".jpg", ".jpeg", ".png", ".bmp"})
@dataclass(frozen=True)
class ImagePair:
"""入力画像と正解マスクの組.
Attributes:
stem: ファイル名の stem(サーバ上の個票にだけ書く.ログ・例外には出さない).
sample: 入力画像のパス.
label: 正解マスクのパス.
"""
stem: str
sample: Path
label: Path
def _images_by_stem(directory: Path, what: str) -> dict[str, Path]:
if not directory.is_dir():
raise ValueError(f"{what}のディレクトリが無い: {directory}")
by_stem: dict[str, Path] = {}
duplicates = 0
for path in directory.iterdir():
if path.is_file() and path.suffix.lower() in IMAGE_SUFFIXES:
if path.stem in by_stem:
duplicates += 1
by_stem[path.stem] = path
if duplicates:
raise ValueError(f"{what}に stem が同じ画像が {duplicates} 件ある(拡張子違いの重複)")
return by_stem
def list_image_pairs(set_dir: Path, sample_dirname: str, label_dirname: str) -> list[ImagePair]:
"""評価セットの入力画像と正解マスクの組を stem の昇順で列挙する.
Args:
set_dir: 評価セットのディレクトリ.
sample_dirname: 入力画像のディレクトリ名.
label_dirname: 正解マスクのディレクトリ名.
Returns:
組の一覧(stem の昇順).
Raises:
ValueError: ディレクトリが無い,画像が 0 枚,stem の重複,または対応の取れない画像が
ある場合(件数のみを示す).
"""
samples = _images_by_stem(set_dir / sample_dirname, "入力画像")
labels = _images_by_stem(set_dir / label_dirname, "正解マスク")
only_sample = len(samples.keys() - labels.keys())
only_label = len(labels.keys() - samples.keys())
if only_sample or only_label:
raise ValueError(
f"入力画像と正解マスクの対応が取れない(正解マスクの無い入力画像 {only_sample} 件・"
f"入力画像の無い正解マスク {only_label} 件)"
)
if not samples:
raise ValueError(f"評価セットに画像が 0 枚である: {set_dir}")
return [ImagePair(stem, samples[stem], labels[stem]) for stem in sorted(samples)]
def _decode(path: Path, flags: int, where: str) -> np.ndarray:
try:
data = np.fromfile(path, dtype=np.uint8)
except OSError as exc:
raise ValueError(f"{where}: 画像を読めない({type(exc).__name__})") from None
image = cv2.imdecode(data, flags)
if image is None:
raise ValueError(f"{where}: 画像を復号できない")
return image
def read_bgr(path: Path, where: str) -> np.ndarray:
"""カラー画像を OpenCV の BGR で読む.
Args:
path: 画像のパス.
where: 例外メッセージに使う位置の表示(ファイル名を含めない).
Returns:
`(H, W, 3)` の uint8(BGR).
Raises:
ValueError: 読めない場合.
"""
return _decode(path, cv2.IMREAD_COLOR, where)
def read_label_mask(path: Path, threshold: int, where: str) -> np.ndarray:
"""正解マスクをグレースケールで読み,`threshold` 以上を前景とする.
Args:
path: 正解マスクのパス.
threshold: 前景とみなす下限の画素値.
where: 例外メッセージに使う位置の表示(ファイル名を含めない).
Returns:
`(H, W)` の bool.
Raises:
ValueError: 読めない場合.
"""
return _decode(path, cv2.IMREAD_GRAYSCALE, where) >= threshold