File size: 5,418 Bytes
9132aca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
075e3dd
9132aca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
075e3dd
9132aca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
"""
Ukrainian passport series/number OCR pipeline.

Full flow:
    passport image  ->  YOLO ROI detection  ->  preprocessing  ->  CRNN OCR

Usage:
    # Python API
    from pipeline import PassportOCR
    ocr = PassportOCR()
    result = ocr("passport.jpg")
    # {"series": "НР", "number": "430098", "full": "НР430098",
    #  "confidence": 0.97, "readable": True}

    # CLI
    python pipeline.py passport.jpg
    python pipeline.py passport.jpg --show
"""

from __future__ import annotations
import argparse
from pathlib import Path

import cv2
import numpy as np

from src.detect     import load_detector, detect_roi, crop_roi
from src.preprocess import preprocess
from src.recognize  import load_recognizer, recognize, CONFIDENCE_THRESHOLD

MODELS_DIR    = Path(__file__).parent / "models"
DETECTOR_PATH = MODELS_DIR / "detector.pt"
RECOGNIZER_PATH = MODELS_DIR / "recognizer.pth"


class PassportOCR:
    """
    End-to-end passport series/number recognizer.

    Args:
        detector_path:   path to YOLO .pt weights.
        recognizer_path: path to CRNN .pth checkpoint.
        det_conf:        YOLO confidence threshold (lower = more sensitive).
    """

    def __init__(
        self,
        detector_path:   str | Path = DETECTOR_PATH,
        recognizer_path: str | Path = RECOGNIZER_PATH,
        det_conf:        float      = 0.30,
    ):
        self.detector,  = load_detector(detector_path),
        self.det_conf   = det_conf
        self.model, self.ckpt = load_recognizer(recognizer_path)

    def __call__(self, image: str | Path | np.ndarray) -> dict:
        """
        Run full pipeline on a passport image.

        Args:
            image: file path (str/Path) or BGR numpy array.

        Returns dict with keys:
            series     — 2 Cyrillic letters  (e.g. "НР")
            number     — 6 digits            (e.g. "430098")
            full       — series + number     (e.g. "НР430098")
            confidence — avg softmax prob    (0.0 – 1.0)
            readable   — False if confidence < threshold
            roi_box    — (x1,y1,x2,y2) or None if detection failed
        """
        # -- Load image --------------------------------------------------------
        if isinstance(image, (str, Path)):
            img_bgr = cv2.imread(str(image))
            if img_bgr is None:
                raise FileNotFoundError(f"Cannot read image: {image}")
        else:
            img_bgr = image

        # -- Detect ROI --------------------------------------------------------
        box = detect_roi(img_bgr, self.detector, conf=self.det_conf)

        if box is None:
            return {
                "series":     None,
                "number":     None,
                "full":       None,
                "confidence": 0.0,
                "readable":   False,
                "roi_box":    None,
                "error":      "ROI not detected",
            }

        roi_crop = crop_roi(img_bgr, box, padding=0.0)

        # -- Preprocess --------------------------------------------------------
        roi_clean = preprocess(roi_crop)

        # -- Recognize ---------------------------------------------------------
        text, conf = recognize(roi_clean, self.model, self.ckpt)

        return {
            "series":     text[:2],
            "number":     text[2:],
            "full":       text,
            "confidence": round(conf, 4),
            "readable":   conf >= CONFIDENCE_THRESHOLD,
            "roi_box":    box,
        }


# -- CLI -----------------------------------------------------------------------

def _show_result(img_bgr: np.ndarray, result: dict) -> None:
    import matplotlib
    matplotlib.use("TkAgg")
    import matplotlib.pyplot as plt

    box = result.get("roi_box")
    annotated = img_bgr.copy()
    if box:
        x1, y1, x2, y2 = box
        cv2.rectangle(annotated, (x1, y1), (x2, y2), (0, 255, 0), 3)

    fig, axes = plt.subplots(1, 2, figsize=(14, 5))
    axes[0].imshow(cv2.cvtColor(annotated, cv2.COLOR_BGR2RGB))
    axes[0].set_title("Detected ROI", fontsize=10)
    axes[0].axis("off")

    if box:
        roi = crop_roi(img_bgr, box, padding=0.0)
        roi_clean = preprocess(roi)
        axes[1].imshow(roi_clean, cmap="gray")
        label = result.get("full") or "NOT DETECTED"
        conf  = result.get("confidence", 0)
        axes[1].set_title(f"{label}  (conf {conf:.1%})", fontsize=12)
        axes[1].axis("off")

    plt.tight_layout()
    plt.show()


if __name__ == "__main__":
    ap = argparse.ArgumentParser(description="Ukrainian passport OCR")
    ap.add_argument("image", help="Path to passport image")
    ap.add_argument("--show", action="store_true", help="Show visual result")
    ap.add_argument("--det-conf", type=float, default=0.30,
                    help="YOLO detection confidence threshold")
    args = ap.parse_args()

    ocr    = PassportOCR(det_conf=args.det_conf)
    result = ocr(args.image)

    if result.get("error"):
        print(f"ERROR: {result['error']}")
    else:
        status = "OK" if result["readable"] else "LOW CONFIDENCE"
        print(f"Result:     {result['full']}")
        print(f"Series:     {result['series']}")
        print(f"Number:     {result['number']}")
        print(f"Confidence: {result['confidence']:.1%}  [{status}]")

    if args.show:
        img = cv2.imread(args.image)
        _show_result(img, result)