| import gradio as gr |
| import cv2 |
| import easyocr |
| import numpy as np |
| import json |
|
|
| |
| 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) |
| |
| |
| blurred = cv2.GaussianBlur(gray, (5, 5), 0) |
| edged = cv2.Canny(blurred, 50, 200) |
| |
| |
| 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) |
| |
| |
| if len(approx) == 4: |
| ktp_contour = approx |
| break |
| |
| |
| if ktp_contour is not None: |
| pts = ktp_contour.reshape(4, 2) |
| 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)] |
| |
| |
| 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 |
| |
| |
| return input_img |
|
|
| def process_ktp(input_img): |
| if input_img is None: |
| return None, "Mohon unggah gambar KTP." |
| |
| |
| img_cropped = detect_and_warp_ktp(input_img) |
| |
| |
| 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) |
| |
| |
| ocr_results = reader.readtext(gray, detail=1, paragraph=False) |
| |
| extracted_data = { |
| "NIK": "-", |
| "Nama": "-", |
| "Tempat/Tgl Lahir": "-" |
| } |
| |
| |
| y_tolerance = 25 |
| |
| |
| 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] |
| |
| |
| if "NIK" in text_clean: |
| digits = ''.join(filter(str.isdigit, text_clean)) |
| 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) |
| |
| |
| 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) |
| |
| |
| 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() |
| 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) |
| |
| |
| final_json_dict = {} |
| for key, val in extracted_data.items(): |
| if val.startswith(":") or val.startswith("."): |
| val = val[1:].strip() |
| |
| |
| 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 |
| |
| |
| json_output_string = json.dumps(final_json_dict, indent=4, ensure_ascii=False) |
| |
| return display_img, json_output_string |
|
|
| |
| 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(): |
| |
| input_image = gr.Image(label="Unggah Foto KTP", height=350) |
| btn = gr.Button("Ekstrak Data", variant="primary") |
| |
| with gr.Column(): |
| |
| output_image = gr.Image(label="Visualisasi Hasil Scan", height=350) |
| |
| 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() |