Spaces:
Sleeping
Sleeping
File size: 10,843 Bytes
73e760d | 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 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 | """
Overlay BMP cell-type color labels onto TIF images for visual verification.
For each TIF/BMP pair in the metadata, produces a PNG showing:
- Top: TIF image with cell-type color overlay + legend
- Bottom (or side): original BMP for comparison
Usage:
python overlay_labels_on_tif.py \
--metadata metadata_with_tif_sizes3.csv \
--tif-dir ./tif --bmp-dir ./bmp \
--out-dir label_overlays
"""
import argparse
import re
import numpy as np
import cv2
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
from skimage.io import imread
# ── Cell type config (must match extract_features.py) ──
CELL_CLASSES = {
"root_cap": 0,
"epidermis": 1,
"exodermis": 2,
"cortex": 3,
"endodermis": 4,
"pericycle": 5,
"xylem": 6,
"phloem": 7,
"stele": 8,
}
LABEL_TO_NAME = {v: k for k, v in CELL_CLASSES.items()}
# HSV ranges (OpenCV scale: H 0-180, S 0-255, V 0-255)
COLOR_RANGES = {
"phloem": [(0, 180, 0, 30, 210, 255)],
"cortex": [(35, 85, 50, 255, 40, 255)],
"epidermis": [(100, 130, 50, 255, 80, 255)],
"stele": [(80, 100, 50, 255, 100, 255)],
"exodermis": [(22, 38, 60, 255, 100, 255)],
"endodermis": [(10, 22, 100, 255, 100, 255)],
"pericycle": [(125, 150, 30, 255, 40, 200)],
"root_cap": [(150, 175, 40, 255, 80, 255)],
"xylem": [(0, 8, 150, 255, 120, 255),
(175, 180, 150, 255, 120, 255)],
}
COLOR_PROCESS_ORDER = [
"phloem", "cortex", "epidermis", "stele", "exodermis",
"endodermis", "pericycle", "root_cap", "xylem",
]
# Display palette (RGB) for overlay
DISPLAY_PALETTE = {
0: (255, 105, 180), # root_cap - pink
1: (0, 0, 255), # epidermis - blue
2: (255, 255, 0), # exodermis - yellow
3: (0, 200, 0), # cortex - green
4: (255, 165, 0), # endodermis - orange
5: (128, 0, 128), # pericycle - purple
6: (255, 0, 0), # xylem - red
7: (255, 255, 255), # phloem - white
8: (0, 255, 255), # stele - cyan
}
def _load_font(size):
for path in [
"/usr/share/fonts/liberation/LiberationMono-Regular.ttf",
"/usr/share/fonts/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
]:
try:
return ImageFont.truetype(path, size)
except (OSError, IOError):
pass
return ImageFont.load_default()
def _normalize_to_uint8(img):
if img.dtype == np.uint8:
return img
img_f = img.astype(np.float32)
lo, hi = float(np.percentile(img_f, 1)), float(np.percentile(img_f, 99))
img_f = (img_f - lo) / (hi - lo + 1e-8)
return (np.clip(img_f, 0.0, 1.0) * 255.0).astype(np.uint8)
def to_2d(img):
if img.ndim == 2:
return img
if img.ndim == 3:
if img.shape[-1] in (3, 4):
return img
return img.max(axis=0)
if img.ndim == 4:
return img.max(axis=0)
raise ValueError(f"Unsupported ndim={img.ndim}")
def ensure_rgb_uint8(img):
img2 = to_2d(img)
if img2.ndim == 2:
g = _normalize_to_uint8(img2)
return np.stack([g, g, g], axis=-1)
if img2.ndim == 3:
if img2.shape[-1] == 1:
g = _normalize_to_uint8(img2[..., 0])
return np.stack([g, g, g], axis=-1)
return _normalize_to_uint8(img2[..., :3])
raise ValueError(f"Unsupported shape: {img2.shape}")
def create_class_mask_from_bmp(bmp_path):
bmp_rgb = np.array(Image.open(str(bmp_path)).convert("RGB"))
bmp_hsv = cv2.cvtColor(bmp_rgb, cv2.COLOR_RGB2HSV)
h, w = bmp_hsv.shape[:2]
class_mask = np.full((h, w), -1, dtype=np.int8)
confidence = np.zeros((h, w), dtype=np.float32)
sat = bmp_hsv[:, :, 1].astype(np.float32) / 255.0
val = bmp_hsv[:, :, 2].astype(np.float32) / 255.0
for class_name in COLOR_PROCESS_ORDER:
ranges = COLOR_RANGES[class_name]
label = CELL_CLASSES[class_name]
combined = np.zeros((h, w), dtype=bool)
for (h_lo, h_hi, s_lo, s_hi, v_lo, v_hi) in ranges:
lower = np.array([h_lo, s_lo, v_lo])
upper = np.array([h_hi, s_hi, v_hi])
combined |= cv2.inRange(bmp_hsv, lower, upper) > 0
conf = val * (1.0 - sat) if class_name == "phloem" else sat * val
update = combined & (conf >= confidence)
class_mask[update] = label
confidence[update] = conf[update]
return class_mask
def colorize_class_mask(class_mask):
"""Convert class mask to RGB image using display palette."""
h, w = class_mask.shape
rgb = np.zeros((h, w, 3), dtype=np.uint8)
for label, color in DISPLAY_PALETTE.items():
rgb[class_mask == label] = color
return rgb
def overlay_on_tif(tif_rgb, class_mask_colored, alpha=0.45):
"""Blend class mask colors onto TIF image where mask is not background."""
has_label = class_mask_colored.sum(axis=-1) > 0
out = tif_rgb.astype(np.float32).copy()
mask_f = has_label[..., None].astype(np.float32)
out = out * (1.0 - alpha * mask_f) + class_mask_colored.astype(np.float32) * (alpha * mask_f)
return np.clip(out, 0, 255).astype(np.uint8)
def draw_legend(draw, x, y, font):
"""Draw cell-type color legend."""
for label in sorted(DISPLAY_PALETTE.keys()):
name = LABEL_TO_NAME[label]
color = DISPLAY_PALETTE[label]
draw.rectangle([x, y, x + 14, y + 14], fill=color, outline=(0, 0, 0))
draw.text((x + 20, y), name, fill=(255, 255, 255), font=font)
draw.text((x + 19, y - 1), name, fill=(0, 0, 0), font=font)
draw.text((x + 20, y), name, fill=(255, 255, 255), font=font)
y += 18
return y
def make_overlay_png(tif_path, bmp_path, out_path, tif_h=None, tif_w=None):
"""
Create a verification PNG:
Top half: TIF with BMP labels overlaid (semi-transparent colors + legend)
Bottom half: original BMP image for reference
"""
# Load TIF
img_raw = imread(str(tif_path))
tif_rgb = ensure_rgb_uint8(img_raw)
th, tw = tif_rgb.shape[:2]
# Load BMP and create class mask
class_mask = create_class_mask_from_bmp(bmp_path)
bmp_rgb = np.array(Image.open(str(bmp_path)).convert("RGB"))
bh, bw = bmp_rgb.shape[:2]
# Resize class mask to TIF dimensions
class_mask_resized = cv2.resize(
class_mask.astype(np.float32), (tw, th),
interpolation=cv2.INTER_NEAREST
).astype(np.int8)
# Colorize and overlay on TIF
class_colored = colorize_class_mask(class_mask_resized)
tif_overlaid = overlay_on_tif(tif_rgb, class_colored, alpha=0.5)
# Resize BMP to same width as TIF for stacking
scale = tw / bw
new_bh = int(bh * scale)
bmp_resized = cv2.resize(bmp_rgb, (tw, new_bh), interpolation=cv2.INTER_LINEAR)
# Add title bars
title_h = 30
font = _load_font(16)
small_font = _load_font(12)
# Total canvas: title + TIF overlay + gap + title + BMP
gap = 4
total_h = title_h + th + gap + title_h + new_bh
canvas = np.zeros((total_h, tw, 3), dtype=np.uint8)
# Place TIF overlay
y_offset = title_h
canvas[y_offset:y_offset + th, :, :] = tif_overlaid
# Place BMP
y_bmp = title_h + th + gap + title_h
canvas[y_bmp:y_bmp + new_bh, :, :] = bmp_resized
# Draw on PIL
im = Image.fromarray(canvas)
draw = ImageDraw.Draw(im)
# Title: TIF + label overlay
tif_name = Path(tif_path).name
bmp_name = Path(bmp_path).name
draw.rectangle([0, 0, tw, title_h], fill=(30, 30, 30))
draw.text((10, 6), f"TIF + BMP labels: {tif_name}", fill=(255, 255, 255), font=font)
# Title: BMP reference
y_title2 = title_h + th + gap
draw.rectangle([0, y_title2, tw, y_title2 + title_h], fill=(30, 30, 30))
draw.text((10, y_title2 + 6), f"BMP reference: {bmp_name}", fill=(255, 255, 255), font=font)
# Legend on TIF overlay (top-right)
legend_x = tw - 130
legend_y = title_h + 10
# Background box for legend
n_classes = len(DISPLAY_PALETTE)
legend_h = n_classes * 18 + 6
draw.rectangle([legend_x - 4, legend_y - 4, tw - 4, legend_y + legend_h],
fill=(0, 0, 0, 180))
draw_legend(draw, legend_x, legend_y, small_font)
im.save(str(out_path))
return out_path
def _parse_size(s):
m = re.match(r"(\d+)\s*x\s*(\d+)", str(s).strip())
return (int(m.group(1)), int(m.group(2))) if m else (None, None)
def main():
parser = argparse.ArgumentParser(
description="Overlay BMP cell-type labels on TIF images for verification"
)
parser.add_argument("--metadata", required=True, help="Path to metadata CSV")
parser.add_argument("--tif-dir", required=True, help="Directory with TIF files")
parser.add_argument("--bmp-dir", required=True, help="Directory with BMP files")
parser.add_argument("--out-dir", default="label_overlays", help="Output directory")
parser.add_argument("--species", default=None, help="Filter by species")
parser.add_argument("--stage", default=None, help="Filter by stage")
parser.add_argument("--gpu", action="store_true", help="Accepted for consistency (not used)")
args = parser.parse_args()
import pandas as pd
out_dir = Path(args.out_dir)
out_dir.mkdir(parents=True, exist_ok=True)
tif_dir = Path(args.tif_dir)
bmp_dir = Path(args.bmp_dir)
df = pd.read_csv(args.metadata)
df.columns = df.columns.str.strip().str.lower()
if args.species:
df = df[df["species"].str.lower() == args.species.lower()]
if args.stage:
df = df[df["stage"].str.lower() == args.stage.lower()]
# Only keep rows with "outlined" in BMP filename (the labeled ones)
df_outlined = df[df["bmp_filename"].str.lower().str.contains("outlined")]
if len(df_outlined) == 0:
print("No 'Outlined' BMPs found, using all rows.")
df_outlined = df
print(f"Processing {len(df_outlined)} TIF/BMP pairs...")
for idx, row in df_outlined.iterrows():
tif_path = tif_dir / row["tif_matched"]
bmp_path = bmp_dir / row["bmp_filename"]
if not tif_path.exists():
print(f" SKIP TIF not found: {tif_path}")
continue
if not bmp_path.exists():
print(f" SKIP BMP not found: {bmp_path}")
continue
species = row["species"]
stage = row["stage"]
stem = tif_path.stem.replace(".aivia", "")
out_name = f"verify_{species}_{stage}_{stem}.png"
out_path = out_dir / out_name
print(f" [{idx+1}] {tif_path.name} + {bmp_path.name}")
try:
make_overlay_png(tif_path, bmp_path, out_path)
print(f" -> {out_path}")
except Exception as e:
print(f" FAILED: {e}")
print(f"\nDone! Overlays saved to: {out_dir}/")
if __name__ == "__main__":
main()
|