EyeQC / src /pipeline.py
rdharini2001's picture
EyeQC
6fe482b
Raw
History Blame Contribute Delete
3.93 kB
"""
Orchestration layer: run the full QC analysis on one image or a batch, and
assemble the artefacts (scores, maps, descriptors) the UI needs.
"""
from __future__ import annotations
import numpy as np
import cv2
from .fov import detect_fov
from .qc_metrics import compute_all_metrics, METRIC_WEIGHTS
from .quality_score import composite_score
from .failure_analysis import composite_problem_map, fov_overlay, per_axis_heatmap
from .batch_effects import descriptor_from_metrics
METRIC_NAMES = list(METRIC_WEIGHTS.keys())
def _to_rgb(img):
a = np.asarray(img)
if a.ndim == 2:
a = cv2.cvtColor(a, cv2.COLOR_GRAY2RGB)
if a.shape[-1] == 4:
a = a[..., :3]
return np.ascontiguousarray(a.astype(np.uint8))
def degradation_map_from_metrics(shape, metrics):
"""Aggregate the spatial maps of failing/borderline axes into one 0-1 map."""
dmap = np.zeros(shape[:2], np.float32)
for m in metrics:
if m.get("_map") is not None and m["score"] < 0.66:
w = (0.66 - m["score"])
dmap = np.maximum(dmap, m["_map"].astype(np.float32) * w)
if dmap.max() > 0:
dmap /= dmap.max()
return dmap
def analyze_image(img, name="image", with_vessels=True):
"""Full single-image analysis. Returns a dict bundle."""
rgb = _to_rgb(img)
fov = detect_fov(rgb)
metrics = compute_all_metrics(rgb, fov)
summary = composite_score(metrics)
problem_overlay, problem_caption = composite_problem_map(rgb, fov, metrics, summary)
descriptor = descriptor_from_metrics(rgb, metrics, fov)
dmap = degradation_map_from_metrics(rgb.shape, metrics)
bundle = dict(
name=name, rgb=rgb, fov=fov, metrics=metrics, summary=summary,
problem_overlay=problem_overlay, problem_caption=problem_caption,
fov_overlay=fov_overlay(rgb, fov), descriptor=descriptor,
degradation_map=dmap,
)
if with_vessels:
try:
from .vessels import analyze_vessels, vessel_gradability_score
v = analyze_vessels(rgb, fov)
v["gradability"], v["parts"] = vessel_gradability_score(v)
bundle["vessels"] = v
except Exception as e:
bundle["vessels_error"] = str(e)
return bundle
def analyze_batch(images, names=None, progress=None):
"""Analyse a list of images. `images` may be file paths or arrays."""
from PIL import Image
results = []
n = len(images)
for i, im in enumerate(images):
if progress is not None:
progress((i + 1) / max(n, 1), desc=f"Analysing {i+1}/{n}")
if isinstance(im, str):
nm = names[i] if names else im.split("/")[-1]
arr = np.array(Image.open(im).convert("RGB"))
else:
nm = names[i] if names else f"image_{i:03d}"
arr = _to_rgb(im)
try:
results.append(analyze_image(arr, nm))
except Exception as e:
results.append(dict(name=nm, error=str(e), rgb=_to_rgb(arr)))
return results
def results_to_dataframe(results):
import pandas as pd
rows = []
for r in results:
if "error" in r:
rows.append(dict(image=r["name"], composite=np.nan, verdict="ERROR"))
continue
row = dict(image=r["name"],
composite=round(r["summary"]["composite"], 1),
verdict=r["summary"]["verdict"],
band=r["summary"]["band"],
primary_reason=r["summary"]["primary_reason"])
for m in r["metrics"]:
row[m["name"]] = round(m["score"], 3)
rows.append(row)
return pd.DataFrame(rows)
def metric_table(result):
"""Per-axis table for one image (list of rows)."""
rows = []
for m in result["metrics"]:
rows.append([m["name"], f'{m["value"]:.2f} {m["unit"]}',
f'{m["score"]:.2f}', m["status"].upper(), m["reason"]])
return rows