import os from ij import IJ, ImagePlus from ij.gui import Roi, Overlay from ij.measure import ResultsTable from ij.plugin.filter import ThresholdToSelection from java.awt import Color # Okabe-Ito colorblind-safe pair COLOR_P63_POSITIVE = Color(230, 159, 0) # orange COLOR_P63_NEGATIVE = Color(86, 180, 233) # sky blue # Stroke width for ROI outlines _STROKE_WIDTH = 1.5 def load_mask(mask_path): """Open the label-image TIFF at mask_path and return an ImagePlus.""" imp = IJ.openImage(mask_path) if imp is None: raise IOError("Could not open mask: {}".format(mask_path)) return imp def masks_to_rois(mask_imp): """Convert each unique non-zero label in mask_imp to an ROI; return list of (label, Roi) tuples. Uses ThresholdToSelection on a copy of the processor for each label so the original image is never modified. Labels are uint16 values from Cellpose output. """ ip = mask_imp.getProcessor().duplicate() # Collect unique non-zero labels from the pixel array pixels = list(ip.getPixels()) labels = sorted(set(int(p) for p in pixels) - {0}) result = [] tts = ThresholdToSelection() work_imp = ImagePlus("_mask_work", ip) for lbl in labels: # Isolate this label: threshold exactly at [lbl, lbl] ip.setThreshold(lbl, lbl) roi = tts.convert(ip) if roi is not None: roi.setStrokeWidth(_STROKE_WIDTH) result.append((lbl, roi)) work_imp.close() return result def _build_mean_lookup(cells_data, variant): """Return {label_int: mean_float} and the chosen threshold float from cells_data rows. cells_data rows are dicts from read_cells_csv -- all values are strings. The chosen variant column is e.g. 't14_positive'. """ by_label = {} for row in cells_data: lbl = int(row["cell_label"]) mean = float(row["red_mean"]) by_label[lbl] = mean return by_label def color_rois_by_p63(rois_with_labels, cells_data, threshold, variant): """Color each ROI orange (p63+) or sky blue (p63-) based on red_mean vs threshold. rois_with_labels -- list of (label_int, Roi) from masks_to_rois cells_data -- list of dicts from read_cells_csv (values are strings) threshold -- float on 0-255 scale (from summary.csv) variant -- string e.g. 'T14' (used only for logging) Returns the same list with ROI stroke colors set in place. """ mean_by_label = _build_mean_lookup(cells_data, variant) n_pos = n_neg = n_missing = 0 for lbl, roi in rois_with_labels: mean = mean_by_label.get(lbl) if mean is None: # Label present in mask but absent from cells.csv -- skip coloring n_missing += 1 continue if mean >= threshold: roi.setStrokeColor(COLOR_P63_POSITIVE) n_pos += 1 else: roi.setStrokeColor(COLOR_P63_NEGATIVE) n_neg += 1 IJ.log("p63+ coloring ({}): pos={} neg={} unmatched={}".format( variant, n_pos, n_neg, n_missing)) return rois_with_labels def build_overlay(rois_with_labels): """Wrap the colored Roi objects from (label, Roi) pairs into an Overlay and return it.""" overlay = Overlay() for _lbl, roi in rois_with_labels: overlay.add(roi) return overlay def populate_results_table(cells_data): """Fill and return a ResultsTable with one row per cell (image, cell_label, red_mean, p63+). cells_data -- list of dicts from read_cells_csv; values are strings. The 'variant_call' column (the primary p63+ call) is mapped to 'p63_positive'. """ rt = ResultsTable() for row in cells_data: rt.incrementCounter() rt.addValue("image", row["image"]) rt.addValue("cell_label", float(row["cell_label"])) rt.addValue("red_mean", float(row["red_mean"])) rt.addValue("p63_positive", float(row["variant_call"])) return rt def display_results(imp, overlay, results_table): """Attach overlay to imp, show it, and display results_table in the ImageJ Results window.""" imp.setOverlay(overlay) imp.show() # updateAndDraw refreshes the canvas so the overlay renders immediately imp.updateAndDraw() results_table.show("p63 Results")