こんにちは、パレイド技術部です。
前回は連載の全体像と環境構築を紹介しました。
第2回は最初のアプローチ——SDXLチェックポイントでTVを生成し、OpenCVで画面領域を検出する方法を試していきます。
結論を先に書くと、このアプローチは一定の条件では動くものの、安定した自動化には至りませんでした。ただ、ここで得た知見が次のControlNet案の土台になっています。
アプローチの考え方
画像生成AIはプロンプト次第で多様な画像を出力します。「テレビの画面」を後工程で検出するなら、生成時点で画面が検出しやすい状態にしておくのが良さそうです。
具体的には、プロンプトに以下のような指定を加えます:
screen off, black screen— 画面を消灯状態にして黒い矩形にするmagenta screen, solid color display— 画面をマゼンタ一色にする(抽出しやすい)cyan screen, blue standby— シアンやブルーのスタンバイ画面にする
これを juggernautXL 等のリアル系チェックポイントで生成し、OpenCV の色検出で画面領域を取り出す作戦です。
Juggernaut XL は、RunDiffusion によって公開されている SDXL ベースの高品質画像生成モデルです。写真系・リアル系に強いチェックポイントで、Fooocus のデフォルトモデルとしても使われています。
プロンプト設計
今回は ComfyUI で juggernautXL やRealVisXL を使い、いくつかのプロンプトパターンを試しました。
# パターン1: 画面オフ
positive: retro CRT television in Japanese room, screen powered off,
black screen, warm lighting, photorealistic
negative: cartoon, 3d render, people, curved screen
# パターン2: マゼンタ画面
positive: retro CRT television in Japanese room,
magenta solid color on screen, vivid magenta display,
warm lighting, photorealistic
negative: cartoon, 3d render, people
# パターン3: シアン/ブルー画面
positive: retro CRT television in Japanese room,
cyan blue standby screen, solid blue display,
warm lighting, photorealistic
negative: cartoon, 3d render, people
生成パラメータは steps=35、cfg=4.5、sampler=dpmpp_2m_sde です。
OpenCV による検出: 3つの戦略
生成画像に対して、HSV色空間フィルタリングとCannyエッジ検出を組み合わせた3つの戦略を並列で走らせます。
戦略1: Cannyエッジ検出(画面オフ用)
黒い画面は周囲との明暗差が大きいため、エッジが明瞭に出ます。
import cv2
import numpy as np
def detect_by_edges(image_bgr: np.ndarray, min_area_ratio: float = 0.05):
"""Canny エッジから最大面積の四角形を探す"""
h, w = image_bgr.shape[:2]
gray = cv2.GaussianBlur(
cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY), (5, 5), 0
)
edges = cv2.Canny(gray, 50, 150)
# モルフォロジーでエッジを繋げる
edges = cv2.dilate(edges, None, iterations=2)
edges = cv2.erode(edges, None, iterations=1)
contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
candidates = []
for cnt in contours:
area = cv2.contourArea(cnt)
if area < h * w * min_area_ratio:
continue
quad = _approx_quad(cnt)
if quad is not None:
candidates.append((quad, area))
return candidates
戦略2: マゼンタ色検出
HSV色空間でマゼンタの色相範囲(140〜175)をフィルタリングします。
def detect_by_magenta(image_bgr: np.ndarray):
"""HSV でマゼンタ領域を検出"""
hsv = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2HSV)
lower = np.array([140, 50, 50])
upper = np.array([175, 255, 255])
mask = cv2.inRange(hsv, lower, upper)
# ノイズ除去
kernel = np.ones((7, 7), np.uint8)
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=2)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=1)
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
return _find_best_quad(contours, image_bgr.shape[:2])
戦略3: シアン/ブルー色検出
同様に、シアン〜ブルーの色相範囲(85〜130)をフィルタリングします。
def detect_by_cyan(image_bgr: np.ndarray):
"""HSV でシアン/ブルー領域を検出"""
hsv = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2HSV)
lower = np.array([85, 50, 50])
upper = np.array([130, 255, 255])
mask = cv2.inRange(hsv, lower, upper)
kernel = np.ones((7, 7), np.uint8)
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=2)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=1)
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
return _find_best_quad(contours, image_bgr.shape[:2])
なぜ「長方形」ではなく「四角形(4頂点)」の検出なのか
ここで重要なのは、検出対象を「長方形」ではなく任意の四角形(4頂点の多角形)として扱っている点です。理由は2つあります。
1. カメラアングルによるパース歪み: 生成画像のテレビは必ずしも正面から描かれるわけではありません。斜めのアングルでは画面が台形に近い形状になり、cv2.minAreaRect のような長方形検出では対応できません。4頂点を個別に取得することで、パースのかかった画面にも対応できます。
2. ブラウン管の凸面形状: ブラウン管テレビの画面はフラットではなく、わずかに湾曲した凸面です。画像上では四辺がゆるやかに膨らんで見えるため、厳密な直線の矩形にはなりません。approxPolyDP のepsilon を段階的に緩和しているのは、このような丸みを帯びた輪郭を4頂点で近似するためです。
以下のコードは簡略化した例です。実際の実装ではさらに多くの考慮が必要になります——たとえば approxPolyDP で4頂点が得られなかった場合の凸包からの象限ベースのコーナー抽出、minAreaRect を最終フォールバックとする多段カスケード、アスペクト比や内角の妥当性チェック(正方形や極端な台形の除外)などです。
def _approx_quad(contour, epsilon_ratios=(0.02, 0.03, 0.05, 0.07)):
"""輪郭を四角形に近似。段階的にepsilonを緩和。
実際の実装では凸包フォールバックや品質スコアリングも必要になる。"""
peri = cv2.arcLength(contour, True)
for ratio in epsilon_ratios:
approx = cv2.approxPolyDP(contour, ratio * peri, True)
if len(approx) == 4:
return approx.reshape(4, 2).astype(np.float32)
return None
def _sort_corners(pts: np.ndarray) -> np.ndarray:
"""左上→右上→右下→左下 の順に並べ替え"""
s = pts.sum(axis=1)
diff = np.diff(pts, axis=1).ravel()
tl = pts[np.argmin(s)]
br = pts[np.argmax(s)]
tr = pts[np.argmin(diff)]
bl = pts[np.argmax(diff)]
return np.array([tl, tr, br, bl], dtype=np.float32)
3戦略の統合と IoU によるマージ
3つの戦略を並列実行し、結果をまとめます。同じ画面を異なる戦略で検出した場合は IoU(重複度)で重複排除します。
def detect_all_screens(image_bgr: np.ndarray, iou_threshold: float = 0.3):
"""3戦略を統合し、IoU で重複排除"""
all_candidates = []
all_candidates.extend(detect_by_edges(image_bgr))
all_candidates.extend(detect_by_magenta(image_bgr))
all_candidates.extend(detect_by_cyan(image_bgr))
# confidence 降順でソート
all_candidates.sort(key=lambda x: x[1], reverse=True)
kept = []
for quad, area in all_candidates:
if all(_polygon_iou(quad, k) < iou_threshold for k, _ in kept):
kept.append((quad, area))
return kept
うまくいかなかった点
実際に数十枚の画像で試した結果、以下の問題に直面しました。
1. プロンプトの効きが安定しない
「magenta screen」と指定しても、SDXLは必ずしもマゼンタ一色の画面を生成してくれません。部屋の照明がマゼンタに染まったり、画面にコンテンツが映り込んだりします。特に cfg を上げるとアーティファクトが増え、下げると指示を無視する傾向がありました。

