Skip to content

Commit

Permalink
Merge pull request #773 from npinter/update-qupath-roi-splitter
Browse files Browse the repository at this point in the history
Update QuPath ROIsplitter to v0.3.2
  • Loading branch information
bgruening authored Sep 2, 2024
2 parents e3d24f3 + b5f974c commit 80eecd9
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 68 deletions.
118 changes: 52 additions & 66 deletions tools/qupath_roi_splitter/qupath_roi_splitter.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,21 @@ def collect_coords(input_coords, feature_index, coord_index=0):
return coords_with_index


def collect_roi_coords(input_roi, feature_index):
all_coords = []
if len(input_roi["geometry"]["coordinates"]) == 1:
# Polygon w/o holes
all_coords.extend(collect_coords(input_roi["geometry"]["coordinates"][0], feature_index))
def collect_roi_coords(input_roi):
coords = input_roi["geometry"]["coordinates"]

def process_coords(coord_list):
if isinstance(coord_list[0], (int, float)):
return [coord_list]
elif all(isinstance(c, list) for c in coord_list):
return coord_list
else:
return [coord_list]

if isinstance(coords[0][0], list):
return [process_coords(sub_coords) for sub_coords in coords]
else:
coord_index = 0
for sub_roi in input_roi["geometry"]["coordinates"]:
if len(sub_roi) == 2:
# Special case: LMD data
all_coords.extend(collect_coords([sub_roi], feature_index, coord_index))
coord_index += 1
else:
# Polygon with holes or MultiPolygon
if not isinstance(sub_roi[0][0], list):
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:
all_coords.extend(collect_coords(sub_coord, feature_index, coord_index))
coord_index += len(sub_coord)
return all_coords
return [process_coords(coords)]


def split_qupath_roi(in_roi):
Expand All @@ -45,70 +37,64 @@ def split_qupath_roi(in_roi):

# HE dimensions
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"]]

if "featureNames" in qupath_roi:
cell_types = [ct.rsplit(" - ", 1)[-1] for ct in qupath_roi["featureNames"]]
else:
cell_types = ["all"]

coords_by_cell_type = {ct: [] for ct in cell_types}
coords_by_cell_type['all'] = [] # For storing all coordinates if args.all is True
if "all" not in coords_by_cell_type:
coords_by_cell_type["all"] = []

for feature_index, roi in enumerate(qupath_roi["features"]):
feature_coords = collect_roi_coords(roi, feature_index)
for roi in qupath_roi["features"]:
feature_coords = collect_roi_coords(roi)

if args.all:
coords_by_cell_type['all'].extend(feature_coords)
if args.all or "classification" not in roi["properties"]:
coords_by_cell_type["all"].append(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)

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

# Convert to numpy array and ensure integer coordinates
coords_arr = np.array(coords).astype(int)

# Sort by feature_index first, then by coord_index
coords_arr = coords_arr[np.lexsort((coords_arr[:, 3], coords_arr[:, 2]))]

# 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_by_cell_type[cell_type].append(feature_coords)

for cell_type, coords_list in coords_by_cell_type.items():
if coords_list:
img = np.ones((dim_plt[1], dim_plt[0], 3), dtype="uint8") * 255

all_coords = []
for feature in coords_list:
for polygon in feature:
# Multiple sub_polygons in LMD data
for sub_poly in polygon if isinstance(polygon[0][0], list) else [polygon]:
pts = np.array(sub_poly, dtype=np.float32).reshape(-1, 2)
pts = pts.astype(np.int32)

# Get filled pixel coordinates
if args.fill:
temp_img = np.ones((dim_plt[1], dim_plt[0]), dtype="uint8") * 255
cv2.fillPoly(temp_img, [pts], color=0)
filled_coords = np.column_stack(np.where(temp_img == 0))
all_coords.extend(filled_coords[:, [1, 0]]) # Swap columns to get (x, y)
cv2.fillPoly(img, [pts], color=0)
else:
cv2.polylines(img, [pts], isClosed=True, color=(0, 0, 0), thickness=1)
all_coords.extend(pts)

all_coords = np.array(all_coords)
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

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 (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.3.1')
parser.add_argument('--version', action='version', version='%(prog)s 0.3.2')
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()
Expand Down
4 changes: 2 additions & 2 deletions tools/qupath_roi_splitter/qupath_roi_splitter.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<tool id="qupath_roi_splitter" name="QuPath ROI Splitter" version="@VERSION@+galaxy@VERSION_SUFFIX@">
<description>Split ROI coordinates of QuPath TMA annotation by cell type (classification)</description>
<macros>
<token name="@VERSION@">0.3.1</token>
<token name="@VERSION@">0.3.2</token>
<token name="@VERSION_SUFFIX@">0</token>
</macros>
<requirements>
Expand Down Expand Up @@ -67,7 +67,7 @@
<output_collection name="output_imgs" type="list" count="4">
<element name="E-5_Tumor.png">
<assert_contents>
<has_size value="459919"/>
<has_size value="1309478"/>
</assert_contents>
</element>
</output_collection>
Expand Down

0 comments on commit 80eecd9

Please sign in to comment.