From 7a7710dbf85357ecad30d67a6472fcf182e96c5c Mon Sep 17 00:00:00 2001 From: Lee Cooper Date: Mon, 12 May 2025 22:47:59 -0500 Subject: [PATCH 1/9] data import script --- data_import.py | 267 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 data_import.py diff --git a/data_import.py b/data_import.py new file mode 100644 index 0000000..a6f0331 --- /dev/null +++ b/data_import.py @@ -0,0 +1,267 @@ +import argparse +from girder_client import GirderClient +import hashlib +import os +from tqdm import tqdm + + +def _import_str(path, file, destination): + '''Format the arguments for the assetstore/id/import endpoint to + import a single file.''' + return dict( + importPath=path, + destinationId=destination, + destinationType='folder', + fileIncludeRegex=f'^{file}$', + progress=True, + ) + + +def _import_file(client, assetstore, folder, file, replace=False): + '''Import a single file given the assetstore, filename and destination. + Optionally replace the item if an item with the same name exists.''' + match = list(client.listItem(folder, name=os.path.split(file)[1])) + import_args = _import_str( + os.path.split(file)[0], os.path.split(file)[1], folder + ) + if len(match) and not replace: + return match[0]['_id'] + elif len(match) and replace: + client.delete(f'item/{match[0]["_id"]}') + client.post(f'assetstore/{assetstore}/import', import_args) + match = list(client.listItem(folder, name=os.path.split(file)[1])) + assert len(match) == 1 + return match[0]['_id'] + + +def _upload_file(client, folder, file, replace=False): + '''Upload a single file given the assetstore, filename and destination. + Optionally replace the item if an item with the same name exists.''' + match = list(client.listItem(folder, name=os.path.split(file)[1])) + if len(match) and replace: + client.delete(f'item/{match[0]["_id"]}') + return client.uploadFileToFolder(folder, file)['_id'] + elif len(match) and not replace: + return match[0]['_id'] + else: + return client.uploadFileToFolder(folder, file)['_id'] + + +def _feature_h5filename(wsi_id, boxes, patchsize=100): + '''Generate h5 feature filename. + wsi_id : str + The girder identifier of the associated whole slide image. + boxes : list of floats + Bounding boxes of objects in the order they appear in the feature array. + This is a 1D array/list with left, top, right, bottom in sequence for + each box at scan magnification. + patchsize : int + The size of the patches used during feature extraction. + ''' + hashval = repr( + dict(itemId=wsi_id, bbox=[int(v) for v in boxes], patchSize=patchsize) + ) + hash = hashlib.new('sha256', hashval.encode()).hexdigest() + return f'feature-{hash}.h5' + + +def pixelmap_annotation(pixelmap_id, scale, boxes): + '''Generate JSON format pixelmap annotation to attach to whole-slide image. + pixelmap_id : string + The girder identifier of the pixelmap image (not the whole-slide image). + scale : float + Scaling ratio between the whole-slide and pixelmap image resolutions. For + example, scale for a wsi at 20X and pixelmap of 5x would be 4. + ''' + + values = [*[0] * (len(boxes) // 4)] + categories = [dict( + label="default", fillColor="rgba(0, 0, 0, 0)", strokeColor="rgba(0, 0, 0, 1)" + )] + transform = dict( + xoffset=0, yoffset=0, matrix=[[scale, 0], [0, scale]] + ) + pixelmap = dict( + type='pixelmap', + girderId=pixelmap_id, + boundaries=True, + transform=transform, + values=values, + categories=categories, + ) + annotation = dict( + name='Superpixel Epoch 0', + elements=[pixelmap], + ) + return annotation + + +def guided_label_import( + client, collection, wsis, features, pixelmaps, boxes, scales, assetstore=None, replace=False +): + '''Import or upload a guided labeling dataset to a digital slide archive instance. + + :param client: An authenticated GirderClient object. + :param collection: Name of the project to create. If a collection with this name exists, + it will be used. + :param wsis: A list of paths to wsi filenames on local (upload) or mounted (import) + storage. + :param features: A list of paths to h5 feature files in the same order as `wsis`. + :param pixelmaps: A list of paths to tiff pixelmap image files in the same order as + `wsis`. + :param boxes: A list of 2D arrays containing the left, top, right, and bottom of the + bounding box for each object in each pixelmap. Coordinates should be listed at + native scan magnification. The order of objects in each 2D array should follow + the order of values in the corresponding pixelmap. + :param scales: The float ratios of resolutions between the whole-slide images and the + corresponding pixelmap images. + :param assetstore: The girder id of the assetstore if data will be imported. + Default value of `None` means data will be uploaded and that all paths in `wsis`, + `features`, and `pixelmaps` are local file paths. + :param bool: If True, replace items during import or upload where filenames match. + ''' + + # if collection does not exist, create it, otherwise get collection id + match = client.get('collection', dict(text=collection, limit=0)) + if len(match): + collection = match[0]['_id'] + else: + collection = client.post('collection', dict(name=collection))['_id'] + + # construct folders if necessary + data_folder = client.loadOrCreateFolder('Data', collection, 'collection')['_id'] + client.addMetadataToFolder(data_folder, {'active_learning': True}) + feature_folder = client.loadOrCreateFolder('Features', data_folder, 'folder')['_id'] + pixelmap_folder = client.loadOrCreateFolder('Annotations', data_folder, 'folder')['_id'] + client.loadOrCreateFolder('Models', data_folder, 'folder')['_id'] + + # import if data assetstore provided, otherwise upload + wsi_ids = {} + feature_ids = {} + pixelmap_ids = {} + for w, f, p, b, s in tqdm( + zip(wsis, features, pixelmaps, boxes, scales), total=len(wsis), + desc='Importing' if assetstore else 'Uploading' + ): + if assetstore: + wsi_ids[w] = _import_file(client, assetstore, data_folder, w, replace) + feature_ids[f] = _import_file(client, assetstore, feature_folder, f, replace) + pixelmap_ids[p] = _import_file(client, assetstore, pixelmap_folder, p, replace) + else: + wsi_ids[w] = _upload_file(client, data_folder, w, replace) + feature_ids[f] = _upload_file(client, feature_folder, f, replace) + pixelmap_ids[f] = _upload_file(client, pixelmap_folder, p, replace) + + # check for existing pixelmap annotation in wsi + # generate and post annotation if necessary + existing = client.get(f'/annotation/item/{wsi_ids[w]}') + document = pixelmap_annotation( + pixelmap_ids[p], s, [x for box in b for x in box] + ) + if len(existing) and replace: + for annotation in existing: + client.delete(f'/annotation/{annotation["_id"]}') + client.post(f'/annotation?itemId={wsi_ids[w]}', json=document) + else: + skip = [ + element['type'] == 'pixelmap' + for annotation in existing + for element in annotation['annotation']['elements'] + ] + if not any(skip): + client.post(f'/annotation?itemId={wsi_ids[w]}', json=document) + return collection + + +def main(): + parser = argparse.ArgumentParser( + description=( + 'Import or upload a guided labeling dataset to a digital slide archive instance.' + ) + ) + parser.add_argument( + 'input', + type=str, + help=( + 'Comma separated file listing wsi, feature (h5), pixelmap (tiff), and bounding box ' + '(csv - local) input files.' + ), + ) + parser.add_argument( + 'key', + type=str, + help=( + 'URL for the (ex. http://dsa.university.edu/api/v1).' + ), + ) + parser.add_argument( + 'collection', + type=str, + help=( + 'Name of the created collection / project.' + ), + ) + parser.add_argument( + '-u', + '--url', + type=str, + default='http://localhost:8080/api/v1', + help=( + 'Optional URL for the DSA API. Efaults to http://localhost:8080/api/v1.' + ), + ) + parser.add_argument( + '-a', + '--assetstore', + type=str, + help=( + 'Optional identifier of the assetstore for file import. Defaults to upload (None).' + ), + ) + parser.add_argument( + '-r', + '--replace', + dest='replace', + action='store_true', + help=( + 'Optional replace existing wsis, features, or pixelmaps. Defaults to no replacement.' + ), + ) + args = parser.parse_args() + + # create and authenticate client + client = GirderClient(apiUrl=args.url) + client.authenticate(apiKey=args.key) + + # parse input to build lists of files + inputs = [] + with open(args.input, 'r') as f: + for line in f: + wsi, feature, pixelmap, box, scales = line.strip().split(',') + inputs.append((wsi, feature, pixelmap, box, scales)) + wsis = [row[0] for row in inputs] + features = [row[1] for row in inputs] + pixelmaps = [row[2] for row in inputs] + boxes = [row[3] for row in inputs] + scales = [row[4] for row in inputs] + + # build list of bounding boxes + bounding = [] + for b in boxes: + with open(b, 'r') as f: + box = [ + [int(x) for x in line.strip().split(',')] + for line in f + ] + bounding.append(box) + + # import if assetstore defined, otherwise upload + guided_label_import( + client, args.collection, + wsis, features, pixelmaps, bounding, scales, + args.assetstore, args.replace + ) + + +if __name__ == "__main__": + main() From 39754f2b646ce5a4fb1e6110de41424e12ca884d Mon Sep 17 00:00:00 2001 From: Lee Cooper Date: Tue, 13 May 2025 00:33:01 -0500 Subject: [PATCH 2/9] Update README.rst --- README.rst | 50 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index a98d7e5..131cf8b 100644 --- a/README.rst +++ b/README.rst @@ -2,13 +2,15 @@ WSI Superpixel Guided Labeling ============================== -WSI Superpixel Guided Labeling is a `Girder 3 `_ plugin designed to be used in conjunction with `HistomicsUI `_ and `HistomicsTK `_ to facilitate active learning on whole slide images. +WSI Superpixel Guided Labeling is a `Girder 3 `_ plugin for interactive development of image classifiers. It is designed to be used in conjunction with `HistomicsUI `_ and `HistomicsTK `_ and enables rapid development of classifiers with whole slide images using active learning. -This plugin leverages the output of certain HistomicsTK/SlicerCLI jobs to allow end users to label superpixel regions of whole slide images to be used as input for machine learning algorithms. +This plugin can be used to classify objects ranging from cell nuclei to high-power fields, and can operate on user provided data or data from a built-in pipeline that parcellates a whole-slide image into superpixels (see ``dsarchive/superpixel:latest``). -An example algorithm is contained within the ``dsarchive/superpixel:latest`` docker image. This can be used to generate superpixels, features, and machine learning models for active learning on a directory of images. See the installation instructions below for how to include the image as part of your Digital Slide Archive deployment. +The `Installation`_ instructions below describe how to install the plugin for your existing `Digital Slide Archive deployment `_. -Once the appropriate data is generated, a new view becomes available for labeling and retraining. +See the `Data Import`_ section for details on the data import script and data formatting. + +.. contents:: Table of Contents: Installation ------------ @@ -133,6 +135,46 @@ You can review trained or predicted superpixels via the review mode. This allows .. image:: docs/screenshots/reviewmode.png :alt: The review mode +Data Import +----------- +Users can provide their own data for use with the platform, providing flexibility in the type of objects, and methods of detection/segmentation and encoding. A command-line import script is provided for the upload or import of this data. The required file formats and script details are described below. + +Data formats +~~~~~~~~~~~~~~~~~~~~~~~~ +Each slide in the dataset requires four files: + +whole-slide image (various formats) + Any format that is supported by `large image `_ can be used. +feature (.h5) + This file contains a single array where each row is a feature embedding for the object. A single blank row should be prepended if the image contains non-object background pixels. +pixelmap (.tiff) + This image is used as a `pixelmap overlay `_ to define object locations for visualization and interactivity. Pixel values reflect the position of the object embedding in the feature file. For an object embedding in row 'i' of the feature array (zero-index), the corresponding pixels for that object should have value 2i, and the border pixels 2i+1. Non-object background pixels should be encoded using zero values. +bounding boxes (.csv) + Each row of this .csv defines the left, top, right, and bottom pixel for a single object. Objects should be listed in the same order as they appear in the feature.h5 file. + +Command-line Import Tool +~~~~~~~~~~~~~~~~~~~~~~~~ +data_import.py is provided to import or upload user-generated data into the plugin. + +Import requires a csv file defining the paths to input files, an API key for your DSA instance, and a project name: :: + + > data_import inputs.csv UI65ixMezye0LpBOyYozArB9czPu3PLNpq0RGlGn new_project + +Here, input.csv lists the whole-slide image, feature h5 file, pixelmap .tiff image, bounding box csv, and pixelmap downscale factor on each row: :: + + > more inputs.csv + /remote/a.svs,/remote/a.svs.feature.h5,/remote/a.svs.pixelmap.tiff,/local/a.svs.boxes.csv,4 + /remote/b.svs,/remote/b.svs.feature.h5,/remote/b.svs.pixelmap.tiff,/local/b.svs.boxes.csv,4 + +Feature h5 filenames should follow the pattern [slide_filename].*.feature.h5, but other filenames are unrestricted. + +If importing data from DSA mounted storage, provide an identifier for the assetstore where the files are mounted using the -a option. This +identifier can be determined from the DSA Admin console. + +-a, --assetstore Identifier for storage assetstore if importing files +-u, --url URL for server. Defaults to http://localhost:8080/api/v1 +-r, --replace Replace existing wsis, features, or pixelmaps + Features -------- From 71fa0b29cc424fc646f1cd94f58c3985d9899b2d Mon Sep 17 00:00:00 2001 From: Lee Cooper Date: Tue, 13 May 2025 00:37:20 -0500 Subject: [PATCH 3/9] key help --- data_import.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data_import.py b/data_import.py index a6f0331..0259f2f 100644 --- a/data_import.py +++ b/data_import.py @@ -191,7 +191,7 @@ def main(): 'key', type=str, help=( - 'URL for the (ex. http://dsa.university.edu/api/v1).' + 'API key for the server (see /api/v1#/api_key/api_key_createKey_post_api_key).' ), ) parser.add_argument( From a7a16d3582aae0aae094a133e29792b2fe5ac042 Mon Sep 17 00:00:00 2001 From: Anders Sildnes Date: Tue, 13 May 2025 11:19:11 -0500 Subject: [PATCH 4/9] Add pixelmap example --- README.rst | 6 +++++- docs/screenshots/pixelmap.png | Bin 0 -> 9128 bytes 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 docs/screenshots/pixelmap.png diff --git a/README.rst b/README.rst index 131cf8b..824ff34 100644 --- a/README.rst +++ b/README.rst @@ -149,8 +149,12 @@ feature (.h5) This file contains a single array where each row is a feature embedding for the object. A single blank row should be prepended if the image contains non-object background pixels. pixelmap (.tiff) This image is used as a `pixelmap overlay `_ to define object locations for visualization and interactivity. Pixel values reflect the position of the object embedding in the feature file. For an object embedding in row 'i' of the feature array (zero-index), the corresponding pixels for that object should have value 2i, and the border pixels 2i+1. Non-object background pixels should be encoded using zero values. + The below example shows an example pixelmap. + + .. image:: docs/screenshots/pixelmap.png + :alt: Pixelmap example bounding boxes (.csv) - Each row of this .csv defines the left, top, right, and bottom pixel for a single object. Objects should be listed in the same order as they appear in the feature.h5 file. + Each row of this .csv defines the left, top, right, and bottom pixel for a single object. Objects should be listed in the same order as they appear in the feature.h5 file. Command-line Import Tool ~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/screenshots/pixelmap.png b/docs/screenshots/pixelmap.png new file mode 100644 index 0000000000000000000000000000000000000000..4b3f1a8e6319eef3ecb0d1aa389be4889a74e352 GIT binary patch literal 9128 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 Date: Tue, 13 May 2025 11:24:13 -0500 Subject: [PATCH 5/9] Add bounding box example --- README.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 824ff34..8a70a39 100644 --- a/README.rst +++ b/README.rst @@ -149,12 +149,18 @@ feature (.h5) This file contains a single array where each row is a feature embedding for the object. A single blank row should be prepended if the image contains non-object background pixels. pixelmap (.tiff) This image is used as a `pixelmap overlay `_ to define object locations for visualization and interactivity. Pixel values reflect the position of the object embedding in the feature file. For an object embedding in row 'i' of the feature array (zero-index), the corresponding pixels for that object should have value 2i, and the border pixels 2i+1. Non-object background pixels should be encoded using zero values. - The below example shows an example pixelmap. + An example pixelmap is below: .. image:: docs/screenshots/pixelmap.png :alt: Pixelmap example bounding boxes (.csv) Each row of this .csv defines the left, top, right, and bottom pixel for a single object. Objects should be listed in the same order as they appear in the feature.h5 file. + For the pixelmap example above, assuming (0,0) is the top left, the csv file would have the following line: + +.. code-block:: csv + + 1,1,4,4 + Command-line Import Tool ~~~~~~~~~~~~~~~~~~~~~~~~ From 40dc9317087ec4b20a7bafb9f837b35f7c76d242 Mon Sep 17 00:00:00 2001 From: Abhishek Sharma <42185229+abs711@users.noreply.github.com> Date: Thu, 15 May 2025 15:32:29 -0500 Subject: [PATCH 6/9] added attributes to pixelmap_annotation --- data_import.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/data_import.py b/data_import.py index 0259f2f..9e8651e 100644 --- a/data_import.py +++ b/data_import.py @@ -89,9 +89,16 @@ def pixelmap_annotation(pixelmap_id, scale, boxes): values=values, categories=categories, ) + attr = dict( + cli = None, + metadata = {}, + params = {"scale_x": scale, "scale_y": scale}, + version = None + ) annotation = dict( name='Superpixel Epoch 0', elements=[pixelmap], + attributes = attr ) return annotation From 3195a2b1ce88995d390892a8abcf31e6a74742f5 Mon Sep 17 00:00:00 2001 From: Abhishek Sharma <42185229+abs711@users.noreply.github.com> Date: Tue, 20 May 2025 14:19:02 -0500 Subject: [PATCH 7/9] Added bounding box coordinates to annotation --- data_import.py | 1 + 1 file changed, 1 insertion(+) diff --git a/data_import.py b/data_import.py index 9e8651e..e4e9608 100644 --- a/data_import.py +++ b/data_import.py @@ -88,6 +88,7 @@ def pixelmap_annotation(pixelmap_id, scale, boxes): transform=transform, values=values, categories=categories, + user= {'bbox': boxes}, ) attr = dict( cli = None, From 1bcdef2fc03361438ce89e84e62e8d277d844e94 Mon Sep 17 00:00:00 2001 From: Abhishek Sharma <42185229+abs711@users.noreply.github.com> Date: Tue, 20 May 2025 14:20:19 -0500 Subject: [PATCH 8/9] Ensure integer scale value --- data_import.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data_import.py b/data_import.py index e4e9608..162f6db 100644 --- a/data_import.py +++ b/data_import.py @@ -251,7 +251,7 @@ def main(): features = [row[1] for row in inputs] pixelmaps = [row[2] for row in inputs] boxes = [row[3] for row in inputs] - scales = [row[4] for row in inputs] + scales = [int(row[4]) for row in inputs] # build list of bounding boxes bounding = [] From cf923a4eb6a7e82c7f72bc879ddae5422e0a49cb Mon Sep 17 00:00:00 2001 From: Abhishek Sharma <42185229+abs711@users.noreply.github.com> Date: Tue, 20 May 2025 14:24:59 -0500 Subject: [PATCH 9/9] Ensure float scales --- data_import.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data_import.py b/data_import.py index 162f6db..86b4206 100644 --- a/data_import.py +++ b/data_import.py @@ -251,7 +251,7 @@ def main(): features = [row[1] for row in inputs] pixelmaps = [row[2] for row in inputs] boxes = [row[3] for row in inputs] - scales = [int(row[4]) for row in inputs] + scales = [float(row[4]) for row in inputs] # build list of bounding boxes bounding = []