board-recognizer / identify_cards.py
wai572's picture
mask arr
1eaacfa
import datetime
import math
import os
import cv2
import numpy as np
from PIL import Image
from utils import arrange_hand
# from transformers import pipeline
# # --- グローバル変数としてTrOCRパイプラインを初期化 ---
# print("TrOCRのAIモデルを読み込んでいます...(初回は数分かかります)")
# try:
# trocr_pipeline = pipeline(
# "image-to-text", model="microsoft/trocr-base-printed"
# )
# print("TrOCRの準備が完了しました。")
# except Exception as e:
# print(f"TrOCRモデルのロード中にエラーが発生しました: {e}")
# trocr_pipeline = None
generate_kwargs_sampling = {
"do_sample": True,
"temperature": 0.7,
"top_k": 50,
"max_length": 2,
}
SUIT_TEMPLATE_PATH = "templates/suits/"
SUITS_BY_COLOR = {"black": "S", "green": "C", "red": "H", "orange": "D"}
SCALE_STANDARD = 2032
IS_DEBUG = False
def load_suit_templates(template_path):
templates = {}
if not os.path.exists(template_path):
return templates
for filename in os.listdir(template_path):
if filename.endswith(".png"):
name = os.path.splitext(filename)[0]
img = cv2.imread(
os.path.join(template_path, filename), cv2.IMREAD_GRAYSCALE
)
if img is not None:
templates[name] = img
return templates
def get_img_with_rect(img, rects, color, thickness):
_img = img.copy()
for rect in rects:
if isinstance(rect, tuple):
x, y, w, h = rect
else:
(x, y), (w, h) = rect["pos"], rect["size"]
cv2.rectangle(_img, (x, y), (x + w, y + h), color)
return _img
def save_img_with_rect(filename, img, rects, color=(0, 255, 0), thickness=2):
if IS_DEBUG:
cv2.imwrite(filename, get_img_with_rect(img, rects, color, thickness))
def get_masks(hsv):
board_candidates = [
((15, 200, 160), (35, 255, 245)), # yellow
((100, 0, 0), (179, 60, 80)), # black
((35, 200, 100), (50, 255, 160)), # light green
((170, 170, 150), (179, 255, 220)), # red
((160, 70, 170), (180, 120, 240)), # pink
((0, 200, 160), (15, 255, 240)), # orange
((90, 110, 160), (120, 210, 240)), # blue
]
for color in board_candidates:
yield cv2.inRange(hsv, color[0], color[1])
def find_best_contour(image, is_best):
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
i = 0
for mask in get_masks(hsv):
kernel = np.ones((7, 7), np.uint8)
closed_mask = cv2.morphologyEx(
mask, cv2.MORPH_CLOSE, kernel, iterations=2
)
if IS_DEBUG:
cv2.imwrite(f"debug_mask{i}.jpg", closed_mask)
contours, _ = cv2.findContours(
closed_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
best_contour = max(contours, key=cv2.contourArea) if contours else None
if best_contour is not None and cv2.contourArea(best_contour) > 50000:
b, res = is_best(best_contour)
if b:
return res
i += 1
return None
def find_board_corners(image):
"""
画像から黄色いボードの輪郭を見つけ、その4つの角の座標を返す。
"""
def is_best(best_contour):
peri = cv2.arcLength(best_contour, True)
approx = cv2.approxPolyDP(best_contour, 0.02 * peri, True)
# 輪郭が4つの角を持つ場合、それを返す
is_rect = len(approx) >= 4
return is_rect, approx.reshape(-1, 2) if is_rect else None
points = find_best_contour(image, is_best)
# if not points:
# return None
sum = points.sum(axis=1)
diff = np.diff(points, axis=1)
top_left = points[np.argmin(sum)]
bottom_right = points[np.argmax(sum)]
top_right = points[np.argmax(diff)]
bottom_left = points[np.argmin(diff)]
corners = np.array(
[top_left, top_right, bottom_right, bottom_left], dtype="int32"
)
return corners
def order_points(pts):
"""
4つの点を左上、右上、右下、左下の順に並べ替える。
"""
rect = np.zeros((4, 2), dtype="float32")
s = pts.sum(axis=1)
rect[0] = pts[np.argmin(s)] # 左上
rect[2] = pts[np.argmax(s)] # 右下
diff = np.diff(pts, axis=1)
rect[1] = pts[np.argmin(diff)] # 右上
rect[3] = pts[np.argmax(diff)] # 左下
return rect
def find_center_box(image):
def is_best(best_contour):
if best_contour is not None and cv2.contourArea(best_contour) > 50000:
print(f"rect:{cv2.boundingRect(best_contour)}")
x, y, w, h = cv2.boundingRect(best_contour)
h_parent, w_parent, _ = image.shape
if (w < h and (w > 0.34 * w_parent or h > 0.8 * h_parent)) or (
w >= h and (h > 0.34 * h_parent or w > 0.8 * w_parent)
):
return False, None
return True, cv2.boundingRect(best_contour)
return False, None
return find_best_contour(image, is_best)
def find_center_seal(image, bw, bh):
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
mask = cv2.inRange(gray, 190, 255)
kernel = np.ones((7, 7), np.uint8)
closed_mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=2)
if IS_DEBUG:
cv2.imwrite(f"debug_seal.jpg", closed_mask)
contours, _ = cv2.findContours(
closed_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
for contour in contours:
x, y, w, h = cv2.boundingRect(contour)
_w = min(w, h)
_h = max(w, h)
if (
_w > 0.33 * bw
and _w < 0.44 * bw
and _h > 0.21 * bh
and _h < 0.24 * bh
):
return x, y
return -1, -1
def rotate_rect(box, w_parent, h_parent, angle):
x, y, w, h = box
if angle % 360 == 0:
return (x, y, w, h)
elif angle % 360 == 90:
return (h_parent - h - y, x, h, w)
elif angle % 360 == 180:
return (w_parent - w - x, h_parent - h - y, w, h)
elif angle % 360 == 270:
return (y, w_parent - w - x, h, w)
def determine_and_correct_orientation(image, progress_fn):
"""
画像の向きを判断し、必要であれば回転させて補正した画像を返す。
"""
progress_fn("写真の向きを自動分析中...")
# ★★★ ステップ1: ボードの4つの角を検出し、射影変換を行う ★★★
print("ボードの傾きを検出・補正しています...")
corners = find_board_corners(image)
print(corners)
if corners is None:
print("エラー: ボードの角を検出できませんでした。")
warped_image = image
else:
# 4つの角を正しい順序に並べ替える
ordered_corners = order_points(corners.astype(np.float32))
(tl, tr, br, bl) = ordered_corners
# 変換後の画像の幅と高さを計算
widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))
widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))
boardWidth = max(int(widthA), int(widthB))
heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
boardHeight = max(int(heightA), int(heightB))
imageHeight, imageWidth, _ = image.shape
CanvasWidth, CanvasHeight = int(imageWidth * 1.2), int(
imageHeight * 1.2
)
x_offset = (CanvasWidth - boardWidth) // 2
y_offset = (CanvasHeight - boardHeight) // 2
# 変換後の座標を定義
dst_pts = np.array(
[
[x_offset, y_offset],
[x_offset + boardWidth - 1, y_offset],
[x_offset + boardWidth - 1, y_offset + boardHeight - 1],
[x_offset, y_offset + boardHeight - 1],
],
dtype="float32",
)
print(dst_pts)
# 射影変換行列を取得し、画像を補正
matrix = cv2.getPerspectiveTransform(ordered_corners, dst_pts)
warped_image = cv2.warpPerspective(
image, matrix, (CanvasWidth, CanvasHeight)
)
# デバッグ用に補正後画像を保存
cv2.imwrite("debug_warped_image.jpg", warped_image)
print("傾き補正後の画像を debug_warped_image.jpg に保存しました。")
# まず中央のボードを見つける
box = find_center_box(warped_image)
if box is None:
progress_fn(
"警告: ボードが見つからないため、向きの自動補正をスキップします。"
)
return warped_image, box, 1
print("box is found")
bx, by, bw, bh = box
h, w, _ = warped_image.shape
scale = max(bw, bh) / SCALE_STANDARD
board_img = warped_image[by : by + bh, bx : bx + bw]
print(box)
image_rotated = warped_image.copy()
if bh < bw:
board_img = cv2.rotate(board_img, cv2.ROTATE_90_CLOCKWISE)
image_rotated = cv2.rotate(warped_image, cv2.ROTATE_90_CLOCKWISE)
box = rotate_rect(box, w, h, 90)
bx, by, bw, bh = box
h, w, _ = image_rotated.shape
_, sy = find_center_seal(board_img, bw, bh)
if sy == -1:
progress_fn("ボードのシールが検出できませんでした")
return image_rotated, box, scale
print(sy, bx, by, bw, bh)
if sy < bh / 2:
return image_rotated, box, scale
else:
return (
cv2.rotate(image_rotated, cv2.ROTATE_180),
rotate_rect(box, w, h, 180),
scale,
)
def get_not_white_mask(img):
lab_patch = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
l_channel = lab_patch[:, :, 0]
a_channel = lab_patch[:, :, 1]
b_channel = lab_patch[:, :, 2]
mask_l = cv2.threshold(l_channel, 170, 255, cv2.THRESH_BINARY_INV)[1]
mask_a = cv2.threshold(a_channel, 130, 255, cv2.THRESH_BINARY)[1]
mask_b = cv2.threshold(b_channel, 130, 255, cv2.THRESH_BINARY)[1]
mask_ab = cv2.bitwise_or(mask_a, mask_b)
text_mask = cv2.bitwise_and(mask_l, mask_ab)
return text_mask
# ★★★ 新しいルールベースの色判定関数(診断モード付き) ★★★
def get_suit_from_image_rules(rank_image_patch, thresholds):
if rank_image_patch is None or rank_image_patch.size == 0:
return "unknown"
text_mask = get_not_white_mask(rank_image_patch)
# マスクを使って元のカラー画像から文字部分のみを抽出
masked_char_image = cv2.bitwise_and(
rank_image_patch, rank_image_patch, mask=text_mask
)
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S_%f")
if IS_DEBUG:
# デバッグ用のフォルダがなければ作成
debug_dir = "debug_chars"
if not os.path.exists(debug_dir):
os.makedirs(debug_dir)
# ユニークなファイル名を生成
debug_filename = os.path.join(
debug_dir, f"masked_char_{timestamp}.png"
)
# 画像を保存
cv2.imwrite(debug_filename, masked_char_image)
lab_patch = cv2.cvtColor(masked_char_image, cv2.COLOR_BGR2LAB)
avg_lab = cv2.mean(lab_patch, mask=text_mask)
if cv2.countNonZero(text_mask) < 20:
print(f" 診断: 文字ピクセルが少なすぎるため判定不可 {timestamp}")
return "unknown", avg_lab
return get_suit_from_color_rules(avg_lab, thresholds, timestamp)
def get_suit_from_color_rules(avg_lab, thresholds, timestamp=0):
L, a, b = avg_lab[0], avg_lab[1], avg_lab[2]
# --- 診断ログを出力 ---
print(f" L: {L:.1f}, a: {a:.1f}, b: {b:.1f} {timestamp}")
# ルールに基づいて判定
# ルール1: 明るさ(L)で黒を判定
if L < thresholds["L_black"]:
print(" ルール1: 明るさ(L)が低いため 'black' と判定")
return "black", avg_lab
# ルール2: a値で緑か赤系かを判断
# a < 128 が緑側, a > 128 が赤側
if a < thresholds["a_green"]: # 緑側の閾値
# if a > thresholds["a_black"] and b > thresholds["b_black"]:
# if a > thresholds["a_black"] and b < thresholds["b_black"]:
# print(" ルール6: 緑っぽいけど 'black' と判定")
# return "black", avg_lab
# elif a > thresholds["a_black2"] and b < thresholds["b_black2"]:
# print(" ルール8: 緑っぽいけど 'black' と判定2")
# return "black", avg_lab
if a + b > thresholds["ab_black"]:
print(" ルール9: a + bが高いため 'black' と判定")
return "black", avg_lab
if b - a < thresholds["ba_black"]:
print(" ルール6: b値がa値を下回っているため 'black' と判定")
return "black", avg_lab
print(" ルール : blackじゃないため 'green' と判定")
return "green", avg_lab
# if b > thresholds["b_black2"]:
# print(" ルール9: b値が高いため 'black' と判定")
# return "black", avg_lab
# if b - a > thresholds["ba_green"]:
# print(" ルール6: b値がa値を上回っているため 'green' と判定")
# return "green", avg_lab
# if b < thresholds["b_black"]:
# print(" ルール8: 緑っぽいけど 'black' と判定")
# return "black", avg_lab
# print(" ルール2: a値が低いため 'green' と判定")
# return "green", avg_lab
elif a > thresholds["a_red"]: # 赤側の閾値
# ルール3: b値で赤とオレンジを区別
# b > 128 が黄側, b < 128 が青側
if a - b > thresholds["a_b_red"]:
print(" ルール : a-bが大きいため 'red' と判定")
return "red", avg_lab
else:
print(" ルール : a-bが小さいため 'orange'と判定")
return "orange", avg_lab
# if b > thresholds["b_orange"]: # 黄色みが強ければオレンジ
# if b < thresholds["b_red"] and a > thresholds["a_orange"]:
# print(" ルール7: オレンジっぽいけど 'red' と判定")
# return "red", avg_lab
# print(" ルール3: a値が高く、b値も高いため 'orange' と判定")
# return "orange", avg_lab
# else: # それ以外は赤
# print(
# " ルール4: a値が高く、b値がそれほど高くないため 'red' と判定"
# )
# return "red", avg_lab
print(" ルール5: どのLABのルールにも一致しなかったため 'black' と判定")
return "black", avg_lab
def preprocess_img(img):
gray_region = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# --- ステップ1: カードマスクの作成 (変更なし) ---
_, card_mask = cv2.threshold(gray_region, 160, 255, cv2.THRESH_BINARY)
kernel_mask = np.ones((5, 5), np.uint8)
card_mask = cv2.dilate(card_mask, kernel_mask, iterations=3)
# cv2.imwrite(f"debug_card_mask_{player_name}.jpg", card_mask)
# --- ステップ2: Cannyエッジ検出による前処理 ---
# メディアンフィルタで元画像のノイズを軽く除去
denoised_gray = cv2.medianBlur(gray_region, 3)
thresholded = cv2.adaptiveThreshold(
denoised_gray,
255,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY_INV,
21,
7,
)
preprocessed = cv2.bitwise_and(thresholded, thresholded, mask=card_mask)
kernel_open = np.ones((3, 3), np.uint8)
preprocessed = cv2.morphologyEx(preprocessed, cv2.MORPH_OPEN, kernel_open)
return preprocessed
def filter_size(contours, scale, img):
res = []
for i, cnt in enumerate(contours):
x, y, w, h = cv2.boundingRect(cnt)
# サイズフィルタを適用
# if y < 400 and w * h > 1500 * scale * scale:
# print(f"{w}x{h} at ({x}, {y})")
if (
40 * scale < h < 95 * scale
and 25 * scale < w < 60 * scale
and 0.25 < w / h < 0.9
and 1500 * scale * scale < w * h < 4000 * scale * scale
):
pad = 10
cropped_img = img[
max(0, y - pad) : min(y + h + pad, img.shape[0]),
max(0, x - pad) : min(x + w + pad, img.shape[1]),
]
no_padding_img = img[
max(0, y) : min(y + h, img.shape[0]),
max(0, x) : min(x + w, img.shape[1]),
]
res.append(
{
"img": cropped_img,
"no_pad": no_padding_img,
"pos": (x, y),
"size": (w, h),
}
)
return res
def filter_thickness(
candidates,
scale,
):
res = []
for candidate in candidates:
no_padding_img = candidate["no_pad"]
cropped_img = candidate["img"]
text_mask = get_not_white_mask(no_padding_img)
# マスクを使って元のカラー画像から文字部分のみを抽出
masked_char_image = cv2.bitwise_and(
no_padding_img, no_padding_img, mask=text_mask
)
cropped_bin = cv2.cvtColor(masked_char_image, cv2.COLOR_BGR2GRAY)
cropped_dist = cv2.distanceTransform(cropped_bin, cv2.DIST_L2, 3)
_, max_val, _, _ = cv2.minMaxLoc(cropped_dist)
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S_%f")
if IS_DEBUG:
debug_dir = "debug_chars"
if not os.path.exists(debug_dir):
os.makedirs(debug_dir)
debug_dir = "debug_chars"
debug_filename = os.path.join(
debug_dir, f"dist_char_{timestamp}.png"
)
# 画像を保存
cv2.imwrite(debug_filename, cropped_dist)
print(
f" 候補 at ({candidate['pos']}) - 厚みスコア: {max_val:.2f} {timestamp}"
)
if max_val > 12.0 * scale and max_val < 100000:
# print(" -> スートと判断し除外")
continue
# else:
# print(" -> ランク候補として採用")
if cropped_img.size > 0:
candidate["thickness"] = max_val
res.append(candidate)
return res
def filter_suit(candidates, suit_templates, threshold):
filtered = []
for candidate in candidates:
is_suit = False
candidate_gray = cv2.cvtColor(candidate["img"], cv2.COLOR_BGR2GRAY)
for _, template in suit_templates.items():
resized_template = cv2.resize(
template, (candidate["size"][0], candidate["size"][1])
)
res = cv2.matchTemplate(
candidate_gray, resized_template, cv2.TM_CCOEFF_NORMED
)
_, max_val, _, _ = cv2.minMaxLoc(res)
if max_val > threshold:
is_suit = True
break
if not is_suit:
filtered.append(candidate)
return filtered
def filter_vertically(candidates, scale):
res = []
for candidate in candidates:
x, y = candidate["pos"]
is_eliminated = False
for _candidate in candidates:
_x, _y = _candidate["pos"]
if (
(
x - _x < 35 * scale
and x - _x > -35 * scale
and y - _y > 40 * scale
)
or (
x - _x < 80 * scale
and x - _x > -80 * scale
and y - _y > 90 * scale
)
or (
x - _x < 300 * scale
and x - _x > -300 * scale
and y - _y > 170 * scale
)
):
is_eliminated = True
if not is_eliminated:
res.append(candidate)
return res
def filter_uniform(candidates, threshold=20.0):
res = []
for candidate in candidates:
text_mask = get_not_white_mask(candidate["no_pad"])
if cv2.countNonZero(text_mask) > 20:
lab_patch = cv2.cvtColor(candidate["no_pad"], cv2.COLOR_BGR2LAB)
_, std_dev = cv2.meanStdDev(lab_patch, mask=text_mask)
color_variance = math.sqrt(std_dev[1][0] ** 2 + std_dev[2][0] ** 2)
# デバッグ用に標準偏差を出力
print(
f" 候補 at ({candidate['pos']}) - 色のばらつき: {color_variance:.2f}"
)
# 標準偏差が閾値より小さければ、色が均一であると判断
if color_variance < threshold:
res.append(candidate)
else:
print(" -> 絵柄と判断し、除外")
return res
def find_rank_candidates(region_image, suit_templates, player_name, scale=1):
print(scale)
if region_image is None or region_image.size == 0:
return []
preprocessed = preprocess_img(region_image)
if IS_DEBUG:
cv2.imwrite(f"debug_ocr_preprocess_{player_name}.jpg", preprocessed)
contours, _ = cv2.findContours(
preprocessed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
SUIT_FILTER_THRESHOLD = 0.6
candidates_size = filter_size(contours, scale, region_image)
candidates_thickness = filter_thickness(candidates_size, scale)
candidates_suit = filter_suit(
candidates_thickness, suit_templates, SUIT_FILTER_THRESHOLD
)
candidates_vertically = filter_vertically(candidates_suit, scale)
candidates_uniform = filter_uniform(candidates_vertically)
save_img_with_rect(
f"debug_ocr_thickness_{player_name}.jpg",
region_image,
candidates_thickness,
)
save_img_with_rect(
f"debug_ocr_candidates_{player_name}.jpg",
region_image,
candidates_size,
)
save_img_with_rect(
f"debug_filter_process_{player_name}.jpg",
region_image,
candidates_uniform,
)
print(
f"フィルタリング過程を debug_filter_process_{player_name}.jpg に保存しました。"
)
print(len(candidates_uniform))
return candidates_uniform
# ★★★ RGBで色を分析するヘルパー関数 ★★★
def get_avg_rgb_from_patch(patch):
"""画像パッチから文字部分の平均色(RGB)を計算する"""
patch_gray = cv2.cvtColor(patch, cv2.COLOR_BGR2GRAY)
_, text_mask = cv2.threshold(patch_gray, 180, 230, cv2.THRESH_BINARY_INV)
if cv2.countNonZero(text_mask) == 0:
return None
# OpenCVの平均色はBGR順なので、RGB順に並べ替えて返す
avg_bgr = cv2.mean(patch, mask=text_mask)[:3]
return (avg_bgr[2], avg_bgr[1], avg_bgr[0]) # (R, G, B)
# ★★★ 最終改善版:Cannyエッジと輪郭階層を利用したカード認識関数 ★★★
def recognize_cards(region_image, suit_templates, player_name, trocr_pipeline):
rank_candidates = find_rank_candidates(
region_image, suit_templates, player_name
)
debug_region = region_image.copy()
VALID_RANKS = [
"A",
"K",
"Q",
"J",
"10",
"9",
"8",
"7",
"6",
"5",
"4",
"3",
"2",
]
recognized_ranks = []
if rank_candidates and trocr_pipeline:
# 重複候補をマージする
# ...(今回は省略。まずは検出できるかが重要)...
candidate_pil_images = [
Image.fromarray(cv2.cvtColor(c["img"], cv2.COLOR_BGR2RGB))
for c in rank_candidates
]
ocr_results = trocr_pipeline(candidate_pil_images)
print([result[0]["generated_text"] for result in ocr_results])
# ocr_results = trocr_pipeline(candidate_pil_images, generate_kwargs=generate_kwargs_sampling)
for i, result in enumerate(ocr_results):
text = result[0]["generated_text"].upper().strip()
# TrOCRが誤認識しやすい文字を補正
if text == "1O":
text = "T"
if text == "0" or text == "O":
text = "T"
if text in VALID_RANKS:
candidate = rank_candidates[i]
# --- 診断ログを出力 ---
print(f"--- 診断中: ランク '{text}' at {candidate['pos']} ---")
color_name = get_suit_from_image_rules(candidate["img"])
print(f" -> 色判定結果: {color_name}")
if color_name in SUITS_BY_COLOR:
suit = SUITS_BY_COLOR[color_name]
card_name = f"{suit}{text}"
is_duplicate = any(
math.sqrt(
(fc["pos"][0] - candidate["pos"][0]) ** 2
+ (fc["pos"][1] - candidate["pos"][1]) ** 2
)
< 20
for fc in recognized_ranks
)
if not is_duplicate:
recognized_ranks.append(
{"name": card_name, "pos": candidate["pos"]}
)
if IS_DEBUG:
for card in recognized_ranks:
cv2.putText(
debug_region,
card["name"],
(card["pos"][0], card["pos"][1] - 10),
cv2.FONT_HERSHEY_SIMPLEX,
1.0,
(255, 255, 0),
2,
cv2.LINE_AA,
)
cv2.imwrite(f"debug_detection_{player_name}.jpg", debug_region)
print(
f"{player_name} の検出結果を debug_detection_{player_name}.jpg に保存しました。"
)
return [c["name"] for c in recognized_ranks]
def main(image_path):
suit_templates = load_suit_templates(SUIT_TEMPLATE_PATH)
if not suit_templates:
print(
"エラー: templates/suits フォルダにスートのテンプレート画像が見つかりません。"
)
return
image = cv2.imread(image_path)
if image is None:
return
box = find_center_box(image)
if box is None:
return
bx, by, bw, bh = box
h, w, _ = image.shape
margin = 200
player_regions = {
"north": image[0:by, :],
"south": image[by + bh : h, :],
"west": image[by - margin : by + bh + margin, 0:bx],
"east": image[by - margin : by + bh + margin, bx + bw : w],
}
for player, region in player_regions.items():
if region is None or region.size == 0:
continue
if player == "north":
player_regions[player] = cv2.rotate(region, cv2.ROTATE_180)
elif player == "east":
player_regions[player] = cv2.rotate(
region, cv2.ROTATE_90_CLOCKWISE
)
elif player == "west":
player_regions[player] = cv2.rotate(
region, cv2.ROTATE_90_COUNTERCLOCKWISE
)
all_hands = {}
for player, region in player_regions.items():
cards = recognize_cards(region, suit_templates, player)
all_hands[player] = arrange_hand(cards)
print("\n--- 最終識別結果 (ルールベース色判定) ---")
for player, hand in all_hands.items():
print(f"{player.capitalize()}: {', '.join(hand)}")
print("---------------------------------------")
if __name__ == "__main__":
IMAGE_FILE_PATH = "PXL_20250611_101254508.jpg"
# if trocr_pipeline:
# main(IMAGE_FILE_PATH)
# else:
# print("TrOCRパイプラインが初期化されていないため、処理を中止します。")
# else:
# print("TrOCRパイプラインが初期化されていないため、処理を中止します。")