Skip to content

Commit

Permalink
Merge pull request #768 from npinter/update-roi-splitter
Browse files Browse the repository at this point in the history
Update ROIsplitter to 0.3.0
  • Loading branch information
bgruening authored Jul 19, 2024
2 parents e75f095 + 3e4628e commit 918ae25
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 67 deletions.
129 changes: 66 additions & 63 deletions tools/qupath_roi_splitter/qupath_roi_splitter.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,101 +6,104 @@
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):
with open(in_roi) as file:
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)
coords_by_cell_type = {ct: [] for ct in cell_types}
coords_by_cell_type['all'] = [] # For storing all coordinates if args.all is True

# get all black pixel
coords_arr = np.column_stack(np.where(img == (0, 0, 0)))
for feature_index, roi in enumerate(qupath_roi["features"]):
feature_coords = collect_roi_coords(roi, feature_index)

# remove duplicated rows
coords_arr_xy = coords_arr[coords_arr[:, 2] == 0]
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)

# remove last column
coords_arr_xy = np.delete(coords_arr_xy, 2, axis=1)
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

# to pandas and rename columns to x and y
coords_df = pd.DataFrame(coords_arr_xy, columns=['y', 'x'])
# Convert to numpy array and ensure integer coordinates
coords_arr = np.array(coords).astype(int)

# reorder columns
coords_df = coords_df[['x', 'y']]
# Sort by feature_index first, then by coord_index
coords_arr = coords_arr[np.lexsort((coords_arr[:, 3], coords_arr[:, 2]))]

# drop duplicates
coords_df = coords_df.drop_duplicates(
subset=['x', 'y'],
keep='last').reset_index(drop=True)

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.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()
Expand Down
8 changes: 4 additions & 4 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.2.1</token>
<token name="@VERSION@">0.3.0</token>
<token name="@VERSION_SUFFIX@">0</token>
</macros>
<requirements>
Expand Down Expand Up @@ -56,15 +56,15 @@
<assert_contents>
<has_text text="x"/>
<has_text text="y"/>
<has_text text="15561"/>
<has_text text="21160"/>
<has_text text="21153"/>
<has_text text="15570"/>
</assert_contents>
</element>
</output_collection>
<output_collection name="output_imgs" type="list" count="4">
<element name="E-5_Tumor.png">
<assert_contents>
<has_size value="1309478"/>
<has_size value="459919"/>
</assert_contents>
</element>
</output_collection>
Expand Down

0 comments on commit 918ae25

Please sign in to comment.