Spaces:
Sleeping
Sleeping
Update main.py
Browse files
main.py
CHANGED
|
@@ -7,6 +7,7 @@ from fastapi.middleware.cors import CORSMiddleware
|
|
| 7 |
from fastapi.responses import JSONResponse
|
| 8 |
from PIL import Image
|
| 9 |
import tensorflow as tf
|
|
|
|
| 10 |
|
| 11 |
# optional gatekeep
|
| 12 |
try:
|
|
@@ -15,21 +16,19 @@ try:
|
|
| 15 |
except Exception:
|
| 16 |
HAS_OPENCV = False
|
| 17 |
|
| 18 |
-
# HF Hub (สำหรับดึง derm-foundation)
|
| 19 |
-
from huggingface_hub import snapshot_download
|
| 20 |
-
|
| 21 |
logging.basicConfig(level=logging.INFO)
|
| 22 |
logger = logging.getLogger("skinclassify")
|
| 23 |
|
| 24 |
# ---------------------- Config ----------------------
|
| 25 |
DERM_MODEL_ID = os.getenv("DERM_MODEL_ID", "google/derm-foundation")
|
| 26 |
-
DERM_LOCAL_DIR = os.getenv("DERM_LOCAL_DIR", "") # path
|
| 27 |
-
|
|
|
|
| 28 |
THRESHOLDS_PATH = os.getenv("THRESHOLDS_PATH", "Models/mlp_thresholds.npy")
|
| 29 |
MU_PATH = os.getenv("MU_PATH", "Models/mu.npy")
|
| 30 |
SD_PATH = os.getenv("SD_PATH", "Models/sd.npy")
|
| 31 |
LABELS_PATH = os.getenv("LABELS_PATH", "Models/class_names.json")
|
| 32 |
-
NPZ_PATH = os.getenv("NPZ_PATH", "") #
|
| 33 |
|
| 34 |
TOPK = int(os.getenv("TOPK", "5"))
|
| 35 |
|
|
@@ -40,15 +39,13 @@ MIN_BRIGHT, MAX_BRIGHT = float(os.getenv("MIN_BRIGHT", "20")), float(os.getenv("
|
|
| 40 |
MIN_SKIN_RATIO = float(os.getenv("MIN_SKIN_RATIO", "0.15"))
|
| 41 |
MIN_SHARPNESS = float(os.getenv("MIN_SHARPNESS", "30.0"))
|
| 42 |
|
| 43 |
-
# Performance
|
| 44 |
os.environ.setdefault("TF_NUM_INTRAOP_THREADS", "1")
|
| 45 |
os.environ.setdefault("TF_NUM_INTEROP_THREADS", "1")
|
| 46 |
os.environ.setdefault("OMP_NUM_THREADS", "1")
|
| 47 |
os.environ.setdefault("TF_CPP_MIN_LOG_LEVEL", "2")
|
| 48 |
|
| 49 |
-
# Upload size (กัน DoS)
|
| 50 |
MAX_UPLOAD = int(os.getenv("MAX_UPLOAD", str(6 * 1024 * 1024))) # 6MB
|
| 51 |
-
|
| 52 |
DF_SIZE = (448, 448)
|
| 53 |
|
| 54 |
app = FastAPI(title="SkinClassify API (Derm-Foundation)", version="2.0.0")
|
|
@@ -79,43 +76,13 @@ else:
|
|
| 79 |
raise RuntimeError("LABELS_PATH not found and NPZ_PATH not provided.")
|
| 80 |
C = len(CLASS_NAMES)
|
| 81 |
|
| 82 |
-
# ---------------------- Load head ----------------------
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
def load_head_any(path: str):
|
| 88 |
-
# กรณีเป็นโฟลเดอร์ SavedModel
|
| 89 |
-
if os.path.isdir(path) and os.path.exists(os.path.join(path, "saved_model.pb")):
|
| 90 |
-
m = tf.saved_model.load(path)
|
| 91 |
-
sig = m.signatures.get("serving_default") or next(iter(m.signatures.values()))
|
| 92 |
-
# หาชื่อ input/output อัตโนมัติ
|
| 93 |
-
in_names = list(sig.structured_input_signature[1].keys())
|
| 94 |
-
out_names = list(sig.structured_outputs.keys())
|
| 95 |
-
|
| 96 |
-
def _predict(z_np: np.ndarray) -> np.ndarray:
|
| 97 |
-
z_tf = tf.convert_to_tensor(z_np, dtype=tf.float32)
|
| 98 |
-
# บางรุ่นรับเป็น args บางรุ่นรับเป็น kwargs
|
| 99 |
-
if len(in_names) == 1:
|
| 100 |
-
out_dict = sig(z_tf)
|
| 101 |
-
else:
|
| 102 |
-
out_dict = sig(**{in_names[0]: z_tf})
|
| 103 |
-
y = out_dict[out_names[0]]
|
| 104 |
-
return y.numpy()
|
| 105 |
-
return _predict
|
| 106 |
-
|
| 107 |
-
# กรณีเป็นไฟล์ .h5 / .keras ที่ tf.keras อ่านได้
|
| 108 |
-
try:
|
| 109 |
-
model = tf.keras.models.load_model(path, compile=False)
|
| 110 |
-
return lambda z_np: model.predict(z_np, verbose=0)
|
| 111 |
-
except Exception as e:
|
| 112 |
-
raise RuntimeError(f"Cannot load head from {path}: {e}")
|
| 113 |
-
|
| 114 |
-
# ตั้ง path ไปยัง SavedModel ที่อัปขึ้น
|
| 115 |
-
HEAD_PATH = os.getenv("HEAD_PATH", "Models/mlp_head_savedmodel/")
|
| 116 |
-
logger.info(f"Loading head from {HEAD_PATH}")
|
| 117 |
-
head_predict = load_head_any(HEAD_PATH)
|
| 118 |
|
|
|
|
| 119 |
|
| 120 |
# ---------------------- Load mu/sd ----------------------
|
| 121 |
def _load_mu_sd():
|
|
@@ -142,14 +109,7 @@ else:
|
|
| 142 |
logger.warning("THRESHOLDS_PATH not found -> default 0.5 for all classes")
|
| 143 |
best_th = np.full(C, 0.5, dtype="float32")
|
| 144 |
|
| 145 |
-
# ---------------------- Wrap head with standardization ----------------------
|
| 146 |
-
inp = tf.keras.Input(shape=(mu.shape[-1],), name="embedding")
|
| 147 |
-
x = tf.keras.layers.Lambda(lambda e: (e - mu) / (sd + 1e-6), name="standardize")(inp)
|
| 148 |
-
out = head(x)
|
| 149 |
-
clf = tf.keras.Model(inp, out, name="head_with_norm")
|
| 150 |
-
|
| 151 |
# ---------------------- Load derm-foundation ----------------------
|
| 152 |
-
# ใช้ snapshot_download + tf.saved_model.load (ถูกกับโมเดลของ Google)
|
| 153 |
logger.info("Loading Derm Foundation (first time may take a while)...")
|
| 154 |
try:
|
| 155 |
if DERM_LOCAL_DIR and os.path.isdir(DERM_LOCAL_DIR) and os.path.exists(os.path.join(DERM_LOCAL_DIR, "saved_model.pb")):
|
|
@@ -225,19 +185,6 @@ def gatekeep_image(img_bytes: bytes) -> Dict[str, Any]:
|
|
| 225 |
if ratio < MIN_SKIN_RATIO: reasons.append("not_skin_like")
|
| 226 |
return {"ok": len(reasons)==0, "reasons": reasons, "metrics": metrics}
|
| 227 |
|
| 228 |
-
# def predict_probs(img_bytes: bytes) -> np.ndarray:
|
| 229 |
-
# pil = Image.open(io.BytesIO(img_bytes)).convert("RGB").resize(DF_SIZE)
|
| 230 |
-
# by = pil_to_png_bytes_448(pil)
|
| 231 |
-
# ex = tf.train.Example(features=tf.train.Features(
|
| 232 |
-
# feature={'image/encoded': tf.train.Feature(bytes_list=tf.train.BytesList(value=[by]))}
|
| 233 |
-
# )).SerializeToString()
|
| 234 |
-
# out = infer(inputs=tf.constant([ex]))
|
| 235 |
-
# # ปกติคีย์จะชื่อ "embedding"
|
| 236 |
-
# if "embedding" not in out:
|
| 237 |
-
# raise RuntimeError(f"Unexpected derm-foundation outputs: {list(out.keys())}")
|
| 238 |
-
# emb = out["embedding"].numpy().astype("float32") # (1, 6144)
|
| 239 |
-
# probs = clf.predict(emb, verbose=0)[0]
|
| 240 |
-
# return probs
|
| 241 |
def predict_probs(img_bytes: bytes) -> np.ndarray:
|
| 242 |
pil = Image.open(io.BytesIO(img_bytes)).convert("RGB").resize(DF_SIZE)
|
| 243 |
by = pil_to_png_bytes_448(pil)
|
|
@@ -247,13 +194,11 @@ def predict_probs(img_bytes: bytes) -> np.ndarray:
|
|
| 247 |
out = infer(inputs=tf.constant([ex]))
|
| 248 |
if "embedding" not in out:
|
| 249 |
raise RuntimeError(f"Unexpected derm-foundation outputs: {list(out.keys())}")
|
| 250 |
-
emb = out["embedding"].numpy().astype("float32") # (1,6144)
|
| 251 |
z = (emb - mu) / (sd + 1e-6)
|
| 252 |
-
probs =
|
| 253 |
return probs
|
| 254 |
|
| 255 |
-
|
| 256 |
-
|
| 257 |
# ---------------------- Endpoints ----------------------
|
| 258 |
@app.get("/health")
|
| 259 |
def health():
|
|
@@ -266,7 +211,6 @@ def health():
|
|
| 266 |
|
| 267 |
@app.post("/predict")
|
| 268 |
async def predict(request: Request, file: UploadFile = File(...)):
|
| 269 |
-
# limit content-length
|
| 270 |
cl = request.headers.get("content-length")
|
| 271 |
if cl and int(cl) > MAX_UPLOAD:
|
| 272 |
raise HTTPException(413, "File too large")
|
|
@@ -297,8 +241,6 @@ async def predict(request: Request, file: UploadFile = File(...)):
|
|
| 297 |
}
|
| 298 |
}
|
| 299 |
|
| 300 |
-
# สำหรับรันนอก Docker (เช่นทดสอบ local)
|
| 301 |
if __name__ == "__main__":
|
| 302 |
import uvicorn
|
| 303 |
-
|
| 304 |
-
uvicorn.run(app, host="0.0.0.0", port=port, workers=1)
|
|
|
|
| 7 |
from fastapi.responses import JSONResponse
|
| 8 |
from PIL import Image
|
| 9 |
import tensorflow as tf
|
| 10 |
+
from huggingface_hub import snapshot_download
|
| 11 |
|
| 12 |
# optional gatekeep
|
| 13 |
try:
|
|
|
|
| 16 |
except Exception:
|
| 17 |
HAS_OPENCV = False
|
| 18 |
|
|
|
|
|
|
|
|
|
|
| 19 |
logging.basicConfig(level=logging.INFO)
|
| 20 |
logger = logging.getLogger("skinclassify")
|
| 21 |
|
| 22 |
# ---------------------- Config ----------------------
|
| 23 |
DERM_MODEL_ID = os.getenv("DERM_MODEL_ID", "google/derm-foundation")
|
| 24 |
+
DERM_LOCAL_DIR = os.getenv("DERM_LOCAL_DIR", "") # path to local SavedModel if offline
|
| 25 |
+
|
| 26 |
+
HEAD_PATH = os.getenv("HEAD_PATH", "Models/mlp_best.keras") # <== ใช้ .keras ตรง ๆ
|
| 27 |
THRESHOLDS_PATH = os.getenv("THRESHOLDS_PATH", "Models/mlp_thresholds.npy")
|
| 28 |
MU_PATH = os.getenv("MU_PATH", "Models/mu.npy")
|
| 29 |
SD_PATH = os.getenv("SD_PATH", "Models/sd.npy")
|
| 30 |
LABELS_PATH = os.getenv("LABELS_PATH", "Models/class_names.json")
|
| 31 |
+
NPZ_PATH = os.getenv("NPZ_PATH", "") # optional fallback for mu/sd/class_names
|
| 32 |
|
| 33 |
TOPK = int(os.getenv("TOPK", "5"))
|
| 34 |
|
|
|
|
| 39 |
MIN_SKIN_RATIO = float(os.getenv("MIN_SKIN_RATIO", "0.15"))
|
| 40 |
MIN_SHARPNESS = float(os.getenv("MIN_SHARPNESS", "30.0"))
|
| 41 |
|
| 42 |
+
# Performance: กัน OOM บน Free Space
|
| 43 |
os.environ.setdefault("TF_NUM_INTRAOP_THREADS", "1")
|
| 44 |
os.environ.setdefault("TF_NUM_INTEROP_THREADS", "1")
|
| 45 |
os.environ.setdefault("OMP_NUM_THREADS", "1")
|
| 46 |
os.environ.setdefault("TF_CPP_MIN_LOG_LEVEL", "2")
|
| 47 |
|
|
|
|
| 48 |
MAX_UPLOAD = int(os.getenv("MAX_UPLOAD", str(6 * 1024 * 1024))) # 6MB
|
|
|
|
| 49 |
DF_SIZE = (448, 448)
|
| 50 |
|
| 51 |
app = FastAPI(title="SkinClassify API (Derm-Foundation)", version="2.0.0")
|
|
|
|
| 76 |
raise RuntimeError("LABELS_PATH not found and NPZ_PATH not provided.")
|
| 77 |
C = len(CLASS_NAMES)
|
| 78 |
|
| 79 |
+
# ---------------------- Load head (.keras via Keras3) ----------------------
|
| 80 |
+
def load_head_keras3(path: str):
|
| 81 |
+
import keras
|
| 82 |
+
logger.info(f"Loading head (.keras) via Keras3 from {path}")
|
| 83 |
+
return keras.saving.load_model(path, compile=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
+
head = load_head_keras3(HEAD_PATH)
|
| 86 |
|
| 87 |
# ---------------------- Load mu/sd ----------------------
|
| 88 |
def _load_mu_sd():
|
|
|
|
| 109 |
logger.warning("THRESHOLDS_PATH not found -> default 0.5 for all classes")
|
| 110 |
best_th = np.full(C, 0.5, dtype="float32")
|
| 111 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
# ---------------------- Load derm-foundation ----------------------
|
|
|
|
| 113 |
logger.info("Loading Derm Foundation (first time may take a while)...")
|
| 114 |
try:
|
| 115 |
if DERM_LOCAL_DIR and os.path.isdir(DERM_LOCAL_DIR) and os.path.exists(os.path.join(DERM_LOCAL_DIR, "saved_model.pb")):
|
|
|
|
| 185 |
if ratio < MIN_SKIN_RATIO: reasons.append("not_skin_like")
|
| 186 |
return {"ok": len(reasons)==0, "reasons": reasons, "metrics": metrics}
|
| 187 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
def predict_probs(img_bytes: bytes) -> np.ndarray:
|
| 189 |
pil = Image.open(io.BytesIO(img_bytes)).convert("RGB").resize(DF_SIZE)
|
| 190 |
by = pil_to_png_bytes_448(pil)
|
|
|
|
| 194 |
out = infer(inputs=tf.constant([ex]))
|
| 195 |
if "embedding" not in out:
|
| 196 |
raise RuntimeError(f"Unexpected derm-foundation outputs: {list(out.keys())}")
|
| 197 |
+
emb = out["embedding"].numpy().astype("float32") # (1, 6144)
|
| 198 |
z = (emb - mu) / (sd + 1e-6)
|
| 199 |
+
probs = head.predict(z, verbose=0)[0] # head (.keras) โดยตรง
|
| 200 |
return probs
|
| 201 |
|
|
|
|
|
|
|
| 202 |
# ---------------------- Endpoints ----------------------
|
| 203 |
@app.get("/health")
|
| 204 |
def health():
|
|
|
|
| 211 |
|
| 212 |
@app.post("/predict")
|
| 213 |
async def predict(request: Request, file: UploadFile = File(...)):
|
|
|
|
| 214 |
cl = request.headers.get("content-length")
|
| 215 |
if cl and int(cl) > MAX_UPLOAD:
|
| 216 |
raise HTTPException(413, "File too large")
|
|
|
|
| 241 |
}
|
| 242 |
}
|
| 243 |
|
|
|
|
| 244 |
if __name__ == "__main__":
|
| 245 |
import uvicorn
|
| 246 |
+
uvicorn.run(app, host="0.0.0.0", port=int(os.getenv("PORT", "7860")), workers=1)
|
|
|