Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New feature that save mask as geojson format #306

Merged
merged 7 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,14 @@ optional arguments:
-f, --force force overwriting of existing files
-b BATCH, --batch BATCH
break results file into subsets of this size
-s SEED, --seed SEED,
-s SEED, --seed SEED
set a seed used to produce a random number in all modules
-n NPROCESSES, --nprocesses NPROCESSES
number of processes to launch
--symlink TARGET_DIR create symlink to outdir in TARGET_DIR



```

Installed or simply git-cloned, a typical command line for running the tool thus looks like:
Expand Down
96 changes: 90 additions & 6 deletions histoqc/SaveModule.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import logging
import os
import cv2
import uuid
import json
import numpy as np
from skimage import io, img_as_ubyte
from distutils.util import strtobool
from skimage import color
import numpy as np

import matplotlib.pyplot as plt

from skimage import color, measure
from copy import deepcopy
from geojson import Polygon, Feature, FeatureCollection, dump

def blend2Images(img, mask):
if (img.ndim == 3):
Expand All @@ -18,6 +20,62 @@ def blend2Images(img, mask):
out = np.concatenate((mask, img, mask), 2)
return out

def binaryMask2Geojson(s, mask):
# get the dimension of slide
(dim_width, dim_height) = s['os_handle'].dimensions
# get the dimension of mask
(mask_height, mask_width) = mask.shape

# convert binary mask to contours
# contours = measure.find_contours(mask)
contours, hierarchy = cv2.findContours(img_as_ubyte(mask), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
children = [[] for i in range(len(contours))]
for i, cnt in enumerate(contours):
# first_child_idx = hier[0, i, 2]
parent_idx = hierarchy[0, i, 3]
if (parent_idx == -1):
continue
# add contour to parent's children node
children[parent_idx].append(cnt)

# no contours detected
if not len(contours):
# set default format
ann_format = "xml"
# warning msg
msg = f"No contour detected at use mask image. Geojson annotation won't be generated."
logging.warning(f"{s['filename']} - {msg}")
s["warnings"].append(msg)
return None

features = []
for i, contour in enumerate(contours):
first_child_idx = hierarchy[0, i, 2]
parent_idx = hierarchy[0, i, 3]

if (parent_idx != -1):
continue

geometry = []
points = np.asarray(contour / [mask_height, mask_width] * [dim_height, dim_width],dtype="int")
points = np.append(points, [points[0]], axis=0)
points = points[:,0].tolist()
points = [tuple(p) for p in points]
geometry.append(points)
if first_child_idx != -1:
for child in children[i]:
child_points = np.asarray(child / [mask_height, mask_width] * [dim_height, dim_width],dtype="int")
child_points = np.append(child_points, [child_points[0]], axis=0)
child_points = child_points[:,0].tolist()
child_points = [tuple(p) for p in child_points]
geometry.append(child_points)
new_feature = Feature(id=uuid.uuid4().hex, geometry=Polygon(geometry),properties={"objectType": "annotation"})

features.append(new_feature)
feature_collection = FeatureCollection(features)

return feature_collection


def saveFinalMask(s, params):
logging.info(f"{s['filename']} - \tsaveUsableRegion")
Expand All @@ -28,6 +86,7 @@ def saveFinalMask(s, params):

io.imsave(s["outdir"] + os.sep + s["filename"] + "_mask_use.png", img_as_ubyte(mask))


if strtobool(params.get("use_mask", "True")): # should we create and save the fusion mask?
img = s.getImgThumb(s["image_work_size"])
out = blend2Images(img, mask)
Expand Down Expand Up @@ -70,7 +129,6 @@ def saveMacro(s, params):
def saveMask(s, params):
logging.info(f"{s['filename']} - \tsaveMaskUse")
suffix = params.get("suffix", None)

# check suffix param
if not suffix:
msg = f"{s['filename']} - \tPlease set the suffix for mask use."
Expand All @@ -80,6 +138,32 @@ def saveMask(s, params):
# save mask
io.imsave(f"{s['outdir']}{os.sep}{s['filename']}_{suffix}.png", img_as_ubyte(s["img_mask_use"]))


def saveMask2Geojson(s, params):

mask_name = params.get('mask_name', 'img_mask_use')
suffix = params.get("suffix", None)
logging.info(f"{s['filename']} - \tsaveMaskUse2Geojson: {mask_name}")
# check suffix param
if not suffix:
msg = f"{s['filename']} - \tPlease set the suffix for mask use in geojson."
logging.error(msg)
return

# check if the mask name exists
if s.get(mask_name, None) is None:
msg = f"{s['filename']} - \tThe `{mask_name}` mask dosen't exist. Please use correct mask name."
logging.error(msg)
return
# convert mask to geojson
geojson = binaryMask2Geojson(s, s[mask_name])

# save mask as genjson file
with open(f"{s['outdir']}{os.sep}{s['filename']}_{suffix}.geojson", 'w') as f:
dump(geojson, f)



def saveThumbnails(s, params):
logging.info(f"{s['filename']} - \tsaveThumbnail")
# we create 2 thumbnails for usage in the front end, one relatively small one, and one larger one
Expand Down
4 changes: 3 additions & 1 deletion histoqc/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,10 @@ def main(argv=None):
parser.add_argument('--symlink', metavar="TARGET_DIR",
help="create symlink to outdir in TARGET_DIR",
default=None)

parser.add_argument('--debug', action='store_true', help="trigger debugging behavior")


args = parser.parse_args(argv)

# --- multiprocessing and logging setup -----------------------------------
Expand All @@ -75,7 +78,6 @@ def main(argv=None):
lm = MultiProcessingLogManager('histoqc', manager=mpm)

# --- parse the pipeline configuration ------------------------------------

config = configparser.ConfigParser()
if not args.config:
lm.logger.warning(f"Configuration file not set (--config), using default")
Expand Down
18 changes: 17 additions & 1 deletion histoqc/config/config_clinical.ini
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ steps= BasicModule.getBasicStats
LightDarkModule.getIntensityThresholdPercent:darktissue
BubbleRegionByRegion.detectSmoothness
SaveModule.saveMask:first
SaveModule.saveMask2Geojson:geo_first
MorphologyModule.removeFatlikeTissue
MorphologyModule.fillSmallHoles
MorphologyModule.removeSmallObjects
SaveModule.saveMask:second
SaveModule.saveMask2Geojson:geo_second
BlurDetectionModule.identifyBlurryRegions
BasicModule.finalProcessingSpur
BasicModule.finalProcessingArea
Expand All @@ -20,6 +22,7 @@ steps= BasicModule.getBasicStats
BrightContrastModule.getBrightnessByChannelinColorSpace:RGB
BrightContrastModule.getBrightnessByChannelinColorSpace:YUV
SaveModule.saveMask:third
SaveModule.saveMask2Geojson:geo_third
DeconvolutionModule.separateStains
SaveModule.saveFinalMask
SaveModule.saveMacro
Expand Down Expand Up @@ -206,5 +209,18 @@ suffix: first
[SaveModule.saveMask:second]
suffix: second


[SaveModule.saveMask:third]
suffix: third
suffix: third


[SaveModule.saveMask2Geojson:geo_first]
suffix: geo_first
mask_name: img_mask_flat

[SaveModule.saveMask2Geojson:geo_second]
suffix: geo_second
mask_name: img_mask_small_filled

[SaveModule.saveMask2Geojson:geo_third]
suffix: geo_third
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ Pillow~=9.4.0
setuptools~=65.6.3
shapely~=2.0.1
flask~=2.3.2

geojson~=3.1.0
cohortfinder==1.0.1