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() |