Spaces:
Running
Running
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パイプラインが初期化されていないため、処理を中止します。") | |