hasari-api / services /ml /visualization.py
erdoganpeker's picture
v0.3.0 — multimodal vehicle damage MVP
e327f0d
"""
visualization.py — Zengin görsel overlay üretimi.
Her inspection için 3 PNG dosyası üretir:
1. annotated.jpg — ana sonuç (parça yarı saydam + hasar bbox/polygon)
2. parts.png — sadece parça segmentasyonu
3. damages.png — sadece hasar mask'ları (şiddete göre renk kodlu)
Kullanim:
from visualization import render_all
paths = render_all(image, damages, parts, output_dir="./visuals",
inspection_id="abc123")
"""
import hashlib
import logging
from pathlib import Path
import cv2
import numpy as np
logger = logging.getLogger(__name__)
# Hasar şiddet renkleri (BGR formatında - OpenCV)
SEVERITY_COLORS_BGR = {
"hafif": (129, 199, 132), # yeşil
"orta": (51, 153, 255), # turuncu/amber
"agir": (51, 51, 239), # kırmızı
None: (180, 180, 180), # gri
}
# Parça için deterministik renkler (hash'ten)
def part_color_bgr(part_name):
"""Parça adından deterministik BGR renk üret."""
h = hashlib.md5(part_name.encode()).digest()
# 0-255 arası 3 byte → BGR
return (int(h[0]) % 200 + 55, int(h[1]) % 200 + 55, int(h[2]) % 200 + 55)
def severity_color(sev_level):
"""Şiddet seviyesinden BGR renk."""
return SEVERITY_COLORS_BGR.get(sev_level, SEVERITY_COLORS_BGR[None])
def overlay_mask(image, mask, color, alpha=0.4):
"""Bir mask'ı görüntü üzerine yarı saydam overlay'le."""
if mask is None or mask.sum() == 0:
return image
overlay = image.copy()
overlay[mask > 0] = color
return cv2.addWeighted(overlay, alpha, image, 1 - alpha, 0)
def draw_polygon(image, polygon, color, thickness=2, fill_alpha=0.0):
"""Polygon çiz. fill_alpha > 0 ise dolu da."""
if polygon is None or len(polygon) < 3:
return image
pts = np.array(polygon, dtype=np.int32).reshape((-1, 2))
if fill_alpha > 0:
overlay = image.copy()
cv2.fillPoly(overlay, [pts], color)
image = cv2.addWeighted(overlay, fill_alpha, image, 1 - fill_alpha, 0)
cv2.polylines(image, [pts], isClosed=True, color=color, thickness=thickness)
return image
def draw_bbox(image, bbox, color, thickness=2, dashed=False):
"""Bounding box çiz, opsiyonel dashed."""
x1, y1, x2, y2 = [int(v) for v in bbox]
if not dashed:
cv2.rectangle(image, (x1, y1), (x2, y2), color, thickness)
else:
_draw_dashed_rect(image, (x1, y1), (x2, y2), color, thickness)
return image
def _draw_dashed_rect(image, p1, p2, color, thickness):
"""Kesik çizgili dikdörtgen."""
x1, y1 = p1
x2, y2 = p2
dash_len = 8
# Üst
for x in range(x1, x2, dash_len * 2):
cv2.line(image, (x, y1), (min(x + dash_len, x2), y1), color, thickness)
# Alt
for x in range(x1, x2, dash_len * 2):
cv2.line(image, (x, y2), (min(x + dash_len, x2), y2), color, thickness)
# Sol
for y in range(y1, y2, dash_len * 2):
cv2.line(image, (x1, y), (x1, min(y + dash_len, y2)), color, thickness)
# Sağ
for y in range(y1, y2, dash_len * 2):
cv2.line(image, (x2, y), (x2, min(y + dash_len, y2)), color, thickness)
def draw_label(image, text, position, bg_color, text_color=(255, 255, 255),
font_scale=0.5, thickness=1):
"""Etiket çiz - arka plan + yazı."""
x, y = position
font = cv2.FONT_HERSHEY_SIMPLEX
(tw, th), baseline = cv2.getTextSize(text, font, font_scale, thickness)
pad = 4
# Arka plan
cv2.rectangle(
image,
(x, y - th - pad * 2),
(x + tw + pad * 2, y),
bg_color,
-1,
)
# Yazı
cv2.putText(
image, text, (x + pad, y - pad),
font, font_scale, text_color, thickness, cv2.LINE_AA,
)
return image
def render_annotated(image, damages, parts):
"""Ana sonuç görseli — parça mask'ları + hasar bbox + polygon."""
result = image.copy()
# 1. Parça maskelerini soluk overlay'le
for p in parts:
if p.mask is not None:
color = part_color_bgr(p.name)
result = overlay_mask(result, p.mask, color, alpha=0.12)
# 2. Hasarları üzerine ekle (şiddete göre renk)
for d in damages:
sev_level = d.severity.get("level") if isinstance(d.severity, dict) else None
color = severity_color(sev_level)
# Polygon dolu + stroke
if d.polygon_normalized:
h, w = image.shape[:2]
poly_px = [(p[0] * w, p[1] * h) for p in d.polygon_normalized]
result = draw_polygon(result, poly_px, color, thickness=2, fill_alpha=0.35)
# Bbox dashed
if d.bbox:
result = draw_bbox(result, d.bbox, color, thickness=1, dashed=True)
# Etiket
x1, y1, _, _ = [int(v) for v in d.bbox]
sev_tr = {"hafif": "Hafif", "orta": "Orta", "agir": "Agir"}.get(sev_level, "?")
label = f"{d.type}{sev_tr}"
result = draw_label(result, label, (x1, max(y1, 20)), color)
return result
def render_parts_only(image, parts):
"""Sadece parça segmentasyonu — her parça farklı renk."""
result = image.copy()
for p in parts:
if p.mask is None:
continue
color = part_color_bgr(p.name)
result = overlay_mask(result, p.mask, color, alpha=0.45)
# Etiket: parçanın merkezi
ys, xs = np.where(p.mask > 0)
if len(xs) > 0:
cx, cy = int(np.mean(xs)), int(np.mean(ys))
result = draw_label(result, p.name, (cx, cy), color)
return result
def render_damages_only(image, damages):
"""Sadece hasar overlay — şiddete göre renk kodlu."""
# Karartılmış arka plan (hasarları öne çıkar)
result = cv2.addWeighted(image, 0.3, np.zeros_like(image), 0.7, 0)
for d in damages:
sev_level = d.severity.get("level") if isinstance(d.severity, dict) else None
color = severity_color(sev_level)
# Mask varsa dolu
if d.mask is not None and d.mask.sum() > 0:
result = overlay_mask(result, d.mask, color, alpha=0.65)
# Bbox + etiket
if d.bbox:
result = draw_bbox(result, d.bbox, color, thickness=2)
x1, y1, _, _ = [int(v) for v in d.bbox]
sev_tr = {"hafif": "Hafif", "orta": "Orta", "agir": "Agir"}.get(sev_level, "?")
label = f"{d.type}{sev_tr}"
result = draw_label(result, label, (x1, max(y1, 20)), color)
return result
def render_all(image, damages, parts, output_dir, inspection_id):
"""Üç ayrı görsel üret ve dosya yollarını döndür."""
out = Path(output_dir)
out.mkdir(parents=True, exist_ok=True)
paths = {}
try:
annotated = render_annotated(image, damages, parts)
p1 = out / f"{inspection_id}_annotated.jpg"
cv2.imwrite(str(p1), annotated, [cv2.IMWRITE_JPEG_QUALITY, 90])
paths["annotated_image"] = str(p1)
except Exception as e:
logger.error(f"annotated render hatası: {e}")
try:
parts_img = render_parts_only(image, parts)
p2 = out / f"{inspection_id}_parts.png"
cv2.imwrite(str(p2), parts_img)
paths["parts_overlay"] = str(p2)
except Exception as e:
logger.error(f"parts render hatası: {e}")
try:
damages_img = render_damages_only(image, damages)
p3 = out / f"{inspection_id}_damages.png"
cv2.imwrite(str(p3), damages_img)
paths["damages_overlay"] = str(p3)
except Exception as e:
logger.error(f"damages render hatası: {e}")
return paths
if __name__ == "__main__":
# CLI test
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--image", required=True)
parser.add_argument("--output_dir", default="./visuals")
parser.add_argument("--damage_weights", required=True)
parser.add_argument("--parts_weights", required=True)
args = parser.parse_args()
from pipeline import DamagePipelineV2
pipe = DamagePipelineV2(
damage_weights=args.damage_weights,
parts_weights=args.parts_weights,
)
image = cv2.imread(args.image)
damages = pipe._detect_damages(image)
parts = pipe._detect_parts(image)
if damages and parts:
pipe._assign_parts_to_damages(damages, parts)
if damages:
pipe._classify_severities(damages, image)
paths = render_all(image, damages, parts, args.output_dir, "test")
print("Üretildi:")
for k, v in paths.items():
print(f" {k}: {v}")