この問題の主因は、SDXL 系チェックポイントの指示追従度(instruction following)の限界にあると考えています。Stable Diffusion 系のモデルはプロンプトに対する忠実度がそこまで高くなく、特に「画面の色を○○にせよ」のような局所的な指定は無視されがちです。ChatGPT(DALL-E 3)や Gemini の画像生成であれば、自然言語の指示をより正確に反映できる可能性はあります。ただし、API 経由での大量生成やワークフローへの組み込みの自由度を考えると、ローカルの SD 系モデルを使いたいのが本音です。
なお、生成画像にこだわらず実写のテレビ画像を素材として用意するという選択肢もあります。実写であれば画面色を物理的にコントロールできるため、検出精度は格段に上がります。ただし、本連載の目的は「画像生成AIの応用と自動化」なので、あえて生成画像で挑戦しています。
2. 画面の形状が不定
生成されるテレビの角度、サイズ、ベゼルの太さが毎回異なります。正面からのショットなら矩形に近いですが、斜めから見たパースのついた画面は approxPolyDP で4頂点に近似しにくくなります。

3. 背景とのコントラスト不足
暗い部屋に暗い画面(screen off)の場合、画面と壁の境界がほぼ同じ輝度になり、Canny エッジが出ません。逆に明るい部屋にマゼンタ画面を置くと、照明のマゼンタ反射と画面本体の区別がつかなくなります。

検出成功率
各条件で数十枚ずつ生成して試してみましたが、ざっくりした手元の計測では、おおよそ以下の結果でした:
| 条件 | 検出成功率 |
|---|---|
| 正面ショット + screen off | 約60% |
| 正面ショット + マゼンタ | 約50% |
| 斜めアングル | 約20% |
| 複数モニタ | 約15%(1台だけ検出等) |
これでは自動化パイプラインとして使うには心もとない数字です。
まとめ
SDXLでテレビを生成し、プロンプトで画面色を制御して OpenCV で検出するアプローチは、発想は自然だが実用には至らないという結果でした。
問題の根本は、生成側と検出側が独立していることです。生成される画像がどんな構図になるかは事前に分からないため、検出側が想定するパターンから外れた画像が頻繁に出てきます。
次回は、この問題を「生成側を制御する」方向で解決を試みます。ControlNet Canny を使い、Python でベゼルの線画テンプレートを描いて構図ごと指定する方法です。
次回(第3回): ControlNet Canny + LoRA に移行する



