diff --git a/src/utils/image_converters.py b/src/utils/image_converters.py index 17b52eb..7597ec5 100644 --- a/src/utils/image_converters.py +++ b/src/utils/image_converters.py @@ -14,8 +14,9 @@ class UT: def __init__(self, roifile_fn: Path): self.roifile_fn = roifile_fn + print("is file", self.roifile_fn.is_file()) self.rois = ImagejRoi.fromfile(self.roifile_fn) - self.stem = self.roifile_fn.stem.strip("-RoiSet") + self.stem = self.roifile_fn.stem.split("Roi-")[1] self.image, self.image_props = self._load_images() def _load_images(self): @@ -23,12 +24,11 @@ class UT: array sequence is CZYX """ print(self.roifile_fn.parent, self.stem) - fns = list(self.roifile_fn.parent.glob(f"{self.stem}*.tif*")) + fns = list(self.roifile_fn.parent.glob(f"{self.stem.lower()}*.tif*")) stems = [fn.stem.split(self.stem)[-1] for fn in fns] n_ch = len(set([stem.split("-ch")[-1].split("t")[0] for stem in stems])) n_p = len(set([stem.split("-")[0] for stem in stems])) n_t = len(set([stem.split("t")[1] for stem in stems])) - print(n_ch, n_p, n_t) with TiffFile(fns[0]) as tif: img = tif.asarray() @@ -42,6 +42,7 @@ class UT: "height": h, "dtype": dtype, } + print("Image props", self.image_props) image_stack = np.zeros((n_ch, n_p, w, h), dtype=dtype) for fn in fns: @@ -49,7 +50,7 @@ class UT: img = tif.asarray() stem = fn.stem.split(self.stem)[-1] ch = int(stem.split("-ch")[-1].split("t")[0]) - p = int(stem.split("-")[0].lstrip("p")) + p = int(stem.split("-")[0].split("p")[1]) t = int(stem.split("t")[1]) print(fn.stem, "ch", ch, "p", p, "t", t) image_stack[ch - 1, p - 1] = img @@ -82,11 +83,22 @@ class UT: ): """Export rois to a file""" with open(path / subfolder / f"{self.stem}.txt", "w") as f: - for roi in self.rois: - # TODO add image coordinates normalization - coords = "" - for x, y in roi.subpixel_coordinates: - coords += f"{x/self.width} {y/self.height}" + for i, roi in enumerate(self.rois): + rc = roi.subpixel_coordinates + if rc is None: + print( + f"No coordinates: {self.roifile_fn}, element {i}, out of {len(self.rois)}" + ) + continue + xmn, ymn = rc.min(axis=0) + xmx, ymx = rc.max(axis=0) + xc = (xmn + xmx) / 2 + yc = (ymn + ymx) / 2 + bw = xmx - xmn + bh = ymx - ymn + coords = f"{xc/self.width} {yc/self.height} {bw/self.width} {bh/self.height} " + for x, y in rc: + coords += f"{x/self.width} {y/self.height} " f.write(f"{class_index} {coords}\n") return @@ -112,11 +124,16 @@ if __name__ == "__main__": import argparse parser = argparse.ArgumentParser() - parser.add_argument("input", type=Path) - parser.add_argument("output", type=Path) + parser.add_argument("-i", "--input", nargs="*", type=Path) + parser.add_argument("-o", "--output", type=Path) args = parser.parse_args() - for rfn in args.input.glob("*.zip"): - ut = UT(rfn) - ut.export_rois(args.output, class_index=0) - ut.export_image(args.output, plane_mode="max projection", channel=0) + for path in args.input: + print("Path:", path) + for rfn in Path(path).glob("*.zip"): + print("Roi FN:", rfn) + ut = UT(rfn) + ut.export_rois(args.output, class_index=0) + ut.export_image(args.output, plane_mode="max projection", channel=0) + + print() diff --git a/tests/show_yolo_seg.py b/tests/show_yolo_seg.py new file mode 100644 index 0000000..4e2b7ad --- /dev/null +++ b/tests/show_yolo_seg.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 +""" +show_yolo_seg.py + +Usage: + python show_yolo_seg.py /path/to/image.jpg /path/to/labels.txt + +Supports: + - Segmentation polygons: "class x1 y1 x2 y2 ... xn yn" + - YOLO bbox lines as fallback: "class x_center y_center width height" +Coordinates can be normalized [0..1] or absolute pixels (auto-detected). +""" +import sys +import cv2 +import numpy as np +import matplotlib.pyplot as plt +import argparse +from pathlib import Path +import random + + +def parse_label_line(line): + parts = line.strip().split() + if not parts: + return None + cls = int(float(parts[0])) + coords = [float(x) for x in parts[1:]] + return cls, coords + + +def coords_are_normalized(coords): + # If every coordinate is between 0 and 1 (inclusive-ish), assume normalized + if not coords: + return False + return max(coords) <= 1.001 + + +def yolo_bbox_to_xyxy(coords, img_w, img_h): + # coords: [xc, yc, w, h] normalized or absolute + xc, yc, w, h = coords[:4] + if max(coords) <= 1.001: + xc *= img_w + yc *= img_h + w *= img_w + h *= img_h + x1 = int(round(xc - w / 2)) + y1 = int(round(yc - h / 2)) + x2 = int(round(xc + w / 2)) + y2 = int(round(yc + h / 2)) + return x1, y1, x2, y2 + + +def poly_to_pts(coords, img_w, img_h): + # coords: [x1 y1 x2 y2 ...] either normalized or absolute + if coords_are_normalized(coords[4:]): + coords = [ + coords[i] * (img_w if i % 2 == 0 else img_h) for i in range(len(coords)) + ] + pts = np.array(coords, dtype=np.int32).reshape(-1, 2) + return pts + + +def random_color_for_class(cls): + random.seed(cls) # deterministic per class + return tuple(int(x) for x in np.array([random.randint(0, 255) for _ in range(3)])) + + +def draw_annotations(img, labels, alpha=0.4, draw_bbox_for_poly=True): + # img: BGR numpy array + overlay = img.copy() + h, w = img.shape[:2] + for cls, coords in labels: + if not coords: + continue + # polygon case (>=6 coordinates) + if len(coords) >= 6: + color = random_color_for_class(cls) + + x1, y1, x2, y2 = yolo_bbox_to_xyxy(coords[:4], w, h) + cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) + + pts = poly_to_pts(coords[4:], w, h) + # fill on overlay + cv2.fillPoly(overlay, [pts], color) + # outline on base image + cv2.polylines(img, [pts], isClosed=True, color=color, thickness=2) + # put class text at first point + x, y = int(pts[0, 0]), int(pts[0, 1]) - 6 + cv2.putText( + img, + str(cls), + (x, max(6, y)), + cv2.FONT_HERSHEY_SIMPLEX, + 0.6, + (255, 255, 255), + 2, + cv2.LINE_AA, + ) + + # YOLO bbox case (4 coords) + elif len(coords) == 4: + x1, y1, x2, y2 = yolo_bbox_to_xyxy(coords, w, h) + color = random_color_for_class(cls) + cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) + cv2.putText( + img, + str(cls), + (x1, max(6, y1 - 4)), + cv2.FONT_HERSHEY_SIMPLEX, + 0.6, + (255, 255, 255), + 2, + cv2.LINE_AA, + ) + else: + # Unknown / invalid format, skip + continue + + # blend overlay for filled polygons + cv2.addWeighted(overlay, alpha, img, 1 - alpha, 0, img) + return img + + +def load_labels_file(label_path): + labels = [] + with open(label_path, "r") as f: + for raw in f: + line = raw.strip() + if not line: + continue + parsed = parse_label_line(line) + if parsed: + labels.append(parsed) + return labels + + +def main(): + parser = argparse.ArgumentParser( + description="Show YOLO segmentation / polygon annotations" + ) + parser.add_argument("image", type=str, help="Path to image file") + parser.add_argument("labels", type=str, help="Path to YOLO label file (polygons)") + parser.add_argument( + "--alpha", type=float, default=0.4, help="Polygon fill alpha (0..1)" + ) + parser.add_argument( + "--no-bbox", action="store_true", help="Don't draw bounding boxes for polygons" + ) + args = parser.parse_args() + + img_path = Path(args.image) + lbl_path = Path(args.labels) + + if not img_path.exists(): + print("Image not found:", img_path) + sys.exit(1) + if not lbl_path.exists(): + print("Label file not found:", lbl_path) + sys.exit(1) + + img = cv2.imread(str(img_path), cv2.IMREAD_COLOR) + if img is None: + print("Could not load image:", img_path) + sys.exit(1) + + labels = load_labels_file(str(lbl_path)) + if not labels: + print("No labels parsed from", lbl_path) + # continue and just show image + out = draw_annotations( + img.copy(), labels, alpha=args.alpha, draw_bbox_for_poly=(not args.no_bbox) + ) + + # Convert BGR -> RGB for matplotlib display + out_rgb = cv2.cvtColor(out, cv2.COLOR_BGR2RGB) + plt.figure(figsize=(10, 10 * out.shape[0] / out.shape[1])) + plt.imshow(out_rgb) + plt.axis("off") + plt.title(f"{img_path.name} ({lbl_path.name})") + plt.show() + + +if __name__ == "__main__": + main()