NuHTC: A Hybrid Task Cascade for Nuclei Instance Segmentation and Classification

April 24, 2026 Β· View on GitHub

Bao Li, et al.

Paper | Docker | Setup | Quick start | Preprocessing | Train | Test | Infer | WSI Segmentation | Feature Extraction | Citation

This repo is the official implementation of NuHTC.

Browser demo on the Hugging Face Space: Web demo

Overlaid Segmentation and Classification Prediction

The demo may take around 10s to load.

🐳 Docker

The official image is on GitHub Container Registry. No local Python environment or separate weight download is required. See DOCKER.md for details.

Pull & run

docker pull ghcr.io/boyden/nuhtc:latest

Patch-level inference

docker run --gpus all --rm --shm-size=32g \
  -v /path/to/your/pngs:/data/imgs \
  -v /path/to/output:/data/imgs_infer \
  ghcr.io/boyden/nuhtc:latest -c "python tools/infer.py /data/imgs \
    models/htc_lite_PanNuke_infer.py \
    models/pannuke.pth --output /data/imgs_infer"

WSI inference

# Patches at 256Γ—256 (~40Γ—, stride 192).
docker run --gpus all --rm --shm-size=32g \
  -v /path/to/your/input:/data/wsi \
  -v /path/to/output:/data/wsi_infer \
  ghcr.io/boyden/nuhtc:latest -c "python tools/infer_wsi.py /data/wsi \
    models/htc_lite_PanNuke_infer.py \
    models/pannuke.pth --patch --seg --stitch \
    --patch_size 256 --step_size 192 --batch_size 16 \
    --save_dir /data/wsi_infer --mode qupath"

After segmentation, merge cross-patch overlapping nuclei (mask-NMS) so each cell appears once in the GeoJSON:

docker run --gpus all --rm \
  -v /path/to/output:/data/wsi_infer \
  ghcr.io/boyden/nuhtc:latest -c "python tools/nuclei_merge.py \
    --geojson /data/wsi_infer/nuclei/<SLIDE_ID>/<SLIDE_ID>.geojson \
    --overlap_threshold 0.05 --merge_strategy probability"

Load the <SLIDE_ID>_merged.geojson version in QuPath for cleaner overlays.

πŸ‘‰ Setup Environment

Set up the Python environment

# Note, please follow the env.
conda create -n nuhtc -y python=3.10 
conda activate nuhtc
conda install pytorch==1.13.1 torchvision==0.14.1 torchaudio==0.13.1 pytorch-cuda=11.6 -c pytorch -c nvidia
# or
# pip install torch==1.13.1+cu116 torchvision==0.14.1+cu116 torchaudio==0.13.1 --extra-index-url https://download.pytorch.org/whl/cu116
pip install -r requirements.txt
pip install mmcv-full==1.7.2 -f https://download.openmmlab.com/mmcv/dist/cu116/torch1.13/index.html
python -m pip install histomicstk==1.2.10 --find-links https://girder.github.io/large_image_wheels -i https://pypi.org/simple

πŸ‘‰ Quick start

When setup is finished, download pannuke.pth from Google Drive and save it as models/pannuke.pth. Run:

python tools/infer.py demo/imgs configs/nuhtc/htc_lite_swin_pytorch_fpn_PanNuke_seasaw_CAS.py models/pannuke.pth --out demo/imgs_infer

Segmentation overlays are saved as PNGs in demo/imgs_infer. Open that folder to view results.

πŸ‘‰ Preprocessing data

First please download and unzip the files from PanNuke dataset, where the folder structure should look like this:

