From 979e05a9303738e8f377459460e9f1c9ec04dfd7 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Thu, 10 Nov 2022 13:57:01 -0500 Subject: [PATCH 01/27] Add a draft of inference Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- pathology/hovernet/infer_wsi.py | 233 ++++++++++++++++++++++++++++ pathology/hovernet/tcga_download.py | 88 +++++++++++ 2 files changed, 321 insertions(+) create mode 100644 pathology/hovernet/infer_wsi.py create mode 100644 pathology/hovernet/tcga_download.py diff --git a/pathology/hovernet/infer_wsi.py b/pathology/hovernet/infer_wsi.py new file mode 100644 index 0000000000..a99d6a2beb --- /dev/null +++ b/pathology/hovernet/infer_wsi.py @@ -0,0 +1,233 @@ +import sys + +from functools import partial +import logging +import os +import time +from argparse import ArgumentParser +import torch +import numpy as np +import pandas as pd +import torch.distributed as dist +from monai.data import DataLoader, MaskedPatchWSIDataset +from monai.networks.nets import HoVerNet +from monai.engines import SupervisedEvaluator +from monai.apps.pathology.transforms import ( + GenerateWatershedMaskd, + GenerateInstanceBorderd, + GenerateDistanceMapd, + GenerateWatershedMarkersd, + Watershedd, + GenerateInstanceContour, + GenerateInstanceCentroid, + GenerateInstanceType, +) +from monai.transforms import ( + Activations, + AsDiscrete, + AsDiscreted, + Compose, + ScaleIntensityRanged, + CastToTyped, + Lambdad, + SplitDimd, + EnsureChannelFirstd, + ComputeHoVerMapsd, + CenterSpatialCropd, + FillHoles, + BoundingRect, + ThresholdIntensity, + GaussianSmooth, +) +from monai.handlers import ( + MeanDice, + StatsHandler, + TensorBoardStatsHandler, + from_engine, +) +from monai.utils import convert_to_tensor, first, HoVerNetBranch + + +def create_output_dir(cfg): + timestamp = time.strftime("%y%m%d-%H%M%S") + run_folder_name = f"{timestamp}_inference_hovernet_ps{cfg['patch_size']}" + log_dir = os.path.join(cfg["logdir"], run_folder_name) + print(f"Logs and outputs are saved at '{log_dir}'.") + if not os.path.exists(log_dir): + os.makedirs(log_dir) + return log_dir + + +def post_process(output, return_binary=True, return_centroids=False, output_classes=None): + pred = output["pred"] + device = pred[HoVerNetBranch.NP.value].device + if HoVerNetBranch.NC.value in pred.keys(): + type_pred = Activations(softmax=True)(pred[HoVerNetBranch.NC.value]) + type_pred = AsDiscrete(argmax=True)(type_pred) + + post_trans_seg = Compose( + [ + GenerateWatershedMaskd(keys=HoVerNetBranch.NP.value, softmax=True), + GenerateInstanceBorderd(keys="mask", hover_map_key=HoVerNetBranch.HV, kernel_size=3), + GenerateDistanceMapd(keys="mask", border_key="border", smooth_fn=GaussianSmooth()), + GenerateWatershedMarkersd( + keys="mask", border_key="border", threshold=0.7, radius=2, postprocess_fn=FillHoles() + ), + Watershedd(keys="dist", mask_key="mask", markers_key="markers"), + ] + ) + pred_inst_dict = post_trans_seg(pred) + pred_inst = pred_inst_dict["dist"] + + inst_id_list = np.unique(pred_inst)[1:] # exclude background + + inst_info_dict = None + if return_centroids: + inst_info_dict = {} + for inst_id in inst_id_list: + inst_map = pred_inst == inst_id + inst_bbox = BoundingRect()(inst_map) + inst_map = inst_map[:, inst_bbox[0][0] : inst_bbox[0][1], inst_bbox[0][2] : inst_bbox[0][3]] + offset = [inst_bbox[0][2], inst_bbox[0][0]] + inst_contour = GenerateInstanceContour()(inst_map, offset) + inst_centroid = GenerateInstanceCentroid()(inst_map, offset) + if inst_contour is not None: + inst_info_dict[inst_id] = { # inst_id should start at 1 + "bounding_box": inst_bbox, + "centroid": inst_centroid, + "contour": inst_contour, + "type_probability": None, + "type": None, + } + + if output_classes is not None: + for inst_id in list(inst_info_dict.keys()): + inst_type, type_prob = GenerateInstanceType()( + bbox=inst_info_dict[inst_id]["bounding_box"], + type_pred=type_pred, + seg_pred=pred_inst, + instance_id=inst_id, + ) + inst_info_dict[inst_id]["type"] = inst_type + inst_info_dict[inst_id]["type_probability"] = type_prob + + pred_inst = convert_to_tensor(pred_inst, device=device) + if return_binary: + pred_inst[pred_inst > 0] = 1 + output["pred"][HoVerNetBranch.NP.value] = pred_inst + output["pred"]["inst_info_dict"] = inst_info_dict + output["pred"]["pred_inst_dict"] = pred_inst_dict + return output + + +def run(cfg): + # -------------------------------------------------------------------------- + # Set Directory and Device + # -------------------------------------------------------------------------- + log_dir = create_output_dir(cfg) + multi_gpu = True if cfg["use_gpu"] and torch.cuda.device_count() > 1 else False + if multi_gpu: + dist.init_process_group(backend="nccl", init_method="env://") + device = torch.device("cuda:{}".format(dist.get_rank())) + torch.cuda.set_device(device) + else: + device = torch.device("cuda" if cfg["use_gpu"] else "cpu") + # -------------------------------------------------------------------------- + # Data Loading and Preprocessing + # -------------------------------------------------------------------------- + # Preprocessing transforms + pre_transforms = Compose( + [ + CastToTyped(keys=["image"], dtype=torch.float32), + ScaleIntensityRanged(keys=["image"], a_min=0.0, a_max=255.0, b_min=0.0, b_max=1.0, clip=True), + ] + ) + # List of whole slide images + data_list = [ + {"image": "TCGA-A1-A0SP-01Z-00-DX1.20D689C6-EFA5-4694-BE76-24475A89ACC0.svs"}, + {"image": "TCGA-A2-A0D0-01Z-00-DX1.4FF6B8E5-703B-400F-920A-104F56E0F874.svs"}, + ] + # Dataset of patches + dataset = MaskedPatchWSIDataset( + data_list, + patch_size=cfg["patch_size"], + patch_level=0, + mask_level=3, + transform=pre_transforms, + reader=cfg["reader"], + ) + # Dataloader + data_loader = DataLoader(dataset, num_workers=cfg["num_workers"], batch_size=cfg["batch_size"], pin_memory=True) + # -------------------------------------------------------------------------- + # Run some sanity checks + # -------------------------------------------------------------------------- + # Check first sample + first_sample = first(data_loader) + if first_sample is None: + raise ValueError("First sample is None!") + print("image: ") + print(" shape", first_sample["image"].shape) + print(" type: ", type(first_sample["image"])) + print(" dtype: ", first_sample["image"].dtype) + print(f"batch size: {cfg['batch_size']}") + print(f"number of batches: {len(data_loader)}") + + # -------------------------------------------------------------------------- + # Model and Handlers + # -------------------------------------------------------------------------- + # Create model and load weights + model = HoVerNet( + mode="fast", in_channels=3, out_classes=7, act=("relu", {"inplace": True}), norm="batch", dropout_prob=0.2 + ).to(device) + # model.load_state_dict(torch.load(cfg["ckpt"])) + model.eval() + # Handlers + inference_handlers = [ + StatsHandler(output_transform=lambda x: None), + TensorBoardStatsHandler(log_dir=log_dir, output_transform=lambda x: None), + ] + if multi_gpu: + model = torch.nn.parallel.DistributedDataParallel( + model, device_ids=[dist.get_rank()], output_device=dist.get_rank() + ) + inference_handlers = inference_handlers if dist.get_rank() == 0 else None + # -------------------------------------------- + # Inference + # -------------------------------------------- + inference = SupervisedEvaluator( + device=device, + val_data_loader=data_loader, + network=model, + postprocessing=partial(post_process, return_binary=True, return_centroids=False, output_classes=None), + val_handlers=inference_handlers, + amp=cfg["amp"], + ) + inference.run() + + if multi_gpu: + dist.destroy_process_group() + + +def main(): + logging.basicConfig(level=logging.INFO) + + parser = ArgumentParser(description="Tumor detection on whole slide pathology images.") + parser.add_argument("--root", type=str, default="./", help="root WSI dir") + parser.add_argument("--logdir", type=str, default="./logs/", dest="logdir", help="log directory") + parser.add_argument("--ckpt", type=str, default="./", dest="ckpt", help="Path to the pytorch checkpoint") + parser.add_argument("--ps", type=int, default=256, dest="patch_size", help="patch size") + parser.add_argument("--bs", type=int, default=8, dest="batch_size", help="batch size") + parser.add_argument("--no-amp", action="store_false", dest="amp", help="deactivate amp") + parser.add_argument("--save_interval", type=int, default=10) + parser.add_argument("--cpu", type=int, default=1, dest="num_workers", help="number of workers") + parser.add_argument("--use_gpu", type=bool, default=False, help="whether to use gpu") + parser.add_argument("--reader", type=str, default="OpenSlide", help="WSI reader backend") + args = parser.parse_args() + + config_dict = vars(args) + print(config_dict) + run(config_dict) + + +if __name__ == "__main__": + main() diff --git a/pathology/hovernet/tcga_download.py b/pathology/hovernet/tcga_download.py new file mode 100644 index 0000000000..fc66ba1614 --- /dev/null +++ b/pathology/hovernet/tcga_download.py @@ -0,0 +1,88 @@ +import requests +import json +import re + +BASE_URL = "https://api.gdc.cancer.gov" + + +def extract_info(filename): + """Extract wsi and patch info from filename + + Args: + filename: name of the rgb or mask file in NuCLS dataset + """ + wsi_name = filename.split("_id")[0] + case_name, dx = wsi_name.rsplit("-", 1) + matches = re.search("left-([0-9]+).*top-([0-9]+).*bottom-([0-9]+).*right-([0-9]+)", filename) + left, top, bottom, right = [int(m) for m in matches.groups()] + location = (top, left) + size = max(right - left, bottom - top) + print(location, size) + file_name_wild = f"{case_name}-*{dx}*" + return file_name_wild, location, size + + +def get_file_id(file_name_wild): + """Retrieve file_id from partial filenames with wildcard + + Args: + file_name_wild: partial filename of a file on TCGA with wildcard + """ + file_filters = { + "op": "=", + "content": { + "field": "files.file_name", + "value": [file_name_wild], + }, + } + file_endpoint = f"{BASE_URL}/files" + params = {"filters": json.dumps(file_filters)} + response = requests.get(file_endpoint, params=params) + print(json.dumps(response.json(), indent=2)) + return response.json()["data"]["hits"][0]["file_id"] + + +def download_file(file_id): + """Download a file based on its file_id + + Args: + file_id: UUID of a file on TCGA + """ + query = f"{BASE_URL}/data/{file_id}" + print(f"Fetching {file_id} ...") + response = requests.get(query, headers={"Content-Type": "application/json"}) + response_head_cd = response.headers["Content-Disposition"] + file_name = re.findall("filename=(.+)", response_head_cd)[0] + + with open(file_name, "wb") as output_file: + output_file.write(response.content) + print(f"{file_id} is saved in {file_name}.") + + +def create_file_info_list(filenames): + """Create information records for each of NuCLS files + + Args: + filenames: list of NuCLS filenames for images or masks + """ + info_list = [] + for filename in filenames: + file_name_wild, location, size = extract_info(filename) + uid = get_file_id(file_name_wild) + info_list.append((uid, filename, location, size)) + return info_list + + +if __name__ == "__main__": + filenames = [ + "TCGA-A1-A0SP-DX1_id-5ea4095addda5f8398977ebc_left-7053_top-53967_bottom-54231_right-7311", + "TCGA-A2-A0D0-DX1_id-5ea40b17ddda5f839899849a_left-69243_top-41106_bottom-41400_right-69527", + ] + info_list = create_file_info_list(filenames) + print(f"{info_list=}") + + file_ids = list(set([r[0] for r in info_list])) + print(f"{file_ids=}") + + for uid in file_ids: + download_file(uid) From d3b77ded9e7b218be686d023568956d8bdb749ea Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Mon, 14 Nov 2022 09:52:57 -0500 Subject: [PATCH 02/27] Uncomment load weights Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- pathology/hovernet/infer_wsi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pathology/hovernet/infer_wsi.py b/pathology/hovernet/infer_wsi.py index a99d6a2beb..bf29db6e28 100644 --- a/pathology/hovernet/infer_wsi.py +++ b/pathology/hovernet/infer_wsi.py @@ -179,7 +179,7 @@ def run(cfg): model = HoVerNet( mode="fast", in_channels=3, out_classes=7, act=("relu", {"inplace": True}), norm="batch", dropout_prob=0.2 ).to(device) - # model.load_state_dict(torch.load(cfg["ckpt"])) + model.load_state_dict(torch.load(cfg["ckpt"])) model.eval() # Handlers inference_handlers = [ From fc271efbf18851eee894b877cd28a3d9f88cfdc6 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Tue, 15 Nov 2022 13:46:11 -0500 Subject: [PATCH 03/27] Add infer_roi Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- pathology/hovernet/infer_roi.py | 259 ++++++++++++++++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 pathology/hovernet/infer_roi.py diff --git a/pathology/hovernet/infer_roi.py b/pathology/hovernet/infer_roi.py new file mode 100644 index 0000000000..1cb6165fd7 --- /dev/null +++ b/pathology/hovernet/infer_roi.py @@ -0,0 +1,259 @@ +import sys + +from functools import partial +from glob import glob +import logging +import os +import time +from argparse import ArgumentParser +import torch +import numpy as np +import pandas as pd +import torch.distributed as dist +from monai.data import DataLoader, Dataset +from monai.networks.nets import HoVerNet +from monai.engines import SupervisedEvaluator +from monai.inferers import SlidingWindowInferer +from monai.apps.pathology.transforms import ( + GenerateWatershedMaskd, + GenerateInstanceBorderd, + GenerateDistanceMapd, + GenerateWatershedMarkersd, + Watershedd, + GenerateInstanceContour, + GenerateInstanceCentroid, + GenerateInstanceType, +) +from monai.transforms import ( + Activations, + AsDiscrete, + AsDiscreted, + Compose, + EnsureChannelFirstd, + CastToTyped, + LoadImaged, + FillHoles, + BoundingRect, + ThresholdIntensity, + GaussianSmooth, + ScaleIntensityRanged, +) +from monai.handlers import ( + MeanDice, + StatsHandler, + TensorBoardStatsHandler, + from_engine, +) +from monai.utils import convert_to_tensor, first, HoVerNetBranch + + +def create_output_dir(cfg): + if cfg["mode"].lower() == "original": + cfg["patch_size"] = 270 + cfg["out_size"] = 80 + elif cfg["mode"].lower() == "fast": + cfg["patch_size"] = 256 + cfg["out_size"] = 164 + + timestamp = time.strftime("%y%m%d-%H%M%S") + run_folder_name = f"{timestamp}_inference_hovernet_ps{cfg['patch_size']}" + log_dir = os.path.join(cfg["logdir"], run_folder_name) + print(f"Logs and outputs are saved at '{log_dir}'.") + if not os.path.exists(log_dir): + os.makedirs(log_dir) + return log_dir + + +def post_process(output, return_binary=True, return_centroids=False, output_classes=None): + device = output[HoVerNetBranch.NP.value].device + if HoVerNetBranch.NC.value in output.keys(): + type_pred = Activations(softmax=True)(output[HoVerNetBranch.NC.value]) + type_pred = AsDiscrete(argmax=True)(type_pred) + + post_trans_seg = Compose( + [ + GenerateWatershedMaskd(keys=HoVerNetBranch.NP.value, softmax=True), + GenerateInstanceBorderd(keys="mask", hover_map_key=HoVerNetBranch.HV, kernel_size=3), + GenerateDistanceMapd(keys="mask", border_key="border", smooth_fn=GaussianSmooth()), + GenerateWatershedMarkersd( + keys="mask", border_key="border", threshold=0.7, radius=2, postprocess_fn=FillHoles() + ), + Watershedd(keys="dist", mask_key="mask", markers_key="markers"), + ] + ) + pred_inst_dict = post_trans_seg(output) + pred_inst = pred_inst_dict["dist"] + + inst_id_list = np.unique(pred_inst)[1:] # exclude background + + inst_info_dict = None + if return_centroids: + inst_info_dict = {} + for inst_id in inst_id_list: + inst_map = pred_inst == inst_id + inst_bbox = BoundingRect()(inst_map) + inst_map = inst_map[:, inst_bbox[0][0] : inst_bbox[0][1], inst_bbox[0][2] : inst_bbox[0][3]] + offset = [inst_bbox[0][2], inst_bbox[0][0]] + inst_contour = GenerateInstanceContour()(inst_map, offset) + inst_centroid = GenerateInstanceCentroid()(inst_map, offset) + if inst_contour is not None: + inst_info_dict[inst_id] = { # inst_id should start at 1 + "bounding_box": inst_bbox, + "centroid": inst_centroid, + "contour": inst_contour, + "type_probability": None, + "type": None, + } + + if output_classes is not None: + for inst_id in list(inst_info_dict.keys()): + inst_type, type_prob = GenerateInstanceType()( + bbox=inst_info_dict[inst_id]["bounding_box"], + type_pred=type_pred, + seg_pred=pred_inst, + instance_id=inst_id, + ) + inst_info_dict[inst_id]["type"] = inst_type + inst_info_dict[inst_id]["type_probability"] = type_prob + + pred_inst = convert_to_tensor(pred_inst, device=device) + if return_binary: + pred_inst[pred_inst > 0] = 1 + output[HoVerNetBranch.NP.value] = pred_inst + output["inst_info_dict"] = inst_info_dict + output["pred_inst_dict"] = pred_inst_dict + return output + + +def run(cfg): + # -------------------------------------------------------------------------- + # Set Directory and Device + # -------------------------------------------------------------------------- + log_dir = create_output_dir(cfg) + multi_gpu = True if cfg["use_gpu"] and torch.cuda.device_count() > 1 else False + if multi_gpu: + dist.init_process_group(backend="nccl", init_method="env://") + device = torch.device("cuda:{}".format(dist.get_rank())) + torch.cuda.set_device(device) + else: + device = torch.device("cuda" if cfg["use_gpu"] else "cpu") + # -------------------------------------------------------------------------- + # Data Loading and Preprocessing + # -------------------------------------------------------------------------- + # Preprocessing transforms + pre_transforms = Compose( + [ + LoadImaged(keys=["image"]), + EnsureChannelFirstd(keys=["image"]), + CastToTyped(keys=["image"], dtype=torch.float32), + ScaleIntensityRanged(keys=["image"], a_min=0.0, a_max=255.0, b_min=0.0, b_max=1.0, clip=True), + ] + ) + # List of whole slide images + data_list = [{"image": image} for image in glob(os.path.join(cfg["root"], "*.png"))] + + dataset = Dataset(data_list, transform=pre_transforms) + + # Dataloader + data_loader = DataLoader(dataset, num_workers=cfg["num_workers"], batch_size=cfg["batch_size"], pin_memory=True) + + # -------------------------------------------------------------------------- + # Run some sanity checks + # -------------------------------------------------------------------------- + # Check first sample + first_sample = first(data_loader) + if first_sample is None: + raise ValueError("First sample is None!") + print("image: ") + print(" shape", first_sample["image"].shape) + print(" type: ", type(first_sample["image"])) + print(" dtype: ", first_sample["image"].dtype) + print(f"batch size: {cfg['batch_size']}") + print(f"number of batches: {len(data_loader)}") + + # -------------------------------------------------------------------------- + # Model and Handlers + # -------------------------------------------------------------------------- + # Create model and load weights + model = HoVerNet( + mode="original", + in_channels=3, + out_classes=5, + act=("relu", {"inplace": True}), + norm="batch", + ).to(device) + model.load_state_dict(torch.load(cfg["ckpt"], map_location=device)) + model.eval() + + # Handlers + inference_handlers = [ + StatsHandler(output_transform=lambda x: None), + TensorBoardStatsHandler(log_dir=log_dir, output_transform=lambda x: None), + ] + if multi_gpu: + model = torch.nn.parallel.DistributedDataParallel( + model, device_ids=[dist.get_rank()], output_device=dist.get_rank() + ) + inference_handlers = inference_handlers if dist.get_rank() == 0 else None + + # -------------------------------------------- + # Inference + # -------------------------------------------- + inference = SlidingWindowInferer( + roi_size=cfg["patch_size"], + sw_batch_size=8, + overlap=1.0 - float(cfg["out_size"]) / float(cfg["patch_size"]), + # overlap=0, + padding_mode="constant", + cval=0, + sw_device=device, + device=device, + progress=True, + extra_input_padding=(cfg["patch_size"] - cfg["out_size"],) * 4, + pad_output=True, + ) + + for data in data_loader: + image = data["image"] + output = inference(image, model) + result = post_process(output) + + if multi_gpu: + dist.destroy_process_group() + + +def main(): + logging.basicConfig(level=logging.INFO) + + parser = ArgumentParser(description="Tumor detection on whole slide pathology images.") + parser.add_argument( + "--root", + type=str, + default="/Users/bhashemian/workspace/project-monai/tutorials/pathology/hovernet/CoNSeP/Test/Images", + help="image root dir", + ) + parser.add_argument("--logdir", type=str, default="./logs/", dest="logdir", help="log directory") + parser.add_argument( + "--ckpt", + type=str, + default="/Users/bhashemian/workspace/project-monai/tutorials/pathology/hovernet/model_CoNSeP_new.pth", + dest="ckpt", + help="Path to the pytorch checkpoint", + ) + + parser.add_argument("--mode", type=str, default="original", help="HoVerNet mode (original/fast)") + parser.add_argument("--bs", type=int, default=1, dest="batch_size", help="batch size") + parser.add_argument("--no-amp", action="store_false", dest="amp", help="deactivate amp") + parser.add_argument("--save_interval", type=int, default=10) + parser.add_argument("--cpu", type=int, default=0, dest="num_workers", help="number of workers") + parser.add_argument("--use_gpu", type=bool, default=False, help="whether to use gpu") + parser.add_argument("--reader", type=str, default="OpenSlide", help="WSI reader backend") + args = parser.parse_args() + + config_dict = vars(args) + print(config_dict) + run(config_dict) + + +if __name__ == "__main__": + main() From 658244251c56dba2ed0e88d067cda0b6d411bc96 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Thu, 17 Nov 2022 16:46:58 -0500 Subject: [PATCH 04/27] Major updates Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- pathology/hovernet/infer_roi.py | 247 +++++++++++++++----------------- 1 file changed, 119 insertions(+), 128 deletions(-) diff --git a/pathology/hovernet/infer_roi.py b/pathology/hovernet/infer_roi.py index 1cb6165fd7..74a9b67c20 100644 --- a/pathology/hovernet/infer_roi.py +++ b/pathology/hovernet/infer_roi.py @@ -1,50 +1,26 @@ -import sys - -from functools import partial -from glob import glob import logging import os import time from argparse import ArgumentParser +from glob import glob + import torch -import numpy as np -import pandas as pd import torch.distributed as dist -from monai.data import DataLoader, Dataset -from monai.networks.nets import HoVerNet -from monai.engines import SupervisedEvaluator -from monai.inferers import SlidingWindowInferer +from imageio import imsave + +from monai.apps.pathology.inferers import SlidingWindowHoVerNetInferer from monai.apps.pathology.transforms import ( - GenerateWatershedMaskd, - GenerateInstanceBorderd, - GenerateDistanceMapd, - GenerateWatershedMarkersd, - Watershedd, - GenerateInstanceContour, - GenerateInstanceCentroid, - GenerateInstanceType, -) -from monai.transforms import ( - Activations, - AsDiscrete, - AsDiscreted, - Compose, - EnsureChannelFirstd, - CastToTyped, - LoadImaged, - FillHoles, - BoundingRect, - ThresholdIntensity, - GaussianSmooth, - ScaleIntensityRanged, -) -from monai.handlers import ( - MeanDice, - StatsHandler, - TensorBoardStatsHandler, - from_engine, -) -from monai.utils import convert_to_tensor, first, HoVerNetBranch + GenerateDistanceMapd, GenerateInstanceBorderd, GenerateWatershedMarkersd, + GenerateWatershedMaskd, HoVerNetNuclearTypePostProcessingd, Watershedd) +from monai.data import DataLoader, Dataset, PILReader +from monai.engines import SupervisedEvaluator +from monai.networks.nets import HoVerNet +from monai.transforms import (Activationsd, AsDiscreted, CastToTyped, + CenterSpatialCropd, Compose, Decollated, + EnsureChannelFirstd, FillHoles, GaussianSmooth, + LoadImaged, ScaleIntensityRanged) +from monai.transforms.utils import apply_transform +from monai.utils import HoVerNetBranch, first def create_output_dir(cfg): @@ -64,72 +40,11 @@ def create_output_dir(cfg): return log_dir -def post_process(output, return_binary=True, return_centroids=False, output_classes=None): - device = output[HoVerNetBranch.NP.value].device - if HoVerNetBranch.NC.value in output.keys(): - type_pred = Activations(softmax=True)(output[HoVerNetBranch.NC.value]) - type_pred = AsDiscrete(argmax=True)(type_pred) - - post_trans_seg = Compose( - [ - GenerateWatershedMaskd(keys=HoVerNetBranch.NP.value, softmax=True), - GenerateInstanceBorderd(keys="mask", hover_map_key=HoVerNetBranch.HV, kernel_size=3), - GenerateDistanceMapd(keys="mask", border_key="border", smooth_fn=GaussianSmooth()), - GenerateWatershedMarkersd( - keys="mask", border_key="border", threshold=0.7, radius=2, postprocess_fn=FillHoles() - ), - Watershedd(keys="dist", mask_key="mask", markers_key="markers"), - ] - ) - pred_inst_dict = post_trans_seg(output) - pred_inst = pred_inst_dict["dist"] - - inst_id_list = np.unique(pred_inst)[1:] # exclude background - - inst_info_dict = None - if return_centroids: - inst_info_dict = {} - for inst_id in inst_id_list: - inst_map = pred_inst == inst_id - inst_bbox = BoundingRect()(inst_map) - inst_map = inst_map[:, inst_bbox[0][0] : inst_bbox[0][1], inst_bbox[0][2] : inst_bbox[0][3]] - offset = [inst_bbox[0][2], inst_bbox[0][0]] - inst_contour = GenerateInstanceContour()(inst_map, offset) - inst_centroid = GenerateInstanceCentroid()(inst_map, offset) - if inst_contour is not None: - inst_info_dict[inst_id] = { # inst_id should start at 1 - "bounding_box": inst_bbox, - "centroid": inst_centroid, - "contour": inst_contour, - "type_probability": None, - "type": None, - } - - if output_classes is not None: - for inst_id in list(inst_info_dict.keys()): - inst_type, type_prob = GenerateInstanceType()( - bbox=inst_info_dict[inst_id]["bounding_box"], - type_pred=type_pred, - seg_pred=pred_inst, - instance_id=inst_id, - ) - inst_info_dict[inst_id]["type"] = inst_type - inst_info_dict[inst_id]["type_probability"] = type_prob - - pred_inst = convert_to_tensor(pred_inst, device=device) - if return_binary: - pred_inst[pred_inst > 0] = 1 - output[HoVerNetBranch.NP.value] = pred_inst - output["inst_info_dict"] = inst_info_dict - output["pred_inst_dict"] = pred_inst_dict - return output - - def run(cfg): # -------------------------------------------------------------------------- # Set Directory and Device # -------------------------------------------------------------------------- - log_dir = create_output_dir(cfg) + output_dir = create_output_dir(cfg) multi_gpu = True if cfg["use_gpu"] and torch.cuda.device_count() > 1 else False if multi_gpu: dist.init_process_group(backend="nccl", init_method="env://") @@ -138,24 +53,73 @@ def run(cfg): else: device = torch.device("cuda" if cfg["use_gpu"] else "cpu") # -------------------------------------------------------------------------- - # Data Loading and Preprocessing + # Transforms # -------------------------------------------------------------------------- # Preprocessing transforms pre_transforms = Compose( [ - LoadImaged(keys=["image"]), + LoadImaged( + keys=["image"], reader=PILReader, converter=lambda x: x.convert("RGB") + ), EnsureChannelFirstd(keys=["image"]), + # CenterSpatialCropd(keys=["image"], roi_size=(80, 80)), # for testing only CastToTyped(keys=["image"], dtype=torch.float32), - ScaleIntensityRanged(keys=["image"], a_min=0.0, a_max=255.0, b_min=0.0, b_max=1.0, clip=True), + ScaleIntensityRanged( + keys=["image"], a_min=0.0, a_max=255.0, b_min=0.0, b_max=1.0, clip=True + ), + ] + ) + # Postprocessing transforms + post_transforms = Compose( + [ + Decollated( + keys=[ + HoVerNetBranch.NC.value, + HoVerNetBranch.NP.value, + HoVerNetBranch.HV.value, + ] + ), + Activationsd(keys=HoVerNetBranch.NC.value, softmax=True), + AsDiscreted(keys=HoVerNetBranch.NC.value, argmax=True), + GenerateWatershedMaskd(keys=HoVerNetBranch.NP.value, softmax=True), + GenerateInstanceBorderd( + keys="mask", hover_map_key=HoVerNetBranch.HV.value, kernel_size=3 + ), + GenerateDistanceMapd( + keys="mask", border_key="border", smooth_fn=GaussianSmooth() + ), + GenerateWatershedMarkersd( + keys="mask", + border_key="border", + threshold=0.7, + radius=2, + postprocess_fn=FillHoles(), + ), + Watershedd(keys="dist", mask_key="mask", markers_key="markers"), + HoVerNetNuclearTypePostProcessingd( + type_pred_key=HoVerNetBranch.NC.value, instance_pred_key="dist" + ), ] ) + # -------------------------------------------------------------------------- + # Data and Data Loading + # -------------------------------------------------------------------------- # List of whole slide images - data_list = [{"image": image} for image in glob(os.path.join(cfg["root"], "*.png"))] + data_list = [ + {"image": image, "filename": image} + for image in glob(os.path.join(cfg["root"], "*.png")) + ] + # Dataset dataset = Dataset(data_list, transform=pre_transforms) # Dataloader - data_loader = DataLoader(dataset, num_workers=cfg["num_workers"], batch_size=cfg["batch_size"], pin_memory=True) + data_loader = DataLoader( + dataset, + num_workers=cfg["num_workers"], + batch_size=cfg["batch_size"], + pin_memory=True, + ) # -------------------------------------------------------------------------- # Run some sanity checks @@ -172,7 +136,7 @@ def run(cfg): print(f"number of batches: {len(data_loader)}") # -------------------------------------------------------------------------- - # Model and Handlers + # Model # -------------------------------------------------------------------------- # Create model and load weights model = HoVerNet( @@ -185,21 +149,15 @@ def run(cfg): model.load_state_dict(torch.load(cfg["ckpt"], map_location=device)) model.eval() - # Handlers - inference_handlers = [ - StatsHandler(output_transform=lambda x: None), - TensorBoardStatsHandler(log_dir=log_dir, output_transform=lambda x: None), - ] if multi_gpu: model = torch.nn.parallel.DistributedDataParallel( model, device_ids=[dist.get_rank()], output_device=dist.get_rank() ) - inference_handlers = inference_handlers if dist.get_rank() == 0 else None # -------------------------------------------- # Inference # -------------------------------------------- - inference = SlidingWindowInferer( + sliding_inferer = SlidingWindowHoVerNetInferer( roi_size=cfg["patch_size"], sw_batch_size=8, overlap=1.0 - float(cfg["out_size"]) / float(cfg["patch_size"]), @@ -209,14 +167,31 @@ def run(cfg): sw_device=device, device=device, progress=True, - extra_input_padding=(cfg["patch_size"] - cfg["out_size"],) * 4, - pad_output=True, + extra_input_padding=((cfg["patch_size"] - cfg["out_size"]) // 2,) * 4, ) - for data in data_loader: + for i, data in enumerate(data_loader): + print(">>>>> ", data["filename"]) image = data["image"] - output = inference(image, model) - result = post_process(output) + output = sliding_inferer(image, model) + results = apply_transform(post_transforms, output) + for i, res in enumerate(results): + filename = os.path.join( + output_dir, + os.path.basename(data["filename"][i]).replace(".png", "_pred.png"), + ) + print(f"Saving {filename}...") + imsave(filename, res["pred_binary"].permute(1, 2, 0).numpy()) + + # evaluator = SupervisedEvaluator( + # device=device, + # val_data_loader=data_loader, + # network=model, + # postprocessing=post_transforms, + # inferer=sliding_inferer, + # amp=cfg["amp"], + # ) + # evaluator.run() if multi_gpu: dist.destroy_process_group() @@ -225,14 +200,18 @@ def run(cfg): def main(): logging.basicConfig(level=logging.INFO) - parser = ArgumentParser(description="Tumor detection on whole slide pathology images.") + parser = ArgumentParser( + description="Tumor detection on whole slide pathology images." + ) parser.add_argument( "--root", type=str, default="/Users/bhashemian/workspace/project-monai/tutorials/pathology/hovernet/CoNSeP/Test/Images", help="image root dir", ) - parser.add_argument("--logdir", type=str, default="./logs/", dest="logdir", help="log directory") + parser.add_argument( + "--logdir", type=str, default="./logs/", dest="logdir", help="log directory" + ) parser.add_argument( "--ckpt", type=str, @@ -241,13 +220,25 @@ def main(): help="Path to the pytorch checkpoint", ) - parser.add_argument("--mode", type=str, default="original", help="HoVerNet mode (original/fast)") - parser.add_argument("--bs", type=int, default=1, dest="batch_size", help="batch size") - parser.add_argument("--no-amp", action="store_false", dest="amp", help="deactivate amp") + parser.add_argument( + "--mode", type=str, default="original", help="HoVerNet mode (original/fast)" + ) + parser.add_argument( + "--bs", type=int, default=1, dest="batch_size", help="batch size" + ) + parser.add_argument( + "--no-amp", action="store_false", dest="amp", help="deactivate amp" + ) parser.add_argument("--save_interval", type=int, default=10) - parser.add_argument("--cpu", type=int, default=0, dest="num_workers", help="number of workers") - parser.add_argument("--use_gpu", type=bool, default=False, help="whether to use gpu") - parser.add_argument("--reader", type=str, default="OpenSlide", help="WSI reader backend") + parser.add_argument( + "--cpu", type=int, default=0, dest="num_workers", help="number of workers" + ) + parser.add_argument( + "--use_gpu", type=bool, default=False, help="whether to use gpu" + ) + parser.add_argument( + "--reader", type=str, default="OpenSlide", help="WSI reader backend" + ) args = parser.parse_args() config_dict = vars(args) From d9d19b5a8927b4e32039fbf3224f043d7cc43ac1 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Mon, 21 Nov 2022 12:58:16 -0500 Subject: [PATCH 05/27] Update the pipeline Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- pathology/hovernet/infer_roi.py | 162 +++++++++++++------------------- 1 file changed, 66 insertions(+), 96 deletions(-) diff --git a/pathology/hovernet/infer_roi.py b/pathology/hovernet/infer_roi.py index 74a9b67c20..478d889c53 100644 --- a/pathology/hovernet/infer_roi.py +++ b/pathology/hovernet/infer_roi.py @@ -6,20 +6,34 @@ import torch import torch.distributed as dist -from imageio import imsave from monai.apps.pathology.inferers import SlidingWindowHoVerNetInferer from monai.apps.pathology.transforms import ( - GenerateDistanceMapd, GenerateInstanceBorderd, GenerateWatershedMarkersd, - GenerateWatershedMaskd, HoVerNetNuclearTypePostProcessingd, Watershedd) + GenerateDistanceMapd, + GenerateInstanceBorderd, + GenerateWatershedMarkersd, + GenerateWatershedMaskd, + HoVerNetNuclearTypePostProcessingd, + Watershedd, +) from monai.data import DataLoader, Dataset, PILReader from monai.engines import SupervisedEvaluator from monai.networks.nets import HoVerNet -from monai.transforms import (Activationsd, AsDiscreted, CastToTyped, - CenterSpatialCropd, Compose, Decollated, - EnsureChannelFirstd, FillHoles, GaussianSmooth, - LoadImaged, ScaleIntensityRanged) -from monai.transforms.utils import apply_transform +from monai.transforms import ( + Activationsd, + AsDiscreted, + CastToTyped, + CenterSpatialCropd, + Compose, + EnsureChannelFirstd, + FillHoles, + FromMetaTensord, + GaussianSmooth, + LoadImaged, + PromoteChildItemsd, + SaveImaged, + ScaleIntensityRanged, +) from monai.utils import HoVerNetBranch, first @@ -33,7 +47,7 @@ def create_output_dir(cfg): timestamp = time.strftime("%y%m%d-%H%M%S") run_folder_name = f"{timestamp}_inference_hovernet_ps{cfg['patch_size']}" - log_dir = os.path.join(cfg["logdir"], run_folder_name) + log_dir = os.path.join(cfg["output"], run_folder_name) print(f"Logs and outputs are saved at '{log_dir}'.") if not os.path.exists(log_dir): os.makedirs(log_dir) @@ -52,42 +66,33 @@ def run(cfg): torch.cuda.set_device(device) else: device = torch.device("cuda" if cfg["use_gpu"] else "cpu") + # -------------------------------------------------------------------------- # Transforms # -------------------------------------------------------------------------- # Preprocessing transforms pre_transforms = Compose( [ - LoadImaged( - keys=["image"], reader=PILReader, converter=lambda x: x.convert("RGB") - ), + LoadImaged(keys=["image"], reader=PILReader, converter=lambda x: x.convert("RGB")), EnsureChannelFirstd(keys=["image"]), - # CenterSpatialCropd(keys=["image"], roi_size=(80, 80)), # for testing only + CenterSpatialCropd(keys=["image"], roi_size=(80, 80)), # for testing only CastToTyped(keys=["image"], dtype=torch.float32), - ScaleIntensityRanged( - keys=["image"], a_min=0.0, a_max=255.0, b_min=0.0, b_max=1.0, clip=True - ), + ScaleIntensityRanged(keys=["image"], a_min=0.0, a_max=255.0, b_min=0.0, b_max=1.0, clip=True), ] ) # Postprocessing transforms post_transforms = Compose( [ - Decollated( - keys=[ - HoVerNetBranch.NC.value, - HoVerNetBranch.NP.value, - HoVerNetBranch.HV.value, - ] + PromoteChildItemsd( + keys="pred", + children_keys=[HoVerNetBranch.NC.value, HoVerNetBranch.NP.value, HoVerNetBranch.HV.value], + delete_keys=True, ), Activationsd(keys=HoVerNetBranch.NC.value, softmax=True), AsDiscreted(keys=HoVerNetBranch.NC.value, argmax=True), GenerateWatershedMaskd(keys=HoVerNetBranch.NP.value, softmax=True), - GenerateInstanceBorderd( - keys="mask", hover_map_key=HoVerNetBranch.HV.value, kernel_size=3 - ), - GenerateDistanceMapd( - keys="mask", border_key="border", smooth_fn=GaussianSmooth() - ), + GenerateInstanceBorderd(keys="mask", hover_map_key=HoVerNetBranch.HV.value, kernel_size=3), + GenerateDistanceMapd(keys="mask", border_key="border", smooth_fn=GaussianSmooth()), GenerateWatershedMarkersd( keys="mask", border_key="border", @@ -96,8 +101,17 @@ def run(cfg): postprocess_fn=FillHoles(), ), Watershedd(keys="dist", mask_key="mask", markers_key="markers"), - HoVerNetNuclearTypePostProcessingd( - type_pred_key=HoVerNetBranch.NC.value, instance_pred_key="dist" + HoVerNetNuclearTypePostProcessingd(type_pred_key=HoVerNetBranch.NC.value, instance_pred_key="dist"), + FromMetaTensord(keys=["image", "pred_binary"]), + SaveImaged( + keys="pred_binary", + meta_keys="image_meta_dict", + output_ext="png", + output_dir=output_dir, + output_postfix="pred", + output_dtype="uint8", + separate_folder=False, + scale=255, ), ] ) @@ -105,10 +119,7 @@ def run(cfg): # Data and Data Loading # -------------------------------------------------------------------------- # List of whole slide images - data_list = [ - {"image": image, "filename": image} - for image in glob(os.path.join(cfg["root"], "*.png")) - ] + data_list = [{"image": image} for image in glob(os.path.join(cfg["root"], "*.png"))] # Dataset dataset = Dataset(data_list, transform=pre_transforms) @@ -140,7 +151,7 @@ def run(cfg): # -------------------------------------------------------------------------- # Create model and load weights model = HoVerNet( - mode="original", + mode=cfg["mode"], in_channels=3, out_classes=5, act=("relu", {"inplace": True}), @@ -157,11 +168,11 @@ def run(cfg): # -------------------------------------------- # Inference # -------------------------------------------- + # Inference engine sliding_inferer = SlidingWindowHoVerNetInferer( roi_size=cfg["patch_size"], sw_batch_size=8, overlap=1.0 - float(cfg["out_size"]) / float(cfg["patch_size"]), - # overlap=0, padding_mode="constant", cval=0, sw_device=device, @@ -170,28 +181,15 @@ def run(cfg): extra_input_padding=((cfg["patch_size"] - cfg["out_size"]) // 2,) * 4, ) - for i, data in enumerate(data_loader): - print(">>>>> ", data["filename"]) - image = data["image"] - output = sliding_inferer(image, model) - results = apply_transform(post_transforms, output) - for i, res in enumerate(results): - filename = os.path.join( - output_dir, - os.path.basename(data["filename"][i]).replace(".png", "_pred.png"), - ) - print(f"Saving {filename}...") - imsave(filename, res["pred_binary"].permute(1, 2, 0).numpy()) - - # evaluator = SupervisedEvaluator( - # device=device, - # val_data_loader=data_loader, - # network=model, - # postprocessing=post_transforms, - # inferer=sliding_inferer, - # amp=cfg["amp"], - # ) - # evaluator.run() + evaluator = SupervisedEvaluator( + device=device, + val_data_loader=data_loader, + network=model, + postprocessing=post_transforms, + inferer=sliding_inferer, + amp=cfg["amp"], + ) + evaluator.run() if multi_gpu: dist.destroy_process_group() @@ -200,45 +198,17 @@ def run(cfg): def main(): logging.basicConfig(level=logging.INFO) - parser = ArgumentParser( - description="Tumor detection on whole slide pathology images." - ) - parser.add_argument( - "--root", - type=str, - default="/Users/bhashemian/workspace/project-monai/tutorials/pathology/hovernet/CoNSeP/Test/Images", - help="image root dir", - ) - parser.add_argument( - "--logdir", type=str, default="./logs/", dest="logdir", help="log directory" - ) - parser.add_argument( - "--ckpt", - type=str, - default="/Users/bhashemian/workspace/project-monai/tutorials/pathology/hovernet/model_CoNSeP_new.pth", - dest="ckpt", - help="Path to the pytorch checkpoint", - ) - - parser.add_argument( - "--mode", type=str, default="original", help="HoVerNet mode (original/fast)" - ) - parser.add_argument( - "--bs", type=int, default=1, dest="batch_size", help="batch size" - ) - parser.add_argument( - "--no-amp", action="store_false", dest="amp", help="deactivate amp" - ) + parser = ArgumentParser(description="Tumor detection on whole slide pathology images.") + parser.add_argument("--root", type=str, default="./CoNSeP/Test/Images", help="image root dir") + parser.add_argument("--output", type=str, default="./logs/", dest="output", help="log directory") + parser.add_argument("--ckpt", type=str, default="./model_CoNSeP_new.pth", help="Path to the pytorch checkpoint") + parser.add_argument("--mode", type=str, default="original", help="HoVerNet mode (original/fast)") + parser.add_argument("--bs", type=int, default=1, dest="batch_size", help="batch size") + parser.add_argument("--no-amp", action="store_false", dest="amp", help="deactivate amp") parser.add_argument("--save_interval", type=int, default=10) - parser.add_argument( - "--cpu", type=int, default=0, dest="num_workers", help="number of workers" - ) - parser.add_argument( - "--use_gpu", type=bool, default=False, help="whether to use gpu" - ) - parser.add_argument( - "--reader", type=str, default="OpenSlide", help="WSI reader backend" - ) + parser.add_argument("--cpu", type=int, default=0, dest="num_workers", help="number of workers") + parser.add_argument("--use_gpu", type=bool, default=False, help="whether to use gpu") + parser.add_argument("--reader", type=str, default="OpenSlide", help="WSI reader backend") args = parser.parse_args() config_dict = vars(args) From 0cdac25da7eaf78d59c1c8f5d56d17c0bcee9809 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Mon, 21 Nov 2022 13:17:54 -0500 Subject: [PATCH 06/27] keep rio inference only Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- pathology/hovernet/infer_wsi.py | 233 ---------------------------- pathology/hovernet/tcga_download.py | 88 ----------- 2 files changed, 321 deletions(-) delete mode 100644 pathology/hovernet/infer_wsi.py delete mode 100644 pathology/hovernet/tcga_download.py diff --git a/pathology/hovernet/infer_wsi.py b/pathology/hovernet/infer_wsi.py deleted file mode 100644 index bf29db6e28..0000000000 --- a/pathology/hovernet/infer_wsi.py +++ /dev/null @@ -1,233 +0,0 @@ -import sys - -from functools import partial -import logging -import os -import time -from argparse import ArgumentParser -import torch -import numpy as np -import pandas as pd -import torch.distributed as dist -from monai.data import DataLoader, MaskedPatchWSIDataset -from monai.networks.nets import HoVerNet -from monai.engines import SupervisedEvaluator -from monai.apps.pathology.transforms import ( - GenerateWatershedMaskd, - GenerateInstanceBorderd, - GenerateDistanceMapd, - GenerateWatershedMarkersd, - Watershedd, - GenerateInstanceContour, - GenerateInstanceCentroid, - GenerateInstanceType, -) -from monai.transforms import ( - Activations, - AsDiscrete, - AsDiscreted, - Compose, - ScaleIntensityRanged, - CastToTyped, - Lambdad, - SplitDimd, - EnsureChannelFirstd, - ComputeHoVerMapsd, - CenterSpatialCropd, - FillHoles, - BoundingRect, - ThresholdIntensity, - GaussianSmooth, -) -from monai.handlers import ( - MeanDice, - StatsHandler, - TensorBoardStatsHandler, - from_engine, -) -from monai.utils import convert_to_tensor, first, HoVerNetBranch - - -def create_output_dir(cfg): - timestamp = time.strftime("%y%m%d-%H%M%S") - run_folder_name = f"{timestamp}_inference_hovernet_ps{cfg['patch_size']}" - log_dir = os.path.join(cfg["logdir"], run_folder_name) - print(f"Logs and outputs are saved at '{log_dir}'.") - if not os.path.exists(log_dir): - os.makedirs(log_dir) - return log_dir - - -def post_process(output, return_binary=True, return_centroids=False, output_classes=None): - pred = output["pred"] - device = pred[HoVerNetBranch.NP.value].device - if HoVerNetBranch.NC.value in pred.keys(): - type_pred = Activations(softmax=True)(pred[HoVerNetBranch.NC.value]) - type_pred = AsDiscrete(argmax=True)(type_pred) - - post_trans_seg = Compose( - [ - GenerateWatershedMaskd(keys=HoVerNetBranch.NP.value, softmax=True), - GenerateInstanceBorderd(keys="mask", hover_map_key=HoVerNetBranch.HV, kernel_size=3), - GenerateDistanceMapd(keys="mask", border_key="border", smooth_fn=GaussianSmooth()), - GenerateWatershedMarkersd( - keys="mask", border_key="border", threshold=0.7, radius=2, postprocess_fn=FillHoles() - ), - Watershedd(keys="dist", mask_key="mask", markers_key="markers"), - ] - ) - pred_inst_dict = post_trans_seg(pred) - pred_inst = pred_inst_dict["dist"] - - inst_id_list = np.unique(pred_inst)[1:] # exclude background - - inst_info_dict = None - if return_centroids: - inst_info_dict = {} - for inst_id in inst_id_list: - inst_map = pred_inst == inst_id - inst_bbox = BoundingRect()(inst_map) - inst_map = inst_map[:, inst_bbox[0][0] : inst_bbox[0][1], inst_bbox[0][2] : inst_bbox[0][3]] - offset = [inst_bbox[0][2], inst_bbox[0][0]] - inst_contour = GenerateInstanceContour()(inst_map, offset) - inst_centroid = GenerateInstanceCentroid()(inst_map, offset) - if inst_contour is not None: - inst_info_dict[inst_id] = { # inst_id should start at 1 - "bounding_box": inst_bbox, - "centroid": inst_centroid, - "contour": inst_contour, - "type_probability": None, - "type": None, - } - - if output_classes is not None: - for inst_id in list(inst_info_dict.keys()): - inst_type, type_prob = GenerateInstanceType()( - bbox=inst_info_dict[inst_id]["bounding_box"], - type_pred=type_pred, - seg_pred=pred_inst, - instance_id=inst_id, - ) - inst_info_dict[inst_id]["type"] = inst_type - inst_info_dict[inst_id]["type_probability"] = type_prob - - pred_inst = convert_to_tensor(pred_inst, device=device) - if return_binary: - pred_inst[pred_inst > 0] = 1 - output["pred"][HoVerNetBranch.NP.value] = pred_inst - output["pred"]["inst_info_dict"] = inst_info_dict - output["pred"]["pred_inst_dict"] = pred_inst_dict - return output - - -def run(cfg): - # -------------------------------------------------------------------------- - # Set Directory and Device - # -------------------------------------------------------------------------- - log_dir = create_output_dir(cfg) - multi_gpu = True if cfg["use_gpu"] and torch.cuda.device_count() > 1 else False - if multi_gpu: - dist.init_process_group(backend="nccl", init_method="env://") - device = torch.device("cuda:{}".format(dist.get_rank())) - torch.cuda.set_device(device) - else: - device = torch.device("cuda" if cfg["use_gpu"] else "cpu") - # -------------------------------------------------------------------------- - # Data Loading and Preprocessing - # -------------------------------------------------------------------------- - # Preprocessing transforms - pre_transforms = Compose( - [ - CastToTyped(keys=["image"], dtype=torch.float32), - ScaleIntensityRanged(keys=["image"], a_min=0.0, a_max=255.0, b_min=0.0, b_max=1.0, clip=True), - ] - ) - # List of whole slide images - data_list = [ - {"image": "TCGA-A1-A0SP-01Z-00-DX1.20D689C6-EFA5-4694-BE76-24475A89ACC0.svs"}, - {"image": "TCGA-A2-A0D0-01Z-00-DX1.4FF6B8E5-703B-400F-920A-104F56E0F874.svs"}, - ] - # Dataset of patches - dataset = MaskedPatchWSIDataset( - data_list, - patch_size=cfg["patch_size"], - patch_level=0, - mask_level=3, - transform=pre_transforms, - reader=cfg["reader"], - ) - # Dataloader - data_loader = DataLoader(dataset, num_workers=cfg["num_workers"], batch_size=cfg["batch_size"], pin_memory=True) - # -------------------------------------------------------------------------- - # Run some sanity checks - # -------------------------------------------------------------------------- - # Check first sample - first_sample = first(data_loader) - if first_sample is None: - raise ValueError("First sample is None!") - print("image: ") - print(" shape", first_sample["image"].shape) - print(" type: ", type(first_sample["image"])) - print(" dtype: ", first_sample["image"].dtype) - print(f"batch size: {cfg['batch_size']}") - print(f"number of batches: {len(data_loader)}") - - # -------------------------------------------------------------------------- - # Model and Handlers - # -------------------------------------------------------------------------- - # Create model and load weights - model = HoVerNet( - mode="fast", in_channels=3, out_classes=7, act=("relu", {"inplace": True}), norm="batch", dropout_prob=0.2 - ).to(device) - model.load_state_dict(torch.load(cfg["ckpt"])) - model.eval() - # Handlers - inference_handlers = [ - StatsHandler(output_transform=lambda x: None), - TensorBoardStatsHandler(log_dir=log_dir, output_transform=lambda x: None), - ] - if multi_gpu: - model = torch.nn.parallel.DistributedDataParallel( - model, device_ids=[dist.get_rank()], output_device=dist.get_rank() - ) - inference_handlers = inference_handlers if dist.get_rank() == 0 else None - # -------------------------------------------- - # Inference - # -------------------------------------------- - inference = SupervisedEvaluator( - device=device, - val_data_loader=data_loader, - network=model, - postprocessing=partial(post_process, return_binary=True, return_centroids=False, output_classes=None), - val_handlers=inference_handlers, - amp=cfg["amp"], - ) - inference.run() - - if multi_gpu: - dist.destroy_process_group() - - -def main(): - logging.basicConfig(level=logging.INFO) - - parser = ArgumentParser(description="Tumor detection on whole slide pathology images.") - parser.add_argument("--root", type=str, default="./", help="root WSI dir") - parser.add_argument("--logdir", type=str, default="./logs/", dest="logdir", help="log directory") - parser.add_argument("--ckpt", type=str, default="./", dest="ckpt", help="Path to the pytorch checkpoint") - parser.add_argument("--ps", type=int, default=256, dest="patch_size", help="patch size") - parser.add_argument("--bs", type=int, default=8, dest="batch_size", help="batch size") - parser.add_argument("--no-amp", action="store_false", dest="amp", help="deactivate amp") - parser.add_argument("--save_interval", type=int, default=10) - parser.add_argument("--cpu", type=int, default=1, dest="num_workers", help="number of workers") - parser.add_argument("--use_gpu", type=bool, default=False, help="whether to use gpu") - parser.add_argument("--reader", type=str, default="OpenSlide", help="WSI reader backend") - args = parser.parse_args() - - config_dict = vars(args) - print(config_dict) - run(config_dict) - - -if __name__ == "__main__": - main() diff --git a/pathology/hovernet/tcga_download.py b/pathology/hovernet/tcga_download.py deleted file mode 100644 index fc66ba1614..0000000000 --- a/pathology/hovernet/tcga_download.py +++ /dev/null @@ -1,88 +0,0 @@ -import requests -import json -import re - -BASE_URL = "https://api.gdc.cancer.gov" - - -def extract_info(filename): - """Extract wsi and patch info from filename - - Args: - filename: name of the rgb or mask file in NuCLS dataset - """ - wsi_name = filename.split("_id")[0] - case_name, dx = wsi_name.rsplit("-", 1) - matches = re.search("left-([0-9]+).*top-([0-9]+).*bottom-([0-9]+).*right-([0-9]+)", filename) - left, top, bottom, right = [int(m) for m in matches.groups()] - location = (top, left) - size = max(right - left, bottom - top) - print(location, size) - file_name_wild = f"{case_name}-*{dx}*" - return file_name_wild, location, size - - -def get_file_id(file_name_wild): - """Retrieve file_id from partial filenames with wildcard - - Args: - file_name_wild: partial filename of a file on TCGA with wildcard - """ - file_filters = { - "op": "=", - "content": { - "field": "files.file_name", - "value": [file_name_wild], - }, - } - file_endpoint = f"{BASE_URL}/files" - params = {"filters": json.dumps(file_filters)} - response = requests.get(file_endpoint, params=params) - print(json.dumps(response.json(), indent=2)) - return response.json()["data"]["hits"][0]["file_id"] - - -def download_file(file_id): - """Download a file based on its file_id - - Args: - file_id: UUID of a file on TCGA - """ - query = f"{BASE_URL}/data/{file_id}" - print(f"Fetching {file_id} ...") - response = requests.get(query, headers={"Content-Type": "application/json"}) - response_head_cd = response.headers["Content-Disposition"] - file_name = re.findall("filename=(.+)", response_head_cd)[0] - - with open(file_name, "wb") as output_file: - output_file.write(response.content) - print(f"{file_id} is saved in {file_name}.") - - -def create_file_info_list(filenames): - """Create information records for each of NuCLS files - - Args: - filenames: list of NuCLS filenames for images or masks - """ - info_list = [] - for filename in filenames: - file_name_wild, location, size = extract_info(filename) - uid = get_file_id(file_name_wild) - info_list.append((uid, filename, location, size)) - return info_list - - -if __name__ == "__main__": - filenames = [ - "TCGA-A1-A0SP-DX1_id-5ea4095addda5f8398977ebc_left-7053_top-53967_bottom-54231_right-7311", - "TCGA-A2-A0D0-DX1_id-5ea40b17ddda5f839899849a_left-69243_top-41106_bottom-41400_right-69527", - ] - info_list = create_file_info_list(filenames) - print(f"{info_list=}") - - file_ids = list(set([r[0] for r in info_list])) - print(f"{file_ids=}") - - for uid in file_ids: - download_file(uid) From e58050c815c78710f95c4656dd3bf4d064540b69 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Mon, 21 Nov 2022 13:20:53 -0500 Subject: [PATCH 07/27] Remove test-oly lines Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- pathology/hovernet/infer_roi.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pathology/hovernet/infer_roi.py b/pathology/hovernet/infer_roi.py index 478d889c53..95f2febd2d 100644 --- a/pathology/hovernet/infer_roi.py +++ b/pathology/hovernet/infer_roi.py @@ -23,7 +23,6 @@ Activationsd, AsDiscreted, CastToTyped, - CenterSpatialCropd, Compose, EnsureChannelFirstd, FillHoles, @@ -75,7 +74,6 @@ def run(cfg): [ LoadImaged(keys=["image"], reader=PILReader, converter=lambda x: x.convert("RGB")), EnsureChannelFirstd(keys=["image"]), - CenterSpatialCropd(keys=["image"], roi_size=(80, 80)), # for testing only CastToTyped(keys=["image"], dtype=torch.float32), ScaleIntensityRanged(keys=["image"], a_min=0.0, a_max=255.0, b_min=0.0, b_max=1.0, clip=True), ] @@ -85,7 +83,7 @@ def run(cfg): [ PromoteChildItemsd( keys="pred", - children_keys=[HoVerNetBranch.NC.value, HoVerNetBranch.NP.value, HoVerNetBranch.HV.value], + child_keys=[HoVerNetBranch.NC.value, HoVerNetBranch.NP.value, HoVerNetBranch.HV.value], delete_keys=True, ), Activationsd(keys=HoVerNetBranch.NC.value, softmax=True), From 123b721d817ffd1a68a41cac14171fef42e4f131 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Mon, 21 Nov 2022 16:35:25 -0500 Subject: [PATCH 08/27] Add sw batch size Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- pathology/hovernet/infer_roi.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pathology/hovernet/infer_roi.py b/pathology/hovernet/infer_roi.py index 95f2febd2d..ba400b924e 100644 --- a/pathology/hovernet/infer_roi.py +++ b/pathology/hovernet/infer_roi.py @@ -169,7 +169,7 @@ def run(cfg): # Inference engine sliding_inferer = SlidingWindowHoVerNetInferer( roi_size=cfg["patch_size"], - sw_batch_size=8, + sw_batch_size=cfg["sw_batch_size"], overlap=1.0 - float(cfg["out_size"]) / float(cfg["patch_size"]), padding_mode="constant", cval=0, @@ -202,6 +202,7 @@ def main(): parser.add_argument("--ckpt", type=str, default="./model_CoNSeP_new.pth", help="Path to the pytorch checkpoint") parser.add_argument("--mode", type=str, default="original", help="HoVerNet mode (original/fast)") parser.add_argument("--bs", type=int, default=1, dest="batch_size", help="batch size") + parser.add_argument("--swbs", type=int, default=8, dest="sw_batch_size", help="sliding window batch size") parser.add_argument("--no-amp", action="store_false", dest="amp", help="deactivate amp") parser.add_argument("--save_interval", type=int, default=10) parser.add_argument("--cpu", type=int, default=0, dest="num_workers", help="number of workers") From 04c1f45abfdaeb669ebffcd90fd0f51df3025044 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Mon, 21 Nov 2022 16:44:43 -0500 Subject: [PATCH 09/27] Change settings: Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- pathology/hovernet/infer_roi.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pathology/hovernet/infer_roi.py b/pathology/hovernet/infer_roi.py index ba400b924e..31c1ca8f54 100644 --- a/pathology/hovernet/infer_roi.py +++ b/pathology/hovernet/infer_roi.py @@ -204,10 +204,8 @@ def main(): parser.add_argument("--bs", type=int, default=1, dest="batch_size", help="batch size") parser.add_argument("--swbs", type=int, default=8, dest="sw_batch_size", help="sliding window batch size") parser.add_argument("--no-amp", action="store_false", dest="amp", help="deactivate amp") - parser.add_argument("--save_interval", type=int, default=10) parser.add_argument("--cpu", type=int, default=0, dest="num_workers", help="number of workers") - parser.add_argument("--use_gpu", type=bool, default=False, help="whether to use gpu") - parser.add_argument("--reader", type=str, default="OpenSlide", help="WSI reader backend") + parser.add_argument("--use-gpu", action="store_true", help="whether to use gpu") args = parser.parse_args() config_dict = vars(args) From 6b61c778d5bb9389968ac4ead4609f345957a185 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Tue, 22 Nov 2022 09:21:08 -0500 Subject: [PATCH 10/27] Address comments Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- pathology/hovernet/infer_roi.py | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/pathology/hovernet/infer_roi.py b/pathology/hovernet/infer_roi.py index 31c1ca8f54..122c77c01e 100644 --- a/pathology/hovernet/infer_roi.py +++ b/pathology/hovernet/infer_roi.py @@ -58,13 +58,7 @@ def run(cfg): # Set Directory and Device # -------------------------------------------------------------------------- output_dir = create_output_dir(cfg) - multi_gpu = True if cfg["use_gpu"] and torch.cuda.device_count() > 1 else False - if multi_gpu: - dist.init_process_group(backend="nccl", init_method="env://") - device = torch.device("cuda:{}".format(dist.get_rank())) - torch.cuda.set_device(device) - else: - device = torch.device("cuda" if cfg["use_gpu"] else "cpu") + device = torch.device("cuda" if cfg["use_gpu"] else "cpu") # -------------------------------------------------------------------------- # Transforms @@ -86,8 +80,6 @@ def run(cfg): child_keys=[HoVerNetBranch.NC.value, HoVerNetBranch.NP.value, HoVerNetBranch.HV.value], delete_keys=True, ), - Activationsd(keys=HoVerNetBranch.NC.value, softmax=True), - AsDiscreted(keys=HoVerNetBranch.NC.value, argmax=True), GenerateWatershedMaskd(keys=HoVerNetBranch.NP.value, softmax=True), GenerateInstanceBorderd(keys="mask", hover_map_key=HoVerNetBranch.HV.value, kernel_size=3), GenerateDistanceMapd(keys="mask", border_key="border", smooth_fn=GaussianSmooth()), @@ -158,11 +150,6 @@ def run(cfg): model.load_state_dict(torch.load(cfg["ckpt"], map_location=device)) model.eval() - if multi_gpu: - model = torch.nn.parallel.DistributedDataParallel( - model, device_ids=[dist.get_rank()], output_device=dist.get_rank() - ) - # -------------------------------------------- # Inference # -------------------------------------------- @@ -189,9 +176,6 @@ def run(cfg): ) evaluator.run() - if multi_gpu: - dist.destroy_process_group() - def main(): logging.basicConfig(level=logging.INFO) From ea99823b1aaf5cf89b9090cb4bb60798e5b6584c Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Tue, 22 Nov 2022 09:43:04 -0500 Subject: [PATCH 11/27] clean up Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- pathology/hovernet/infer_roi.py | 38 ++++++++++++++++----------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/pathology/hovernet/infer_roi.py b/pathology/hovernet/infer_roi.py index 122c77c01e..1c9a123625 100644 --- a/pathology/hovernet/infer_roi.py +++ b/pathology/hovernet/infer_roi.py @@ -20,8 +20,6 @@ from monai.engines import SupervisedEvaluator from monai.networks.nets import HoVerNet from monai.transforms import ( - Activationsd, - AsDiscreted, CastToTyped, Compose, EnsureChannelFirstd, @@ -37,28 +35,21 @@ def create_output_dir(cfg): - if cfg["mode"].lower() == "original": - cfg["patch_size"] = 270 - cfg["out_size"] = 80 - elif cfg["mode"].lower() == "fast": - cfg["patch_size"] = 256 - cfg["out_size"] = 164 - timestamp = time.strftime("%y%m%d-%H%M%S") run_folder_name = f"{timestamp}_inference_hovernet_ps{cfg['patch_size']}" - log_dir = os.path.join(cfg["output"], run_folder_name) - print(f"Logs and outputs are saved at '{log_dir}'.") - if not os.path.exists(log_dir): - os.makedirs(log_dir) - return log_dir + output_dir = os.path.join(cfg["output"], run_folder_name) + print(f"Outputs are saved at '{output_dir}'.") + if not os.path.exists(output_dir): + os.makedirs(output_dir) + return output_dir def run(cfg): # -------------------------------------------------------------------------- - # Set Directory and Device + # Set Directory, Device, # -------------------------------------------------------------------------- output_dir = create_output_dir(cfg) - device = torch.device("cuda" if cfg["use_gpu"] else "cpu") + device = torch.device("cuda" if not cfg["no_gpu"] and torch.cuda.is_available() else "cpu") # -------------------------------------------------------------------------- # Transforms @@ -117,7 +108,7 @@ def run(cfg): # Dataloader data_loader = DataLoader( dataset, - num_workers=cfg["num_workers"], + ncpu=cfg["ncpu"], batch_size=cfg["batch_size"], pin_memory=True, ) @@ -188,11 +179,20 @@ def main(): parser.add_argument("--bs", type=int, default=1, dest="batch_size", help="batch size") parser.add_argument("--swbs", type=int, default=8, dest="sw_batch_size", help="sliding window batch size") parser.add_argument("--no-amp", action="store_false", dest="amp", help="deactivate amp") - parser.add_argument("--cpu", type=int, default=0, dest="num_workers", help="number of workers") - parser.add_argument("--use-gpu", action="store_true", help="whether to use gpu") + parser.add_argument("--ncpu", type=int, default=4, help="number of CPU workers") + parser.add_argument("--no-gpu", action="store_false", help="deactivate use of gpu") args = parser.parse_args() config_dict = vars(args) + if config_dict["mode"].lower() == "original": + config_dict["patch_size"] = 270 + config_dict["out_size"] = 80 + elif config_dict["mode"].lower() == "fast": + config_dict["patch_size"] = 256 + config_dict["out_size"] = 164 + else: + raise ValueError("`--mode` should be either `original` or `fast`.") + print(config_dict) run(config_dict) From e79f5f85ff86f6c215579a46770c509106d9d7b3 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Tue, 22 Nov 2022 09:50:26 -0500 Subject: [PATCH 12/27] fix a typo Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- pathology/hovernet/infer_roi.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/pathology/hovernet/infer_roi.py b/pathology/hovernet/infer_roi.py index 1c9a123625..d1c1bf251b 100644 --- a/pathology/hovernet/infer_roi.py +++ b/pathology/hovernet/infer_roi.py @@ -106,12 +106,7 @@ def run(cfg): dataset = Dataset(data_list, transform=pre_transforms) # Dataloader - data_loader = DataLoader( - dataset, - ncpu=cfg["ncpu"], - batch_size=cfg["batch_size"], - pin_memory=True, - ) + data_loader = DataLoader(dataset, num_workers=cfg["ncpu"], batch_size=cfg["batch_size"], pin_memory=True) # -------------------------------------------------------------------------- # Run some sanity checks @@ -179,7 +174,7 @@ def main(): parser.add_argument("--bs", type=int, default=1, dest="batch_size", help="batch size") parser.add_argument("--swbs", type=int, default=8, dest="sw_batch_size", help="sliding window batch size") parser.add_argument("--no-amp", action="store_false", dest="amp", help="deactivate amp") - parser.add_argument("--ncpu", type=int, default=4, help="number of CPU workers") + parser.add_argument("--ncpu", type=int, default=0, help="number of CPU workers") parser.add_argument("--no-gpu", action="store_false", help="deactivate use of gpu") args = parser.parse_args() From 0f0884f51e17a53bfaf5bf49471d9fcdc5c6dcbd Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Tue, 22 Nov 2022 10:02:14 -0500 Subject: [PATCH 13/27] change logic of few args Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- pathology/hovernet/infer_roi.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pathology/hovernet/infer_roi.py b/pathology/hovernet/infer_roi.py index d1c1bf251b..f2561e3017 100644 --- a/pathology/hovernet/infer_roi.py +++ b/pathology/hovernet/infer_roi.py @@ -158,7 +158,7 @@ def run(cfg): network=model, postprocessing=post_transforms, inferer=sliding_inferer, - amp=cfg["amp"], + amp=not cfg["no_amp"], ) evaluator.run() @@ -173,9 +173,9 @@ def main(): parser.add_argument("--mode", type=str, default="original", help="HoVerNet mode (original/fast)") parser.add_argument("--bs", type=int, default=1, dest="batch_size", help="batch size") parser.add_argument("--swbs", type=int, default=8, dest="sw_batch_size", help="sliding window batch size") - parser.add_argument("--no-amp", action="store_false", dest="amp", help="deactivate amp") + parser.add_argument("--no-amp", action="store_true", help="deactivate use of amp") + parser.add_argument("--no-gpu", action="store_true", help="deactivate use of gpu") parser.add_argument("--ncpu", type=int, default=0, help="number of CPU workers") - parser.add_argument("--no-gpu", action="store_false", help="deactivate use of gpu") args = parser.parse_args() config_dict = vars(args) From b1dac056a2262ac097f96657678066342e53bf44 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Tue, 22 Nov 2022 10:20:14 -0500 Subject: [PATCH 14/27] Add multi-gpu Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- pathology/hovernet/infer_roi.py | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/pathology/hovernet/infer_roi.py b/pathology/hovernet/infer_roi.py index f2561e3017..65c140d81b 100644 --- a/pathology/hovernet/infer_roi.py +++ b/pathology/hovernet/infer_roi.py @@ -16,7 +16,7 @@ HoVerNetNuclearTypePostProcessingd, Watershedd, ) -from monai.data import DataLoader, Dataset, PILReader +from monai.data import DataLoader, Dataset, PILReader, partition_dataset from monai.engines import SupervisedEvaluator from monai.networks.nets import HoVerNet from monai.transforms import ( @@ -49,8 +49,13 @@ def run(cfg): # Set Directory, Device, # -------------------------------------------------------------------------- output_dir = create_output_dir(cfg) - device = torch.device("cuda" if not cfg["no_gpu"] and torch.cuda.is_available() else "cpu") - + multi_gpu = cfg["use_gpu"] if torch.cuda.device_count() > 1 else False + if multi_gpu: + dist.init_process_group(backend="nccl", init_method="env://") + device = torch.device("cuda:{}".format(dist.get_rank())) + torch.cuda.set_device(device) + else: + device = torch.device("cuda" if cfg["use_gpu"] and torch.cuda.is_available() else "cpu") # -------------------------------------------------------------------------- # Transforms # -------------------------------------------------------------------------- @@ -102,8 +107,14 @@ def run(cfg): # List of whole slide images data_list = [{"image": image} for image in glob(os.path.join(cfg["root"], "*.png"))] + if multi_gpu: + print(f">>> rank = {dist.get_rank()}") + data = partition_dataset(data=data_list, num_partitions=dist.get_world_size())[dist.get_rank()] + else: + data = data_list + # Dataset - dataset = Dataset(data_list, transform=pre_transforms) + dataset = Dataset(data, transform=pre_transforms) # Dataloader data_loader = DataLoader(dataset, num_workers=cfg["ncpu"], batch_size=cfg["batch_size"], pin_memory=True) @@ -135,6 +146,10 @@ def run(cfg): ).to(device) model.load_state_dict(torch.load(cfg["ckpt"], map_location=device)) model.eval() + if multi_gpu: + model = torch.nn.parallel.DistributedDataParallel( + model, device_ids=[dist.get_rank()], output_device=dist.get_rank() + ) # -------------------------------------------- # Inference @@ -158,10 +173,13 @@ def run(cfg): network=model, postprocessing=post_transforms, inferer=sliding_inferer, - amp=not cfg["no_amp"], + amp=cfg["use_amp"], ) evaluator.run() + if multi_gpu: + dist.destroy_process_group() + def main(): logging.basicConfig(level=logging.INFO) @@ -173,8 +191,8 @@ def main(): parser.add_argument("--mode", type=str, default="original", help="HoVerNet mode (original/fast)") parser.add_argument("--bs", type=int, default=1, dest="batch_size", help="batch size") parser.add_argument("--swbs", type=int, default=8, dest="sw_batch_size", help="sliding window batch size") - parser.add_argument("--no-amp", action="store_true", help="deactivate use of amp") - parser.add_argument("--no-gpu", action="store_true", help="deactivate use of gpu") + parser.add_argument("--no-amp", action="store_false", dest="use_amp", help="deactivate use of amp") + parser.add_argument("--no-gpu", action="store_false", dest="use_gpu", help="deactivate use of gpu") parser.add_argument("--ncpu", type=int, default=0, help="number of CPU workers") args = parser.parse_args() From ccc62ed2831a2dc2a497aa001a20ddaf4dc6920a Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Tue, 22 Nov 2022 10:34:00 -0500 Subject: [PATCH 15/27] Add/remove prints Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- pathology/hovernet/infer_roi.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pathology/hovernet/infer_roi.py b/pathology/hovernet/infer_roi.py index 65c140d81b..fffb1bd71f 100644 --- a/pathology/hovernet/infer_roi.py +++ b/pathology/hovernet/infer_roi.py @@ -54,6 +54,8 @@ def run(cfg): dist.init_process_group(backend="nccl", init_method="env://") device = torch.device("cuda:{}".format(dist.get_rank())) torch.cuda.set_device(device) + if dist.get_rank() == 0: + print(f"Running multi-gpu with {dist.get_world_size()} GPUs") else: device = torch.device("cuda" if cfg["use_gpu"] and torch.cuda.is_available() else "cpu") # -------------------------------------------------------------------------- @@ -108,7 +110,6 @@ def run(cfg): data_list = [{"image": image} for image in glob(os.path.join(cfg["root"], "*.png"))] if multi_gpu: - print(f">>> rank = {dist.get_rank()}") data = partition_dataset(data=data_list, num_partitions=dist.get_world_size())[dist.get_rank()] else: data = data_list From a10f7a8abc2426f9cb8e59ee8c1254bfe4bd9338 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Tue, 22 Nov 2022 10:48:29 -0500 Subject: [PATCH 16/27] Rename to inference Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- pathology/hovernet/{infer_roi.py => inference.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename pathology/hovernet/{infer_roi.py => inference.py} (100%) diff --git a/pathology/hovernet/infer_roi.py b/pathology/hovernet/inference.py similarity index 100% rename from pathology/hovernet/infer_roi.py rename to pathology/hovernet/inference.py From ec9d8f1db1e3fba2e19d9b6608450551f9573552 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Tue, 22 Nov 2022 16:15:32 -0500 Subject: [PATCH 17/27] Add output class Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- pathology/hovernet/inference.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pathology/hovernet/inference.py b/pathology/hovernet/inference.py index fffb1bd71f..c1fa499aa4 100644 --- a/pathology/hovernet/inference.py +++ b/pathology/hovernet/inference.py @@ -89,7 +89,9 @@ def run(cfg): postprocess_fn=FillHoles(), ), Watershedd(keys="dist", mask_key="mask", markers_key="markers"), - HoVerNetNuclearTypePostProcessingd(type_pred_key=HoVerNetBranch.NC.value, instance_pred_key="dist"), + HoVerNetNuclearTypePostProcessingd( + type_pred_key=HoVerNetBranch.NC.value, instance_pred_key="dist", output_classes=5 + ), FromMetaTensord(keys=["image", "pred_binary"]), SaveImaged( keys="pred_binary", From 73400f2a68ca97b830e84fb01875edf49aaee128 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Wed, 23 Nov 2022 14:21:31 -0500 Subject: [PATCH 18/27] Update to FalttenSubKeysd Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- pathology/hovernet/inference.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pathology/hovernet/inference.py b/pathology/hovernet/inference.py index c1fa499aa4..8e970c57c0 100644 --- a/pathology/hovernet/inference.py +++ b/pathology/hovernet/inference.py @@ -27,7 +27,7 @@ FromMetaTensord, GaussianSmooth, LoadImaged, - PromoteChildItemsd, + FlattenSubKeysd, SaveImaged, ScaleIntensityRanged, ) @@ -73,9 +73,9 @@ def run(cfg): # Postprocessing transforms post_transforms = Compose( [ - PromoteChildItemsd( + FlattenSubKeysd( keys="pred", - child_keys=[HoVerNetBranch.NC.value, HoVerNetBranch.NP.value, HoVerNetBranch.HV.value], + sub_keys=[HoVerNetBranch.NC.value, HoVerNetBranch.NP.value, HoVerNetBranch.HV.value], delete_keys=True, ), GenerateWatershedMaskd(keys=HoVerNetBranch.NP.value, softmax=True), From aa4a77002eca82e09394d5c33952feb9b2be9809 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Thu, 1 Dec 2022 18:54:45 -0500 Subject: [PATCH 19/27] Update with the new hovernet postprocessing Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- pathology/hovernet/inference.py | 70 +++++++++++++++++---------------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/pathology/hovernet/inference.py b/pathology/hovernet/inference.py index 8e970c57c0..adebab9942 100644 --- a/pathology/hovernet/inference.py +++ b/pathology/hovernet/inference.py @@ -9,23 +9,18 @@ from monai.apps.pathology.inferers import SlidingWindowHoVerNetInferer from monai.apps.pathology.transforms import ( - GenerateDistanceMapd, - GenerateInstanceBorderd, - GenerateWatershedMarkersd, - GenerateWatershedMaskd, - HoVerNetNuclearTypePostProcessingd, - Watershedd, + HoVerNetInstanceMapPostProcessingd, + HoVerNetTypeMapPostProcessingd, ) from monai.data import DataLoader, Dataset, PILReader, partition_dataset from monai.engines import SupervisedEvaluator from monai.networks.nets import HoVerNet from monai.transforms import ( CastToTyped, + CenterSpatialCropd, Compose, EnsureChannelFirstd, - FillHoles, FromMetaTensord, - GaussianSmooth, LoadImaged, FlattenSubKeysd, SaveImaged, @@ -64,10 +59,11 @@ def run(cfg): # Preprocessing transforms pre_transforms = Compose( [ - LoadImaged(keys=["image"], reader=PILReader, converter=lambda x: x.convert("RGB")), - EnsureChannelFirstd(keys=["image"]), - CastToTyped(keys=["image"], dtype=torch.float32), - ScaleIntensityRanged(keys=["image"], a_min=0.0, a_max=255.0, b_min=0.0, b_max=1.0, clip=True), + LoadImaged(keys="image", reader=PILReader, converter=lambda x: x.convert("RGB")), + EnsureChannelFirstd(keys="image"), + CenterSpatialCropd(keys="image", roi_size=(80, 80)), + CastToTyped(keys="image", dtype=torch.float32), + ScaleIntensityRanged(keys="image", a_min=0.0, a_max=255.0, b_min=0.0, b_max=1.0, clip=True), ] ) # Postprocessing transforms @@ -78,30 +74,26 @@ def run(cfg): sub_keys=[HoVerNetBranch.NC.value, HoVerNetBranch.NP.value, HoVerNetBranch.HV.value], delete_keys=True, ), - GenerateWatershedMaskd(keys=HoVerNetBranch.NP.value, softmax=True), - GenerateInstanceBorderd(keys="mask", hover_map_key=HoVerNetBranch.HV.value, kernel_size=3), - GenerateDistanceMapd(keys="mask", border_key="border", smooth_fn=GaussianSmooth()), - GenerateWatershedMarkersd( - keys="mask", - border_key="border", - threshold=0.7, - radius=2, - postprocess_fn=FillHoles(), - ), - Watershedd(keys="dist", mask_key="mask", markers_key="markers"), - HoVerNetNuclearTypePostProcessingd( - type_pred_key=HoVerNetBranch.NC.value, instance_pred_key="dist", output_classes=5 + HoVerNetInstanceMapPostProcessingd(sobel_kernel_size=3, marker_threshold=0.7, marker_radius=2), + HoVerNetTypeMapPostProcessingd(), + FromMetaTensord(keys=["image"]), + SaveImaged( + keys="instance_map", + meta_keys="image_meta_dict", + output_ext="tiff", + output_dir=output_dir, + output_postfix="instance_map", + output_dtype="uint8", + separate_folder=False, ), - FromMetaTensord(keys=["image", "pred_binary"]), SaveImaged( - keys="pred_binary", + keys="type_map", meta_keys="image_meta_dict", - output_ext="png", + output_ext="tiff", output_dir=output_dir, - output_postfix="pred", + output_postfix="type_map", output_dtype="uint8", separate_folder=False, - scale=255, ), ] ) @@ -143,7 +135,7 @@ def run(cfg): model = HoVerNet( mode=cfg["mode"], in_channels=3, - out_classes=5, + out_classes=cfg["out_classes"], act=("relu", {"inplace": True}), norm="batch", ).to(device) @@ -188,10 +180,22 @@ def main(): logging.basicConfig(level=logging.INFO) parser = ArgumentParser(description="Tumor detection on whole slide pathology images.") - parser.add_argument("--root", type=str, default="./CoNSeP/Test/Images", help="image root dir") + parser.add_argument( + "--root", + type=str, + default="/Users/bhashemian/workspace/project-monai/tutorials/pathology/hovernet/CoNSeP/Test/Images", + help="image root dir", + ) parser.add_argument("--output", type=str, default="./logs/", dest="output", help="log directory") - parser.add_argument("--ckpt", type=str, default="./model_CoNSeP_new.pth", help="Path to the pytorch checkpoint") + parser.add_argument( + "--ckpt", + type=str, + default="/Users/bhashemian/workspace/project-monai/tutorials/pathology/hovernet/model_CoNSeP_new.pth", + help="Path to the pytorch checkpoint", + ) parser.add_argument("--mode", type=str, default="original", help="HoVerNet mode (original/fast)") + + parser.add_argument("--out-classes", type=int, default=5, help="number of output classes") parser.add_argument("--bs", type=int, default=1, dest="batch_size", help="batch size") parser.add_argument("--swbs", type=int, default=8, dest="sw_batch_size", help="sliding window batch size") parser.add_argument("--no-amp", action="store_false", dest="use_amp", help="deactivate use of amp") From a4aa476a0d88c2d1e96f3b70b30649534f8cbc1c Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Fri, 2 Dec 2022 16:06:18 -0500 Subject: [PATCH 20/27] change to nuclear type Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- pathology/hovernet/inference.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pathology/hovernet/inference.py b/pathology/hovernet/inference.py index adebab9942..3824981cc4 100644 --- a/pathology/hovernet/inference.py +++ b/pathology/hovernet/inference.py @@ -10,7 +10,7 @@ from monai.apps.pathology.inferers import SlidingWindowHoVerNetInferer from monai.apps.pathology.transforms import ( HoVerNetInstanceMapPostProcessingd, - HoVerNetTypeMapPostProcessingd, + HoVerNetNuclearTypePostProcessingd, ) from monai.data import DataLoader, Dataset, PILReader, partition_dataset from monai.engines import SupervisedEvaluator @@ -75,7 +75,7 @@ def run(cfg): delete_keys=True, ), HoVerNetInstanceMapPostProcessingd(sobel_kernel_size=3, marker_threshold=0.7, marker_radius=2), - HoVerNetTypeMapPostProcessingd(), + HoVerNetNuclearTypePostProcessingd(), FromMetaTensord(keys=["image"]), SaveImaged( keys="instance_map", From 4deb7c5ca0799c1cc54f4fea2a4e2937ecdeb25f Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Sun, 4 Dec 2022 22:04:42 -0500 Subject: [PATCH 21/27] to png Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- pathology/hovernet/inference.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pathology/hovernet/inference.py b/pathology/hovernet/inference.py index 3824981cc4..ce8113d4e0 100644 --- a/pathology/hovernet/inference.py +++ b/pathology/hovernet/inference.py @@ -80,7 +80,7 @@ def run(cfg): SaveImaged( keys="instance_map", meta_keys="image_meta_dict", - output_ext="tiff", + output_ext="png", output_dir=output_dir, output_postfix="instance_map", output_dtype="uint8", @@ -89,7 +89,7 @@ def run(cfg): SaveImaged( keys="type_map", meta_keys="image_meta_dict", - output_ext="tiff", + output_ext="png", output_dir=output_dir, output_postfix="type_map", output_dtype="uint8", From 16ad09e10904d55ba17735a5aab271bcee98142c Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Sun, 4 Dec 2022 22:18:09 -0500 Subject: [PATCH 22/27] remove test transform Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- pathology/hovernet/inference.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pathology/hovernet/inference.py b/pathology/hovernet/inference.py index ce8113d4e0..075c48c442 100644 --- a/pathology/hovernet/inference.py +++ b/pathology/hovernet/inference.py @@ -61,7 +61,6 @@ def run(cfg): [ LoadImaged(keys="image", reader=PILReader, converter=lambda x: x.convert("RGB")), EnsureChannelFirstd(keys="image"), - CenterSpatialCropd(keys="image", roi_size=(80, 80)), CastToTyped(keys="image", dtype=torch.float32), ScaleIntensityRanged(keys="image", a_min=0.0, a_max=255.0, b_min=0.0, b_max=1.0, clip=True), ] @@ -136,8 +135,6 @@ def run(cfg): mode=cfg["mode"], in_channels=3, out_classes=cfg["out_classes"], - act=("relu", {"inplace": True}), - norm="batch", ).to(device) model.load_state_dict(torch.load(cfg["ckpt"], map_location=device)) model.eval() From 9c4a2bffb4a2681624caf65c140f8d85312a2929 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Thu, 8 Dec 2022 10:21:27 -0500 Subject: [PATCH 23/27] Some updates Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- pathology/hovernet/README.MD | 44 ++++++++++++++++------ pathology/hovernet/inference.py | 7 ++-- pathology/hovernet/training.py | 66 ++++++++++++++++++--------------- 3 files changed, 72 insertions(+), 45 deletions(-) diff --git a/pathology/hovernet/README.MD b/pathology/hovernet/README.MD index fef6268eb2..d6e3da90fd 100644 --- a/pathology/hovernet/README.MD +++ b/pathology/hovernet/README.MD @@ -3,15 +3,16 @@ This folder contains ignite version examples to run train and validate a HoVerNet model. It also has torch version notebooks to run training and evaluation.
-
implementation based on:
-Simon Graham et al., HoVer-Net: Simultaneous Segmentation and Classification of Nuclei in Multi-Tissue Histology Images.' Medical Image Analysis, (2019). https://arxiv.org/abs/1812.06499
+Simon Graham et al., HoVer-Net: Simultaneous Segmentation and Classification of Nuclei in Multi-Tissue Histology Images.' Medical Image Analysis, (2019).