Skip to content

Commit

Permalink
Merge pull request #306 from nanli-emory/export_geojson
Browse files Browse the repository at this point in the history
New feature that save mask as geojson format
  • Loading branch information
jacksonjacobs1 authored Dec 16, 2024
2 parents 13a9439 + 82acf1d commit 74757b9
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 10 deletions.
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

0 comments on commit 74757b9

Please sign in to comment.