NuHTC
β”œβ”€β”€ ...
β”œβ”€β”€ datasets
β”‚   β”œβ”€β”€ PanNuke
β”‚   β”‚   β”œβ”€β”€ images
β”‚   β”‚   β”‚   β”œβ”€β”€ fold1
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ images.npy
β”‚   β”‚   β”‚   β”œβ”€β”€ fold2
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ images.npy
β”‚   β”‚   β”‚   β”œβ”€β”€ fold3
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ images.npy
β”‚   β”‚   β”œβ”€β”€ masks
β”‚   β”‚   β”‚   β”œβ”€β”€ fold1
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ masks.npy
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ types.npy
β”‚   β”‚   β”‚   β”œβ”€β”€ fold2
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ masks.npy
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ types.npy
β”‚   β”‚   β”‚   β”œβ”€β”€ fold3
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ masks.npy
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ types.npy
β”œβ”€β”€ ...

For the coco format annotation, please download the coco folder json file from Google Drive

NuHTC
β”œβ”€β”€ ...
β”œβ”€β”€ coco
β”‚   β”œβ”€β”€ PanNuke
β”‚   β”‚   β”œβ”€β”€ PanNuke_annt_RLE_fold1.json
β”‚   β”‚   β”œβ”€β”€ PanNuke_annt_RLE_fold2.json
β”‚   β”‚   β”œβ”€β”€ PanNuke_annt_RLE_fold3.json
β”œβ”€β”€ ...

Then, generate png files for training and testing.

import os
import numpy as np
from PIL import Image
from tqdm import tqdm

basedir = './datasets/PanNuke'
for fold in range(3):
    print(f'Preprocessing images: fold{fold+1}')
    imgdir = f'{basedir}/images/fold{fold+1}'
    img_data = np.load(f'{imgdir}/images.npy', mmap_mode='r')
    for i in tqdm(range(img_data.shape[0])):
        img = Image.fromarray(img_data[i].astype(np.uint8))
        os.makedirs(f'{basedir}/rgb', exist_ok=True)
        if not os.path.exists(f'{basedir}/rgb/fold{fold+1}_{i+1}.png'):
            img.convert('RGB').save(f'{basedir}/rgb/fold{fold+1}_{i+1}.png')

for fold in range(3):
    print(f'Preprocessing masks: fold{fold+1}')
    imgdir = f'{basedir}/masks/fold{fold+1}'
    img_data = np.load(f'{imgdir}/masks.npy', mmap_mode='r')
    for i in tqdm(range(img_data.shape[0])):
        img = 1 - img_data[i, :, :, 5]
        img = Image.fromarray(img.astype(np.uint8))
        os.makedirs(f'{basedir}/rgb_seg', exist_ok=True)
        if not os.path.exists(f'{basedir}/rgb_seg/fold{fold+1}_{i+1}.png'):
            img.save(f'{basedir}/rgb_seg/fold{fold+1}_{i+1}.png')

πŸ‘‰ Train

This is an example of training NuHTC on the first fold. To train on other folds, update the fold = 1 content in htc_lite_swin_pytorch_fpn_PanNuke_seasaw_CAS.py to other folds.

CUDA_VISIBLE_DEVICES=0 python tools/train.py configs/nuhtc/htc_lite_swin_pytorch_fpn_PanNuke_seasaw_CAS.py --no-validate

Note, recent update (~May 2024, driver version 555.85, 555.99, 556.12) of Nvidia driver may lead to UnicodeDecodeError: 'utf-8' codec can't decode byte 0xf8 in position 0: invalid start byte in init wandb package. If your Nvidia driver version is greater than 552.44, downgrade to the Nvidia 552.44 studio driver or update to a version newer than 560.70 to train the models successfully. For more details, see the wandb issue.

πŸ‘‰ Test

CONFIG_NAME=htc_lite_swin_pytorch_fpn_PanNuke_seasaw_CAS.py
WEIGHT_BASE_PATH=work_dirs/htc_lite_swin_pytorch_seasaw_FPN_AttenROI_thres_96_base_aug_cas_PanNuke_full_epoch_200_fold1

# predict nuclei from images
CUDA_VISIBLE_DEVICES=0 python tools/test.py $WEIGHT_BASE_PATH/$CONFIG_NAME $WEIGHT_BASE_PATH/latest.pth \
--eval bbox --samples_per_gpu 16 \
--eval-options save=True format=pannuke save_path=$WEIGHT_BASE_PATH overlay=False

