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.

- hovernet scheme 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). ### 1. Data -CoNSeP datasets which are used in the examples can be downloaded from https://warwick.ac.uk/fac/cross_fac/tia/data/hovernet/. +CoNSeP datasets which are used in the examples can be downloaded from . + - First download CoNSeP dataset to `data_root`. - Run prepare_patches.py to prepare patches from images. @@ -21,10 +22,11 @@ CoNSeP datasets which are used in the examples can be downloaded from https://wa - For bugs relating to MONAI functionality, please create an issue on the [main repository](https://github.com/Project-MONAI/MONAI/issues). - For bugs relating to the running of a tutorial, please create an issue in [this repository](https://github.com/Project-MONAI/Tutorials/issues). - ### 3. List of notebooks and examples + #### [Prepare Your Data](./prepare_patches.py) -This example is used to prepare patches from tiles referring to the implementation from https://github.com/vqdang/hover_net/blob/master/extract_patches.py. Prepared patches will be saved in `data_root`/Prepared. + +This example is used to prepare patches from tiles referring to the implementation from . Prepared patches will be saved in `data_root`/Prepared. ```bash # Run to know all possible options @@ -36,19 +38,19 @@ python ./prepare_patches.py \ ``` #### [HoVerNet Training](./training.py) + This example uses MONAI workflow to train a HoVerNet model on prepared CoNSeP dataset. Since HoVerNet is training via a two-stage approach. First initialised the model with pre-trained weights on the [ImageNet dataset](https://ieeexplore.ieee.org/document/5206848), trained only the decoders for the first 50 epochs, and then fine-tuned all layers for another 50 epochs. We need to specify `--stage` during training. Each user is responsible for checking the content of models/datasets and the applicable licenses and determining if suitable for the intended use. The license for the pre-trained model used in examples is different than MONAI license. Please check the source where these weights are obtained from: -https://github.com/vqdang/hover_net#data-format - + ```bash -# Run to know all possible options +# Run to get all possible command-line arguments python ./training.py -h -# Train a hovernet model on single-gpu(replace with your own ckpt path) +# Train a HoVerNet model on single-gpu(replace with your own ckpt path) export CUDA_VISIBLE_DEVICES=0; python training.py \ --ep 50 \ --stage 0 \ @@ -61,7 +63,7 @@ export CUDA_VISIBLE_DEVICES=0; python training.py \ --root `save_root` \ --ckpt logs/stage0/checkpoint_epoch=50.pt -# Train a hovernet model on multi-gpu (NVIDIA)(replace with your own ckpt path) +# Train a HoVerNet model on multi-gpu (NVIDIA)(replace with your own ckpt path) torchrun --nnodes=1 --nproc_per_node=2 training.py \ --ep 50 \ --bs 8 \ @@ -76,10 +78,12 @@ torchrun --nnodes=1 --nproc_per_node=2 training.py \ ``` #### [HoVerNet Validation](./evaluation.py) + This example uses MONAI workflow to evaluate the trained HoVerNet model on prepared test data from CoNSeP dataset. With their metrics on original mode. We reproduce the results with Dice: 0.82762; PQ: 0.48976; F1d: 0.73592. + ```bash -# Run to know all possible options +# Run to get all possible command-line arguments python ./evaluation.py -h # Evaluate a HoVerNet model @@ -88,6 +92,24 @@ python ./evaluation.py --ckpt logs/stage0/checkpoint_epoch=50.pt ``` +#### [HoVerNet Inference](./inference.py) + +This example uses MONAI workflow to run inference for HoVerNet model on arbitrary sized region of interest. +Under the hood, it will use a sliding window approach to run inference on overlapping patches and then put the results +of the inference together and makes an output image the same size as the input. Then it will run the post-processing on +this output image and create the final results. This example save the instance map and type map as png files but it can +be modified to save any output of interest. + +```bash +# Run to get all possible command-line arguments +python ./inference.py -h + +# Run HoVerNet inference +python ./inference.py + --root `save_root` \ + --ckpt logs/stage0/checkpoint_epoch=50.pt +``` + ## Disclaimer This is an example, not to be used for diagnostic purposes. diff --git a/pathology/hovernet/inference.py b/pathology/hovernet/inference.py index 075c48c442..c0a17a87f8 100644 --- a/pathology/hovernet/inference.py +++ b/pathology/hovernet/inference.py @@ -180,18 +180,17 @@ def main(): parser.add_argument( "--root", type=str, - default="/Users/bhashemian/workspace/project-monai/tutorials/pathology/hovernet/CoNSeP/Test/Images", - help="image root dir", + default="/workspace/Data/Pathology/CoNSeP/Test/Images", + help="Images root dir", ) parser.add_argument("--output", type=str, default="./logs/", dest="output", help="log directory") parser.add_argument( "--ckpt", type=str, - default="/Users/bhashemian/workspace/project-monai/tutorials/pathology/hovernet/model_CoNSeP_new.pth", + default="./logs/stage0/checkpoint_epoch=50.pt", 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") diff --git a/pathology/hovernet/training.py b/pathology/hovernet/training.py index d402dc5e0b..919d4e3cd1 100644 --- a/pathology/hovernet/training.py +++ b/pathology/hovernet/training.py @@ -46,9 +46,7 @@ def create_log_dir(cfg): timestamp = time.strftime("%y%m%d-%H%M") - run_folder_name = ( - f"{timestamp}_hovernet_bs{cfg['batch_size']}_ep{cfg['n_epochs']}_lr{cfg['lr']}_seed{cfg['seed']}_stage{cfg['stage']}" - ) + run_folder_name = f"{timestamp}_hovernet_bs{cfg['batch_size']}_ep{cfg['n_epochs']}_lr{cfg['lr']}_seed{cfg['seed']}_stage{cfg['stage']}" log_dir = os.path.join(cfg["logdir"], run_folder_name) print(f"Logs and model are saved at '{log_dir}'.") if not os.path.exists(log_dir): @@ -58,12 +56,9 @@ def create_log_dir(cfg): def prepare_data(data_dir, phase): # prepare datalist - images = sorted( - glob.glob(os.path.join(data_dir, f"{phase}/*image.npy"))) - inst_maps = sorted( - glob.glob(os.path.join(data_dir, f"{phase}/*inst_map.npy"))) - type_maps = sorted( - glob.glob(os.path.join(data_dir, f"{phase}/*type_map.npy"))) + images = sorted(glob.glob(os.path.join(data_dir, f"{phase}/*image.npy"))) + inst_maps = sorted(glob.glob(os.path.join(data_dir, f"{phase}/*inst_map.npy"))) + type_maps = sorted(glob.glob(os.path.join(data_dir, f"{phase}/*type_map.npy"))) data_dicts = [ {"image": _image, "label_inst": _inst_map, "label_type": _type_map} @@ -104,13 +99,10 @@ def get_loaders(cfg, train_transforms, val_transforms): batch_size=cfg["batch_size"], num_workers=cfg["num_workers"], shuffle=True, - pin_memory=torch.cuda.is_available() + pin_memory=torch.cuda.is_available(), ) val_loader = DataLoader( - valid_ds, - batch_size=cfg["batch_size"], - num_workers=cfg["num_workers"], - pin_memory=torch.cuda.is_available() + valid_ds, batch_size=cfg["batch_size"], num_workers=cfg["num_workers"], pin_memory=torch.cuda.is_available() ) return train_loader, val_loader @@ -144,7 +136,7 @@ def create_model(cfg, device): pretrained_url=None, freeze_encoder=False, ).to(device) - model.load_state_dict(torch.load(cfg["ckpt_path"])['net']) + model.load_state_dict(torch.load(cfg["ckpt_path"])["net"]) print(f'stage{cfg["stage"]}, success load weight!') return model @@ -194,11 +186,13 @@ def run(log_dir, cfg): ), RandFlipd(keys=["image", "label_inst", "label_type"], prob=0.5, spatial_axis=0), RandFlipd(keys=["image", "label_inst", "label_type"], prob=0.5, spatial_axis=1), - OneOf(transforms=[ - RandGaussianSmoothd(keys=["image"], sigma_x=(0.1, 1.1), sigma_y=(0.1, 1.1), prob=1.0), - MedianSmoothd(keys=["image"], radius=1), - RandGaussianNoised(keys=["image"], prob=1.0, std=0.05) - ]), + OneOf( + transforms=[ + RandGaussianSmoothd(keys=["image"], sigma_x=(0.1, 1.1), sigma_y=(0.1, 1.1), prob=1.0), + MedianSmoothd(keys=["image"], radius=1), + RandGaussianNoised(keys=["image"], prob=1.0, std=0.05), + ] + ), CastToTyped(keys="image", dtype=np.uint8), TorchVisiond( keys=["image"], @@ -206,7 +200,7 @@ def run(log_dir, cfg): brightness=(229 / 255.0, 281 / 255.0), contrast=(0.95, 1.10), saturation=(0.8, 1.2), - hue=(-0.04, 0.04) + hue=(-0.04, 0.04), ), AsDiscreted(keys=["label_type"], to_onehot=[5]), ScaleIntensityRanged(keys=["image"], a_min=0.0, a_max=255.0, b_min=0.0, b_max=1.0, clip=True), @@ -258,10 +252,12 @@ def run(log_dir, cfg): loss_function = HoVerNetLoss(lambda_hv_mse=1.0) optimizer = torch.optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr=cfg["lr"], weight_decay=1e-5) lr_scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=25) - post_process_np = Compose([ - Activationsd(keys=HoVerNetBranch.NP.value, softmax=True), - AsDiscreted(keys=HoVerNetBranch.NP.value, argmax=True), - ]) + post_process_np = Compose( + [ + Activationsd(keys=HoVerNetBranch.NP.value, softmax=True), + AsDiscreted(keys=HoVerNetBranch.NP.value, argmax=True), + ] + ) post_process = Lambdad(keys="pred", func=post_process_np) # -------------------------------------------- @@ -282,10 +278,15 @@ def run(log_dir, cfg): evaluator = SupervisedEvaluator( device=device, val_data_loader=val_loader, - prepare_batch=PrepareBatchHoVerNet(extra_keys=['label_type', 'hover_label_inst']), + prepare_batch=PrepareBatchHoVerNet(extra_keys=["label_type", "hover_label_inst"]), network=model, postprocessing=post_process, - key_val_metric={"val_dice": MeanDice(include_background=False, output_transform=from_engine_hovernet(keys=["pred", "label"], nested_key=HoVerNetBranch.NP.value))}, + key_val_metric={ + "val_dice": MeanDice( + include_background=False, + output_transform=from_engine_hovernet(keys=["pred", "label"], nested_key=HoVerNetBranch.NP.value), + ) + }, val_handlers=val_handlers, amp=cfg["amp"], ) @@ -311,12 +312,17 @@ def run(log_dir, cfg): device=device, max_epochs=cfg["n_epochs"], train_data_loader=train_loader, - prepare_batch=PrepareBatchHoVerNet(extra_keys=['label_type', 'hover_label_inst']), + prepare_batch=PrepareBatchHoVerNet(extra_keys=["label_type", "hover_label_inst"]), network=model, optimizer=optimizer, loss_function=loss_function, postprocessing=post_process, - key_train_metric={"train_dice": MeanDice(include_background=False, output_transform=from_engine_hovernet(keys=["pred", "label"], nested_key=HoVerNetBranch.NP.value))}, + key_train_metric={ + "train_dice": MeanDice( + include_background=False, + output_transform=from_engine_hovernet(keys=["pred", "label"], nested_key=HoVerNetBranch.NP.value), + ) + }, train_handlers=train_handlers, amp=cfg["amp"], ) @@ -331,7 +337,7 @@ def main(): parser.add_argument( "--root", type=str, - default="/workspace/Data/CoNSeP/Prepared", + default="/workspace/Data/Pathology/CoNSeP/Prepared", help="root data dir", ) parser.add_argument("--logdir", type=str, default="./logs/", dest="logdir", help="log directory") From d03067ada2bce7f40a2e01e2c52ebf5760695dc2 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Thu, 8 Dec 2022 10:40:49 -0500 Subject: [PATCH 24/27] Remove device Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- pathology/hovernet/inference.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pathology/hovernet/inference.py b/pathology/hovernet/inference.py index c0a17a87f8..d83c551569 100644 --- a/pathology/hovernet/inference.py +++ b/pathology/hovernet/inference.py @@ -154,7 +154,6 @@ def run(cfg): padding_mode="constant", cval=0, sw_device=device, - device=device, progress=True, extra_input_padding=((cfg["patch_size"] - cfg["out_size"]) // 2,) * 4, ) From 045096367abb52405b95234265e86066b49cec59 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Thu, 8 Dec 2022 19:08:41 +0000 Subject: [PATCH 25/27] few improvements Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- pathology/hovernet/README.MD | 53 +++++++++++++++------------ pathology/hovernet/inference.py | 1 - pathology/hovernet/prepare_patches.py | 26 ++++++------- pathology/hovernet/training.py | 21 ++++++----- 4 files changed, 54 insertions(+), 47 deletions(-) diff --git a/pathology/hovernet/README.MD b/pathology/hovernet/README.MD index d6e3da90fd..e362752c4c 100644 --- a/pathology/hovernet/README.MD +++ b/pathology/hovernet/README.MD @@ -13,8 +13,8 @@ Simon Graham et al., HoVer-Net: Simultaneous Segmentation and Classification of CoNSeP datasets which are used in the examples can be downloaded from . -- First download CoNSeP dataset to `data_root`. -- Run prepare_patches.py to prepare patches from images. +- First download CoNSeP dataset to `DATA_ROOT` (default is `"/workspace/Data/Pathology/CoNSeP"`). +- Run `python prepare_patches.py` to prepare patches from images. ### 2. Questions and bugs @@ -26,55 +26,62 @@ CoNSeP datasets which are used in the examples can be downloaded from . Prepared patches will be saved in `data_root`/Prepared. +This example is used to prepare patches from tiles referring to the implementation from . Prepared patches will be saved in `DATA_ROOT`/Prepared. ```bash -# Run to know all possible options +# Run to get all possible arguments python ./prepare_patches.py -h -# Prepare patches from images +# Prepare patches from images using default arguments +python ./prepare_patches.py + +# Prepare patch to use custom arguments python ./prepare_patches.py \ - --root `data_root` + --root `DATA_ROOT` \ + --ps 540 540 \ + --ss 164 164 ``` #### [HoVerNet Training](./training.py) This example uses MONAI workflow to train a HoVerNet model on prepared CoNSeP dataset. -Since HoVerNet is training via a two-stage approach. First initialised the model with pre-trained weights on the [ImageNet dataset](https://ieeexplore.ieee.org/document/5206848), trained only the decoders for the first 50 epochs, and then fine-tuned all layers for another 50 epochs. We need to specify `--stage` during training. +Since HoVerNet is training via a two-stage approach. First initialized the model with pre-trained weights on the [ImageNet dataset](https://ieeexplore.ieee.org/document/5206848), trained only the decoders for the first 50 epochs, and then fine-tuned all layers for another 50 epochs. We need to specify `--stage` during training. Each user is responsible for checking the content of models/datasets and the applicable licenses and determining if suitable for the intended use. The license for the pre-trained model used in examples is different than MONAI license. Please check the source where these weights are obtained from: +If you didn't use the default value in data preparation, set ``--root `DATA_ROOT`/Prepared`` for each of the training commands. + ```bash -# Run to get all possible command-line arguments +# Run to get all possible arguments python ./training.py -h # Train a HoVerNet model on single-gpu(replace with your own ckpt path) export CUDA_VISIBLE_DEVICES=0; python training.py \ - --ep 50 \ --stage 0 \ + --ep 50 \ --bs 16 \ - --root `save_root` + --log-dir ./logs export CUDA_VISIBLE_DEVICES=0; python training.py \ - --ep 50 \ --stage 1 \ - --bs 4 \ - --root `save_root` \ - --ckpt logs/stage0/checkpoint_epoch=50.pt + --ep 50 \ + --bs 16 \ + --log-dir ./logs \ + --ckpt logs/stage0/model.pt # Train a HoVerNet model on multi-gpu (NVIDIA)(replace with your own ckpt path) torchrun --nnodes=1 --nproc_per_node=2 training.py \ - --ep 50 \ - --bs 8 \ - --root `save_root` \ --stage 0 -torchrun --nnodes=1 --nproc_per_node=2 training.py \ --ep 50 \ - --bs 2 \ - --root `save_root` \ + --bs 16 \ + --log-dir ./logs +torchrun --nnodes=1 --nproc_per_node=2 training.py \ --stage 1 \ - --ckpt logs/stage0/checkpoint_epoch=50.pt + --ep 50 \ + --bs 16 \ + --log-dir ./logs \ + --ckpt logs/stage0/model.pt ``` #### [HoVerNet Validation](./evaluation.py) @@ -83,7 +90,7 @@ This example uses MONAI workflow to evaluate the trained HoVerNet model on prepa With their metrics on original mode. We reproduce the results with Dice: 0.82762; PQ: 0.48976; F1d: 0.73592. ```bash -# Run to get all possible command-line arguments +# Run to get all possible arguments python ./evaluation.py -h # Evaluate a HoVerNet model @@ -101,7 +108,7 @@ this output image and create the final results. This example save the instance m be modified to save any output of interest. ```bash -# Run to get all possible command-line arguments +# Run to get all possible arguments python ./inference.py -h # Run HoVerNet inference diff --git a/pathology/hovernet/inference.py b/pathology/hovernet/inference.py index d83c551569..ee91078632 100644 --- a/pathology/hovernet/inference.py +++ b/pathology/hovernet/inference.py @@ -17,7 +17,6 @@ from monai.networks.nets import HoVerNet from monai.transforms import ( CastToTyped, - CenterSpatialCropd, Compose, EnsureChannelFirstd, FromMetaTensord, diff --git a/pathology/hovernet/prepare_patches.py b/pathology/hovernet/prepare_patches.py index bf8e90f9a4..73deca8973 100644 --- a/pathology/hovernet/prepare_patches.py +++ b/pathology/hovernet/prepare_patches.py @@ -1,14 +1,14 @@ -import os -import math -import tqdm import glob -import shutil +import math +import os import pathlib +import shutil +from argparse import ArgumentParser import numpy as np import scipy.io as sio +import tqdm from PIL import Image -from argparse import ArgumentParser def load_img(path): @@ -30,7 +30,7 @@ def load_ann(path): return ann -class PatchExtractor(): +class PatchExtractor: """Extractor to generate patches with or without padding. Turn on debug mode to see how it is done. @@ -42,9 +42,9 @@ class PatchExtractor(): a list of sub patches, each patch has dtype same as x Examples: - >>> xtractor = PatchExtractor((450, 450), (120, 120)) + >>> extractor = PatchExtractor((450, 450), (120, 120)) >>> img = np.full([1200, 1200, 3], 255, np.uint8) - >>> patches = xtractor.extract(img, 'mirror') + >>> patches = extractor.extract(img, 'mirror') """ @@ -166,9 +166,7 @@ def main(cfg): os.makedirs(out_dir) pbar_format = "Process File: |{bar}| {n_fmt}/{total_fmt}[{elapsed}<{remaining},{rate_fmt}]" - pbarx = tqdm.tqdm( - total=len(file_list), bar_format=pbar_format, ascii=True, position=0 - ) + pbarx = tqdm.tqdm(total=len(file_list), bar_format=pbar_format, ascii=True, position=0) for file_path in file_list: base_name = pathlib.Path(file_path).stem @@ -210,12 +208,12 @@ def parse_arguments(): parser.add_argument( "--root", type=str, - default="/home/yunliu/Workspace/Data/CoNSeP", + default="/workspace/Data/Pathology/CoNSeP", help="root path to image folder containing training/test", ) parser.add_argument("--type", type=str, default="mirror", dest="extract_type", help="Choose 'mirror' or 'valid'") - parser.add_argument("--ps", nargs='+', type=int, default=[540, 540], dest="patch_size", help="patch size") - parser.add_argument("--ss", nargs='+', type=int, default=[164, 164], dest="step_size", help="patch size") + parser.add_argument("--ps", nargs="+", type=int, default=[540, 540], dest="patch_size", help="patch size") + parser.add_argument("--ss", nargs="+", type=int, default=[164, 164], dest="step_size", help="patch size") args = parser.parse_args() config_dict = vars(args) diff --git a/pathology/hovernet/training.py b/pathology/hovernet/training.py index 919d4e3cd1..9cb0af2c92 100644 --- a/pathology/hovernet/training.py +++ b/pathology/hovernet/training.py @@ -45,10 +45,10 @@ def create_log_dir(cfg): - timestamp = time.strftime("%y%m%d-%H%M") - run_folder_name = f"{timestamp}_hovernet_bs{cfg['batch_size']}_ep{cfg['n_epochs']}_lr{cfg['lr']}_seed{cfg['seed']}_stage{cfg['stage']}" - log_dir = os.path.join(cfg["logdir"], run_folder_name) - print(f"Logs and model are saved at '{log_dir}'.") + log_dir = cfg["log_dir"] + if cfg["stage"] == 0: + log_dir = os.path.join(log_dir, "stage0") + print(f"Logs and models are saved at '{log_dir}'.") if not os.path.exists(log_dir): os.makedirs(log_dir, exist_ok=True) return log_dir @@ -164,7 +164,6 @@ def run(log_dir, cfg): # Data Loading and Preprocessing # -------------------------------------------------------------------------- # __________________________________________________________________________ - # __________________________________________________________________________ # Build MONAI preprocessing train_transforms = Compose( [ @@ -299,6 +298,8 @@ def run(log_dir, cfg): save_dir=log_dir, save_dict={"net": model, "opt": optimizer}, save_interval=cfg["save_interval"], + save_final=True, + final_filename="model.pt", epoch_level=True, ), StatsHandler(tag_name="train_loss", output_transform=from_engine(["loss"], first=True)), @@ -340,15 +341,15 @@ def main(): default="/workspace/Data/Pathology/CoNSeP/Prepared", help="root data dir", ) - parser.add_argument("--logdir", type=str, default="./logs/", dest="logdir", help="log directory") + parser.add_argument("--log-dir", type=str, default="./logs/", help="log directory") parser.add_argument("-s", "--seed", type=int, default=24) parser.add_argument("--bs", type=int, default=16, dest="batch_size", help="batch size") - parser.add_argument("--ep", type=int, default=3, dest="n_epochs", help="number of epochs") + parser.add_argument("--ep", type=int, default=50, dest="n_epochs", help="number of epochs") parser.add_argument("--lr", type=float, default=1e-4, dest="lr", help="initial learning rate") parser.add_argument("--step", type=int, default=25, dest="step_size", help="period of learning rate decay") parser.add_argument("-f", "--val_freq", type=int, default=1, help="validation frequence") - parser.add_argument("--stage", type=int, default=0, dest="stage", help="training stage") + parser.add_argument("--stage", type=int, default=0, help="training stage") parser.add_argument("--no-amp", action="store_false", dest="amp", help="deactivate amp") parser.add_argument("--classes", type=int, default=5, dest="out_classes", help="output classes") parser.add_argument("--mode", type=str, default="original", help="choose either `original` or `fast`") @@ -356,10 +357,12 @@ def main(): parser.add_argument("--save_interval", type=int, default=10) parser.add_argument("--cpu", type=int, default=8, dest="num_workers", help="number of workers") parser.add_argument("--no-gpu", action="store_false", dest="use_gpu", help="deactivate use of gpu") - parser.add_argument("--ckpt", type=str, dest="ckpt_path", help="checkpoint path") + parser.add_argument("--ckpt", type=str, dest="ckpt_path", help="model checkpoint path") args = parser.parse_args() cfg = vars(args) + if cfg["stage"] == 1 and not cfg["ckpt_path"] and cfg["log_dir"]: + cfg["ckpt_path"] = os.path.join(cfg["log_dir"], "stage0", "model.pt") print(cfg) logging.basicConfig(level=logging.INFO) From 56d2e98f69aa4e6c44c0033623ca978c475368a9 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Thu, 8 Dec 2022 19:24:16 +0000 Subject: [PATCH 26/27] improvments and bug fix Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- pathology/hovernet/evaluation.py | 54 ++++++++++++++++++-------------- pathology/hovernet/inference.py | 8 ++--- pathology/hovernet/training.py | 23 +++++++------- 3 files changed, 46 insertions(+), 39 deletions(-) diff --git a/pathology/hovernet/evaluation.py b/pathology/hovernet/evaluation.py index 476da843ca..723c4e45d8 100644 --- a/pathology/hovernet/evaluation.py +++ b/pathology/hovernet/evaluation.py @@ -28,21 +28,18 @@ def prepare_data(data_dir, phase): - data_dir = os.path.join(data_dir, phase) + """prepare data list""" - images = list(sorted( - glob.glob(os.path.join(data_dir, "*/*image.npy")))) - inst_maps = list(sorted( - glob.glob(os.path.join(data_dir, "*/*inst_map.npy")))) - type_maps = list(sorted( - glob.glob(os.path.join(data_dir, "*/*type_map.npy")))) + data_dir = os.path.join(data_dir, phase) + images = sorted(glob.glob(os.path.join(data_dir, "*image.npy"))) + inst_maps = sorted(glob.glob(os.path.join(data_dir, "*inst_map.npy"))) + type_maps = sorted(glob.glob(os.path.join(data_dir, "*type_map.npy"))) - data_dicts = [ + data_list = [ {"image": _image, "label_inst": _inst_map, "label_type": _type_map} for _image, _inst_map, _type_map in zip(images, inst_maps, type_maps) ] - - return data_dicts + return data_list def run(cfg): @@ -75,13 +72,10 @@ def run(cfg): ) # Create MONAI DataLoaders - valid_data = prepare_data(cfg["root"], "valid") + valid_data = prepare_data(cfg["root"], "Test") valid_ds = CacheDataset(data=valid_data, transform=val_transforms, cache_rate=1.0, num_workers=4) val_loader = DataLoader( - valid_ds, - batch_size=cfg["batch_size"], - num_workers=cfg["num_workers"], - pin_memory=torch.cuda.is_available() + valid_ds, batch_size=cfg["batch_size"], num_workers=cfg["num_workers"], pin_memory=torch.cuda.is_available() ) # initialize model @@ -95,23 +89,31 @@ def run(cfg): freeze_encoder=False, ).to(device) - post_process_np = Compose([ - Activationsd(keys=HoVerNetBranch.NP.value, softmax=True), - Lambdad(keys=HoVerNetBranch.NP.value, func=lambda x: x[1: 2, ...] > 0.5)]) + post_process_np = Compose( + [ + Activationsd(keys=HoVerNetBranch.NP.value, softmax=True), + Lambdad(keys=HoVerNetBranch.NP.value, func=lambda x: x[1:2, ...] > 0.5), + ] + ) post_process = Lambdad(keys="pred", func=post_process_np) # Evaluator val_handlers = [ - CheckpointLoader(load_path=cfg["ckpt_path"], load_dict={"net": model}), + CheckpointLoader(load_path=cfg["ckpt"], load_dict={"net": model}), StatsHandler(output_transform=lambda x: None), ] evaluator = SupervisedEvaluator( device=device, val_data_loader=val_loader, - prepare_batch=PrepareBatchHoVerNet(extra_keys=['label_type', 'hover_label_inst']), + prepare_batch=PrepareBatchHoVerNet(extra_keys=["label_type", "hover_label_inst"]), network=model, postprocessing=post_process, - key_val_metric={"val_dice": MeanDice(include_background=False, output_transform=from_engine_hovernet(keys=["pred", "label"], nested_key=HoVerNetBranch.NP.value))}, + key_val_metric={ + "val_dice": MeanDice( + include_background=False, + output_transform=from_engine_hovernet(keys=["pred", "label"], nested_key=HoVerNetBranch.NP.value), + ) + }, val_handlers=val_handlers, amp=cfg["amp"], ) @@ -125,10 +127,15 @@ def main(): parser.add_argument( "--root", type=str, - default="/workspace/Data/CoNSeP/Prepared/consep", + default="/workspace/Data/Pathology/CoNSeP/Prepared", help="root data dir", ) - + parser.add_argument( + "--ckpt", + type=str, + default="./logs/model.pt", + help="Path to the pytorch checkpoint", + ) parser.add_argument("--bs", type=int, default=16, dest="batch_size", help="batch size") parser.add_argument("--no-amp", action="store_false", dest="amp", help="deactivate amp") parser.add_argument("--classes", type=int, default=5, dest="out_classes", help="output classes") @@ -136,7 +143,6 @@ def main(): parser.add_argument("--cpu", type=int, default=8, dest="num_workers", help="number of workers") parser.add_argument("--use_gpu", type=bool, default=True, dest="use_gpu", help="whether to use gpu") - parser.add_argument("--ckpt", type=str, dest="ckpt_path", help="checkpoint path") args = parser.parse_args() cfg = vars(args) diff --git a/pathology/hovernet/inference.py b/pathology/hovernet/inference.py index ee91078632..009f5595b3 100644 --- a/pathology/hovernet/inference.py +++ b/pathology/hovernet/inference.py @@ -30,7 +30,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']}" + run_folder_name = f"inference_hovernet_ps{cfg['patch_size']}_{timestamp}" 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): @@ -135,7 +135,7 @@ def run(cfg): in_channels=3, out_classes=cfg["out_classes"], ).to(device) - model.load_state_dict(torch.load(cfg["ckpt"], map_location=device)) + model.load_state_dict(torch.load(cfg["ckpt"], map_location=device)["net"]) model.eval() if multi_gpu: model = torch.nn.parallel.DistributedDataParallel( @@ -181,11 +181,11 @@ def main(): default="/workspace/Data/Pathology/CoNSeP/Test/Images", help="Images root dir", ) - parser.add_argument("--output", type=str, default="./logs/", dest="output", help="log directory") + parser.add_argument("--output", type=str, default="./eval/", dest="output", help="log directory") parser.add_argument( "--ckpt", type=str, - default="./logs/stage0/checkpoint_epoch=50.pt", + default="./logs/model.pt", help="Path to the pytorch checkpoint", ) parser.add_argument("--mode", type=str, default="original", help="HoVerNet mode (original/fast)") diff --git a/pathology/hovernet/training.py b/pathology/hovernet/training.py index 9cb0af2c92..6c79e9f74c 100644 --- a/pathology/hovernet/training.py +++ b/pathology/hovernet/training.py @@ -55,17 +55,18 @@ def create_log_dir(cfg): def prepare_data(data_dir, phase): - # prepare datalist - images = sorted(glob.glob(os.path.join(data_dir, f"{phase}/*image.npy"))) - inst_maps = sorted(glob.glob(os.path.join(data_dir, f"{phase}/*inst_map.npy"))) - type_maps = sorted(glob.glob(os.path.join(data_dir, f"{phase}/*type_map.npy"))) + """prepare data list""" - data_dicts = [ + data_dir = os.path.join(data_dir, phase) + images = sorted(glob.glob(os.path.join(data_dir, "*image.npy"))) + inst_maps = sorted(glob.glob(os.path.join(data_dir, "*inst_map.npy"))) + type_maps = sorted(glob.glob(os.path.join(data_dir, "*type_map.npy"))) + + data_list = [ {"image": _image, "label_inst": _inst_map, "label_type": _type_map} for _image, _inst_map, _type_map in zip(images, inst_maps, type_maps) ] - - return data_dicts + return data_list def get_loaders(cfg, train_transforms, val_transforms): @@ -136,7 +137,7 @@ def create_model(cfg, device): pretrained_url=None, freeze_encoder=False, ).to(device) - model.load_state_dict(torch.load(cfg["ckpt_path"])["net"]) + model.load_state_dict(torch.load(cfg["ckpt"])["net"]) print(f'stage{cfg["stage"]}, success load weight!') return model @@ -357,12 +358,12 @@ def main(): parser.add_argument("--save_interval", type=int, default=10) parser.add_argument("--cpu", type=int, default=8, dest="num_workers", help="number of workers") parser.add_argument("--no-gpu", action="store_false", dest="use_gpu", help="deactivate use of gpu") - parser.add_argument("--ckpt", type=str, dest="ckpt_path", help="model checkpoint path") + parser.add_argument("--ckpt", type=str, dest="ckpt", help="model checkpoint path") args = parser.parse_args() cfg = vars(args) - if cfg["stage"] == 1 and not cfg["ckpt_path"] and cfg["log_dir"]: - cfg["ckpt_path"] = os.path.join(cfg["log_dir"], "stage0", "model.pt") + if cfg["stage"] == 1 and not cfg["ckpt"] and cfg["log_dir"]: + cfg["ckpt"] = os.path.join(cfg["log_dir"], "stage0", "model.pt") print(cfg) logging.basicConfig(level=logging.INFO) From 8df9008626a8494e5fce356ead77d8628f9740a6 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Thu, 8 Dec 2022 20:07:18 +0000 Subject: [PATCH 27/27] Update default run Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- pathology/hovernet/README.MD | 37 +++++++++++++++------------------ pathology/hovernet/inference.py | 4 +--- 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/pathology/hovernet/README.MD b/pathology/hovernet/README.MD index e362752c4c..b5bdc4ac58 100644 --- a/pathology/hovernet/README.MD +++ b/pathology/hovernet/README.MD @@ -57,7 +57,7 @@ If you didn't use the default value in data preparation, set ``--root `DATA_ROOT # Run to get all possible arguments python ./training.py -h -# Train a HoVerNet model on single-gpu(replace with your own ckpt path) +# Train a HoVerNet model on single-GPU or CPU-only (replace with your own ckpt path) export CUDA_VISIBLE_DEVICES=0; python training.py \ --stage 0 \ --ep 50 \ @@ -70,18 +70,9 @@ export CUDA_VISIBLE_DEVICES=0; python training.py \ --log-dir ./logs \ --ckpt logs/stage0/model.pt -# Train a HoVerNet model on multi-gpu (NVIDIA)(replace with your own ckpt path) -torchrun --nnodes=1 --nproc_per_node=2 training.py \ - --stage 0 - --ep 50 \ - --bs 16 \ - --log-dir ./logs -torchrun --nnodes=1 --nproc_per_node=2 training.py \ - --stage 1 \ - --ep 50 \ - --bs 16 \ - --log-dir ./logs \ - --ckpt logs/stage0/model.pt +# Train a HoVerNet model on multi-GPU with default arguments +torchrun --nnodes=1 --nproc_per_node=2 training.py +torchrun --nnodes=1 --nproc_per_node=2 training.py --stage 1 ``` #### [HoVerNet Validation](./evaluation.py) @@ -93,10 +84,13 @@ With their metrics on original mode. We reproduce the results with Dice: 0.82762 # Run to get all possible arguments python ./evaluation.py -h -# Evaluate a HoVerNet model -python ./evaluation.py +# Evaluate a HoVerNet model on single-GPU or CPU-only +python ./evaluation.py \ --root `save_root` \ - --ckpt logs/stage0/checkpoint_epoch=50.pt + --ckpt logs/stage0/model.pt + +# Evaluate a HoVerNet model on multi-GPU with default arguments +torchrun --nnodes=1 --nproc_per_node=2 evaluation.py ``` #### [HoVerNet Inference](./inference.py) @@ -111,10 +105,13 @@ be modified to save any output of interest. # Run to get all possible arguments python ./inference.py -h -# Run HoVerNet inference -python ./inference.py - --root `save_root` \ - --ckpt logs/stage0/checkpoint_epoch=50.pt +# Run HoVerNet inference on single-GPU or CPU-only +python ./inference.py \ + --root `save_root` \ + --ckpt logs/stage0/model.pt + +# Run HoVerNet inference on multi-GPU with default arguments +torchrun --nnodes=1 --nproc_per_node=2 ./inference.py ``` ## Disclaimer diff --git a/pathology/hovernet/inference.py b/pathology/hovernet/inference.py index 009f5595b3..5af7ef9d82 100644 --- a/pathology/hovernet/inference.py +++ b/pathology/hovernet/inference.py @@ -29,9 +29,7 @@ def create_output_dir(cfg): - timestamp = time.strftime("%y%m%d-%H%M%S") - run_folder_name = f"inference_hovernet_ps{cfg['patch_size']}_{timestamp}" - output_dir = os.path.join(cfg["output"], run_folder_name) + output_dir = cfg["output"] print(f"Outputs are saved at '{output_dir}'.") if not os.path.exists(output_dir): os.makedirs(output_dir)