From 00fbb9727a4d9c037ff12187f160dcad29880fe0 Mon Sep 17 00:00:00 2001 From: npinter Date: Mon, 15 Jul 2024 15:33:40 +0200 Subject: [PATCH 1/3] Update ROIsplitter to 0.2.0 --- .../qupath_roi_splitter.py | 130 +++++++++--------- .../qupath_roi_splitter.xml | 6 +- 2 files changed, 69 insertions(+), 67 deletions(-) diff --git a/tools/qupath_roi_splitter/qupath_roi_splitter.py b/tools/qupath_roi_splitter/qupath_roi_splitter.py index 9f727a039..271db96aa 100644 --- a/tools/qupath_roi_splitter/qupath_roi_splitter.py +++ b/tools/qupath_roi_splitter/qupath_roi_splitter.py @@ -1,48 +1,36 @@ import argparse - import cv2 import geojson import numpy as np import pandas as pd -def draw_poly(input_df, input_img, col=(0, 0, 0), fill=False): - s = np.array(input_df) - if fill: - output_img = cv2.fillPoly(input_img, pts=np.int32([s]), color=col) - else: - output_img = cv2.polylines(input_img, np.int32([s]), True, color=col, thickness=1) - return output_img +def collect_coords(input_coords, feature_index, coord_index=0): + coords_with_index = [] + for coord in input_coords: + coords_with_index.append((coord[0], coord[1], feature_index, coord_index)) + coord_index += 1 + return coords_with_index -def draw_roi(input_roi, input_img, fill): +def collect_roi_coords(input_roi, feature_index): + all_coords = [] if len(input_roi["geometry"]["coordinates"]) == 1: # Polygon w/o holes - input_img = draw_poly(input_roi["geometry"]["coordinates"][0], input_img, fill=fill) + all_coords.extend(collect_coords(input_roi["geometry"]["coordinates"][0], feature_index)) else: - first_roi = True + coord_index = 0 for sub_roi in input_roi["geometry"]["coordinates"]: - # Polygon with holes + # Polygon with holes or MultiPolygon if not isinstance(sub_roi[0][0], list): - if first_roi: - first_roi = False - col = (0, 0, 0) - else: - # holes in ROI - col = (255, 255, 255) if not fill else (0, 0, 0) - input_img = draw_poly(sub_roi, input_img, col=col, fill=fill) + all_coords.extend(collect_coords(sub_roi, feature_index, coord_index)) + coord_index += len(sub_roi) else: # MultiPolygon with holes for sub_coord in sub_roi: - if first_roi: - first_roi = False - col = (0, 0, 0) - else: - # holes in ROI - col = (255, 255, 255) if not fill else (0, 0, 0) - input_img = draw_poly(sub_coord, input_img, col=col, fill=fill) - - return input_img + all_coords.extend(collect_coords(sub_coord, feature_index, coord_index)) + coord_index += len(sub_coord) + return all_coords def split_qupath_roi(in_roi): @@ -50,57 +38,71 @@ def split_qupath_roi(in_roi): qupath_roi = geojson.load(file) # HE dimensions - dim_plt = [qupath_roi["dim"]["width"], qupath_roi["dim"]["height"]] + dim_plt = [int(qupath_roi["dim"]["width"]), int(qupath_roi["dim"]["height"])] tma_name = qupath_roi["name"] cell_types = [ct.rsplit(" - ", 1)[-1] for ct in qupath_roi["featureNames"]] - for cell_type in cell_types: - # create numpy array with white background - img = np.zeros((dim_plt[1], dim_plt[0], 3), dtype="uint8") - img.fill(255) - - for i, roi in enumerate(qupath_roi["features"]): - if not args.all: - if "classification" not in roi["properties"]: - continue - if roi["properties"]["classification"]["name"] == cell_type: - img = draw_roi(roi, img, args.fill) - else: - img = draw_roi(roi, img, args.fill) - - # get all black pixel - coords_arr = np.column_stack(np.where(img == (0, 0, 0))) + coords_by_cell_type = {ct: [] for ct in cell_types} + coords_by_cell_type['all'] = [] # For storing all coordinates if args.all is True - # remove duplicated rows - coords_arr_xy = coords_arr[coords_arr[:, 2] == 0] + for feature_index, roi in enumerate(qupath_roi["features"]): + feature_coords = collect_roi_coords(roi, feature_index) - # remove last column - coords_arr_xy = np.delete(coords_arr_xy, 2, axis=1) + if args.all: + coords_by_cell_type['all'].extend(feature_coords) + elif "classification" in roi["properties"]: + cell_type = roi["properties"]["classification"]["name"] + if cell_type in cell_types: + coords_by_cell_type[cell_type].extend(feature_coords) - # to pandas and rename columns to x and y - coords_df = pd.DataFrame(coords_arr_xy, columns=['y', 'x']) + for cell_type, coords in coords_by_cell_type.items(): + if coords: + # Generate image (white background) + img = np.ones((dim_plt[1], dim_plt[0]), dtype="uint8") * 255 - # reorder columns - coords_df = coords_df[['x', 'y']] + # Convert to numpy array and ensure integer coordinates + coords_arr = np.array(coords).astype(int) - # drop duplicates - coords_df = coords_df.drop_duplicates( - subset=['x', 'y'], - keep='last').reset_index(drop=True) + # Sort by feature_index first, then by coord_index + coords_arr = coords_arr[np.lexsort((coords_arr[:, 3], coords_arr[:, 2]))] - coords_df.to_csv("{}_{}.txt".format(tma_name, cell_type), sep='\t', index=False) + # Get filled pixel coordinates + if args.fill: + filled_coords = np.column_stack(np.where(img == 0)) + all_coords = np.unique(np.vstack((coords_arr[:, :2], filled_coords[:, ::-1])), axis=0) + else: + all_coords = coords_arr[:, :2] + + # Save all coordinates to CSV + coords_df = pd.DataFrame(all_coords, columns=['x', 'y'], dtype=int) + coords_df.to_csv("{}_{}.txt".format(tma_name, cell_type), sep='\t', index=False) + + # Generate image for visualization if --img is specified + if args.img: + # Group coordinates by feature_index + features = {} + for x, y, feature_index, coord_index in coords_arr: + if feature_index not in features: + features[feature_index] = [] + features[feature_index].append((x, y)) + + # Draw each feature separately + for feature_coords in features.values(): + pts = np.array(feature_coords, dtype=np.int32) + if args.fill: + cv2.fillPoly(img, [pts], color=0) # Black fill + else: + cv2.polylines(img, [pts], isClosed=True, color=0, thickness=1) # Black outline - # img save - if args.img: - cv2.imwrite("{}_{}.png".format(tma_name, cell_type), img) + cv2.imwrite("{}_{}.png".format(tma_name, cell_type), img) if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Split ROI coordinates of QuPath TMA annotation by cell type (classfication)") + parser = argparse.ArgumentParser(description="Split ROI coordinates of QuPath TMA annotation by cell type (classification)") parser.add_argument("--qupath_roi", default=False, help="Input QuPath annotation (GeoJSON file)") - parser.add_argument("--fill", action="store_true", required=False, help="Fill pixels in ROIs") - parser.add_argument('--version', action='version', version='%(prog)s 0.1.0') + parser.add_argument("--fill", action="store_true", required=False, help="Fill pixels in ROIs (order of coordinates will be lost)") + parser.add_argument('--version', action='version', version='%(prog)s 0.2.0') parser.add_argument("--all", action="store_true", required=False, help="Extracts all ROIs") parser.add_argument("--img", action="store_true", required=False, help="Generates image of ROIs") args = parser.parse_args() diff --git a/tools/qupath_roi_splitter/qupath_roi_splitter.xml b/tools/qupath_roi_splitter/qupath_roi_splitter.xml index 757dfc728..cf062103a 100644 --- a/tools/qupath_roi_splitter/qupath_roi_splitter.xml +++ b/tools/qupath_roi_splitter/qupath_roi_splitter.xml @@ -56,15 +56,15 @@ - - + + - + From 15c99fc2b784a4895adda20f59ea7d71654ee28c Mon Sep 17 00:00:00 2001 From: npinter Date: Mon, 15 Jul 2024 15:36:43 +0200 Subject: [PATCH 2/3] Increase ROIsplitter version number --- tools/qupath_roi_splitter/qupath_roi_splitter.py | 2 +- tools/qupath_roi_splitter/qupath_roi_splitter.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/qupath_roi_splitter/qupath_roi_splitter.py b/tools/qupath_roi_splitter/qupath_roi_splitter.py index 271db96aa..a02c68d0e 100644 --- a/tools/qupath_roi_splitter/qupath_roi_splitter.py +++ b/tools/qupath_roi_splitter/qupath_roi_splitter.py @@ -102,7 +102,7 @@ def split_qupath_roi(in_roi): parser = argparse.ArgumentParser(description="Split ROI coordinates of QuPath TMA annotation by cell type (classification)") parser.add_argument("--qupath_roi", default=False, help="Input QuPath annotation (GeoJSON file)") parser.add_argument("--fill", action="store_true", required=False, help="Fill pixels in ROIs (order of coordinates will be lost)") - parser.add_argument('--version', action='version', version='%(prog)s 0.2.0') + parser.add_argument('--version', action='version', version='%(prog)s 0.3.0') parser.add_argument("--all", action="store_true", required=False, help="Extracts all ROIs") parser.add_argument("--img", action="store_true", required=False, help="Generates image of ROIs") args = parser.parse_args() diff --git a/tools/qupath_roi_splitter/qupath_roi_splitter.xml b/tools/qupath_roi_splitter/qupath_roi_splitter.xml index cf062103a..6cd8b0169 100644 --- a/tools/qupath_roi_splitter/qupath_roi_splitter.xml +++ b/tools/qupath_roi_splitter/qupath_roi_splitter.xml @@ -1,7 +1,7 @@ Split ROI coordinates of QuPath TMA annotation by cell type (classification) - 0.2.1 + 0.3.0 0 From f90b67b9d26b440b4c45f5ef6568937741d2dc04 Mon Sep 17 00:00:00 2001 From: npinter Date: Mon, 15 Jul 2024 15:41:12 +0200 Subject: [PATCH 3/3] Fix linting --- tools/qupath_roi_splitter/qupath_roi_splitter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/qupath_roi_splitter/qupath_roi_splitter.py b/tools/qupath_roi_splitter/qupath_roi_splitter.py index a02c68d0e..1091d5af1 100644 --- a/tools/qupath_roi_splitter/qupath_roi_splitter.py +++ b/tools/qupath_roi_splitter/qupath_roi_splitter.py @@ -1,4 +1,5 @@ import argparse + import cv2 import geojson import numpy as np