# calculate the metric
python tools/analysis_tools/pannuke/compute_stats.py --true_path=datasets/PanNuke/masks/fold3/masks.npy --type_path=datasets/PanNuke/masks/fold3/types.npy \
--pred_path=$WEIGHT_BASE_PATH/PanNukeCocoDataset/preds_pannuke.npy --save_path=$WEIGHT_BASE_PATH

πŸ‘‰ Infer

Our trained checkpoint can be downloaded from the models folder in the Google Drive.

# Segment image by image
CUDA_VISIBLE_DEVICES=0 python tools/infer.py demo/imgs configs/nuhtc/htc_lite_swin_pytorch_fpn_PanNuke_seasaw_CAS.py models/pannuke.pth --out demo/imgs_infer

πŸš€ Segment the Whole Slide Image

Run WSI segmentation and export results as qupath, sql, dsa, or coco. Magnification is not auto-selectedβ€”use the scale that matches your slides (default pipeline assumes 40Γ—).

Note: QuPath export includes both point and contour representations for nuclei. COCO is currently used only for per-patch segmentation outputs.

  1. WSI Segmentation
CUDA_VISIBLE_DEVICES=0 python tools/infer_wsi.py demo/wsi configs/nuhtc/htc_lite_swin_pytorch_fpn_PanNuke_seasaw_CAS.py models/pannuke.pth \
--patch --seg --stitch --patch_size 256 --step_size 192 --margin 1 --min_area 10 \
--batch_size 32 --save_dir demo/wsi_infer --mode qupath
  1. Merge Overlapping Nuclei

After segmentation, mask non-maximum suppression (NMS) is applied to the WSI to remove the overlapping nuclei.

# --geojson: path to the slide segmentation .geojson file (e.g. from infer_wsi: .../nuclei/<SLIDE_ID>/<SLIDE_ID>.geojson).
python tools/nuclei_merge.py \
--geojson demo/wsi_res/TCGA-AC-A2FK-01Z-00-DX1.033F3C27-9860-4EF3-9330-37DE5EC45724.geojson \
--overlap_threshold 0.05 --merge_strategy probability

We provide a WSI example from TCGA (filename: TCGA-AC-A2FK-01Z-00-DX1.033F3C27-9860-4EF3-9330-37DE5EC45724.svs), which includes the geojson file for both nuclei points and contours. These can be easily dragged into, viewed, and edited using QuPath (checked in QuPath Version 0.5.1). The WSI example can be downloaded from Google Drive.

The dsa is a format supported by Digital Slide Archive, a powerful containerized web-based platform for storing, managing, viewing, and analysing WSIs. If you are interested in using the DSA platform, please refer to its deployment instructions.

Our model is trained with a patch size 256Γ—256 at 40Γ— magnification. During inference, it maintains strong performance even when evaluated with a larger patch size of 512Γ—512. To run inference using 512Γ—512 patches, please specify the arguments --patch_size 512 --step_size 448.

πŸ”¬ Extract the Nuclei Feature

Make sure you have successfully installed the histomicstk. We support two approaches for extracting nucleus features:

  1. Extract a patch centered on each nucleus and then compute features individually (recommended).

  2. Tile the WSI and extract the nucleus features from each tile sequentially.

We recommend using the first way. Here is an example:

# demo/wsi is the path to the folder containing raw WSI image files
# segdir is the path to the folder containing segmentation files
python tools/wsi_feat_extract.py demo/wsi --segdir demo/wsi_res --mag 40

For the second way that tiles images first, please specify --mode coco or --mode all during WSI inference.

python tools/nuclei_feat_extract.py demo/wsi_res
# datadir (str)
# Path to the folder containing raw WSI image files.

