Alicesong's picture
Deploy Detection + Segmentation Studio
1efe34a verified
Raw
History Blame Contribute Delete
5.83 kB
"""Shared helpers for visualization and array handling."""
from __future__ import annotations
import math
from io import BytesIO
from typing import Iterable, Sequence
import cv2
import matplotlib
import numpy as np
from matplotlib import pyplot as plt
matplotlib.use("Agg")
def to_rgb(image: np.ndarray) -> np.ndarray:
"""Convert a grayscale or BGR image into RGB uint8."""
if image.ndim == 2:
return cv2.cvtColor(image, cv2.COLOR_GRAY2RGB)
if image.shape[2] == 4:
return cv2.cvtColor(image, cv2.COLOR_RGBA2RGB)
return cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
def normalize_uint8(image: np.ndarray) -> np.ndarray:
"""Scale a float image to uint8 if needed."""
if image.dtype == np.uint8:
return image
clipped = np.clip(image, 0.0, 1.0)
return (clipped * 255).astype(np.uint8)
def figure_to_image(fig: plt.Figure) -> np.ndarray:
"""Render a Matplotlib figure into an RGB numpy image."""
buffer = BytesIO()
fig.savefig(buffer, format="png", bbox_inches="tight", dpi=160)
plt.close(fig)
buffer.seek(0)
data = np.frombuffer(buffer.getvalue(), dtype=np.uint8)
decoded = cv2.imdecode(data, cv2.IMREAD_COLOR)
return cv2.cvtColor(decoded, cv2.COLOR_BGR2RGB)
def plot_confusion_matrix(
matrix: np.ndarray,
labels: Sequence[str],
title: str,
cmap: str = "YlOrRd",
) -> np.ndarray:
"""Create a labeled confusion-matrix heatmap."""
fig, ax = plt.subplots(figsize=(6.2, 5.2))
heat = ax.imshow(matrix, cmap=cmap)
ax.set_title(title, fontsize=14, fontweight="bold")
ax.set_xticks(range(len(labels)))
ax.set_yticks(range(len(labels)))
ax.set_xticklabels(labels, rotation=25, ha="right")
ax.set_yticklabels(labels)
ax.set_xlabel("Predicted")
ax.set_ylabel("True")
ax.figure.colorbar(heat, ax=ax, shrink=0.82)
threshold = float(matrix.max()) * 0.55 if matrix.size else 0.0
for row in range(matrix.shape[0]):
for col in range(matrix.shape[1]):
value = int(matrix[row, col])
color = "white" if value >= threshold else "#2f1d0b"
ax.text(col, row, value, ha="center", va="center", color=color, fontsize=11)
fig.tight_layout()
return figure_to_image(fig)
def plot_lines(
series: Sequence[tuple[str, Sequence[float], str]],
title: str,
xlabel: str = "Epoch",
ylabel: str = "Value",
) -> np.ndarray:
"""Plot multiple lines on a single axis."""
fig, ax = plt.subplots(figsize=(7.2, 4.0))
for name, values, color in series:
xs = np.arange(1, len(values) + 1)
ax.plot(xs, values, label=name, linewidth=2.2, color=color)
ax.set_title(title, fontsize=14, fontweight="bold")
ax.set_xlabel(xlabel)
ax.set_ylabel(ylabel)
ax.grid(alpha=0.25)
ax.legend(frameon=False)
fig.tight_layout()
return figure_to_image(fig)
def plot_bar(
labels: Sequence[str],
values: Sequence[float],
title: str,
color: str = "#ef7d32",
ylabel: str = "Value",
) -> np.ndarray:
"""Create a simple labeled bar chart."""
fig, ax = plt.subplots(figsize=(7.0, 4.0))
positions = np.arange(len(labels))
ax.bar(positions, values, color=color, edgecolor="#7a3310")
ax.set_title(title, fontsize=14, fontweight="bold")
ax.set_xticks(positions)
ax.set_xticklabels(labels, rotation=20, ha="right")
ax.set_ylabel(ylabel)
ax.grid(axis="y", alpha=0.25)
upper = max(values) if values else 1.0
for idx, value in enumerate(values):
ax.text(idx, value + upper * 0.02, f"{value:.2f}", ha="center", va="bottom", fontsize=10)
fig.tight_layout()
return figure_to_image(fig)
def make_image_grid(
images: Sequence[np.ndarray],
captions: Sequence[str] | None = None,
columns: int = 4,
tile_size: tuple[int, int] | None = None,
background: tuple[int, int, int] = (247, 241, 232),
) -> np.ndarray:
"""Lay out images on a canvas with optional captions."""
if not images:
blank = np.full((240, 320, 3), background, dtype=np.uint8)
cv2.putText(
blank,
"No images",
(92, 126),
cv2.FONT_HERSHEY_SIMPLEX,
0.9,
(74, 67, 61),
2,
cv2.LINE_AA,
)
return blank
rgb_images = [normalize_uint8(to_rgb(image)) for image in images]
if tile_size is None:
tile_h = max(image.shape[0] for image in rgb_images)
tile_w = max(image.shape[1] for image in rgb_images)
else:
tile_w, tile_h = tile_size
columns = max(1, columns)
rows = math.ceil(len(rgb_images) / columns)
caption_h = 28 if captions else 0
canvas = np.full(
(rows * (tile_h + caption_h + 18) + 20, columns * (tile_w + 18) + 20, 3),
background,
dtype=np.uint8,
)
for index, image in enumerate(rgb_images):
row = index // columns
col = index % columns
y = 12 + row * (tile_h + caption_h + 18)
x = 12 + col * (tile_w + 18)
resized = cv2.resize(image, (tile_w, tile_h), interpolation=cv2.INTER_AREA)
canvas[y : y + tile_h, x : x + tile_w] = resized
cv2.rectangle(canvas, (x, y), (x + tile_w, y + tile_h), (201, 165, 130), 1)
if captions:
text = captions[index]
cv2.putText(
canvas,
text,
(x, y + tile_h + 19),
cv2.FONT_HERSHEY_SIMPLEX,
0.52,
(84, 60, 42),
1,
cv2.LINE_AA,
)
return canvas
def softmax(logits: np.ndarray) -> np.ndarray:
"""Numerically stable softmax."""
shifted = logits - logits.max(axis=1, keepdims=True)
exp_values = np.exp(shifted)
return exp_values / exp_values.sum(axis=1, keepdims=True)