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: