Mercurial > repos > galaxyp > qupath_roi_splitter
comparison qupath_roi_splitter.py @ 6:6a8cf86fd3b7 draft default tip
planemo upload for repository hhttps://github.com/npinter/ROIsplitter commit 80eecd9912892296aad3231be75fcee3cc6a6b02
author | galaxyp |
---|---|
date | Mon, 02 Sep 2024 19:07:23 +0000 |
parents | 17c54a716a5b |
children |
comparison
equal
deleted
inserted
replaced
5:17c54a716a5b | 6:6a8cf86fd3b7 |
---|---|
12 coords_with_index.append((coord[0], coord[1], feature_index, coord_index)) | 12 coords_with_index.append((coord[0], coord[1], feature_index, coord_index)) |
13 coord_index += 1 | 13 coord_index += 1 |
14 return coords_with_index | 14 return coords_with_index |
15 | 15 |
16 | 16 |
17 def collect_roi_coords(input_roi, feature_index): | 17 def collect_roi_coords(input_roi): |
18 all_coords = [] | 18 coords = input_roi["geometry"]["coordinates"] |
19 if len(input_roi["geometry"]["coordinates"]) == 1: | 19 |
20 # Polygon w/o holes | 20 def process_coords(coord_list): |
21 all_coords.extend(collect_coords(input_roi["geometry"]["coordinates"][0], feature_index)) | 21 if isinstance(coord_list[0], (int, float)): |
22 return [coord_list] | |
23 elif all(isinstance(c, list) for c in coord_list): | |
24 return coord_list | |
25 else: | |
26 return [coord_list] | |
27 | |
28 if isinstance(coords[0][0], list): | |
29 return [process_coords(sub_coords) for sub_coords in coords] | |
22 else: | 30 else: |
23 coord_index = 0 | 31 return [process_coords(coords)] |
24 for sub_roi in input_roi["geometry"]["coordinates"]: | |
25 if len(sub_roi) == 2: | |
26 # Special case: LMD data | |
27 all_coords.extend(collect_coords([sub_roi], feature_index, coord_index)) | |
28 coord_index += 1 | |
29 else: | |
30 # Polygon with holes or MultiPolygon | |
31 if not isinstance(sub_roi[0][0], list): | |
32 all_coords.extend(collect_coords(sub_roi, feature_index, coord_index)) | |
33 coord_index += len(sub_roi) | |
34 else: | |
35 # MultiPolygon with holes | |
36 for sub_coord in sub_roi: | |
37 all_coords.extend(collect_coords(sub_coord, feature_index, coord_index)) | |
38 coord_index += len(sub_coord) | |
39 return all_coords | |
40 | 32 |
41 | 33 |
42 def split_qupath_roi(in_roi): | 34 def split_qupath_roi(in_roi): |
43 with open(in_roi) as file: | 35 with open(in_roi) as file: |
44 qupath_roi = geojson.load(file) | 36 qupath_roi = geojson.load(file) |
45 | 37 |
46 # HE dimensions | 38 # HE dimensions |
47 dim_plt = [int(qupath_roi["dim"]["width"]), int(qupath_roi["dim"]["height"])] | 39 dim_plt = [int(qupath_roi["dim"]["width"]), int(qupath_roi["dim"]["height"])] |
40 tma_name = qupath_roi["name"] | |
48 | 41 |
49 tma_name = qupath_roi["name"] | 42 if "featureNames" in qupath_roi: |
50 cell_types = [ct.rsplit(" - ", 1)[-1] for ct in qupath_roi["featureNames"]] | 43 cell_types = [ct.rsplit(" - ", 1)[-1] for ct in qupath_roi["featureNames"]] |
44 else: | |
45 cell_types = ["all"] | |
51 | 46 |
52 coords_by_cell_type = {ct: [] for ct in cell_types} | 47 coords_by_cell_type = {ct: [] for ct in cell_types} |
53 coords_by_cell_type['all'] = [] # For storing all coordinates if args.all is True | 48 if "all" not in coords_by_cell_type: |
49 coords_by_cell_type["all"] = [] | |
54 | 50 |
55 for feature_index, roi in enumerate(qupath_roi["features"]): | 51 for roi in qupath_roi["features"]: |
56 feature_coords = collect_roi_coords(roi, feature_index) | 52 feature_coords = collect_roi_coords(roi) |
57 | 53 |
58 if args.all: | 54 if args.all or "classification" not in roi["properties"]: |
59 coords_by_cell_type['all'].extend(feature_coords) | 55 coords_by_cell_type["all"].append(feature_coords) |
60 elif "classification" in roi["properties"]: | 56 elif "classification" in roi["properties"]: |
61 cell_type = roi["properties"]["classification"]["name"] | 57 cell_type = roi["properties"]["classification"]["name"] |
62 if cell_type in cell_types: | 58 if cell_type in cell_types: |
63 coords_by_cell_type[cell_type].extend(feature_coords) | 59 coords_by_cell_type[cell_type].append(feature_coords) |
64 | 60 |
65 for cell_type, coords in coords_by_cell_type.items(): | 61 for cell_type, coords_list in coords_by_cell_type.items(): |
66 if coords: | 62 if coords_list: |
67 # Generate image (white background) | 63 img = np.ones((dim_plt[1], dim_plt[0], 3), dtype="uint8") * 255 |
68 img = np.ones((dim_plt[1], dim_plt[0]), dtype="uint8") * 255 | |
69 | 64 |
70 # Convert to numpy array and ensure integer coordinates | 65 all_coords = [] |
71 coords_arr = np.array(coords).astype(int) | 66 for feature in coords_list: |
67 for polygon in feature: | |
68 # Multiple sub_polygons in LMD data | |
69 for sub_poly in polygon if isinstance(polygon[0][0], list) else [polygon]: | |
70 pts = np.array(sub_poly, dtype=np.float32).reshape(-1, 2) | |
71 pts = pts.astype(np.int32) | |
72 | 72 |
73 # Sort by feature_index first, then by coord_index | 73 # Get filled pixel coordinates |
74 coords_arr = coords_arr[np.lexsort((coords_arr[:, 3], coords_arr[:, 2]))] | 74 if args.fill: |
75 temp_img = np.ones((dim_plt[1], dim_plt[0]), dtype="uint8") * 255 | |
76 cv2.fillPoly(temp_img, [pts], color=0) | |
77 filled_coords = np.column_stack(np.where(temp_img == 0)) | |
78 all_coords.extend(filled_coords[:, [1, 0]]) # Swap columns to get (x, y) | |
79 cv2.fillPoly(img, [pts], color=0) | |
80 else: | |
81 cv2.polylines(img, [pts], isClosed=True, color=(0, 0, 0), thickness=1) | |
82 all_coords.extend(pts) | |
75 | 83 |
76 # Get filled pixel coordinates | 84 all_coords = np.array(all_coords) |
77 if args.fill: | |
78 filled_coords = np.column_stack(np.where(img == 0)) | |
79 all_coords = np.unique(np.vstack((coords_arr[:, :2], filled_coords[:, ::-1])), axis=0) | |
80 else: | |
81 all_coords = coords_arr[:, :2] | |
82 | |
83 # Save all coordinates to CSV | |
84 coords_df = pd.DataFrame(all_coords, columns=['x', 'y'], dtype=int) | 85 coords_df = pd.DataFrame(all_coords, columns=['x', 'y'], dtype=int) |
85 coords_df.to_csv("{}_{}.txt".format(tma_name, cell_type), sep='\t', index=False) | 86 coords_df.to_csv("{}_{}.txt".format(tma_name, cell_type), sep='\t', index=False) |
86 | 87 |
87 # Generate image for visualization if --img is specified | 88 # Generate image for visualization if --img is specified |
88 if args.img: | 89 if args.img: |
89 # Group coordinates by feature_index | |
90 features = {} | |
91 for x, y, feature_index, coord_index in coords_arr: | |
92 if feature_index not in features: | |
93 features[feature_index] = [] | |
94 features[feature_index].append((x, y)) | |
95 | |
96 # Draw each feature separately | |
97 for feature_coords in features.values(): | |
98 pts = np.array(feature_coords, dtype=np.int32) | |
99 if args.fill: | |
100 cv2.fillPoly(img, [pts], color=0) # Black fill | |
101 else: | |
102 cv2.polylines(img, [pts], isClosed=True, color=0, thickness=1) # Black outline | |
103 | |
104 cv2.imwrite("{}_{}.png".format(tma_name, cell_type), img) | 90 cv2.imwrite("{}_{}.png".format(tma_name, cell_type), img) |
105 | 91 |
106 | 92 |
107 if __name__ == "__main__": | 93 if __name__ == "__main__": |
108 parser = argparse.ArgumentParser(description="Split ROI coordinates of QuPath TMA annotation by cell type (classification)") | 94 parser = argparse.ArgumentParser(description="Split ROI coordinates of QuPath TMA annotation by cell type (classification)") |
109 parser.add_argument("--qupath_roi", default=False, help="Input QuPath annotation (GeoJSON file)") | 95 parser.add_argument("--qupath_roi", default=False, help="Input QuPath annotation (GeoJSON file)") |
110 parser.add_argument("--fill", action="store_true", required=False, help="Fill pixels in ROIs (order of coordinates will be lost)") | 96 parser.add_argument("--fill", action="store_true", required=False, help="Fill pixels in ROIs (order of coordinates will be lost)") |
111 parser.add_argument('--version', action='version', version='%(prog)s 0.3.1') | 97 parser.add_argument('--version', action='version', version='%(prog)s 0.3.2') |
112 parser.add_argument("--all", action="store_true", required=False, help="Extracts all ROIs") | 98 parser.add_argument("--all", action="store_true", required=False, help="Extracts all ROIs") |
113 parser.add_argument("--img", action="store_true", required=False, help="Generates image of ROIs") | 99 parser.add_argument("--img", action="store_true", required=False, help="Generates image of ROIs") |
114 args = parser.parse_args() | 100 args = parser.parse_args() |
115 | 101 |
116 if args.qupath_roi: | 102 if args.qupath_roi: |