File size: 6,564 Bytes
1d197a4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
"""Video rendering utilities."""

import numpy as np
from PIL import Image, ImageDraw

from ..processing.preprocessing import ensure_uint8


def draw_contour(draw: ImageDraw.ImageDraw, x_points, y_points, color):
    """Draw a closed contour if points are available."""
    if not x_points or not y_points:
        return
    points = [(float(x_points[i]), float(y_points[i])) for i in range(len(x_points))]
    if len(points) < 2:
        return
    points.append(points[0])
    draw.line(points, fill=color, width=2)


def _require_imageio():
    try:
        import imageio.v2 as imageio
    except Exception as exc:
        raise RuntimeError(
            "Video export requires imageio and imageio-ffmpeg. "
            "Install with: python -m pip install imageio imageio-ffmpeg"
        ) from exc
    return imageio


def write_overlay_video(images: np.ndarray, lumen, plaque, video_path: str, fps: float) -> None:
    """Write contour overlay video with lumen/plaque channels."""
    imageio = _require_imageio()
    images_u8 = ensure_uint8(images)
    with imageio.get_writer(video_path, fps=fps) as writer:
        for frame_idx in range(images_u8.shape[0]):
            frame = Image.fromarray(images_u8[frame_idx], mode="L").convert("RGB")
            draw = ImageDraw.Draw(frame)
            draw_contour(draw, lumen[0][frame_idx], lumen[1][frame_idx], (0, 255, 0))
            draw_contour(draw, plaque[0][frame_idx], plaque[1][frame_idx], (255, 64, 64))
            writer.append_data(np.asarray(frame))


def write_overlay_video_with_bifurcation_flags(
    images: np.ndarray,
    lumen,
    plaque,
    video_path: str,
    fps: float,
    bifurcation_probabilities: np.ndarray,
    bifurcation_labels: np.ndarray,
    threshold: float,
) -> None:
    """Write contour overlay video with per-frame bifurcation text labels."""
    imageio = _require_imageio()
    images_u8 = ensure_uint8(images)
    probs = np.asarray(bifurcation_probabilities, dtype=np.float32)
    labels = np.asarray(bifurcation_labels, dtype=np.int32)
    with imageio.get_writer(video_path, fps=fps) as writer:
        for frame_idx in range(images_u8.shape[0]):
            frame = Image.fromarray(images_u8[frame_idx], mode="L").convert("RGB")
            draw = ImageDraw.Draw(frame)
            draw_contour(draw, lumen[0][frame_idx], lumen[1][frame_idx], (0, 255, 0))
            draw_contour(draw, plaque[0][frame_idx], plaque[1][frame_idx], (255, 64, 64))

            if frame_idx < probs.shape[0] and frame_idx < labels.shape[0]:
                prob = float(probs[frame_idx])
                is_bif = bool(labels[frame_idx])
                tag = "Branch" if is_bif else "Non-branch"
                text = f"{tag}  p={prob:.2f}  t={float(threshold):.2f}"
                text_color = (255, 90, 90) if is_bif else (100, 220, 255)
                draw.rectangle([(10, 10), (290, 36)], fill=(0, 0, 0))
                draw.text((16, 16), text, fill=text_color)

            writer.append_data(np.asarray(frame))


def write_model_comparison_video(images: np.ndarray, tf_lumen, sam_lumen, video_path: str, fps: float) -> None:
    """Write a two-model comparison video (TF red, SAM blue)."""
    imageio = _require_imageio()
    images_u8 = ensure_uint8(images)
    with imageio.get_writer(video_path, fps=fps) as writer:
        for frame_idx in range(images_u8.shape[0]):
            frame = Image.fromarray(images_u8[frame_idx], mode="L").convert("RGB")
            draw = ImageDraw.Draw(frame)
            draw_contour(draw, tf_lumen[0][frame_idx], tf_lumen[1][frame_idx], (255, 0, 0))
            draw_contour(draw, sam_lumen[0][frame_idx], sam_lumen[1][frame_idx], (0, 0, 255))
            writer.append_data(np.asarray(frame))


def _draw_graph_panel(frame_rgb: np.ndarray, scores: np.ndarray, sustained_flags: np.ndarray, frame_idx: int) -> np.ndarray:
    panel_h = 150
    h, w = frame_rgb.shape[:2]
    canvas = Image.new("RGB", (w, h + panel_h), color=(0, 0, 0))
    canvas.paste(Image.fromarray(frame_rgb), (0, 0))
    draw = ImageDraw.Draw(canvas)

    top = h + 12
    left = 16
    right = w - 16
    bottom = h + panel_h - 18
    draw.rectangle([(left, top), (right, bottom)], fill=(20, 20, 20), outline=(90, 90, 90), width=1)

    n = len(scores)
    if n > 1:
        in_run = False
        run_start = 0
        for i in range(n):
            if sustained_flags[i] and not in_run:
                run_start = i
                in_run = True
            if (not sustained_flags[i] or i == n - 1) and in_run:
                run_end = i if not sustained_flags[i] else i + 1
                x0 = left + int((run_start / (n - 1)) * (right - left))
                x1 = left + int(((run_end - 1) / (n - 1)) * (right - left))
                draw.rectangle([(x0, top), (x1, bottom)], fill=(70, 20, 20))
                in_run = False

        def px(i):
            return left + int((i / (n - 1)) * (right - left))

        def py(v):
            v = float(np.clip(v, 0.0, 1.0))
            return bottom - int(v * (bottom - top))

        pts = [(px(i), py(scores[i])) for i in range(n)]
        if len(pts) > 1:
            draw.line(pts, fill=(120, 255, 120), width=2)

        cur = min(max(frame_idx, 0), n - 1)
        cx = px(cur)
        draw.line([(cx, top), (cx, bottom)], fill=(255, 255, 0), width=2)
        flag_txt = "SUSTAINED BRANCH SIGNAL" if sustained_flags[cur] else "normal"
        draw.text((left + 6, top + 4), f"Oblongness: {float(scores[cur]):.3f}  [{flag_txt}]", fill=(230, 230, 230))
    else:
        draw.text((left + 6, top + 4), "Oblongness: insufficient data", fill=(230, 230, 230))

    return np.asarray(canvas)


def write_overlay_video_with_graph(
    images: np.ndarray,
    lumen,
    plaque,
    video_path: str,
    fps: float,
    oblong_scores: np.ndarray,
    sustained_flags: np.ndarray,
) -> None:
    """Write the fused overlay video with an animated bottom graph panel."""
    imageio = _require_imageio()
    images_u8 = ensure_uint8(images)
    with imageio.get_writer(video_path, fps=fps) as writer:
        for frame_idx in range(images_u8.shape[0]):
            frame = Image.fromarray(images_u8[frame_idx], mode="L").convert("RGB")
            draw = ImageDraw.Draw(frame)
            draw_contour(draw, lumen[0][frame_idx], lumen[1][frame_idx], (0, 255, 0))
            draw_contour(draw, plaque[0][frame_idx], plaque[1][frame_idx], (255, 64, 64))
            with_panel = _draw_graph_panel(np.asarray(frame), oblong_scores, sustained_flags, frame_idx)
            writer.append_data(with_panel)