DimasMP3 commited on
Commit ·
d5412a0
1
Parent(s): 59d02f9
apaya
Browse files- app.py +7 -18
- inference.py +103 -116
app.py
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
from inference import predict, predict_batch
|
| 3 |
|
| 4 |
-
# Tema dan tata letak diatur di sini
|
| 5 |
with gr.Blocks(theme=gr.themes.Soft(), css=None, analytics_enabled=False) as iface:
|
| 6 |
gr.Markdown(
|
| 7 |
"""
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
| 11 |
"""
|
| 12 |
)
|
|
|
|
| 13 |
with gr.Row():
|
| 14 |
with gr.Column(scale=1):
|
| 15 |
image_input = gr.Image(type="pil", label="Unggah Gambar Wajah")
|
|
@@ -17,27 +19,14 @@ with gr.Blocks(theme=gr.themes.Soft(), css=None, analytics_enabled=False) as ifa
|
|
| 17 |
with gr.Column(scale=1):
|
| 18 |
label_output = gr.Label(num_top_classes=3, label="Hasil Prediksi")
|
| 19 |
|
| 20 |
-
|
| 21 |
-
gr.Examples(
|
| 22 |
-
examples=[
|
| 23 |
-
],
|
| 24 |
-
inputs=image_input,
|
| 25 |
-
outputs=label_output,
|
| 26 |
-
fn=predict,
|
| 27 |
-
cache_examples=True
|
| 28 |
-
)
|
| 29 |
-
|
| 30 |
-
# Hubungkan tombol "Prediksi" dengan fungsi predict yang sudah diimpor
|
| 31 |
submit_btn.click(fn=predict, inputs=image_input, outputs=label_output)
|
| 32 |
|
| 33 |
-
# Endpoint batch (opsional) untuk klien JSON; menjaga compat endpoint default tetap single
|
| 34 |
with gr.Accordion("Batch (opsional)", open=False):
|
| 35 |
files_in = gr.Files(file_count="multiple", file_types=["image"], label="Unggah beberapa gambar")
|
| 36 |
json_out = gr.JSON(label="Best of batch (JSON)")
|
| 37 |
batch_btn = gr.Button("Prediksi Batch")
|
| 38 |
batch_btn.click(fn=predict_batch, inputs=files_in, outputs=json_out, api_name="predict_batch")
|
| 39 |
|
| 40 |
-
# Luncurkan aplikasi jika file ini dijalankan secara langsung
|
| 41 |
if __name__ == "__main__":
|
| 42 |
iface.launch()
|
| 43 |
-
|
|
|
|
| 1 |
+
# app.py
|
| 2 |
import gradio as gr
|
| 3 |
from inference import predict, predict_batch
|
| 4 |
|
|
|
|
| 5 |
with gr.Blocks(theme=gr.themes.Soft(), css=None, analytics_enabled=False) as iface:
|
| 6 |
gr.Markdown(
|
| 7 |
"""
|
| 8 |
+
# Deteksi Bentuk Wajah
|
| 9 |
+
|
| 10 |
+
Model **EfficientNetB4** (5 kelas: Heart, Oblong, Oval, Round, Square) yang dimuat dari `models/model.keras`.
|
| 11 |
+
Gunakan foto wajah frontal untuk hasil terbaik.
|
| 12 |
"""
|
| 13 |
)
|
| 14 |
+
|
| 15 |
with gr.Row():
|
| 16 |
with gr.Column(scale=1):
|
| 17 |
image_input = gr.Image(type="pil", label="Unggah Gambar Wajah")
|
|
|
|
| 19 |
with gr.Column(scale=1):
|
| 20 |
label_output = gr.Label(num_top_classes=3, label="Hasil Prediksi")
|
| 21 |
|
| 22 |
+
gr.Examples(examples=[], inputs=image_input, outputs=label_output, fn=predict, cache_examples=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
submit_btn.click(fn=predict, inputs=image_input, outputs=label_output)
|
| 24 |
|
|
|
|
| 25 |
with gr.Accordion("Batch (opsional)", open=False):
|
| 26 |
files_in = gr.Files(file_count="multiple", file_types=["image"], label="Unggah beberapa gambar")
|
| 27 |
json_out = gr.JSON(label="Best of batch (JSON)")
|
| 28 |
batch_btn = gr.Button("Prediksi Batch")
|
| 29 |
batch_btn.click(fn=predict_batch, inputs=files_in, outputs=json_out, api_name="predict_batch")
|
| 30 |
|
|
|
|
| 31 |
if __name__ == "__main__":
|
| 32 |
iface.launch()
|
|
|
inference.py
CHANGED
|
@@ -1,145 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import numpy as np
|
| 2 |
from PIL import Image
|
| 3 |
import tensorflow as tf
|
| 4 |
-
from typing import List, Dict, Tuple, Any
|
| 5 |
-
import time
|
| 6 |
-
import os
|
| 7 |
-
|
| 8 |
-
# --- 1. Konfigurasi Model ---
|
| 9 |
-
LABELS: List[str] = ["Heart", "Oblong", "Oval", "Round", "Square"]
|
| 10 |
-
IMG_SIZE = 244
|
| 11 |
-
|
| 12 |
-
# --- 2. Fungsi Preprocessing (Tidak Berubah) ---
|
| 13 |
-
def preprocess_image(image: Image.Image) -> np.ndarray:
|
| 14 |
-
if image.mode != "RGB":
|
| 15 |
-
image = image.convert("RGB")
|
| 16 |
-
image = image.resize((IMG_SIZE, IMG_SIZE))
|
| 17 |
-
image_array = np.asarray(image)
|
| 18 |
-
image_array = np.expand_dims(image_array, axis=0)
|
| 19 |
-
processed_array = tf.keras.applications.efficientnet.preprocess_input(image_array)
|
| 20 |
-
return processed_array
|
| 21 |
|
| 22 |
-
# ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
class FaceShapeModel:
|
| 24 |
-
def __init__(self, model_path: str = "models
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
try:
|
| 31 |
-
|
| 32 |
-
inputs = tf.keras.layers.Input(shape=(IMG_SIZE, IMG_SIZE, 3))
|
| 33 |
-
base_model = tf.keras.applications.EfficientNetB2(
|
| 34 |
-
weights=None,
|
| 35 |
-
include_top=False,
|
| 36 |
-
pooling='avg',
|
| 37 |
-
input_tensor=inputs
|
| 38 |
-
)
|
| 39 |
-
x = tf.keras.layers.Dropout(0.4)(base_model.output)
|
| 40 |
-
outputs = tf.keras.layers.Dense(len(LABELS), activation='softmax')(x)
|
| 41 |
-
|
| 42 |
-
rebuilt_model = tf.keras.Model(inputs=inputs, outputs=outputs)
|
| 43 |
-
print("Arsitektur model baru berhasil dibuat.")
|
| 44 |
-
|
| 45 |
-
# 2. Muat HANYA bobotnya ke dalam arsitektur yang sudah benar
|
| 46 |
-
full_path = os.path.join(os.getcwd(), model_path)
|
| 47 |
-
print(f"Mencoba memuat bobot dari: {full_path}")
|
| 48 |
-
rebuilt_model.load_weights(full_path)
|
| 49 |
-
|
| 50 |
-
self.model = rebuilt_model
|
| 51 |
-
print("Model berhasil dibangun ulang dan bobot telah dimuat.")
|
| 52 |
-
|
| 53 |
-
# Warm-up: satu inference dummy agar jalur model siap (mengurangi cold-start)
|
| 54 |
-
try:
|
| 55 |
-
_ = self.model(tf.zeros((1, IMG_SIZE, IMG_SIZE, 3)))
|
| 56 |
-
print("Warm-up inference completed.")
|
| 57 |
-
except Exception as werr:
|
| 58 |
-
print(f"Warm-up failed (non-fatal): {werr}")
|
| 59 |
-
|
| 60 |
except Exception as e:
|
| 61 |
-
print(
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
def predict_image(self, image: Image.Image) -> Dict[str, float]:
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
return {"Error": "Model tidak dapat dimuat."}
|
| 68 |
if image is None:
|
| 69 |
-
return {"Error": "Gambar
|
| 70 |
-
|
| 71 |
t0 = time.perf_counter()
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
print(f"predict_image: took {dt:.1f} ms")
|
| 78 |
-
return confidences
|
| 79 |
|
| 80 |
-
#
|
| 81 |
-
|
| 82 |
|
|
|
|
| 83 |
def predict(image_pil: Image.Image) -> Dict[str, float]:
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
return model_instance.predict_image(image_pil)
|
| 87 |
return {"Error": "Instance model tidak tersedia."}
|
| 88 |
|
| 89 |
-
|
| 90 |
-
# --- Batch prediction opsional ---
|
| 91 |
def _dict_to_payload(d: Dict[str, float]) -> Dict[str, object]:
|
| 92 |
-
|
| 93 |
-
if not d or any(k == "Error" for k in d.keys()):
|
| 94 |
return {"label": "", "confidences": []}
|
| 95 |
-
items
|
| 96 |
-
|
| 97 |
-
confs = [{"label": k, "confidence": float(v)} for k, v in items]
|
| 98 |
-
return {"label": top, "confidences": confs}
|
| 99 |
-
|
| 100 |
|
| 101 |
def _to_pil_list(files: List[Any]) -> List[Image.Image]:
|
| 102 |
-
out
|
| 103 |
-
for f in files:
|
| 104 |
try:
|
| 105 |
if isinstance(f, Image.Image):
|
| 106 |
-
out.append(f)
|
| 107 |
-
|
| 108 |
-
# Gradio >=4: FileData with .path
|
| 109 |
-
path = None
|
| 110 |
-
if hasattr(f, "path") and isinstance(getattr(f, "path"), str):
|
| 111 |
-
path = getattr(f, "path")
|
| 112 |
-
elif isinstance(f, str):
|
| 113 |
-
path = f
|
| 114 |
-
elif hasattr(f, "name") and isinstance(getattr(f, "name"), str):
|
| 115 |
-
path = getattr(f, "name")
|
| 116 |
if path:
|
| 117 |
-
|
| 118 |
-
out.append(img)
|
| 119 |
except Exception:
|
| 120 |
continue
|
| 121 |
return out
|
| 122 |
|
| 123 |
-
|
| 124 |
def predict_batch(files: List[Any]) -> Dict[str, object]:
|
| 125 |
-
|
| 126 |
-
if not model_instance:
|
| 127 |
return {"error": "Instance model tidak tersedia."}
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
payload = _dict_to_payload(d)
|
| 140 |
-
conf = float(payload.get("confidences", [{"confidence": 0.0}])[0]["confidence"]) if payload.get("confidences") else 0.0
|
| 141 |
-
if conf > best_conf:
|
| 142 |
-
best_conf = conf
|
| 143 |
-
best_payload = payload
|
| 144 |
-
return best_payload
|
| 145 |
-
|
|
|
|
| 1 |
+
# inference.py
|
| 2 |
+
import os, json, time
|
| 3 |
+
from typing import List, Dict, Tuple, Any
|
| 4 |
import numpy as np
|
| 5 |
from PIL import Image
|
| 6 |
import tensorflow as tf
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
+
# ---------- Label handling ----------
|
| 9 |
+
_DEFAULT_LABELS = ["Heart","Oblong","Oval","Round","Square"]
|
| 10 |
+
|
| 11 |
+
def _load_labels() -> List[str]:
|
| 12 |
+
p_ci = os.path.join("models", "class_indices.json")
|
| 13 |
+
p_i2c = os.path.join("models", "idx2class.json")
|
| 14 |
+
# Prioritas: class_indices.json
|
| 15 |
+
try:
|
| 16 |
+
with open(p_ci, "r") as f:
|
| 17 |
+
d = json.load(f) # {"Label": idx, ...}
|
| 18 |
+
items = sorted(d.items(), key=lambda kv: kv[1])
|
| 19 |
+
labels = [k for k,_ in items]
|
| 20 |
+
print(f"[LABEL] from class_indices.json -> {labels}")
|
| 21 |
+
return labels
|
| 22 |
+
except Exception:
|
| 23 |
+
pass
|
| 24 |
+
# Fallback: idx2class.json
|
| 25 |
+
try:
|
| 26 |
+
with open(p_i2c, "r") as f:
|
| 27 |
+
d = json.load(f) # {"0":"Label",...}
|
| 28 |
+
items = sorted(((int(k),v) for k,v in d.items()), key=lambda kv: kv[0])
|
| 29 |
+
labels = [v for _,v in items]
|
| 30 |
+
print(f"[LABEL] from idx2class.json -> {labels}")
|
| 31 |
+
return labels
|
| 32 |
+
except Exception:
|
| 33 |
+
print(f"[LABEL] fallback default -> {_DEFAULT_LABELS}")
|
| 34 |
+
return _DEFAULT_LABELS
|
| 35 |
+
|
| 36 |
+
LABELS: List[str] = _load_labels()
|
| 37 |
+
|
| 38 |
+
# ---------- Model loader ----------
|
| 39 |
class FaceShapeModel:
|
| 40 |
+
def __init__(self, model_path: str = os.path.join("models","model.keras")) -> None:
|
| 41 |
+
self.model = None
|
| 42 |
+
self.img_size = 224
|
| 43 |
+
self.external_rescale = False
|
| 44 |
+
|
| 45 |
+
full_path = os.path.join(os.getcwd(), model_path)
|
| 46 |
+
print(f"[LOAD] {full_path}")
|
| 47 |
+
self.model = tf.keras.models.load_model(full_path, compile=False)
|
| 48 |
+
|
| 49 |
+
# Ambil ukuran input dari model (None, H, W, 3)
|
| 50 |
+
ishape = self.model.input_shape
|
| 51 |
+
if isinstance(ishape, (list, tuple)):
|
| 52 |
+
h = ishape[1] if len(ishape) > 1 else None
|
| 53 |
+
if isinstance(h, int) and h > 0:
|
| 54 |
+
self.img_size = h
|
| 55 |
+
print(f"[MODEL] input img_size = {self.img_size}")
|
| 56 |
+
|
| 57 |
+
# Deteksi preprocessing internal (rescaling/normalization)
|
| 58 |
+
first_names = [l.name.lower() for l in self.model.layers[:8]]
|
| 59 |
+
has_internal_pp = any(("rescaling" in n) or ("normalization" in n) for n in first_names)
|
| 60 |
+
self.external_rescale = not has_internal_pp
|
| 61 |
+
print(f"[MODEL] internal_preproc={has_internal_pp} -> external_rescale={self.external_rescale}")
|
| 62 |
+
|
| 63 |
+
# Warm-up (optional)
|
| 64 |
try:
|
| 65 |
+
_ = self.model(tf.zeros((1, self.img_size, self.img_size, 3), dtype=tf.float32))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
except Exception as e:
|
| 67 |
+
print("[WARN] warmup failed:", e)
|
| 68 |
+
|
| 69 |
+
def _preprocess(self, image: Image.Image) -> np.ndarray:
|
| 70 |
+
if image.mode != "RGB":
|
| 71 |
+
image = image.convert("RGB")
|
| 72 |
+
image = image.resize((self.img_size, self.img_size))
|
| 73 |
+
x = np.asarray(image, dtype=np.float32) # [0..255]
|
| 74 |
+
if self.external_rescale:
|
| 75 |
+
x = x / 255.0
|
| 76 |
+
return np.expand_dims(x, axis=0)
|
| 77 |
|
| 78 |
def predict_image(self, image: Image.Image) -> Dict[str, float]:
|
| 79 |
+
if self.model is None:
|
| 80 |
+
return {"Error": "Model tidak dimuat."}
|
|
|
|
| 81 |
if image is None:
|
| 82 |
+
return {"Error": "Gambar kosong."}
|
|
|
|
| 83 |
t0 = time.perf_counter()
|
| 84 |
+
xin = self._preprocess(image)
|
| 85 |
+
probs = self.model.predict(xin, verbose=0)[0] # (C,)
|
| 86 |
+
out = {lbl: float(p) for lbl, p in zip(LABELS, probs)}
|
| 87 |
+
print(f"[INF] {len(LABELS)}-class in {(time.perf_counter()-t0)*1000:.1f} ms")
|
| 88 |
+
return out
|
|
|
|
|
|
|
| 89 |
|
| 90 |
+
# singleton
|
| 91 |
+
_model = FaceShapeModel()
|
| 92 |
|
| 93 |
+
# gradio APIs
|
| 94 |
def predict(image_pil: Image.Image) -> Dict[str, float]:
|
| 95 |
+
if _model and _model.model is not None:
|
| 96 |
+
return _model.predict_image(image_pil)
|
|
|
|
| 97 |
return {"Error": "Instance model tidak tersedia."}
|
| 98 |
|
| 99 |
+
# (opsional) batch
|
|
|
|
| 100 |
def _dict_to_payload(d: Dict[str, float]) -> Dict[str, object]:
|
| 101 |
+
if not d or any(k == "Error" for k in d):
|
|
|
|
| 102 |
return {"label": "", "confidences": []}
|
| 103 |
+
items = sorted(d.items(), key=lambda kv: kv[1], reverse=True)
|
| 104 |
+
return {"label": items[0][0], "confidences": [{"label": k, "confidence": float(v)} for k,v in items]}
|
|
|
|
|
|
|
|
|
|
| 105 |
|
| 106 |
def _to_pil_list(files: List[Any]) -> List[Image.Image]:
|
| 107 |
+
out = []
|
| 108 |
+
for f in files or []:
|
| 109 |
try:
|
| 110 |
if isinstance(f, Image.Image):
|
| 111 |
+
out.append(f); continue
|
| 112 |
+
path = getattr(f, "path", None) or getattr(f, "name", None) or (f if isinstance(f,str) else None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
if path:
|
| 114 |
+
out.append(Image.open(path).convert("RGB"))
|
|
|
|
| 115 |
except Exception:
|
| 116 |
continue
|
| 117 |
return out
|
| 118 |
|
|
|
|
| 119 |
def predict_batch(files: List[Any]) -> Dict[str, object]:
|
| 120 |
+
if not _model or _model.model is None:
|
|
|
|
| 121 |
return {"error": "Instance model tidak tersedia."}
|
| 122 |
+
imgs = _to_pil_list(files)
|
| 123 |
+
if not imgs:
|
| 124 |
+
return {"error": "Tidak ada gambar valid."}
|
| 125 |
+
best, bestc = {"label":"", "confidences":[]}, -1.0
|
| 126 |
+
for img in imgs:
|
| 127 |
+
d = _model.predict_image(img)
|
| 128 |
+
p = _dict_to_payload(d)
|
| 129 |
+
c = float(p["confidences"][0]["confidence"]) if p["confidences"] else 0.0
|
| 130 |
+
if c > bestc:
|
| 131 |
+
best, bestc = p, c
|
| 132 |
+
return best
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|