From d388f8a17b3466cc7e5a731d88e7e37acef65cda Mon Sep 17 00:00:00 2001 From: nanli-emory Date: Tue, 11 Jun 2024 22:18:05 -0400 Subject: [PATCH 1/7] add --geojson option into HistoQC --- Readme.md | 6 ++- histoqc/BaseImage.py | 3 +- histoqc/SaveModule.py | 89 ++++++++++++++++++++++++++++++++++++++++--- histoqc/__main__.py | 11 +++++- histoqc/_worker.py | 4 +- 5 files changed, 102 insertions(+), 11 deletions(-) diff --git a/Readme.md b/Readme.md index 0fe0414..409d043 100644 --- a/Readme.md +++ b/Readme.md @@ -106,12 +106,16 @@ 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 + -g GEOJSON, --geojson GEOJSON + save binary mask as geojson format if set geojson True. The default value is False --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: diff --git a/histoqc/BaseImage.py b/histoqc/BaseImage.py index 16bd764..70a5738 100644 --- a/histoqc/BaseImage.py +++ b/histoqc/BaseImage.py @@ -18,7 +18,7 @@ class BaseImage(dict): - def __init__(self, fname, fname_outdir, seed, params): + def __init__(self, fname, fname_outdir, seed, geojson, params): dict.__init__(self) self.in_memory_compression = strtobool(params.get("in_memory_compression", "False")) @@ -32,6 +32,7 @@ def __init__(self, fname, fname_outdir, seed, params): self["outdir"] = fname_outdir self["seed"] = seed + self["geojson"] = geojson self["dir"] = os.path.dirname(fname) self["os_handle"] = openslide.OpenSlide(fname) diff --git a/histoqc/SaveModule.py b/histoqc/SaveModule.py index 627f8ca..fd03442 100644 --- a/histoqc/SaveModule.py +++ b/histoqc/SaveModule.py @@ -1,12 +1,12 @@ import logging import os +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 def blend2Images(img, mask): if (img.ndim == 3): @@ -19,6 +19,78 @@ def blend2Images(img, mask): return out +''' +the followings are helper functions for generating geojson +''' + +feature_template = { + "type": "Feature", + "id": None, + "geometry": { + "type": None, + "coordinates": [] + }, + "properties": { + "objectType": "annotation" + } +} + +feature_collection_template = { + "type": "FeatureCollection", + "features": [] +} + + + + + + + +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) + + # 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 + + # copy feature collection template in geojson + feature_collection = deepcopy(feature_collection_template) + for contour in contours: + # copy feature template in geojson + new_feature = deepcopy(feature_template) + # set id + new_feature["id"] = uuid.uuid4().hex + + # scale up the coordinate + points = np.asarray(np.flip(contour / [mask_height, mask_width] * [dim_height, dim_width]),dtype="int") + + # if first and last points has same coordinates then it is polygon + if (contour[0]==contour[-1]).all(): + # polygon + new_feature['geometry']['type'] = 'Polygon' + new_feature['geometry']['coordinates'].append(points.tolist()) + else: + # LineString + new_feature['geometry']['type'] = 'LineString' + new_feature['geometry']['coordinates'] = points.tolist() + + feature_collection['features'].append(new_feature) + + return feature_collection + + def saveFinalMask(s, params): logging.info(f"{s['filename']} - \tsaveUsableRegion") @@ -28,6 +100,13 @@ def saveFinalMask(s, params): io.imsave(s["outdir"] + os.sep + s["filename"] + "_mask_use.png", img_as_ubyte(mask)) + if s['geojson']: + geojson = binaryMask2Geojson(s, mask) + # save as genjson file + with open(s["outdir"] + os.sep + s["filename"] + "_mask_use.geojson", 'w') as f: + json.dump(geojson, f) + + 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) diff --git a/histoqc/__main__.py b/histoqc/__main__.py index 1324cf2..f5d52a3 100644 --- a/histoqc/__main__.py +++ b/histoqc/__main__.py @@ -62,10 +62,17 @@ def main(argv=None): help="number of processes to launch", type=int, default=1) + parser.add_argument('-g', '--geojson', + help="export binary mask as geojson format", + type=bool, + default=False) 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 ----------------------------------- @@ -75,7 +82,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") @@ -164,7 +170,8 @@ def main(argv=None): 'num_files': num_files, 'force': args.force, 'seed': args.seed, - 'debug': args.debug + 'debug': args.debug, + 'geojson': args.geojson } failed = mpm.list() setup_plotting_backend(lm.logger, debug=args.debug) diff --git a/histoqc/_worker.py b/histoqc/_worker.py index 2e92e19..a48a12d 100644 --- a/histoqc/_worker.py +++ b/histoqc/_worker.py @@ -16,7 +16,7 @@ def worker_setup(c): def worker(idx, file_name, *, - process_queue, config, outdir, log_manager, lock, shared_dict, num_files, force, seed, debug): + process_queue, config, outdir, log_manager, lock, shared_dict, num_files, force, seed, debug, geojson): """pipeline worker function""" # set the seed if seed is not None: @@ -39,7 +39,7 @@ def worker(idx, file_name, *, log_manager.logger.info(f"-----Working on:\t{file_name}\t\t{idx+1} of {num_files}") try: - s = BaseImage(file_name, fname_outdir, seed, dict(config.items("BaseImage.BaseImage"))) + s = BaseImage(file_name, fname_outdir, seed, geojson, dict(config.items("BaseImage.BaseImage"))) for process, process_params in process_queue: process_params["lock"] = lock From ed817bf22473fd9806f3ded709826afff981d21d Mon Sep 17 00:00:00 2001 From: nanli-emory Date: Wed, 12 Jun 2024 14:34:29 -0400 Subject: [PATCH 2/7] add 'saveMask2Geojson' function in 'saveMask' module.And remove geojson option in HistoQC module --- Readme.md | 2 -- histoqc/BaseImage.py | 3 +-- histoqc/SaveModule.py | 27 ++++++++++++++++++++------- histoqc/__main__.py | 7 +------ histoqc/_worker.py | 4 ++-- histoqc/config/config_clinical.ini | 15 ++++++++++++++- 6 files changed, 38 insertions(+), 20 deletions(-) diff --git a/Readme.md b/Readme.md index 409d043..61ae48f 100644 --- a/Readme.md +++ b/Readme.md @@ -110,8 +110,6 @@ optional arguments: set a seed used to produce a random number in all modules -n NPROCESSES, --nprocesses NPROCESSES number of processes to launch - -g GEOJSON, --geojson GEOJSON - save binary mask as geojson format if set geojson True. The default value is False --symlink TARGET_DIR create symlink to outdir in TARGET_DIR diff --git a/histoqc/BaseImage.py b/histoqc/BaseImage.py index 70a5738..16bd764 100644 --- a/histoqc/BaseImage.py +++ b/histoqc/BaseImage.py @@ -18,7 +18,7 @@ class BaseImage(dict): - def __init__(self, fname, fname_outdir, seed, geojson, params): + def __init__(self, fname, fname_outdir, seed, params): dict.__init__(self) self.in_memory_compression = strtobool(params.get("in_memory_compression", "False")) @@ -32,7 +32,6 @@ def __init__(self, fname, fname_outdir, seed, geojson, params): self["outdir"] = fname_outdir self["seed"] = seed - self["geojson"] = geojson self["dir"] = os.path.dirname(fname) self["os_handle"] = openslide.OpenSlide(fname) diff --git a/histoqc/SaveModule.py b/histoqc/SaveModule.py index fd03442..b1c539e 100644 --- a/histoqc/SaveModule.py +++ b/histoqc/SaveModule.py @@ -100,12 +100,6 @@ def saveFinalMask(s, params): io.imsave(s["outdir"] + os.sep + s["filename"] + "_mask_use.png", img_as_ubyte(mask)) - if s['geojson']: - geojson = binaryMask2Geojson(s, mask) - # save as genjson file - with open(s["outdir"] + os.sep + s["filename"] + "_mask_use.geojson", 'w') as f: - json.dump(geojson, f) - if strtobool(params.get("use_mask", "True")): # should we create and save the fusion mask? img = s.getImgThumb(s["image_work_size"]) @@ -149,7 +143,7 @@ def saveMacro(s, params): def saveMask(s, params): logging.info(f"{s['filename']} - \tsaveMaskUse") suffix = params.get("suffix", None) - + geojson = strtobool(params.get("geojson", "False")) # check suffix param if not suffix: msg = f"{s['filename']} - \tPlease set the suffix for mask use." @@ -159,6 +153,25 @@ 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): + logging.info(f"{s['filename']} - \tsaveMaskUse2Geojson") + suffix = params.get("suffix", None) + # check suffix param + if not suffix: + msg = f"{s['filename']} - \tPlease set the suffix for mask use in geojson." + logging.error(msg) + return + + # convert mask to geojson + geojson = binaryMask2Geojson(s, s["img_mask_use"]) + + # save mask as genjson file + with open(f"{s['outdir']}{os.sep}{s['filename']}_{suffix}.geojson", 'w') as f: + json.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 diff --git a/histoqc/__main__.py b/histoqc/__main__.py index f5d52a3..a0f54a6 100644 --- a/histoqc/__main__.py +++ b/histoqc/__main__.py @@ -62,10 +62,6 @@ def main(argv=None): help="number of processes to launch", type=int, default=1) - parser.add_argument('-g', '--geojson', - help="export binary mask as geojson format", - type=bool, - default=False) parser.add_argument('--symlink', metavar="TARGET_DIR", help="create symlink to outdir in TARGET_DIR", default=None) @@ -170,8 +166,7 @@ def main(argv=None): 'num_files': num_files, 'force': args.force, 'seed': args.seed, - 'debug': args.debug, - 'geojson': args.geojson + 'debug': args.debug } failed = mpm.list() setup_plotting_backend(lm.logger, debug=args.debug) diff --git a/histoqc/_worker.py b/histoqc/_worker.py index a48a12d..2e92e19 100644 --- a/histoqc/_worker.py +++ b/histoqc/_worker.py @@ -16,7 +16,7 @@ def worker_setup(c): def worker(idx, file_name, *, - process_queue, config, outdir, log_manager, lock, shared_dict, num_files, force, seed, debug, geojson): + process_queue, config, outdir, log_manager, lock, shared_dict, num_files, force, seed, debug): """pipeline worker function""" # set the seed if seed is not None: @@ -39,7 +39,7 @@ def worker(idx, file_name, *, log_manager.logger.info(f"-----Working on:\t{file_name}\t\t{idx+1} of {num_files}") try: - s = BaseImage(file_name, fname_outdir, seed, geojson, dict(config.items("BaseImage.BaseImage"))) + s = BaseImage(file_name, fname_outdir, seed, dict(config.items("BaseImage.BaseImage"))) for process, process_params in process_queue: process_params["lock"] = lock diff --git a/histoqc/config/config_clinical.ini b/histoqc/config/config_clinical.ini index f0c471c..39423a1 100644 --- a/histoqc/config/config_clinical.ini +++ b/histoqc/config/config_clinical.ini @@ -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 @@ -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 @@ -207,4 +210,14 @@ suffix: first suffix: second [SaveModule.saveMask:third] -suffix: third \ No newline at end of file +suffix: third + + +[SaveModule.saveMask2Geojson:geo_first] +suffix: geo_first + +[SaveModule.saveMask2Geojson:geo_second] +suffix: geo_second + +[SaveModule.saveMask2Geojson:geo_third] +suffix: geo_third \ No newline at end of file From 9269a654f1a1af5acebb450a90a00914d538e895 Mon Sep 17 00:00:00 2001 From: nanli-emory Date: Wed, 12 Jun 2024 14:38:03 -0400 Subject: [PATCH 3/7] remove the unused logic on the previous code --- histoqc/SaveModule.py | 1 - 1 file changed, 1 deletion(-) diff --git a/histoqc/SaveModule.py b/histoqc/SaveModule.py index b1c539e..916fa9b 100644 --- a/histoqc/SaveModule.py +++ b/histoqc/SaveModule.py @@ -143,7 +143,6 @@ def saveMacro(s, params): def saveMask(s, params): logging.info(f"{s['filename']} - \tsaveMaskUse") suffix = params.get("suffix", None) - geojson = strtobool(params.get("geojson", "False")) # check suffix param if not suffix: msg = f"{s['filename']} - \tPlease set the suffix for mask use." From 9e4ca1f26eb87664944f29fcf997b102432708ec Mon Sep 17 00:00:00 2001 From: nanli-emory Date: Tue, 18 Jun 2024 15:28:38 -0400 Subject: [PATCH 4/7] add an mask_name option to point what mask to save --- histoqc/SaveModule.py | 13 ++++++++++--- histoqc/config/config_clinical.ini | 3 +++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/histoqc/SaveModule.py b/histoqc/SaveModule.py index 916fa9b..b79848c 100644 --- a/histoqc/SaveModule.py +++ b/histoqc/SaveModule.py @@ -154,16 +154,23 @@ def saveMask(s, params): def saveMask2Geojson(s, params): - logging.info(f"{s['filename']} - \tsaveMaskUse2Geojson") + + 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["img_mask_use"]) + 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: diff --git a/histoqc/config/config_clinical.ini b/histoqc/config/config_clinical.ini index 39423a1..40a9165 100644 --- a/histoqc/config/config_clinical.ini +++ b/histoqc/config/config_clinical.ini @@ -209,15 +209,18 @@ suffix: first [SaveModule.saveMask:second] suffix: second + [SaveModule.saveMask:third] suffix: third [SaveModule.saveMask2Geojson:geo_first] suffix: geo_first +mask_name: img_maskflat [SaveModule.saveMask2Geojson:geo_second] suffix: geo_second +mask_name: img_mask_small_filled [SaveModule.saveMask2Geojson:geo_third] suffix: geo_third \ No newline at end of file From c739d52a37a977a3bb47cb88e3cd9ca285fddda3 Mon Sep 17 00:00:00 2001 From: nanli-emory Date: Tue, 18 Jun 2024 15:30:17 -0400 Subject: [PATCH 5/7] correct typo --- histoqc/config/config_clinical.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/histoqc/config/config_clinical.ini b/histoqc/config/config_clinical.ini index 40a9165..0bf4382 100644 --- a/histoqc/config/config_clinical.ini +++ b/histoqc/config/config_clinical.ini @@ -216,7 +216,7 @@ suffix: third [SaveModule.saveMask2Geojson:geo_first] suffix: geo_first -mask_name: img_maskflat +mask_name: img_mask_flat [SaveModule.saveMask2Geojson:geo_second] suffix: geo_second From bbcb89c1cf14f81be99f79ccfd51022dcef5daa5 Mon Sep 17 00:00:00 2001 From: nanli-emory Date: Fri, 15 Nov 2024 11:35:01 -0500 Subject: [PATCH 6/7] fixed the polygon only and has multipolygon in geojson --- histoqc/SaveModule.py | 45 ++++++++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/histoqc/SaveModule.py b/histoqc/SaveModule.py index b79848c..22c4e32 100644 --- a/histoqc/SaveModule.py +++ b/histoqc/SaveModule.py @@ -1,5 +1,6 @@ import logging import os +import cv2 import uuid import json import numpy as np @@ -53,7 +54,16 @@ def binaryMask2Geojson(s, mask): (mask_height, mask_width) = mask.shape # convert binary mask to contours - contours = measure.find_contours(mask) + # 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): @@ -67,25 +77,34 @@ def binaryMask2Geojson(s, mask): # copy feature collection template in geojson feature_collection = deepcopy(feature_collection_template) - for contour in contours: + for i, contour in enumerate(contours): + first_child_idx = hierarchy[0, i, 2] + parent_idx = hierarchy[0, i, 3] + + if (parent_idx != -1): + continue + # copy feature template in geojson new_feature = deepcopy(feature_template) # set id new_feature["id"] = uuid.uuid4().hex - # scale up the coordinate - points = np.asarray(np.flip(contour / [mask_height, mask_width] * [dim_height, dim_width]),dtype="int") - - # if first and last points has same coordinates then it is polygon - if (contour[0]==contour[-1]).all(): - # polygon + # points = np.asarray(np.flip(contour / [mask_height, mask_width] * [dim_height, dim_width]),dtype="int") + 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] + + if first_child_idx == -1: new_feature['geometry']['type'] = 'Polygon' - new_feature['geometry']['coordinates'].append(points.tolist()) + new_feature['geometry']['coordinates'].append(points.tolist()) else: - # LineString - new_feature['geometry']['type'] = 'LineString' - new_feature['geometry']['coordinates'] = points.tolist() - + new_feature['geometry']['type'] = 'MultiPolygon' + new_feature['geometry']['coordinates'].append([points.tolist()]) + 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] + new_feature['geometry']['coordinates'].append([child_points.tolist()]) feature_collection['features'].append(new_feature) return feature_collection From 82acf1d17976ec3abbf98062774d584e37ca1052 Mon Sep 17 00:00:00 2001 From: nanli-emory Date: Thu, 12 Dec 2024 00:31:28 -0500 Subject: [PATCH 7/7] using geojson lib --- histoqc/SaveModule.py | 67 +++++++++++-------------------------------- requirements.txt | 2 +- 2 files changed, 18 insertions(+), 51 deletions(-) diff --git a/histoqc/SaveModule.py b/histoqc/SaveModule.py index 22c4e32..596bc41 100644 --- a/histoqc/SaveModule.py +++ b/histoqc/SaveModule.py @@ -8,6 +8,7 @@ from distutils.util import strtobool from skimage import color, measure from copy import deepcopy +from geojson import Polygon, Feature, FeatureCollection, dump def blend2Images(img, mask): if (img.ndim == 3): @@ -19,34 +20,6 @@ def blend2Images(img, mask): out = np.concatenate((mask, img, mask), 2) return out - -''' -the followings are helper functions for generating geojson -''' - -feature_template = { - "type": "Feature", - "id": None, - "geometry": { - "type": None, - "coordinates": [] - }, - "properties": { - "objectType": "annotation" - } -} - -feature_collection_template = { - "type": "FeatureCollection", - "features": [] -} - - - - - - - def binaryMask2Geojson(s, mask): # get the dimension of slide (dim_width, dim_height) = s['os_handle'].dimensions @@ -75,38 +48,32 @@ def binaryMask2Geojson(s, mask): s["warnings"].append(msg) return None - # copy feature collection template in geojson - feature_collection = deepcopy(feature_collection_template) + 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 - - # copy feature template in geojson - new_feature = deepcopy(feature_template) - # set id - new_feature["id"] = uuid.uuid4().hex - # scale up the coordinate - # points = np.asarray(np.flip(contour / [mask_height, mask_width] * [dim_height, dim_width]),dtype="int") + + 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] - - if first_child_idx == -1: - new_feature['geometry']['type'] = 'Polygon' - new_feature['geometry']['coordinates'].append(points.tolist()) - else: - new_feature['geometry']['type'] = 'MultiPolygon' - new_feature['geometry']['coordinates'].append([points.tolist()]) + 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] - new_feature['geometry']['coordinates'].append([child_points.tolist()]) - feature_collection['features'].append(new_feature) - + 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 @@ -193,7 +160,7 @@ def saveMask2Geojson(s, params): # save mask as genjson file with open(f"{s['outdir']}{os.sep}{s['filename']}_{suffix}.geojson", 'w') as f: - json.dump(geojson, f) + dump(geojson, f) diff --git a/requirements.txt b/requirements.txt index aa2d094..bc63292 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 \ No newline at end of file