# --start (int, default: 0)
# Starting index of the slides to process. Useful for batching or parallel execution.
# --end (int, default: None)
# Ending index (exclusive) of the slides to process. If not specified, all remaining slides will be processed.
# --min_num (int, default: 8)
# Minimum number of nuclei required in a patch. Patches with fewer nuclei will be excluded.
# --patch_size (int, default: 512)
# Size (in pixels) of each image patch. Should match the expected input size (e.g., 256 or 512 for 40Γ— resolution) used during inference.
# --reverse (flag, default: False)
# If specified, slide IDs will be processed in reverse order.

It will extract the nuclei feature for each image and then store them in a csv file. The following is an example for the nuclei feature csv file.

LabelIdentifier.XminIdentifier.YminIdentifier.XmaxIdentifier.YmaxIdentifier.CentroidXIdentifier.CentroidYIdentifier.WeightedCentroidXIdentifier.WeightedCentroidYOrientation.OrientationSize.AreaSize.ConvexHullAreaSize.MajorAxisLengthSize.MinorAxisLengthSize.PerimeterShape.CircularityShape.EccentricityShape.EquivalentDiameterShape.ExtentShape.FractalDimensionShape.MinorMajorAxisRatioShape.SolidityShape.HuMoments1Shape.HuMoments2Shape.HuMoments3Shape.HuMoments4Shape.HuMoments5Shape.HuMoments6Shape.HuMoments7Shape.WeightedHuMoments1Shape.WeightedHuMoments2Shape.WeightedHuMoments3Shape.WeightedHuMoments4Shape.WeightedHuMoments5Shape.WeightedHuMoments6Shape.WeightedHuMoments7Shape.FSD1Shape.FSD2Shape.FSD3Shape.FSD4Shape.FSD5Shape.FSD6Nucleus.Intensity.MinNucleus.Intensity.MaxNucleus.Intensity.MeanNucleus.Intensity.MedianNucleus.Intensity.MeanMedianDiffNucleus.Intensity.StdNucleus.Intensity.IQRNucleus.Intensity.MADNucleus.Intensity.SkewnessNucleus.Intensity.KurtosisNucleus.Intensity.HistEnergyNucleus.Intensity.HistEntropyNucleus.Gradient.Mag.MeanNucleus.Gradient.Mag.StdNucleus.Gradient.Mag.SkewnessNucleus.Gradient.Mag.KurtosisNucleus.Gradient.Mag.HistEntropyNucleus.Gradient.Mag.HistEnergyNucleus.Gradient.Canny.SumNucleus.Gradient.Canny.MeanNucleus.Haralick.ASM.MeanNucleus.Haralick.ASM.RangeNucleus.Haralick.Contrast.MeanNucleus.Haralick.Contrast.RangeNucleus.Haralick.Correlation.MeanNucleus.Haralick.Correlation.RangeNucleus.Haralick.SumOfSquares.MeanNucleus.Haralick.SumOfSquares.RangeNucleus.Haralick.IDM.MeanNucleus.Haralick.IDM.RangeNucleus.Haralick.SumAverage.MeanNucleus.Haralick.SumAverage.RangeNucleus.Haralick.SumVariance.MeanNucleus.Haralick.SumVariance.RangeNucleus.Haralick.SumEntropy.MeanNucleus.Haralick.SumEntropy.RangeNucleus.Haralick.Entropy.MeanNucleus.Haralick.Entropy.RangeNucleus.Haralick.DifferenceVariance.MeanNucleus.Haralick.DifferenceVariance.RangeNucleus.Haralick.DifferenceEntropy.MeanNucleus.Haralick.DifferenceEntropy.RangeNucleus.Haralick.IMC1.MeanNucleus.Haralick.IMC1.RangeNucleus.Haralick.IMC2.MeanNucleus.Haralick.IMC2.Rangecell_typeimg_idimg_typeimg_objsfile_name
2111322402356442338.111421.776337.964422.0380.3191028.0001079.00040.40032.539131.6980.7450.59336.1790.7560.8510.8050.9530.1640.0010.0000.000-0.000-0.000-0.0000.0010.0000.0000.0000.000-0.0000.0000.2310.0030.0040.0060.1190.30794.000251.000210.930214.000-3.07021.92826.00013.000-1.1932.7620.2241.67710.84910.6231.9333.9101.3940.340139.0000.1350.0210.0054.8974.2110.9500.04549.7062.7710.5630.08044.0980.556193.92915.2965.1820.0586.7600.3830.0080.0022.2520.432-0.3990.1070.9820.017E28C3228.png
2122260464294506276.493484.931276.498484.7680.2741076.0001141.00042.79332.271136.2840.7280.65737.0140.7540.9210.7540.9430.1670.0020.0000.0000.0000.0000.0000.0010.0000.0000.000-0.000-0.000-0.0000.3910.0020.0050.0100.0400.27099.000252.000218.624223.500-4.87621.56721.00010.500-1.8695.0510.2611.57911.73413.0131.9463.1751.4930.332179.0000.1660.0280.0135.4756.9510.9460.07051.3002.9370.5520.17346.6750.499199.72316.8084.9700.0526.4880.5960.0070.0032.3020.703-0.3810.1660.9740.032E28C3228.png
2133292446328486309.592465.089309.533465.186-0.4641128.0001186.00042.41334.066136.8700.7570.59637.8970.7830.8050.8030.9510.1640.0010.0000.000-0.000-0.0000.0000.0010.0000.0000.000-0.000-0.000-0.0000.4640.0010.0040.0090.0320.23275.000249.000206.606215.000-8.39427.99633.00014.000-1.3802.0140.2261.72811.47610.9011.6742.2241.6550.262165.0000.1460.0250.0053.7732.7980.9490.04037.2972.1460.5680.10145.5450.499145.41711.3845.0820.0506.5500.3380.0080.0022.1470.414-0.4080.1000.9820.015E28C3228.png
2144328448360486342.695466.659342.382466.577-0.532904.000954.00040.65328.492124.8700.7290.71333.9270.7431.0740.7010.9480.1700.0030.0000.0000.0000.0000.0000.0010.0000.0000.000-0.000-0.0000.0000.4200.0000.0020.0060.1380.202101.000245.000185.750184.0001.75025.64430.00015.000-0.014-0.0120.1651.9699.3955.3120.8510.6422.0060.154150.0000.1660.0170.0061.7861.3590.9630.03024.2751.3080.5970.14240.4200.42695.3156.5105.1730.0446.4500.5090.0100.0031.7540.396-0.4640.1250.9890.012E28C3228.png
2155266404316454290.478427.398290.924427.688-0.7851808.0001892.00060.81638.141180.7700.6950.77947.9790.7230.8260.6270.9560.1780.0060.0000.0000.0000.000-0.0000.0010.0000.0000.0000.0000.000-0.0000.5080.0050.0090.0120.0090.21270.000255.000196.299201.000-4.70130.02436.00018.000-0.8861.0160.1891.84112.02010.0982.0424.5911.4570.298346.0000.1910.0120.0044.3004.4450.9520.05144.9631.1190.5300.12941.7600.324175.5508.6105.4440.0327.1760.5620.0080.0032.2040.547-0.3930.1310.9840.020E28C3228.png

πŸ—“οΈ Ongoing

  • Merge overlapping nuclei during WSI segmentation
  • Support nuclei feature extraction from WSIs
  • Add support for Docker

πŸ“– Citation

@article{li2025nuhtc,
  title={NuHTC: A hybrid task cascade for nuclei instance segmentation and classification},
  author={Li, Bao and Liu, Zhenyu and Zhang, Song and Liu, Xiangyu and Sun, Caixia and Liu, Jiangang and Qiu, Bensheng and Tian, Jie},
  journal={Medical Image Analysis},
  volume={103},
  pages={103595},
  year={2025},
  publisher={Elsevier}
}