File size: 7,729 Bytes
24497ad
 
 
 
e3e499d
24497ad
b77a642
24497ad
 
e3e499d
 
 
 
 
 
 
 
 
 
 
 
b77a642
e3e499d
 
 
 
 
 
 
 
 
 
 
 
 
b77a642
e3e499d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24497ad
 
 
e3e499d
 
 
 
 
3009816
e3e499d
 
 
 
24497ad
3009816
e3e499d
 
3009816
e3e499d
3009816
 
 
cf4ebde
24497ad
e3e499d
 
 
1db9854
e3e499d
3009816
 
e3e499d
a8226be
e3e499d
 
cf4ebde
3009816
b77a642
3009816
 
a8226be
 
 
 
 
 
 
 
e3e499d
cf4ebde
 
 
 
 
 
 
e3e499d
1db9854
a8226be
 
 
 
 
b77a642
a8226be
 
 
e3e499d
a8226be
 
e3e499d
 
1db9854
24497ad
3009816
 
e3e499d
1db9854
 
 
 
 
 
 
 
e3e499d
 
 
1db9854
24497ad
a8226be
24497ad
 
e3e499d
 
24497ad
 
f88027a
 
24497ad
e3e499d
24497ad
f88027a
 
 
 
e3e499d
24497ad
 
 
 
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
166
167
168
169
170
171
172
173
174
175
176
177
import gradio as gr
import cv2
import easyocr
import numpy as np
import json

# OCR Process - Inisialisasi Reader EasyOCR
reader = easyocr.Reader(['id', 'en'])

def detect_and_warp_ktp(input_img):
    """
    Fungsi untuk mendeteksi tepi KTP, melakukan cropping, 
    dan meluruskan perspektif gambar secara otomatis.
    """
    clone = input_img.copy()
    gray = cv2.cvtColor(input_img, cv2.COLOR_RGB2GRAY)
    
    # Efek blur dan deteksi tepi untuk mengekstrak kontur kartu
    blurred = cv2.GaussianBlur(gray, (5, 5), 0)
    edged = cv2.Canny(blurred, 50, 200)
    
    # Contour Detection - Mencari 5 kontur terbesar (KTP biasanya berbentuk persegi panjang dominan)
    contours, _ = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    contours = sorted(contours, key=cv2.contourArea, reverse=True)[:5]
    
    ktp_contour = None
    for c in contours:
        peri = cv2.arcLength(c, True)
        approx = cv2.approxPolyDP(c, 0.02 * peri, True)
        
        # Jika kontur memiliki tepat 4 titik sudut, asumsikan sebagai KTP
        if len(approx) == 4:
            ktp_contour = approx
            break
            
    # Perspective Warp - Jika kartu terdeteksi, lakukan transformasi perspektif (Pelurusan)
    if ktp_contour is not None:
        pts = ktp_contour.reshape(4, 2)
        rect = np.zeros((4, 2), dtype="float32")
        
        # Mengurutkan koordinat titik sudut
        s = pts.sum(axis=1)
        rect[0] = pts[np.argmin(s)]  # Top-left
        rect[2] = pts[np.argmax(s)]  # Bottom-right
        
        diff = np.diff(pts, axis=1)
        rect[1] = pts[np.argmin(diff)] # Top-right
        rect[3] = pts[np.argmax(diff)] # Bottom-left
        
        # Standarisasi ukuran dimensi hasil crop (Rasio KTP standard)
        width = 600
        height = 380
        
        dst = np.array([
            [0, 0],
            [width - 1, 0],
            [width - 1, height - 1],
            [0, height - 1]], dtype="float32")
        
        M = cv2.getPerspectiveTransform(rect, dst)
        warped = cv2.warpPerspective(clone, M, (width, height))
        return warped
        
    # Jika gagal mendeteksi kartu, kembalikan gambar asli
    return input_img

