import datetime import math import os import cv2 import numpy as np from PIL import Image # from transformers import pipeline from utils import arrange_hand # # --- グローバル変数として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, 120, 255, cv2.THRESH_BINARY)[1] mask_b = cv2.threshold(b_channel, 120, 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パイプラインが初期化されていないため、処理を中止します。")