A compact, fully distributed algorithm that extracts edges from RGB images through four stages:
-
L0 — pixel harmonic contrast
Each pixel is compared to its eight neighbors in a learned luminance/chroma metric. The result is a per-pixel harmonic field. -
L1 — oriented pooling
L0 harmonics are summed over small, overlapping patches on a sparse cell grid. Each cell gets a dominant orientationθ. -
Seed — facilitation and suppression
Cell responses pass through Naka–Rushton gain control, collinear facilitation along contours, and cross-orientation surround suppression. A divisive readout yields per-cell contour densityρ. -
Render — ridge back-projection
Cellρis splatted back to full resolution with learned 1D kernels aligned to localθ, producing a soft boundary map. Non-max suppression yields the final edge map.
Pre-print: The paper is included in this repository (
assets/hci.pdf); the official pre-print release is awaiting publication and will be linked here once available.
Left: input image. Right: HCI edge map from the bundled pretrained model.
HCI/
├── pyproject.toml
├── requirements.txt
├── params.py # all hyperparameters
├── train.py # HCIE2E training
├── test.py # ODS, OIS, AP evaluation
├── infer.py # single-image inference + diagnostics
├── hci/
│ ├── L0.py # pixel-level contrast
│ ├── L1.py # cell-level z₂ moments (E, C, θ)
│ ├── seed.py # η_z NR + collinear + surround → cell ρ for splat
│ ├── renderer.py # learned ridge projection
│ └── diagnostics_viz.py # visualisation utilities
├── data/
│ ├── train/imgs, train/gt # training pairs
│ ├── test/imgs, test/gt # evaluation pairs
│ └── infer/ # inference images
├── pretrained/
│ └── final.pt # bundled weights — infer / test without training
└── output/
├── checkpoints/ # final.pt, intermediate.pt
└── test/results.json
Python 3.12+.
Install everything from pyproject.toml / requirements.txt (includes CPU torch from PyPI).
uv:
uv syncpip:
pip install -r requirements.txtCheck for a Nvidia GPU
nvidia-smiFor CUDA, note the driver version in the output and pick the matching PyTorch index tag (e.g. CUDA 12.8 → cu128). See pytorch.org/get-started.
- Install non-torch dependencies (or full sync).
- Install CUDA
torchfrom the PyTorch wheel index — use--reinstallso it replaces the CPU build fromuv sync.
uv:
uv sync
uv pip install --reinstall torch --index-url https://download.pytorch.org/whl/cu128pip:
pip install torch --index-url https://download.pytorch.org/whl/cu128
pip install matplotlib>=3.10.8 numpy>=2.0.0 pillow>=12.1.0 pyyaml>=6.0.0 scipy>=1.17.1Replace cu128 with your tag. Do not run pip install -r requirements.txt after the CUDA wheel, as that will pin CPU torch.
Verify:
uv run python -c "import torch; print(torch.__version__, torch.cuda.is_available())"Expect True for the second value on the CUDA path. Training auto-selects cuda when available (device=cuda in the log); pass --device cpu to force CPU.
infer.py takes a filename (-i / --image) and looks for that file under --input_dir (default: data/infer/):
data/infer/
cat.png # default location — run with -i cat.png
photo.jpg
Put the image you want to process in data/infer/, then pass only its basename:
uv run infer.py -i cat.png --model pretrained/final.ptIf the image lives elsewhere, set --input_dir to that folder (path only — still pass the basename with -i):
uv run infer.py -i cat.png --input_dir ~/Pictures --model pretrained/final.pt
# reads ~/Pictures/cat.pngOutputs go to --output_dir (default: output/results/). Add -d / --diagnostics for pinwheel, ρ maps, and overlay PNGs; add -v / --verbose to print learned parameters.
A pretrained checkpoint is included at pretrained/final.pt (learned L0 metric, seed, renderer). Use it to run infer or test without training your own weights:
# inference (image in data/infer/ by default)
uv run infer.py -i cat.png --model pretrained/final.pt
# evaluation on a paired test set
uv run test.py --images data/test/imgs --test_gt data/test/gt --model pretrained/final.ptTraining still writes new checkpoints under output/checkpoints/; pass --model to point at those instead.
uv run train.py --train_imgs data/train/imgs --train_gt data/train/gtMain flags: --epochs, --lr (default 5e-2), --batch_size, --max_val_ratio, --device, --output_dir, --checkpoints_dir, --cache_dir, --gt_format (png / mat; auto-detected from GT dir if omitted).
BIPED/ is in .gitignore, place the dataset at the repo root yourself. Training uses train.py with the train RGB and edge-map folders below. Layout expected by the default commands (RGB + PNG edge maps):
BIPED/edges/imgs/train/rgbr/real/ # training RGB (.jpg / .png)
BIPED/edges/edge_maps/train/rgbr/real/ # training GT edges (same stem per image)
BIPED/edges/imgs/test/rgbr/ # test RGB
BIPED/edges/edge_maps/test/rgbr/ # test GT edges
If you have the Kaggle CLI set up (or you already downloaded/unzipped the dataset), you can generate this layout with:
sh scripts/biped.sh --version bipedv2 --kaggle # recommended
# or: sh scripts/biped.sh --version biped --src-dir /path/to/extracted/BIPED
# optional: --data-root BIPEDv2Train on the BIPED train split (writes checkpoints under output/checkpoints unless overridden):
uv run train.py \
--train_imgs BIPED/edges/imgs/train/rgbr/real \
--train_gt BIPED/edges/edge_maps/train/rgbr/real \
--cache_dir cache/biped_train \Use a dedicated --cache_dir so BIPED caches do not mix with other experiments. Lower --batch_size if you hit GPU memory limits.
Test on the BIPED test split (ODS / OIS / AP; pairs images to GT by filename stem):
uv run test.py \
--images BIPED/edges/imgs/test/rgbr \
--test_gt BIPED/edges/edge_maps/test/rgbr \
--output_dir output/test_bipedQuick smoke test: add --max_images 10. If your GT folder uses BSDS-style .mat files instead of PNG, pass --gt_format mat.
BRIND is a BSDS-based edge benchmark annotated for different discontinuity types (reflectance, illuminance, normal, depth) plus a combined all edge map.
To remap BRIND into the HCI directory layout used by train.py/test.py:
sh scripts/brind.sh --src-dir /path/to/extracted/BRIND --data-root BRIND --gt-type allTrain on BRIND (combined all edges by default):
uv run train.py \
--train_imgs BRIND/edges/imgs/train/rgbr/real \
--train_gt BRIND/edges/edge_maps/train/rgbr/real \
--cache_dir cache/brind_trainTest on BRIND:
uv run test.py \
--images BRIND/edges/imgs/test/rgbr \
--test_gt BRIND/edges/edge_maps/test/rgbr \
--output_dir output/test_brindIf the ground truth maps are stored as .mat files in your BRIND extraction, pass --gt_format mat to test.py and --gt_format mat to train.py.
Clone the mirror at the repo root (e.g. next to this project): BIDS/BSDS500.
git clone https://github.com/BIDS/BSDS500.gitPaths below assume the usual layout inside the clone: BSDS500/BSDS500/data/images/{train,test} and BSDS500/BSDS500/data/groundTruth/{train,test}.
Train on the BSDS500 train split (MAT ground truth):
uv run train.py \
--train_imgs BSDS500/BSDS500/data/images/train \
--train_gt BSDS500/BSDS500/data/groundTruth/train \
--gt_format mat \
--cache_dir cache/bsds_train \Use a dedicated --cache_dir so BSDS caches do not mix with BIPED or other runs. Lower --batch_size if you run out of memory.
Test on the BSDS500 test split (MAT ground truth):
uv run test.py \
--images BSDS500/BSDS500/data/images/test \
--test_gt BSDS500/BSDS500/data/groundTruth/test \
--gt_format mat \
--output_dir output/test_bsds500NYUD v2 is primarily an RGB-D dataset, but it is often used for edge detection through derived edge annotations. HCI matches RGB images and GT by filename stem (for example img_5001.png in both folders).
Default layout after running nyud.sh:
NYUDv2/
images/ # RGB images (.jpg / .png)
GT/ # edge GT maps (.png)
Train on the downloaded NYUDv2 layout (writes checkpoints under output/checkpoints unless overridden):
uv run train.py \
--train_imgs NYUDv2/images \
--train_gt NYUDv2/GT \
--cache_dir cache/nyudv2_trainUse a dedicated --cache_dir so NYUD caches do not mix with other experiments. Lower --batch_size if you hit GPU memory limits.
Test on the downloaded NYUDv2 layout:
uv run test.py \
--images NYUDv2/images \
--test_gt NYUDv2/GT \
--output_dir output/test_nyudv2Quick smoke test: add --max_images 20.
If your NYUD edge labels are stored as MAT files, add --gt_format mat.
Optional split-based layout (if you create your own train/test split):
NYUDv2/
images/train/
images/test/
edges/train/
edges/test/
Use the corresponding split paths with train.py and test.py.
Walks a single image directory, pairs each image with ground truth by matching filename stems:
uv run test.py --model output/checkpoints/final.pt| Flag | Default | Role |
|---|---|---|
--images |
data/test/imgs |
RGB test images (.jpg/.png) |
--test_gt |
data/test/gt |
Ground truth maps (.png/.jpg/.mat) |
--gt_format |
auto | png or mat (BSDS) |
--model |
output/checkpoints/final.pt |
Checkpoint |
--output_dir |
output/test |
Output directory |
--max_images |
all | Cap number of images |
--device |
CUDA if available | cpu, cuda, or mps |
--tol |
0.0075 |
Precision-match radius factor (max_dist = tol * image_diagonal) |
Single-image edge detection. Resolves the input as {input_dir}/{image} (see Inference input layout above).
# default: data/infer/photo.png → output/results/
uv run infer.py -i photo.png --model pretrained/final.pt
# custom input folder + diagnostics
uv run infer.py -i photo.png --input_dir /path/to/images \
--model output/checkpoints/final.pt -d -v| Flag | Default | Role |
|---|---|---|
-i, --image |
(required) | Image filename (resolved under --input_dir) |
--input_dir |
data/infer |
Folder containing the input image |
--model |
output/checkpoints/intermediate.pt |
Checkpoint |
--output_dir |
output/results |
Where edge PNGs (and diagnostics) are written |
-d, --diagnostics |
off | Save pinwheel, ρ maps, geometry, overlay, etc. |
-t, --threshold |
0.5 |
Binarization threshold on the soft boundary map |
--ridge-nms |
on | Directional NMS along renderer θ; use --no-ridge-nms for raw map |
--device |
CUDA if available | cpu, cuda, or mps |