def process_ktp(input_img):
    if input_img is None:
        return None, "Mohon unggah gambar KTP."
            
    # TAHAP 1: Deteksi area kartu dan perbaikan perspektif secara dinamis
    img_cropped = detect_and_warp_ktp(input_img)
    
    # Standarisasi ukuran prapemrosesan akhir
    scale_percent = 150 
    width = int(img_cropped.shape[1] * scale_percent / 100)
    height = int(img_cropped.shape[0] * scale_percent / 100)
    img = cv2.resize(img_cropped, (width, height), interpolation=cv2.INTER_CUBIC)
            
    display_img = img.copy()
    gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
            
    # TAHAP 2: Jalankan OCR pada gambar yang sudah bersih dan lurus
    ocr_results = reader.readtext(gray, detail=1, paragraph=False)
            
    extracted_data = {
        "NIK": "-",
        "Nama": "-",
        "Tempat/Tgl Lahir": "-"
    }
            
    # Toleransi piksel vertikal (Y) menjadi sangat konsisten pasca pelurusan
    y_tolerance = 25         
    
    # TAHAP 3: Proses Ekstraksi Dinamis Berdasarkan Kedekatan Koordinat
    for i, (bbox, text, prob) in enumerate(ocr_results):
        text_clean = text.upper().strip()
        (tl, tr, br, bl) = bbox                        
        key_y_center = (tl[1] + bl[1]) / 2
        key_x_max = tr[0]                        
        
        # --- DETEKSI NIK ---
        if "NIK" in text_clean:
            digits = ''.join(filter(str.isdigit, text_clean)) # Filtrasi Angka NIK
            if len(digits) >= 12:
                extracted_data["NIK"] = digits
            else:
                for sub_bbox, sub_text, _ in ocr_results:
                    sub_y_center = (sub_bbox[0][1] + sub_bbox[3][1]) / 2
                    if abs(sub_y_center - key_y_center) < y_tolerance and sub_bbox[0][0] > key_x_max:
                        sub_digits = ''.join(filter(str.isdigit, sub_text))
                        if len(sub_digits) >= 12:
                            extracted_data["NIK"] = sub_digits
                            cv2.rectangle(display_img, (int(sub_bbox[0][0]), int(sub_bbox[0][1])), (int(sub_bbox[2][0]), int(sub_bbox[2][1])), (0, 255, 0), 2)
                
        # --- DETEKSI NAMA ---
        elif "NAMA" in text_clean and not "IBU" in text_clean:
            for sub_bbox, sub_text, _ in ocr_results:
                sub_y_center = (sub_bbox[0][1] + sub_bbox[3][1]) / 2
                if abs(sub_y_center - key_y_center) < y_tolerance and sub_bbox[0][0] > key_x_max:
                    extracted_data["Nama"] = sub_text.replace(":", "").strip()
                    cv2.rectangle(display_img, (int(sub_bbox[0][0]), int(sub_bbox[0][1])), (int(sub_bbox[2][0]), int(sub_bbox[2][1])), (0, 255, 0), 2)
                
        # --- DETEKSI TEMPAT/TGL LAHIR ---
        elif "TEMPAT" in text_clean or "LAHIR" in text_clean or "TGL" in text_clean:
            ttl_parts = []
            for sub_bbox, sub_text, _ in ocr_results:
                sub_y_center = (sub_bbox[0][1] + sub_bbox[3][1]) / 2
                if abs(sub_y_center - key_y_center) < y_tolerance and sub_bbox[0][0] > key_x_max:
                    clean_part = sub_text.replace(":", "").strip() # Cleaning Text
                    if clean_part and clean_part not in ttl_parts:
                        ttl_parts.append(clean_part)
                        cv2.rectangle(display_img, (int(sub_bbox[0][0]), int(sub_bbox[0][1])), (int(sub_bbox[2][0]), int(sub_bbox[2][1])), (0, 255, 0), 2)
                        
            if ttl_parts:
                extracted_data["Tempat/Tgl Lahir"] = " ".join(ttl_parts)
                
    # TAHAP 4: Post-Processing & Pembersihan Simbol Sisa
    final_json_dict = {}
    for key, val in extracted_data.items():
        if val.startswith(":") or val.startswith("."):
            val = val[1:].strip()
                    
        # Mapping nama key menjadi lowercase untuk standarisasi JSON API
        if key == "NIK":
            final_json_dict["nik"] = val
        elif key == "Nama":
            final_json_dict["nama"] = val
        elif key == "Tempat/Tgl Lahir":
            final_json_dict["tempat_tgl_lahir"] = val
            
    # TAHAP 5: Konversi Dictionary ke Format Valid String JSON dengan Indentasi 4 Spasi
    json_output_string = json.dumps(final_json_dict, indent=4, ensure_ascii=False)
                
    return display_img, json_output_string

# --- ANTARMUKA GRADIO ---
with gr.Blocks(title="KTP Indonesia OCR Scanner") as demo:
    gr.Markdown("# 🪪 Indonesia ID Card (KTP) OCR Scanner")
    gr.Markdown("Aplikasi menggunakan EasyOCR dengan Deteksi Area Otomatis & Luaran Berformat JSON Standardized.")
            
    with gr.Row():
        with gr.Column():
            # MENAMBAHKAN parameter height agar ukuran kotak unggah terkunci (tidak melar)
            input_image = gr.Image(label="Unggah Foto KTP", height=350)
            btn = gr.Button("Ekstrak Data", variant="primary")
                            
        with gr.Column():
            # MENAMBAHKAN parameter height yang sama agar visualisasi hasil simetris
            output_image = gr.Image(label="Visualisasi Hasil Scan", height=350)
            # Mengunci jumlah baris tampilan agar layout tidak melompat saat teks JSON masuk
            output_results = gr.Textbox(label="Data Terdeteksi (JSON format)", lines=10, max_lines=10)
                    
    btn.click(fn=process_ktp, inputs=input_image, outputs=[output_image, output_results])

if __name__ == "__main__":
    demo.launch()