104 lines
3.7 KiB
Python
104 lines
3.7 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
テンプレート画像内の赤枠・青枠を検出し、OCR抽出用の矩形領域を返すモジュール。
|
||
|
||
前提:
|
||
- テンプレートは元PDFと同じ用紙サイズ・解像度でスキャン/作成されていること
|
||
- 赤枠 = 氏名欄、青枠 = 日付欄 (必要に応じて色を追加可能)
|
||
"""
|
||
|
||
import cv2
|
||
import numpy as np
|
||
|
||
|
||
# HSV色空間での色範囲定義(赤は色相環の両端にまたがるため2レンジ)
|
||
COLOR_RANGES = {
|
||
"red": [
|
||
# (lower_hsv, upper_hsv)
|
||
(np.array([0, 100, 100]), np.array([10, 255, 255])),
|
||
(np.array([160, 100, 100]), np.array([180, 255, 255])),
|
||
],
|
||
"blue": [
|
||
(np.array([100, 100, 100]), np.array([130, 255, 255])),
|
||
],
|
||
}
|
||
|
||
MIN_BOX_AREA = 500 # ノイズ除去用の最小面積(px^2)。テンプレート解像度に応じて調整してください
|
||
|
||
|
||
def _detect_color_boxes(img_bgr: np.ndarray, color_name: str) -> list[tuple[int, int, int, int]]:
|
||
"""指定色の矩形枠を検出し、[(x, y, w, h), ...] のリストを返す(枠線内側の矩形を返す)"""
|
||
hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV)
|
||
mask = np.zeros(hsv.shape[:2], dtype=np.uint8)
|
||
for lower, upper in COLOR_RANGES[color_name]:
|
||
mask |= cv2.inRange(hsv, lower, upper)
|
||
|
||
# 枠線の途切れを補正
|
||
kernel = np.ones((5, 5), np.uint8)
|
||
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=2)
|
||
|
||
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
||
|
||
boxes = []
|
||
for c in contours:
|
||
area = cv2.contourArea(c)
|
||
if area < MIN_BOX_AREA:
|
||
continue
|
||
x, y, w, h = cv2.boundingRect(c)
|
||
boxes.append((x, y, w, h))
|
||
return boxes
|
||
|
||
|
||
def detect_template_fields(template_path: str) -> dict[str, tuple[int, int, int, int]]:
|
||
"""
|
||
テンプレート画像から赤枠(氏名)・青枠(日付)を検出する。
|
||
|
||
Returns:
|
||
{
|
||
"name": (x, y, w, h), # 赤枠が見つかった場合
|
||
"date": (x, y, w, h), # 青枠が見つかった場合
|
||
}
|
||
画像サイズは呼び出し側で正規化(比率)して使うため、あわせて画像サイズも返す。
|
||
"""
|
||
img = cv2.imread(template_path)
|
||
if img is None:
|
||
raise FileNotFoundError(f"テンプレート画像を読み込めません: {template_path}")
|
||
|
||
h_img, w_img = img.shape[:2]
|
||
result = {"_image_size": (w_img, h_img)}
|
||
|
||
red_boxes = _detect_color_boxes(img, "red")
|
||
blue_boxes = _detect_color_boxes(img, "blue")
|
||
|
||
if red_boxes:
|
||
# 最大面積のものを採用(複数検出された場合のノイズ対策)
|
||
result["name"] = max(red_boxes, key=lambda b: b[2] * b[3])
|
||
if blue_boxes:
|
||
result["date"] = max(blue_boxes, key=lambda b: b[2] * b[3])
|
||
|
||
return result
|
||
|
||
|
||
def to_ratio_box(box: tuple[int, int, int, int], image_size: tuple[int, int]) -> tuple[float, float, float, float]:
|
||
"""ピクセル座標を画像サイズに対する比率(0.0-1.0)に変換する。
|
||
対象PDFの解像度がテンプレートと異なっていても位置を再現できるようにするため。"""
|
||
x, y, w, h = box
|
||
iw, ih = image_size
|
||
return (x / iw, y / ih, w / iw, h / ih)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
import sys
|
||
import json
|
||
|
||
if len(sys.argv) < 2:
|
||
print("使い方: python box_detector.py <テンプレート画像パス>")
|
||
sys.exit(1)
|
||
|
||
fields = detect_template_fields(sys.argv[1])
|
||
image_size = fields.pop("_image_size")
|
||
print(f"画像サイズ: {image_size}")
|
||
for label, box in fields.items():
|
||
ratio = to_ratio_box(box, image_size)
|
||
print(f"{label}: pixel={box} ratio={tuple(round(v, 4) for v in ratio)}")
|