DimasMP3 commited on
Commit ·
318b10c
1
Parent(s): ac7382e
fix import
Browse files- app.py +29 -40
- inference.py +52 -58
app.py
CHANGED
|
@@ -1,45 +1,34 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
return _MODEL.predict_dict(image)
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
-
|
| 9 |
-
""
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
"""
|
| 16 |
-
results: List[Dict[str, float]] = []
|
| 17 |
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
if isinstance(x, (str, bytes, os.PathLike)):
|
| 25 |
-
try:
|
| 26 |
-
return Image.open(x).convert("RGB")
|
| 27 |
-
except Exception:
|
| 28 |
-
return None
|
| 29 |
-
try:
|
| 30 |
-
return Image.open(x).convert("RGB")
|
| 31 |
-
except Exception:
|
| 32 |
-
return None
|
| 33 |
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
results.append(_MODEL.predict_dict(pil))
|
| 41 |
-
return results
|
| 42 |
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
__all__ = ["predict", "predict_batch"]
|
|
|
|
| 1 |
+
# app.py
|
| 2 |
+
import os
|
| 3 |
+
import gradio as gr
|
| 4 |
+
from inference import predict, predict_batch # <-- gunakan fungsi dari inference.py
|
|
|
|
| 5 |
|
| 6 |
+
def _payload(d):
|
| 7 |
+
# ubah dict {label: prob} -> payload gradio.Label
|
| 8 |
+
items = sorted(d.items(), key=lambda kv: kv[1], reverse=True)
|
| 9 |
+
return {"label": items[0][0], "confidences": [{"label": k, "confidence": float(v)} for k, v in items]}
|
| 10 |
|
| 11 |
+
with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
| 12 |
+
gr.Markdown("# Face Shape Classification — EfficientNetB4 (300×300)")
|
| 13 |
+
with gr.Row():
|
| 14 |
+
inp = gr.Image(type="pil", label="Upload face (frontal)")
|
| 15 |
+
out = gr.Label(num_top_classes=5, label="Predictions")
|
| 16 |
+
btn = gr.Button("Predict")
|
| 17 |
+
btn.click(lambda im: _payload(predict(im)), inputs=inp, outputs=out)
|
|
|
|
|
|
|
| 18 |
|
| 19 |
+
# Batch tab (opsional)
|
| 20 |
+
with gr.Tab("Batch"):
|
| 21 |
+
gal = gr.Gallery(label="Images").style(grid=4)
|
| 22 |
+
out_gal = gr.JSON(label="Batch outputs")
|
| 23 |
+
runb = gr.Button("Run batch")
|
| 24 |
+
runb.click(lambda ims: [predict_batch(ims)], inputs=gal, outputs=out_gal)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
+
# Examples (jika ada folder examples/)
|
| 27 |
+
if os.path.exists("examples"):
|
| 28 |
+
gr.Examples(
|
| 29 |
+
examples=[os.path.join("examples", f) for f in os.listdir("examples")],
|
| 30 |
+
inputs=inp
|
| 31 |
+
)
|
|
|
|
|
|
|
| 32 |
|
| 33 |
+
if __name__ == "__main__":
|
| 34 |
+
demo.launch()
|
|
|
inference.py
CHANGED
|
@@ -1,31 +1,24 @@
|
|
| 1 |
-
|
| 2 |
-
import json
|
| 3 |
-
import time
|
| 4 |
from typing import List, Dict, Iterable, Any, Optional
|
| 5 |
import numpy as np
|
| 6 |
from PIL import Image
|
| 7 |
import tensorflow as tf
|
| 8 |
|
| 9 |
-
# ----------
|
| 10 |
-
|
| 11 |
-
_DEFAULT_LABELS = ["Heart", "Oblong", "Oval", "Round", "Square"]
|
| 12 |
|
| 13 |
def _load_labels() -> List[str]:
|
| 14 |
-
""
|
| 15 |
-
|
| 16 |
-
p_i2c = os.path.join("models", "idx2class.json")
|
| 17 |
-
|
| 18 |
-
# 1) class_indices.json (label->idx) → urutkan by idx
|
| 19 |
try:
|
| 20 |
with open(p_ci, "r") as f:
|
| 21 |
ci = json.load(f)
|
| 22 |
-
labels = [k for k,
|
| 23 |
print("[LABEL] from class_indices.json ->", labels)
|
| 24 |
return labels
|
| 25 |
except Exception:
|
| 26 |
pass
|
| 27 |
-
|
| 28 |
-
# 2) idx2class.json (idx->label) → urutkan by idx
|
| 29 |
try:
|
| 30 |
with open(p_i2c, "r") as f:
|
| 31 |
i2c = json.load(f)
|
|
@@ -34,23 +27,14 @@ def _load_labels() -> List[str]:
|
|
| 34 |
return labels
|
| 35 |
except Exception:
|
| 36 |
pass
|
| 37 |
-
|
| 38 |
-
# 3) fallback
|
| 39 |
print("[LABEL] fallback default ->", _DEFAULT_LABELS)
|
| 40 |
return _DEFAULT_LABELS
|
| 41 |
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
"""Buat config.json jika belum ada (metadata untuk Space)."""
|
| 45 |
-
if os.path.exists(path):
|
| 46 |
-
return
|
| 47 |
ishape = model.input_shape
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
assert h > 0
|
| 51 |
-
except Exception as e:
|
| 52 |
-
raise AssertionError(f"Input shape tidak valid untuk config: {ishape}") from e
|
| 53 |
-
|
| 54 |
cfg = {
|
| 55 |
"architectures": ["EfficientNetB4"],
|
| 56 |
"image_size": h,
|
|
@@ -58,31 +42,25 @@ def _generate_config_if_missing(model: tf.keras.Model, labels: List[str], path:
|
|
| 58 |
"id2label": {str(i): lbl for i, lbl in enumerate(labels)},
|
| 59 |
"label2id": {lbl: i for i, lbl in enumerate(labels)},
|
| 60 |
}
|
| 61 |
-
with open(path, "w") as f:
|
| 62 |
-
json.dump(cfg, f, indent=2)
|
| 63 |
print(f"[CFG] wrote {path} (image_size={h}, num_labels={len(labels)})")
|
| 64 |
|
| 65 |
-
|
| 66 |
-
# -------------------- Model wrapper --------------------
|
| 67 |
-
|
| 68 |
class FaceShapeModel:
|
| 69 |
-
def __init__(self, model_path
|
| 70 |
-
self.labels
|
| 71 |
-
|
| 72 |
full_path = os.path.join(os.getcwd(), model_path)
|
| 73 |
print(f"[LOAD] {full_path}")
|
| 74 |
-
self.model
|
| 75 |
|
| 76 |
-
# ambil ukuran input (H=W)
|
| 77 |
ishape = self.model.input_shape
|
| 78 |
-
self.img_size
|
| 79 |
print(f"[MODEL] input img_size = {self.img_size}")
|
| 80 |
|
| 81 |
-
|
| 82 |
names_lower = [l.name.lower() for l in self.model.layers[:10]]
|
| 83 |
-
|
| 84 |
-
self.external_rescale
|
| 85 |
-
print(f"[MODEL] internal_preproc={
|
| 86 |
|
| 87 |
_generate_config_if_missing(self.model, self.labels)
|
| 88 |
|
|
@@ -91,32 +69,48 @@ class FaceShapeModel:
|
|
| 91 |
except Exception as e:
|
| 92 |
print("[WARN] warmup failed:", e)
|
| 93 |
|
| 94 |
-
# ---- preprocessing ----
|
| 95 |
def _to_rgb(self, img: Image.Image) -> Image.Image:
|
| 96 |
return img if img.mode == "RGB" else img.convert("RGB")
|
| 97 |
|
| 98 |
def _preprocess(self, img: Image.Image) -> np.ndarray:
|
| 99 |
-
img = self._to_rgb(img)
|
| 100 |
-
img = img.resize((self.img_size, self.img_size))
|
| 101 |
x = np.asarray(img, dtype=np.float32)
|
| 102 |
-
if self.external_rescale:
|
| 103 |
-
|
| 104 |
-
return np.expand_dims(x, axis=0)
|
| 105 |
|
| 106 |
-
# ---- predict ----
|
| 107 |
def predict_dict(self, img: Image.Image) -> Dict[str, float]:
|
| 108 |
-
"""Kembalikan mapping {label: prob} (cocok untuk gradio.Label)."""
|
| 109 |
t0 = time.perf_counter()
|
| 110 |
-
|
| 111 |
-
probs = self.model.predict(x, verbose=0)[0] # (C,)
|
| 112 |
-
out = {lbl: float(p) for lbl, p in zip(self.labels, probs)}
|
| 113 |
dt = (time.perf_counter() - t0) * 1000.0
|
| 114 |
print(f"[INF] {len(self.labels)}-class in {dt:.1f} ms")
|
| 115 |
-
return
|
| 116 |
|
| 117 |
-
|
| 118 |
-
# singleton model (di-load sekali saat import)
|
| 119 |
_MODEL = FaceShapeModel()
|
| 120 |
|
| 121 |
-
|
| 122 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# inference.py
|
| 2 |
+
import os, json, time
|
|
|
|
| 3 |
from typing import List, Dict, Iterable, Any, Optional
|
| 4 |
import numpy as np
|
| 5 |
from PIL import Image
|
| 6 |
import tensorflow as tf
|
| 7 |
|
| 8 |
+
# ---------- Label utils ----------
|
| 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") # {"Label": idx}
|
| 13 |
+
p_i2c = os.path.join("models", "idx2class.json") # {"0":"Label"}
|
|
|
|
|
|
|
|
|
|
| 14 |
try:
|
| 15 |
with open(p_ci, "r") as f:
|
| 16 |
ci = json.load(f)
|
| 17 |
+
labels = [k for k,_ in sorted(ci.items(), key=lambda kv: kv[1])]
|
| 18 |
print("[LABEL] from class_indices.json ->", labels)
|
| 19 |
return labels
|
| 20 |
except Exception:
|
| 21 |
pass
|
|
|
|
|
|
|
| 22 |
try:
|
| 23 |
with open(p_i2c, "r") as f:
|
| 24 |
i2c = json.load(f)
|
|
|
|
| 27 |
return labels
|
| 28 |
except Exception:
|
| 29 |
pass
|
|
|
|
|
|
|
| 30 |
print("[LABEL] fallback default ->", _DEFAULT_LABELS)
|
| 31 |
return _DEFAULT_LABELS
|
| 32 |
|
| 33 |
+
def _generate_config_if_missing(model: tf.keras.Model, labels: List[str], path="config.json"):
|
| 34 |
+
if os.path.exists(path): return
|
|
|
|
|
|
|
|
|
|
| 35 |
ishape = model.input_shape
|
| 36 |
+
h = int(ishape[1])
|
| 37 |
+
assert h > 0, f"Input shape aneh: {ishape}"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
cfg = {
|
| 39 |
"architectures": ["EfficientNetB4"],
|
| 40 |
"image_size": h,
|
|
|
|
| 42 |
"id2label": {str(i): lbl for i, lbl in enumerate(labels)},
|
| 43 |
"label2id": {lbl: i for i, lbl in enumerate(labels)},
|
| 44 |
}
|
| 45 |
+
with open(path, "w") as f: json.dump(cfg, f, indent=2)
|
|
|
|
| 46 |
print(f"[CFG] wrote {path} (image_size={h}, num_labels={len(labels)})")
|
| 47 |
|
| 48 |
+
# ---------- Model wrapper ----------
|
|
|
|
|
|
|
| 49 |
class FaceShapeModel:
|
| 50 |
+
def __init__(self, model_path="models/model.keras"):
|
| 51 |
+
self.labels = _load_labels()
|
|
|
|
| 52 |
full_path = os.path.join(os.getcwd(), model_path)
|
| 53 |
print(f"[LOAD] {full_path}")
|
| 54 |
+
self.model = tf.keras.models.load_model(full_path, compile=False)
|
| 55 |
|
|
|
|
| 56 |
ishape = self.model.input_shape
|
| 57 |
+
self.img_size = int(ishape[1])
|
| 58 |
print(f"[MODEL] input img_size = {self.img_size}")
|
| 59 |
|
|
|
|
| 60 |
names_lower = [l.name.lower() for l in self.model.layers[:10]]
|
| 61 |
+
has_pp = any(("rescaling" in n) or ("normalization" in n) for n in names_lower)
|
| 62 |
+
self.external_rescale = not has_pp
|
| 63 |
+
print(f"[MODEL] internal_preproc={has_pp} -> external_rescale={self.external_rescale}")
|
| 64 |
|
| 65 |
_generate_config_if_missing(self.model, self.labels)
|
| 66 |
|
|
|
|
| 69 |
except Exception as e:
|
| 70 |
print("[WARN] warmup failed:", e)
|
| 71 |
|
|
|
|
| 72 |
def _to_rgb(self, img: Image.Image) -> Image.Image:
|
| 73 |
return img if img.mode == "RGB" else img.convert("RGB")
|
| 74 |
|
| 75 |
def _preprocess(self, img: Image.Image) -> np.ndarray:
|
| 76 |
+
img = self._to_rgb(img).resize((self.img_size, self.img_size))
|
|
|
|
| 77 |
x = np.asarray(img, dtype=np.float32)
|
| 78 |
+
if self.external_rescale: x = x / 255.0
|
| 79 |
+
return np.expand_dims(x, 0) # (1,H,W,3)
|
|
|
|
| 80 |
|
|
|
|
| 81 |
def predict_dict(self, img: Image.Image) -> Dict[str, float]:
|
|
|
|
| 82 |
t0 = time.perf_counter()
|
| 83 |
+
p = self.model.predict(self._preprocess(img), verbose=0)[0]
|
|
|
|
|
|
|
| 84 |
dt = (time.perf_counter() - t0) * 1000.0
|
| 85 |
print(f"[INF] {len(self.labels)}-class in {dt:.1f} ms")
|
| 86 |
+
return {lbl: float(prob) for lbl, prob in zip(self.labels, p)}
|
| 87 |
|
| 88 |
+
# singleton
|
|
|
|
| 89 |
_MODEL = FaceShapeModel()
|
| 90 |
|
| 91 |
+
# ---------- Public API ----------
|
| 92 |
+
def predict(image: Image.Image) -> Dict[str, float]:
|
| 93 |
+
if image is None:
|
| 94 |
+
return {"Error": "No image"}
|
| 95 |
+
return _MODEL.predict_dict(image)
|
| 96 |
+
|
| 97 |
+
def predict_batch(images: Iterable[Any]) -> List[Dict[str, float]]:
|
| 98 |
+
from PIL import Image as _PILImage
|
| 99 |
+
import os as _os
|
| 100 |
+
results: List[Dict[str, float]] = []
|
| 101 |
+
|
| 102 |
+
def _as_pil(x: Any) -> Optional[_PILImage.Image]:
|
| 103 |
+
if x is None: return None
|
| 104 |
+
if isinstance(x, _PILImage.Image): return x
|
| 105 |
+
if isinstance(x, (str, bytes, _os.PathLike)):
|
| 106 |
+
try: return _PILImage.open(x).convert("RGB")
|
| 107 |
+
except Exception: return None
|
| 108 |
+
try: return _PILImage.open(x).convert("RGB")
|
| 109 |
+
except Exception: return None
|
| 110 |
+
|
| 111 |
+
for x in (images or []):
|
| 112 |
+
im = _as_pil(x)
|
| 113 |
+
results.append({"Error": "Invalid image"} if im is None else _MODEL.predict_dict(im))
|
| 114 |
+
return results
|
| 115 |
+
|
| 116 |
+
__all__ = ["predict", "predict_batch"]
|