197 lines
5.3 KiB
Python
197 lines
5.3 KiB
Python
# backend\ml\predict_scene_model.py
|
|
|
|
import argparse
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import torch
|
|
from PIL import Image
|
|
from torchvision import models
|
|
|
|
import subprocess
|
|
import sys
|
|
|
|
|
|
def empty_prediction(source="no_model"):
|
|
return {
|
|
"modelAvailable": False,
|
|
"source": source,
|
|
"peopleCount": 0,
|
|
"maleCount": 0,
|
|
"femaleCount": 0,
|
|
"unknownCount": 0,
|
|
"sexPosition": "unknown",
|
|
"sexPositionScore": 0,
|
|
"bodyPartsPresent": [],
|
|
"objectsPresent": [],
|
|
"clothingPresent": [],
|
|
"boxes": [],
|
|
}
|
|
|
|
|
|
def load_encoder():
|
|
weights = models.ResNet18_Weights.DEFAULT
|
|
model = models.resnet18(weights=weights)
|
|
model.fc = torch.nn.Identity()
|
|
model.eval()
|
|
|
|
preprocess = weights.transforms()
|
|
return model, preprocess
|
|
|
|
|
|
def embed_image(model, preprocess, image_path: Path):
|
|
img = Image.open(image_path).convert("RGB")
|
|
x = preprocess(img).unsqueeze(0)
|
|
|
|
with torch.no_grad():
|
|
emb = model(x).cpu().numpy()[0].astype("float32")
|
|
|
|
norm = np.linalg.norm(emb)
|
|
if norm > 0:
|
|
emb = emb / norm
|
|
|
|
return emb
|
|
|
|
|
|
def weighted_mode_string(items, weights, fallback="unknown"):
|
|
scores = {}
|
|
|
|
for item, weight in zip(items, weights):
|
|
key = str(item or fallback)
|
|
scores[key] = scores.get(key, 0.0) + float(weight)
|
|
|
|
if not scores:
|
|
return fallback, 0.0
|
|
|
|
label, score = max(scores.items(), key=lambda x: x[1])
|
|
total = sum(scores.values()) or 1.0
|
|
return label, float(score / total)
|
|
|
|
|
|
def weighted_int(items, weights):
|
|
if not items:
|
|
return 0
|
|
|
|
total_w = float(np.sum(weights)) or 1.0
|
|
value = sum(float(v or 0) * float(w) for v, w in zip(items, weights)) / total_w
|
|
return int(round(value))
|
|
|
|
|
|
def weighted_multilabel(targets, key, weights, threshold=0.35):
|
|
scores = {}
|
|
total = float(np.sum(weights)) or 1.0
|
|
|
|
for target, weight in zip(targets, weights):
|
|
for label in target.get(key) or []:
|
|
label = str(label).strip()
|
|
if not label:
|
|
continue
|
|
scores[label] = scores.get(label, 0.0) + float(weight)
|
|
|
|
out = []
|
|
for label, score in scores.items():
|
|
conf = float(score / total)
|
|
if conf >= threshold:
|
|
out.append({
|
|
"label": label,
|
|
"score": conf,
|
|
})
|
|
|
|
out.sort(key=lambda x: x["score"], reverse=True)
|
|
return out
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--root", required=True)
|
|
parser.add_argument("--image", required=True)
|
|
args = parser.parse_args()
|
|
|
|
root = Path(args.root)
|
|
image_path = Path(args.image)
|
|
|
|
model_path = root / "model" / "scene_knn.npz"
|
|
targets_path = root / "model" / "targets.json"
|
|
|
|
if not model_path.exists() or not targets_path.exists():
|
|
print(json.dumps(empty_prediction("model_missing")))
|
|
return
|
|
|
|
data = np.load(model_path)
|
|
embeddings = data["embeddings"].astype("float32")
|
|
|
|
with targets_path.open("r", encoding="utf-8") as f:
|
|
targets = json.load(f)
|
|
|
|
if len(embeddings) == 0 or len(targets) == 0:
|
|
print(json.dumps(empty_prediction("model_empty")))
|
|
return
|
|
|
|
encoder, preprocess = load_encoder()
|
|
emb = embed_image(encoder, preprocess, image_path)
|
|
|
|
sims = embeddings @ emb
|
|
|
|
k = min(7, len(sims))
|
|
idx = np.argsort(-sims)[:k]
|
|
top_targets = [targets[int(i)] for i in idx]
|
|
|
|
raw_weights = np.maximum(sims[idx], 0.0)
|
|
if float(np.sum(raw_weights)) <= 0:
|
|
raw_weights = np.ones_like(raw_weights, dtype="float32")
|
|
|
|
weights = raw_weights / (float(np.sum(raw_weights)) or 1.0)
|
|
|
|
sex_labels = [t.get("sexPosition") or "unknown" for t in top_targets]
|
|
sex_position, sex_score = weighted_mode_string(sex_labels, weights, "unknown")
|
|
|
|
pred = {
|
|
"modelAvailable": True,
|
|
"source": "resnet18_knn",
|
|
"peopleCount": weighted_int([t.get("peopleCount") for t in top_targets], weights),
|
|
"maleCount": weighted_int([t.get("maleCount") for t in top_targets], weights),
|
|
"femaleCount": weighted_int([t.get("femaleCount") for t in top_targets], weights),
|
|
"unknownCount": weighted_int([t.get("unknownCount") for t in top_targets], weights),
|
|
"sexPosition": sex_position,
|
|
"sexPositionScore": sex_score,
|
|
"bodyPartsPresent": weighted_multilabel(top_targets, "bodyPartsPresent", weights),
|
|
"objectsPresent": weighted_multilabel(top_targets, "objectsPresent", weights),
|
|
"clothingPresent": weighted_multilabel(top_targets, "clothingPresent", weights),
|
|
"boxes": predict_boxes(root, image_path),
|
|
}
|
|
|
|
print(json.dumps(pred, ensure_ascii=False))
|
|
|
|
def predict_boxes(root: Path, image_path: Path):
|
|
script = Path(__file__).parent / "predict_detector_model.py"
|
|
|
|
if not script.exists():
|
|
return []
|
|
|
|
try:
|
|
proc = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
str(script),
|
|
"--root",
|
|
str(root),
|
|
"--image",
|
|
str(image_path),
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=20,
|
|
)
|
|
|
|
if proc.returncode != 0:
|
|
return []
|
|
|
|
data = json.loads(proc.stdout or "{}")
|
|
return data.get("boxes") or []
|
|
except Exception:
|
|
return []
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |