adhyhh commited on
Commit
0b60706
Β·
verified Β·
1 Parent(s): 346a2bd

Upload 4 files

Browse files
app.py ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ import base64
4
+ import cv2
5
+ import numpy as np
6
+ import tensorflow as tf
7
+ from tensorflow import keras
8
+ from tensorflow.keras import layers
9
+ from fastapi import FastAPI, Security, HTTPException, status
10
+ from fastapi.security.api_key import APIKeyHeader
11
+ from pydantic import BaseModel
12
+
13
+ # ─────────────────────────────────────────
14
+ # 1. DEFINISI CUSTOM LAYER CORNERSTONES
15
+ # ─────────────────────────────────────────
16
+ class ChannelAttention(layers.Layer):
17
+ def __init__(self, reduction_ratio=8, **kwargs):
18
+ super().__init__(**kwargs)
19
+ self.reduction_ratio = reduction_ratio
20
+
21
+ def build(self, input_shape):
22
+ channels = input_shape[-1]
23
+ self.gap = layers.GlobalAveragePooling2D()
24
+ self.fc1 = layers.Dense(max(1, channels // self.reduction_ratio), activation="relu")
25
+ self.fc2 = layers.Dense(channels, activation="sigmoid")
26
+ self.reshape = layers.Reshape((1, 1, channels))
27
+
28
+ def call(self, x):
29
+ scale = self.gap(x)
30
+ scale = self.fc1(scale)
31
+ scale = self.fc2(scale)
32
+ scale = self.reshape(scale)
33
+ return x * scale
34
+
35
+ def get_config(self):
36
+ cfg = super().get_config()
37
+ cfg.update({"reduction_ratio": self.reduction_ratio})
38
+ return cfg
39
+
40
+ class LabelSmoothingCrossEntropy(keras.losses.Loss):
41
+ def __init__(self, smoothing=0.1, **kwargs):
42
+ super().__init__(**kwargs)
43
+ self.smoothing = smoothing
44
+
45
+ def call(self, y_true, y_pred):
46
+ num_classes = tf.cast(tf.shape(y_pred)[-1], tf.float32)
47
+ y_true = tf.cast(y_true, tf.float32)
48
+ if len(y_true.shape) == 1 or (len(y_true.shape) == 2 and y_true.shape[-1] == 1):
49
+ y_true = tf.one_hot(tf.cast(tf.reshape(y_true, [-1]), tf.int32), tf.cast(num_classes, tf.int32))
50
+ smooth_labels = y_true * (1.0 - self.smoothing) + self.smoothing / num_classes
51
+ log_probs = tf.nn.log_softmax(y_pred, axis=-1)
52
+ loss = -tf.reduce_sum(smooth_labels * log_probs, axis=-1)
53
+ return tf.reduce_mean(loss)
54
+
55
+ def get_config(self):
56
+ cfg = super().get_config()
57
+ cfg.update({"smoothing": self.smoothing})
58
+ return cfg
59
+
60
+ def mish(x):
61
+ return x * tf.math.tanh(tf.math.softplus(x))
62
+
63
+ # Map index EMNIST Anda ke Karakter Nyata (Sesuaikan urutan ini dengan label training Anda!)
64
+ EMNIST_MAPPING = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
65
+
66
+ # ─────────────────────────────────────────
67
+ # 2. INITIALIZE FASTAPI & LOAD MODEL
68
+ # ─────────────────────────────────────────
69
+ app = FastAPI(title="EMNIST Complete Grid Sheet OCR API")
70
+
71
+ API_KEY_NAME = "X-API-Key"
72
+ api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
73
+ API_KEY = os.environ.get("MY_SECRET_API_KEY", "KunciRahasia123!")
74
+
75
+ model = keras.models.load_model(
76
+ "best_model.keras",
77
+ custom_objects={
78
+ "ChannelAttention": ChannelAttention,
79
+ "LabelSmoothingCrossEntropy": LabelSmoothingCrossEntropy,
80
+ "mish": mish
81
+ }
82
+ )
83
+
84
+ async def get_api_key(api_key_header: str = Security(api_key_header)):
85
+ if api_key_header == API_KEY: return api_key_header
86
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Invalid API Key")
87
+
88
+ # Skema Request Input untuk menerima gambar Base64 dari Web Frontend
89
+ class ImageInput(BaseModel):
90
+ image_base64: str
91
+
92
+ @app.get("/")
93
+ def home():
94
+ return {"status": "OCR Engine Active"}
95
+
96
+ # ─────────────────────────────────────────
97
+ # 3. ENDPOINT INFERENCE DENGAN LOGIKA GRID
98
+ # ─────────────────────────────────────────
99
+ @app.post("/predict-sheet", dependencies=[Security(get_api_key)])
100
+ def predict_sheet(payload: ImageInput):
101
+ try:
102
+ # Decode gambar base64 yang dikirim frontend ke format OpenCV Mat
103
+ encoded_data = payload.image_base64.split(",")[-1]
104
+ nparr = np.frombuffer(base64.b64decode(encoded_data), np.uint8)
105
+ img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
106
+
107
+ if img is None:
108
+ raise HTTPException(status_code=400, detail="Format gambar tidak valid.")
109
+
110
+ # Preprocessing ke Grayscale dan Thresholding biner
111
+ gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
112
+ _, thresh = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY_INV)
113
+
114
+ # --- LOGIKA SEPARASI GRID ANDA ---
115
+ scale = 28
116
+ horizontal_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (scale, 1))
117
+ vertical_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1, scale))
118
+ detect_horizontal = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, horizontal_kernel, iterations=2)
119
+ detect_vertical = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, vertical_kernel, iterations=2)
120
+
121
+ clean_grid_mask = cv2.addWeighted(detect_horizontal, 0.5, detect_vertical, 0.5, 0)
122
+ clean_grid_mask = cv2.threshold(clean_grid_mask, 10, 255, cv2.THRESH_BINARY)[1]
123
+
124
+ contours, _ = cv2.findContours(clean_grid_mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
125
+ valid_boxes = []
126
+ for contour in contours:
127
+ x, y, w, h = cv2.boundingRect(contour)
128
+ if 20 < w < 200 and 20 < h < 200:
129
+ valid_boxes.append((x, y, w, h))
130
+
131
+ if not valid_boxes:
132
+ return {"status": "success", "text": ""}
133
+
134
+ # Sort & Grouping kotak ke baris-baris (Row-by-Row)
135
+ valid_boxes = sorted(valid_boxes, key=lambda b: b[1])
136
+ rows = []
137
+ current_row = []
138
+ prev_y = valid_boxes[0][1]
139
+ cell_height_threshold = 20
140
+
141
+ for box in valid_boxes:
142
+ if abs(box[1] - prev_y) > cell_height_threshold:
143
+ rows.append(sorted(current_row, key=lambda b: b[0]))
144
+ current_row = [box]
145
+ prev_y = box[1]
146
+ else:
147
+ current_row.append(box)
148
+ if current_row:
149
+ rows.append(sorted(current_row, key=lambda b: b[0]))
150
+
151
+ # --- PROSES EVALUASI KOTAK BLANK VS MODEL PREDICT ---
152
+ extracted_text = []
153
+
154
+ for row in rows:
155
+ space_allocated = False
156
+ has_content_yet = False
157
+ row_text = ""
158
+
159
+ for x, y, w, h in row:
160
+ padding = 5
161
+ cropped_thresh_cell = thresh[y+padding : y+h-padding, x+padding : x+w-padding]
162
+
163
+ # Proteksi ukuran jika cropping di tepi gambar bermasalah
164
+ if cropped_thresh_cell.size == 0:
165
+ continue
166
+
167
+ text_pixel_count = cv2.countNonZero(cropped_thresh_cell)
168
+ total_pixels = cropped_thresh_cell.shape[0] * cropped_thresh_cell.shape[1]
169
+ text_ratio = (text_pixel_count / total_pixels) * 100
170
+
171
+ is_blank = text_ratio <= 1.5
172
+
173
+ if not is_blank:
174
+ # ADA ISI -> Resize kotak ke (28, 28) untuk dimasukkan ke model Keras
175
+ cell_resized = cv2.resize(cropped_thresh_cell, (28, 28), interpolation=cv2.INTER_AREA)
176
+
177
+ # Normalisasi data pixel 0.0 - 1.0
178
+ cell_input = cell_resized.astype(np.float32) / 255.0
179
+ cell_input = np.expand_dims(cell_input, axis=(0, -1)) # Shape menjadi (1, 28, 28, 1)
180
+
181
+ # Prediksi menggunakan model
182
+ logits = model.predict(cell_input, verbose=0)
183
+ pred_idx = np.argmax(logits, axis=-1)[0]
184
+
185
+ # Terjemahkan index angka ke karakter asli string
186
+ char_result = EMNIST_MAPPING[pred_idx] if pred_idx < len(EMNIST_MAPPING) else "?"
187
+ row_text += char_result
188
+
189
+ has_content_yet = True
190
+ space_allocated = False
191
+ else:
192
+ # KOTAK BLANK -> Simpan spasi hanya untuk space pertama pasca konten teks
193
+ if has_content_yet and not space_allocated:
194
+ row_text += " " # <-- DI SINI JADI SPASI
195
+ space_allocated = True
196
+
197
+ if row_text.strip(): # Masukkan baris jika tidak sepenuhnya kosong
198
+ extracted_text.append(row_text.rstrip())
199
+
200
+ # Gabungkan semua baris teks dengan pemisah baris baru (\n)
201
+ final_output_string = "\n".join(extracted_text)
202
+
203
+ return {
204
+ "status": "success",
205
+ "result_text": final_output_string
206
+ }
207
+
208
+ except Exception as e:
209
+ raise HTTPException(status_code=500, detail=f"Gagal memproses kertas grid: {str(e)}")
events.out.tfevents.1780063842.1a82ff0eb631.57.0.v2 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0bf5ccff81ae89528b3daa489b70ce7c3a8902a9ea921d0d5340a4f90e34f8af
3
+ size 1732626
events.out.tfevents.1780063881.1a82ff0eb631.57.1.v2 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7de365a95bf32dab9c994b3d51cd35272af297de4f639d8bed472c6441c64309
3
+ size 9676
requirement.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ fastapi==0.111.0
2
+ uvicorn==0.30.1
3
+ tensorflow-cpu==2.16.1
4
+ numpy==1.26.4
5
+ scikit-learn==1.5.0