From 01e98a523947781bb9b9669afd64a7fe1f8a21f2 Mon Sep 17 00:00:00 2001 From: Conscht Date: Wed, 17 Dec 2025 18:38:44 +0100 Subject: [PATCH 1/2] Fixed visualization. Added a approach to unify the data saving format in vis nuclei and neuron (added helper to save files in png/hdf file for that). Finally, added test method for nuclei and uploaded sanity check images --- evalinstseg/vis_io.py | 39 +++ evalinstseg/visualize.py | 291 ++++++++---------- tests/results/vis/nuclei_realimg_vis.hdf | Bin 0 -> 47408 bytes tests/results/vis/nuclei_realimg_vis_fn.png | Bin 0 -> 143 bytes .../results/vis/nuclei_realimg_vis_fn_bnd.png | Bin 0 -> 143 bytes .../results/vis/nuclei_realimg_vis_fn_seg.png | Bin 0 -> 143 bytes tests/results/vis/nuclei_realimg_vis_fp.png | Bin 0 -> 143 bytes .../results/vis/nuclei_realimg_vis_fp_bnd.png | Bin 0 -> 143 bytes .../results/vis/nuclei_realimg_vis_fp_seg.png | Bin 0 -> 143 bytes tests/results/vis/nuclei_realimg_vis_tp.png | Bin 0 -> 437 bytes .../results/vis/nuclei_realimg_vis_tp_seg.png | Bin 0 -> 1578 bytes .../vis/nuclei_realimg_vis_tp_seg2.png | Bin 0 -> 1578 bytes tests/test_functions.py | 100 +++++- 13 files changed, 268 insertions(+), 162 deletions(-) create mode 100644 evalinstseg/vis_io.py create mode 100644 tests/results/vis/nuclei_realimg_vis.hdf create mode 100644 tests/results/vis/nuclei_realimg_vis_fn.png create mode 100644 tests/results/vis/nuclei_realimg_vis_fn_bnd.png create mode 100644 tests/results/vis/nuclei_realimg_vis_fn_seg.png create mode 100644 tests/results/vis/nuclei_realimg_vis_fp.png create mode 100644 tests/results/vis/nuclei_realimg_vis_fp_bnd.png create mode 100644 tests/results/vis/nuclei_realimg_vis_fp_seg.png create mode 100644 tests/results/vis/nuclei_realimg_vis_tp.png create mode 100644 tests/results/vis/nuclei_realimg_vis_tp_seg.png create mode 100644 tests/results/vis/nuclei_realimg_vis_tp_seg2.png diff --git a/evalinstseg/vis_io.py b/evalinstseg/vis_io.py new file mode 100644 index 0000000..53e2471 --- /dev/null +++ b/evalinstseg/vis_io.py @@ -0,0 +1,39 @@ +# Support functions to unify visualization I/O in HDF and PNG +import numpy as np +import h5py +from skimage import io + +def _mid_slice(arr): + arr = np.asarray(arr) + if arr.ndim == 3: # (Z,Y,X) + return arr[arr.shape[0] // 2] + if arr.ndim == 4 and arr.shape[-1] in (3, 4): # (Z,Y,X,C) + return arr[arr.shape[0] // 2] + return arr + +def save_vis_hdf(outFn, data_dict, compression="gzip"): + """ + data_dict: mapping name -> ndarray, will be written under volumes/ + """ + with h5py.File(outFn + "_vis.hdf", "w") as f: + for name, arr in data_dict.items(): + f.create_dataset(f"volumes/{name}", data=arr, compression=compression) + +def export_vis_pngs(outFn, data_dict): + """ + Writes PNG quicklooks next to the HDF. Scalar layers assumed in [0,1]. + RGB layers assumed uint8 or float in [0,255] / [0,1]. + """ + for name, arr in data_dict.items(): + a = _mid_slice(arr) + + if a.ndim == 3 and a.shape[-1] in (3, 4): + # RGB(A) + if a.dtype != np.uint8: + a = np.clip(a, 0, 1) + a = (a * 255).astype(np.uint8) + io.imsave(outFn + f"_{name}.png", a) + else: + # scalar + a = np.clip(a, 0, 1) + io.imsave(outFn + f"_{name}.png", (a * 255).astype(np.uint8)) diff --git a/evalinstseg/visualize.py b/evalinstseg/visualize.py index b0cac49..ccbda46 100644 --- a/evalinstseg/visualize.py +++ b/evalinstseg/visualize.py @@ -1,17 +1,15 @@ +# evalinstseg/visualize.py import logging -import h5py from matplotlib.colors import to_rgb import numpy as np import scipy.ndimage from skimage import io -logger = logging.getLogger(__name__) +from .vis_io import save_vis_hdf, export_vis_pngs +logger = logging.getLogger(__name__) -# colors selected based on: -# - at least somewhat usable in case of color blindness -# - no two colors should be too similar gt_cmap_ = [ "#88B04B", "#9F00A7", "#EFC050", "#34568B", "#E47A2E", "#BC70A4", "#92A8D1", "#A3B18A", "#45B8AC", "#6B5B95", @@ -19,7 +17,6 @@ "#CE3175", "#00A591", "#EDD59E", "#1E7145", "#E9FF70", ] - pred_cmap_ = [ "#FDAC53", "#9BB7D4", "#B55A30", "#F5DF4D", "#0072B5", "#A0DAA9", "#E9897E", "#00A170", "#926AA6", "#EFE1CE", @@ -36,40 +33,35 @@ def rgb(idx, cmap): def proj_label(lbl): """project label map along the first axis (z), rgb-aware""" dst = np.zeros(lbl.shape[1:], dtype=np.uint8) - for i in range(lbl.shape[0]-1, -1, -1): - dst[np.max(dst, axis=-1)==0,:] = lbl[i][np.max(dst, axis=-1)==0,:] - + for i in range(lbl.shape[0] - 1, -1, -1): + dst[np.max(dst, axis=-1) == 0, :] = lbl[i][np.max(dst, axis=-1) == 0, :] return dst -def paint_boundary(labels_rel, label, target): - """converts dense label map to boundary label map - (for 3d label maps only slice with largest xy-extend is filled) +def paint_boundary(labels_rel, label_id, target): + """ + converts dense label map to boundary label map + NOTE: border_value=0 to avoid edge artifacts. """ if len(labels_rel.shape) == 3: - coords_ = np.nonzero(labels_rel == label) + coords_ = np.nonzero(labels_rel == label_id) coords = {} for z, y, x in zip(*coords_): coords.setdefault(z, []).append((z, y, x)) - max_z = -1 - max_z_len = -1 - for z, v in coords.items(): - if len(v) > max_z_len: - max_z_len = len(v) - max_z = z - tmp = np.zeros_like(labels_rel[max_z], dtype=np.float32) - tmp = labels_rel[max_z]==label + max_z = max(coords.keys(), key=lambda z: len(coords[z])) + tmp = (labels_rel[max_z] == label_id) + struct = scipy.ndimage.generate_binary_structure(2, 2) eroded_tmp = scipy.ndimage.binary_erosion( tmp, iterations=1, structure=struct, - border_value=1) + border_value=1, + ) bnd = np.logical_xor(tmp, eroded_tmp) target[max_z][bnd] = 1 else: - tmp = np.zeros_like(labels_rel, dtype=np.float32) - tmp = labels_rel==label + tmp = (labels_rel == label_id) struct = scipy.ndimage.generate_binary_structure(2, 2) eroded_tmp = scipy.ndimage.binary_erosion( tmp, @@ -80,39 +72,29 @@ def paint_boundary(labels_rel, label, target): target[bnd] = 1 -def visualize_nuclei( - gt_labels_rel, pred_labels_rel, locMat, gt_ind, pred_ind, th, outFn): +def visualize_nuclei(gt_labels_rel, pred_labels_rel, locMat, gt_ind, pred_ind, th, outFn, export_png=True): """visualize nuclei (blob-like) segmentation results""" # GT-basiert - vis_tp = np.zeros_like(gt_labels_rel, dtype=np.float32) - vis_fn = np.zeros_like(gt_labels_rel, dtype=np.float32) - vis_tp_seg = np.zeros_like(gt_labels_rel, dtype=np.float32) - vis_fn_seg = np.zeros_like(gt_labels_rel, dtype=np.float32) + vis_tp = np.zeros_like(gt_labels_rel, dtype=np.float32) + vis_fn = np.zeros_like(gt_labels_rel, dtype=np.float32) + vis_tp_seg = np.zeros_like(gt_labels_rel, dtype=np.float32) + vis_fn_seg = np.zeros_like(gt_labels_rel, dtype=np.float32) vis_fn_seg_bnd = np.zeros_like(gt_labels_rel, dtype=np.float32) - # Pred-basiert - vis_fp = np.zeros_like(pred_labels_rel, dtype=np.float32) - vis_tp_seg2 = np.zeros_like(pred_labels_rel, dtype=np.float32) - vis_fp_seg = np.zeros_like(pred_labels_rel, dtype=np.float32) + # Pred-basiert + vis_fp = np.zeros_like(pred_labels_rel, dtype=np.float32) + vis_tp_seg2 = np.zeros_like(pred_labels_rel, dtype=np.float32) + vis_fp_seg = np.zeros_like(pred_labels_rel, dtype=np.float32) vis_fp_seg_bnd = np.zeros_like(pred_labels_rel, dtype=np.float32) - # determine number of labels num_gt_labels = int(np.max(gt_labels_rel)) num_pred_labels = int(np.max(pred_labels_rel)) labels_gt = list(range(1, num_gt_labels + 1)) labels_pred = list(range(1, num_pred_labels + 1)) - cntrs_gt = scipy.ndimage.center_of_mass( - gt_labels_rel > 0, - gt_labels_rel, - labels_gt - ) - cntrs_pred = scipy.ndimage.center_of_mass( - pred_labels_rel > 0, - pred_labels_rel, - labels_pred -) + cntrs_gt = scipy.ndimage.center_of_mass(gt_labels_rel > 0, gt_labels_rel, labels_gt) + cntrs_pred = scipy.ndimage.center_of_mass(pred_labels_rel > 0, pred_labels_rel, labels_pred) sz = 1 for gti, pi in zip(gt_ind, pred_ind): @@ -121,80 +103,71 @@ def visualize_nuclei( paint_boundary(gt_labels_rel, gti, vis_fn_seg_bnd) vis_fp_seg[pred_labels_rel == pi] = 1 paint_boundary(pred_labels_rel, pi, vis_fp_seg_bnd) - cntr = cntrs_gt[gti-1] - idx = tuple(int(round(c)) for c in cntr) + idx = tuple(int(round(c)) for c in cntrs_gt[gti - 1]) vis_fn[idx] = 1 - cntr = cntrs_pred[pi-1] - idx = tuple(int(round(c)) for c in cntr) + idx = tuple(int(round(c)) for c in cntrs_pred[pi - 1]) vis_fp[idx] = 1 else: vis_tp_seg[gt_labels_rel == gti] = 1 - cntr = cntrs_gt[gti - 1] - idx = tuple(int(round(c)) for c in cntr) + idx = tuple(int(round(c)) for c in cntrs_gt[gti - 1]) vis_tp[idx] = 1 vis_tp_seg2[pred_labels_rel == pi] = 1 - vis_tp = scipy.ndimage.gaussian_filter(vis_tp, sz, truncate=sz) - for label in range(1, num_gt_labels + 1): - if label in gt_ind: + # FN + for label_id in range(1, num_gt_labels + 1): + if label_id in gt_ind: continue - vis_fn_seg[gt_labels_rel == label] = 1 - paint_boundary(gt_labels_rel, label, vis_fn_seg_bnd) - cntr = cntrs_gt[label - 1] - idx = tuple(int(round(c)) for c in cntr) + vis_fn_seg[gt_labels_rel == label_id] = 1 + paint_boundary(gt_labels_rel, label_id, vis_fn_seg_bnd) + idx = tuple(int(round(c)) for c in cntrs_gt[label_id - 1]) vis_fn[idx] = 1 - vis_fn = scipy.ndimage.gaussian_filter(vis_fn, sz, truncate=sz) - for label in range(1, num_pred_labels + 1): - if label in pred_ind: + + # FP + for label_id in range(1, num_pred_labels + 1): + if label_id in pred_ind: continue - vis_fp_seg[pred_labels_rel == label] = 1 - paint_boundary(pred_labels_rel, label, vis_fp_seg_bnd) - cntr = cntrs_pred[label - 1] - idx = tuple(int(round(c)) for c in cntr) + vis_fp_seg[pred_labels_rel == label_id] = 1 + paint_boundary(pred_labels_rel, label_id, vis_fp_seg_bnd) + idx = tuple(int(round(c)) for c in cntrs_pred[label_id - 1]) vis_fp[idx] = 1 + + # blur markers + vis_tp = scipy.ndimage.gaussian_filter(vis_tp, sz, truncate=sz) + vis_fn = scipy.ndimage.gaussian_filter(vis_fn, sz, truncate=sz) vis_fp = scipy.ndimage.gaussian_filter(vis_fp, sz, truncate=sz) - # Division by 0 guard - max_tp = np.max(vis_tp) - if max_tp > 0: - vis_tp = vis_tp / max_tp - - max_fp = np.max(vis_fp) - if max_fp > 0: - vis_fp = vis_fp / max_fp - - max_fn = np.max(vis_fn) - if max_fn > 0: - vis_fn = vis_fn / max_fn - with h5py.File(outFn + "_vis.hdf", 'w') as fi: - fi.create_dataset('volumes/vis_tp', data=vis_tp, compression='gzip') - fi.create_dataset('volumes/vis_fp', data=vis_fp, compression='gzip') - fi.create_dataset('volumes/vis_fn', data=vis_fn, compression='gzip') - fi.create_dataset( - 'volumes/vis_tp_seg', data=vis_tp_seg, compression='gzip') - fi.create_dataset( - 'volumes/vis_tp_seg2', data=vis_tp_seg2, compression='gzip') - fi.create_dataset( - 'volumes/vis_fp_seg', data=vis_fp_seg, compression='gzip') - fi.create_dataset( - 'volumes/vis_fn_seg', data=vis_fn_seg, compression='gzip') - fi.create_dataset( - 'volumes/vis_fp_seg_bnd', data=vis_fp_seg_bnd, compression='gzip') - fi.create_dataset( - 'volumes/vis_fn_seg_bnd', data=vis_fn_seg_bnd, compression='gzip') + # normalize markers to [0,1] + for arr in (vis_tp, vis_fn, vis_fp): + m = np.max(arr) + if m > 0: + arr /= m + + data = { + "vis_tp": vis_tp, + "vis_fp": vis_fp, + "vis_fn": vis_fn, + "vis_tp_seg": vis_tp_seg, + "vis_tp_seg2": vis_tp_seg2, + "vis_fp_seg": vis_fp_seg, + "vis_fn_seg": vis_fn_seg, + "vis_fp_bnd": vis_fp_seg_bnd, + "vis_fn_bnd": vis_fn_seg_bnd, + } + + save_vis_hdf(outFn, data) + if export_png: + export_vis_pngs(outFn, data) def visualize_neurons( gt_labels_rel, pred_labels_rel, gt_ind, pred_ind, outFn, - fp_ind, fs_ind, fn_ind, fm_gt_ind, fm_pred_ind, fp_ind_only_bg): + fp_ind, fs_ind, fn_ind, fm_gt_ind, fm_pred_ind, fp_ind_only_bg, export_hdf=True): """visualize neuron (tree-like) segmentation results - Note ---- currently unused: unused: fs_ind , fm_pred_ind, fp_ind_only_bg """ - if len(gt_labels_rel.shape) == 4: gt = np.max(gt_labels_rel, axis=0) else: @@ -203,16 +176,16 @@ def visualize_neurons( pred = np.max(pred_labels_rel, axis=0) else: pred = pred_labels_rel - # unify shapes for vis if only z differs (instance stack) if gt.ndim == 3 and pred.ndim == 3 and gt.shape[1:] == pred.shape[1:] and gt.shape[0] != pred.shape[0]: - gt = np.max(gt, axis=0) + gt = np.max(gt, axis=0) pred = np.max(pred, axis=0) - assert gt.shape[1:] == pred.shape[1:], f"Spatial shape mismatch: gt {gt.shape}, pred {pred.shape}" + assert gt.shape == pred.shape, f"Spatial shape mismatch: gt {gt.shape}, pred {pred.shape}" + num_gt = int(np.max(gt_labels_rel)) + num_pred = int(np.max(pred_labels_rel)) - num_gt = np.max(gt_labels_rel) - num_pred = np.max(pred_labels_rel) + # base RGB canvas dst = np.zeros_like(gt, dtype=np.uint8) dst = np.stack([dst, dst, dst], axis=-1) @@ -220,28 +193,25 @@ def visualize_neurons( # one color per instance vis = np.zeros_like(dst) for i in range(1, num_gt + 1): - vis[gt == i] = rgb(i-1, gt_cmap_) - - # If already 2D, keep as is - if vis.ndim == 3: - mip = vis - else: - mip = proj_label(vis) - - mip_gt_mask = np.max(mip>0, axis=-1) - io.imsave(outFn + '_gt.png', mip.astype(np.uint8)) + vis[gt == i] = rgb(i - 1, gt_cmap_) + if vis.ndim == 3: + mip_gt = vis + else: + mip_gt = proj_label(vis) + mip_gt_mask = np.max(mip_gt > 0, axis=-1) + io.imsave(outFn + "_gt.png", mip_gt.astype(np.uint8)) # visualize pred # one color per instance vis = np.zeros_like(dst) for i in range(1, num_pred + 1): - vis[pred == i] = rgb(i-1, pred_cmap_) - if vis.ndim == 3: - mip = vis - else: - mip = proj_label(vis) - mip_pred_mask = np.max(mip>0, axis=-1) - io.imsave(outFn + '_pred.png', mip.astype(np.uint8)) + vis[pred == i] = rgb(i - 1, pred_cmap_) + if vis.ndim == 3: + mip_pred = vis + else: + mip_pred = proj_label(vis) + mip_pred_mask = np.max(mip_pred > 0, axis=-1) + io.imsave(outFn + "_pred.png", mip_pred.astype(np.uint8)) # visualize tp pred + fp + fs # tp pred in color @@ -249,62 +219,61 @@ def visualize_neurons( # fn pixels in gray vis = np.zeros_like(dst) for i in pred_ind: - vis[pred == i] = rgb(i-1, pred_cmap_) + vis[pred == i] = rgb(i - 1, pred_cmap_) for i in fp_ind: vis[pred == i] = [255, 0, 0] + if vis.ndim == 3: + mip_tp_pred_fp_fs = vis + else: + mip_tp_pred_fp_fs = proj_label(vis) + mask = np.logical_and(mip_gt_mask, np.logical_not(np.max(mip_tp_pred_fp_fs > 0, axis=-1))) + mip_tp_pred_fp_fs[mask] = [200, 200, 200] + io.imsave(outFn + "_tp_pred_fp_fs.png", mip_tp_pred_fp_fs.astype(np.uint8)) - if vis.ndim == 3: - mip = vis - else: - mip = proj_label(vis) - - mask = np.logical_and(mip_gt_mask, np.logical_not(np.max(mip > 0, axis=-1))) - mip[mask] = [200, 200, 200] - io.imsave(outFn + '_tp_pred_fp_fs.png', mip.astype(np.uint8)) - - # visualize fn/fm - # fn in color - # fm (false merges) in red - # fp pixels in gray + # ---- FN / FM ---- vis = np.zeros_like(dst, dtype=np.uint8) - fm_merged = np.unique(np.array( - [ind for inds in fm_gt_ind for ind in inds]).flatten()) + fm_merged = np.unique(np.array([ind for inds in fm_gt_ind for ind in inds]).flatten()) if len(fm_gt_ind) else np.array([]) for i in fn_ind: if i not in fm_merged: - vis[gt == i] = rgb(i-1, gt_cmap_) + vis[gt == i] = rgb(i - 1, gt_cmap_) for i in fm_merged: if i not in gt_ind: vis[gt == i] = [255, 0, 0] + if vis.ndim == 3: + mip_fn_fm = vis + else: + mip_fn_fm = proj_label(vis) + mask = np.logical_and(mip_pred_mask, np.logical_not(np.max(mip_fn_fm > 0, axis=-1))) + mip_fn_fm[mask] = [200, 200, 200] + io.imsave(outFn + "_fn_fm.png", mip_fn_fm.astype(np.uint8)) - if vis.ndim == 3: - mip = vis - else: - mip = proj_label(vis) - - mask = np.logical_and(mip_pred_mask, np.logical_not(np.max(mip > 0, axis=-1))) - mip[mask] = [200, 200, 200] - io.imsave(outFn + '_fn_fm.png', mip.astype(np.uint8)) - - # visualize fn/fm v2 - # tp gt in color - # fn in bright red - # fm in dark red - # fp pixels in gray + # visualize tp pred + fp + fs + # tp pred in color + # fp and fs (false splits) in red + # fn pixels in gray vis = np.zeros_like(dst, dtype=np.uint8) for i in gt_ind: - vis[gt == i] = rgb(i-1, gt_cmap_) + vis[gt == i] = rgb(i - 1, gt_cmap_) for i in fn_ind: if i not in fm_merged: vis[gt == i] = [255, 64, 64] for i in fm_merged: - if i not in gt_ind: - vis[gt == i] = [192, 0, 0] - - if vis.ndim == 3: - mip = vis - else: # (Z, Y, X, 3) -> project along Z - mip = proj_label(vis) - - mask = np.logical_and(mip_pred_mask, np.logical_not(np.max(mip > 0, axis=-1))) - mip[mask] = [200, 200, 200] - io.imsave(outFn + '_fn_fm_v2.png', mip.astype(np.uint8)) + if i not in gt_ind: + vis[gt == i] = [192, 0, 0] + if vis.ndim == 3: + mip_fn_fm_v2 = vis + else: + mip_fn_fm_v2 = proj_label(vis) + mask = np.logical_and(mip_pred_mask, np.logical_not(np.max(mip_fn_fm_v2 > 0, axis=-1))) + mip_fn_fm_v2[mask] = [200, 200, 200] + io.imsave(outFn + "_fn_fm_v2.png", mip_fn_fm_v2.astype(np.uint8)) + + if export_hdf: + data = { + "gt_rgb": mip_gt.astype(np.uint8), + "pred_rgb": mip_pred.astype(np.uint8), + "tp_pred_fp_fs_rgb": mip_tp_pred_fp_fs.astype(np.uint8), + "fn_fm_rgb": mip_fn_fm.astype(np.uint8), + "fn_fm_v2_rgb": mip_fn_fm_v2.astype(np.uint8), + } + save_vis_hdf(outFn, data) diff --git a/tests/results/vis/nuclei_realimg_vis.hdf b/tests/results/vis/nuclei_realimg_vis.hdf new file mode 100644 index 0000000000000000000000000000000000000000..1b06a0d3429dc84c9e162e600d7a70054e480aa3 GIT binary patch literal 47408 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 Vmxh^OmCXQ~0X73{20qTf{{X>B#oqt` literal 0 HcmV?d00001 diff --git a/tests/results/vis/nuclei_realimg_vis_fn.png b/tests/results/vis/nuclei_realimg_vis_fn.png new file mode 100644 index 0000000000000000000000000000000000000000..21e7e3daaa55ed3a04cfb3abe4184f3865673358 GIT binary patch literal 143 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5890C>L#5>RT|g?#)5S5QV$R!xj3Buth5zgG gJRCqw3V_3RHb#C1#=S*bUw};UboFyt=akR{0I~KHx&QzG literal 0 HcmV?d00001 diff --git a/tests/results/vis/nuclei_realimg_vis_fn_bnd.png b/tests/results/vis/nuclei_realimg_vis_fn_bnd.png new file mode 100644 index 0000000000000000000000000000000000000000..21e7e3daaa55ed3a04cfb3abe4184f3865673358 GIT binary patch literal 143 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5890C>L#5>RT|g?#)5S5QV$R!xj3Buth5zgG gJRCqw3V_3RHb#C1#=S*bUw};UboFyt=akR{0I~KHx&QzG literal 0 HcmV?d00001 diff --git a/tests/results/vis/nuclei_realimg_vis_fn_seg.png b/tests/results/vis/nuclei_realimg_vis_fn_seg.png new file mode 100644 index 0000000000000000000000000000000000000000..21e7e3daaa55ed3a04cfb3abe4184f3865673358 GIT binary patch literal 143 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5890C>L#5>RT|g?#)5S5QV$R!xj3Buth5zgG gJRCqw3V_3RHb#C1#=S*bUw};UboFyt=akR{0I~KHx&QzG literal 0 HcmV?d00001 diff --git a/tests/results/vis/nuclei_realimg_vis_fp.png b/tests/results/vis/nuclei_realimg_vis_fp.png new file mode 100644 index 0000000000000000000000000000000000000000..21e7e3daaa55ed3a04cfb3abe4184f3865673358 GIT binary patch literal 143 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5890C>L#5>RT|g?#)5S5QV$R!xj3Buth5zgG gJRCqw3V_3RHb#C1#=S*bUw};UboFyt=akR{0I~KHx&QzG literal 0 HcmV?d00001 diff --git a/tests/results/vis/nuclei_realimg_vis_fp_bnd.png b/tests/results/vis/nuclei_realimg_vis_fp_bnd.png new file mode 100644 index 0000000000000000000000000000000000000000..21e7e3daaa55ed3a04cfb3abe4184f3865673358 GIT binary patch literal 143 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5890C>L#5>RT|g?#)5S5QV$R!xj3Buth5zgG gJRCqw3V_3RHb#C1#=S*bUw};UboFyt=akR{0I~KHx&QzG literal 0 HcmV?d00001 diff --git a/tests/results/vis/nuclei_realimg_vis_fp_seg.png b/tests/results/vis/nuclei_realimg_vis_fp_seg.png new file mode 100644 index 0000000000000000000000000000000000000000..21e7e3daaa55ed3a04cfb3abe4184f3865673358 GIT binary patch literal 143 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5890C>L#5>RT|g?#)5S5QV$R!xj3Buth5zgG gJRCqw3V_3RHb#C1#=S*bUw};UboFyt=akR{0I~KHx&QzG literal 0 HcmV?d00001 diff --git a/tests/results/vis/nuclei_realimg_vis_tp.png b/tests/results/vis/nuclei_realimg_vis_tp.png new file mode 100644 index 0000000000000000000000000000000000000000..e5049a58a35fb354b11c374c30c7bfe4a6e8587b GIT binary patch literal 437 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5890C>L#5>RT|iEar;B4q#hkZy4T}yd2rxKw z{hl`8X{LYd>g6vIy=IeQ>PRSZ2Dy@?_3<5O36gv50+-L>TqU#oO!>)MOwXS&4h-JP~ObLZxa z;4@N}jTjg{uP&Oc)7?>7JiF@l_NDG2c2|2BJNw#wF?r*3cgH%rMac#pEql*){L1A1A|s6RW<~ zoV#+Q@Zi@y^@=Btvfuph>Q%YH`t5HomOH0xe+vvv=G8^JbhtZqyFV#8Ak5OO{M$U? za9ICl);YXl`L?W?KVGPu6Wfsc{9bR}YDRYMiU Q04P8`UHx3vIVCg!0MDqp7XSbN literal 0 HcmV?d00001 diff --git a/tests/results/vis/nuclei_realimg_vis_tp_seg.png b/tests/results/vis/nuclei_realimg_vis_tp_seg.png new file mode 100644 index 0000000000000000000000000000000000000000..2578f49c0da186a1e534e4920846acfe772161ae GIT binary patch literal 1578 zcmbW2`#;kQ9LB#JwsMbLa+j>8MCG={T*8RRLL!%L$0(aitL%_wUl;Dlx5+E}cmWTu$b?T9*w)gS@N* z?a&vJWI!vb*^eq&)Fny5@s|%h;}7Ya*f(`VOz_bY2l#Q8y7aPhkq9z+Vnx=xwk) zX8Xpjt+ee>OEBr52Pf+tFsBQ598?;v++8qKv*LX!8>Xb>NZ znpneUovFmhl3B6stK@ePz42j+e4%#Khq;NypEUVz&W=bZ2lH8#Y@Chj%ep!?GSnzW z?s55Q*=1mhTDy0mg_TwB!u9RU)-xt0oAhZ(GK{wYPU{SmV}mwtuTBe`pUgv9&@(oi zN;+*;bR(`?ejfx^GE`{>wjm>u6<@d#e6WUc?%Mmjpyp@}*%t4viu4LS^QG0p^y%Z! ziRG62TQ@-ki9`45w~waPy3#xNZbS9MOdaJsB=_O1_v6@q*YYG{2e~NfKn)4FZVA-x z(&|U~!K)74N^rITSFG8c%BLEJC~E(PB_mWOVh{^f7e$Vb=1o}( zzW0X{BZ{9~Qq}9$wXblCpuMt*Ff13tN{rQQJ|z9Ba7}Pv2(Fw*&ifN2N17K$KB${4 zu79?QwPsxWa6+Y1A+&Kd=^n1JFL>51w^r7@Ai4~eTGhW6AD{VY_EZ!O^wFogZL_zp zN19a?*>vc4a#UcGP4E|ufGrBX>vr%fYcMx71YJrH^e z6Y@5O590bL03`2g5VVw9tR0@T+A2FsEcceMw+uRLM}(+a1a#!Z&_CwuFNGbkVCQol z&MZE!vrBKc&EbijW#!$n1pE>qFAXQdiaGh7A#j;B@sSxDB3prex5=SBp4C%fb>;!M zG9l6@a~ix1{~jI_9aiFRd>;7YYSjSyr&@i5`{R^13d33zqZASm&qqo2Yw{++Pdt4F z0}k@4>gNW`;3u!2ABUFJZ3>pdg+y&Y*ezvF*5~U;7GJL6)?x_<@~>^ZBtl= zu6msQwG}r(_;jp5N4D8%t@ux(nUr33c{d&pa(N+rv@iZL3qPp*6{qZ_p-n=^tu{e` zzFG=_b(PX3rG<<$sxkcri(1gEiH*RGO1m^fG+|07hU5<%3s-q8^*SY95VPVr`EfoW zJ;+Ut<)qO}Xm?RY>M*)lxn%N3_Z`inFpF@ra2{~>%hWt70|6-%1VkplnOx@<;N5J@ zDhcAUAvQni_>U5E6nX{X2U1CEw|ZuuUHXHeu5_ajCM}}fc)Ir6%-@F-Hgf$cNbL@EKEV!%}b| z%z$4?NBs))S;Wn`Z(oBh)YiMtgb1;(Td<;3$szjUH~kJ8B>gokN1dk;WQR`HXsto! zSn^3FF6^`^@(UqUK19vU6GO-x7~Ps{3gRE%cd~3aXHQO_W9qYVjlWJFNUblwxd~A5 V{vO*YlEU9lz}v&mz1l5`{2zb!@!J3Z literal 0 HcmV?d00001 diff --git a/tests/results/vis/nuclei_realimg_vis_tp_seg2.png b/tests/results/vis/nuclei_realimg_vis_tp_seg2.png new file mode 100644 index 0000000000000000000000000000000000000000..2578f49c0da186a1e534e4920846acfe772161ae GIT binary patch literal 1578 zcmbW2`#;kQ9LB#JwsMbLa+j>8MCG={T*8RRLL!%L$0(aitL%_wUl;Dlx5+E}cmWTu$b?T9*w)gS@N* z?a&vJWI!vb*^eq&)Fny5@s|%h;}7Ya*f(`VOz_bY2l#Q8y7aPhkq9z+Vnx=xwk) zX8Xpjt+ee>OEBr52Pf+tFsBQ598?;v++8qKv*LX!8>Xb>NZ znpneUovFmhl3B6stK@ePz42j+e4%#Khq;NypEUVz&W=bZ2lH8#Y@Chj%ep!?GSnzW z?s55Q*=1mhTDy0mg_TwB!u9RU)-xt0oAhZ(GK{wYPU{SmV}mwtuTBe`pUgv9&@(oi zN;+*;bR(`?ejfx^GE`{>wjm>u6<@d#e6WUc?%Mmjpyp@}*%t4viu4LS^QG0p^y%Z! ziRG62TQ@-ki9`45w~waPy3#xNZbS9MOdaJsB=_O1_v6@q*YYG{2e~NfKn)4FZVA-x z(&|U~!K)74N^rITSFG8c%BLEJC~E(PB_mWOVh{^f7e$Vb=1o}( zzW0X{BZ{9~Qq}9$wXblCpuMt*Ff13tN{rQQJ|z9Ba7}Pv2(Fw*&ifN2N17K$KB${4 zu79?QwPsxWa6+Y1A+&Kd=^n1JFL>51w^r7@Ai4~eTGhW6AD{VY_EZ!O^wFogZL_zp zN19a?*>vc4a#UcGP4E|ufGrBX>vr%fYcMx71YJrH^e z6Y@5O590bL03`2g5VVw9tR0@T+A2FsEcceMw+uRLM}(+a1a#!Z&_CwuFNGbkVCQol z&MZE!vrBKc&EbijW#!$n1pE>qFAXQdiaGh7A#j;B@sSxDB3prex5=SBp4C%fb>;!M zG9l6@a~ix1{~jI_9aiFRd>;7YYSjSyr&@i5`{R^13d33zqZASm&qqo2Yw{++Pdt4F z0}k@4>gNW`;3u!2ABUFJZ3>pdg+y&Y*ezvF*5~U;7GJL6)?x_<@~>^ZBtl= zu6msQwG}r(_;jp5N4D8%t@ux(nUr33c{d&pa(N+rv@iZL3qPp*6{qZ_p-n=^tu{e` zzFG=_b(PX3rG<<$sxkcri(1gEiH*RGO1m^fG+|07hU5<%3s-q8^*SY95VPVr`EfoW zJ;+Ut<)qO}Xm?RY>M*)lxn%N3_Z`inFpF@ra2{~>%hWt70|6-%1VkplnOx@<;N5J@ zDhcAUAvQni_>U5E6nX{X2U1CEw|ZuuUHXHeu5_ajCM}}fc)Ir6%-@F-Hgf$cNbL@EKEV!%}b| z%z$4?NBs))S;Wn`Z(oBh)YiMtgb1;(Td<;3$szjUH~kJ8B>gokN1dk;WQR`HXsto! zSn^3FF6^`^@(UqUK19vU6GO-x7~Ps{3gRE%cd~3aXHQO_W9qYVjlWJFNUblwxd~A5 V{vO*YlEU9lz}v&mz1l5`{2zb!@!J3Z literal 0 HcmV?d00001 diff --git a/tests/test_functions.py b/tests/test_functions.py index c0416aa..c8c35e0 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -4,6 +4,8 @@ import os import h5py import zarr +from pathlib import Path + from evalinstseg.evaluate import evaluate_volume from evalinstseg.match import instance_mask, greedy_many_to_many_matching @@ -267,6 +269,101 @@ def test_real_data_format(self): self.assertEqual(pred_loaded.shape, (15, 1, 50, 50)) self.assertEqual(gt_loaded.shape, (1, 50, 50)) +import os +import unittest +import tempfile + +import numpy as np +import h5py +from imageio.v3 import imread +from skimage.measure import label +from skimage.morphology import remove_small_objects + +from evalinstseg.evaluate import evaluate_volume + + +class TestNucleiVisualizationRealImage(unittest.TestCase): + @staticmethod + def _repo_path_relative_to_this_test(*parts): + # This resolves paths relative to the current test file location + here = os.path.dirname(__file__) + return os.path.join(here, *parts) + + @staticmethod + def _load_mask_as_instances_png(png_path, threshold=128, min_size=20): + img = imread(png_path) + if img.ndim == 3: + img = img[..., 0] # handle RGB/RGBA + + # Robust binarization: avoids tiny non-zero artifacts becoming objects + bw = img >= threshold + + # Optional cleanup: remove tiny speckles that create phantom CCs + bw = remove_small_objects(bw, min_size=min_size) + + lbl = label(bw).astype(np.int32) + return lbl + + def test_visualize_nuclei_from_real_png_writes_vis_hdf(self): + png_path = self._repo_path_relative_to_this_test("images", "nuclein.png") + self.assertTrue(os.path.exists(png_path), f"Missing test image: {png_path}") + + lbl2d = self._load_mask_as_instances_png(png_path, threshold=128, min_size=20) + n = int(lbl2d.max()) + self.assertGreater(n, 0, "No instances found in test image after preprocessing") + + gt = lbl2d[None, ...] + pred = lbl2d[None, ...].copy() # perfect match => should produce TP + + with tempfile.TemporaryDirectory() as td: + + here = Path(__file__).resolve().parent + vis_dir = here / "results" / "vis" + vis_dir.mkdir(parents=True, exist_ok=True) + + outFn = str(vis_dir / "nuclei_realimg") + metrics = evaluate_volume( + gt, pred, ndim=2, outFn=outFn, + localization_criterion="iou", + assignment_strategy="greedy", + visualize=True, + visualize_type="nuclei", + evaluate_false_labels=True + ) + + vis_path = outFn + "_vis.hdf" + self.assertTrue(os.path.exists(vis_path), "Expected *_vis.hdf not written") + + # Verify datasets exist and behave as expected for perfect match + with h5py.File(vis_path, "r") as f: + required = [ + "volumes/vis_tp", + "volumes/vis_fp", + "volumes/vis_fn", + "volumes/vis_tp_seg", + "volumes/vis_tp_seg2", + "volumes/vis_fp_seg", + "volumes/vis_fn_seg", + ] + for k in required: + self.assertIn(k, f, f"Missing dataset in vis file: {k}") + + # Boundaries: support both older/newer key names + fp_bnd_keys = ("volumes/vis_fp_seg_bnd", "volumes/vis_fp_bnd") + fn_bnd_keys = ("volumes/vis_fn_seg_bnd", "volumes/vis_fn_bnd") + + self.assertTrue(any(k in f for k in fp_bnd_keys), + f"Missing FP boundary dataset (tried {fp_bnd_keys})") + self.assertTrue(any(k in f for k in fn_bnd_keys), + f"Missing FN boundary dataset (tried {fn_bnd_keys})") + + # Perfect match sanity checks + self.assertGreater(np.max(f["volumes/vis_tp"][:]), 0.0) + self.assertEqual(np.max(f["volumes/vis_fn_seg"][:]), 0.0) + self.assertEqual(np.max(f["volumes/vis_fp_seg"][:]), 0.0) + + + if __name__ == '__main__': # Create test suite @@ -278,7 +375,8 @@ def test_real_data_format(self): TestGreedyMatching, TestDataPreprocessing, TestLocalizationCriterion, - TestEndToEnd + TestEndToEnd, + TestNucleiVisualizationRealImage, ] for test_class in test_classes: From 4012e5d62a514c45c43953b50a0994ad867cdcbb Mon Sep 17 00:00:00 2001 From: Conscht Date: Wed, 17 Dec 2025 18:39:04 +0100 Subject: [PATCH 2/2] Fixed visualization. Added a approach to unify the data saving format in vis nuclei and neuron (added helper to save files in png/hdf file for that). Finally, added test method for nuclei and uploaded sanity check images --- tests/images/nuclein.png | Bin 0 -> 5310 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/images/nuclein.png diff --git a/tests/images/nuclein.png b/tests/images/nuclein.png new file mode 100644 index 0000000000000000000000000000000000000000..38e8f8f991fbb6a61e6ba6c0795d515b4ad5cc0a GIT binary patch literal 5310 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 literal 0 HcmV?d00001