Show HN:我的紧凑、受生物启发的分布式边缘检测算法
Show HN: My compact, bio inspired distributed edge detection algorithm

原始链接: https://github.com/RenderBear/haci

HaCI 是一个紧凑、完全分布式的边缘检测器,包含四个阶段:像素谐波对比度、定向池化、通过促进/抑制与除法上下文读取的轮廓积分,以及带非极大值抑制的岭回投影。它生成软且细化的边缘图,并在 BRIND 上达到 ODS 0.7179。仓库包含训练、测试和推理命令行工具、预训练权重、BRIND、BIPED、BSDS500 和 NYUDv2 的数据集设置脚本,以及 Berkeley 边界基准评估。它支持 CPU、CUDA 和 MPS,其中 MPS 渲染使用 Metal 内核。典型命令为 `uv run train`、`uv run test` 和 `uv run infer`;BRIND 是默认数据集。评估报告 99 个阈值下的 ODS、OIS、AP、精确率和召回率。在 Apple M4 上,481×321 图像的性能约为 36 毫秒。

RenderBear 在 Hacker News 的 Show HN 帖子介绍了一种紧凑的、受生物启发的分布式边缘检测算法。基准测试将其定位在 Canny/Sobel 等经典方法与现代深度网络之间,同时保留可解释性。作者计划进一步改进视频帧率、硬件性能和下游应用,并邀请计算机视觉工程师、分布式系统工程师和研究人员合作。
相关文章

原文

A compact, fully distributed algorithm that extracts edges from RGB images through four stages:

  1. L0 — pixel harmonic contrast
    Each pixel is compared to its eight neighbors in a learned luminance/chroma metric. Each direction's contrast is divided by its own energy plus a learned spatial pool of local contrast (contrast gain control). The result is a per-pixel harmonic field.

  2. L1 — oriented pooling
    L0 harmonics are summed over small, overlapping patches on a sparse cell grid. Each cell gets eight orientation-tuned responses and a dominant orientation θ.

  3. Seed — facilitation and suppression
    Each orientation response passes through Naka–Rushton gain control. Support from neighbouring cells along the contour (an association field with a learned curvature tolerance) scales the response and bridges gaps that have support on both sides. A divisive readout then divides each orientation response by the same orientation's response in the neighbouring cells, so a cell is coded relative to its same-orientation context. Parallel contours on both sides of a cell (flank suppression) divide it further, which damps texture and gratings but leaves a lone edge, or an edge beside a single parallel one, untouched. The result is per-cell contour density ρ.

  4. 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.

Paper: HaCI: Harmonic Contour Integration for Edge Detection, DOI 10.5281/zenodo.23403643.

For collaboration contact me on: [email protected]

Input example Detected edges

Left: input image. Right: HaCI edge map from the bundled pretrained model.

Scores for pretrained/final.pt on the BRIND test set (200 images; the model was trained on the 300-image BRIND train split). Evaluation uses the Berkeley boundary benchmark: 99 thresholds and maxDist 0.0075 (≈4.3 px on 481×321 images). Predictions are thinned before one-to-one matching; see Test.

Output ODS Precision Recall OIS AP
NMS-thinned map (s_eval) 0.7179 0.6791 0.7614 0.7402 0.7651
Raw soft map (c_eval) 0.7158 0.6842 0.7505 0.7373 0.6723

Precision and recall are taken at the ODS threshold (0.71 for both maps).

Speed and memory, measured on MPS (Apple M4) with baselines/speed.py.

Resolution Time Host memory GPU memory
481×321 36 ms 124 MB 1.1 GB
1280×720 201 ms 171 MB 1.1 GB
1920×1080 459 ms 233 MB 2.2 GB
HaCI/
├── pyproject.toml
├── requirements.txt
├── params.py                # all hyperparameters
├── train.py                 # HaCIE2E training
├── test.py                  # ODS, OIS, AP evaluation
├── infer.py                 # single-image inference + diagnostics
├── scripts/                 # biped.sh, brind.sh, nyud.sh — fetch and lay out datasets
├── haci/
│   ├── L0.py                # pixel-level contrast + gain control
│   ├── L1.py                # cell-level z₂ moments (E, C, θ)
│   ├── seed.py              # η_z NR + association field + same-orientation surround + flank suppression → cell ρ for splat
│   ├── renderer.py          # learned ridge projection
│   ├── splat_metal.py       # the renderer's splat as Metal kernels, used with --device mps
│   ├── boundary_bench.py    # Berkeley boundary benchmark (ODS, OIS, AP)
│   ├── cli.py               # `uv run train` / `test` / `infer` entry points
│   └── diagnostics_viz.py   # visualisation utilities
├── data/                    # BRIND, the default dataset (scripts/brind.sh)
│   ├── train/imgs, train/gt # 300 training pairs
│   └── test/imgs, test/gt   # 200 evaluation pairs
├── BIPED/, NYUDv2/          # other datasets, same {train,test}/{imgs,gt} layout (scripts/)
├── pretrained/
│   └── final.pt             # bundled weights — infer / test without training
└── output/
    ├── checkpoints/         # final.pt, intermediate.pt
    └── test/{c_eval,s_eval}/ # results.json, eval_bdry*.txt, preds/

Python 3.12+.

Install everything from pyproject.toml / requirements.txt (includes CPU torch from PyPI).

uv:

pip:

pip install -r requirements.txt

Optional — GPU Acceleration

Check for a Nvidia GPU

For 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.


  1. Install non-torch dependencies (or full sync).
  2. Install CUDA torch from the PyTorch wheel index — use --reinstall so it replaces the CPU build from uv sync.

uv:

uv sync
uv pip install --reinstall torch --index-url https://download.pytorch.org/whl/cu128

pip:

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.1

Replace 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. train, test and infer pick cuda when available, then mps on Apple silicon, then cpu (the choice is printed as device=...); pass --device cpu to force CPU.

On mps, the renderer's splat and its gradient run as Metal kernels (haci/splat_metal.py) and compute the same sums as the CPU path, so results agree up to float rounding.

uv run train, uv run test and uv run infer run train.py, test.py and infer.py from the repo root. Without uv, run the scripts directly (python test.py ...) with the same flags.

infer takes the path to the image with -i / --image:

uv run infer -i assets/base.png
uv run infer -i ~/Pictures/photo.jpg

Outputs 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 and gain control, seed, renderer). It's the default --model for infer and test, so you can run them without training your own weights. test and train default to BRIND in data/, which scripts/brind.sh creates:

# inference
uv run infer -i assets/base.png

# evaluation on the BRIND test set
sh scripts/brind.sh
uv run test

Training still writes new checkpoints under output/checkpoints/; pass --model to point at those instead.

Trains on BRIND in data/train by default:

Main flags: --train_imgs, --train_gt, --gt_format (png / mat; auto-detected from the GT folder if omitted), --epochs (default 50), --lr (default 5e-2), --batch_size, -n (cap the number of images), --device, --output_dir, --checkpoints_dir, --cache_dir, --num_workers (default 0: cached items load in milliseconds, and starting worker processes each epoch costs more than it saves), --grad_clip, --gt_min_agreement, --resume. --debug-seed runs one batch and prints the seed's parameters and gradients.

To continue an interrupted run, add --resume: it loads intermediate.pt from --checkpoints_dir (or the path you give it) and picks up at the next epoch.

BIPED has 250 outdoor 1280×720 images with expert edge annotations: 200 train, 50 test. scripts/biped.sh writes (BIPED/ is in .gitignore):

BIPED/train/imgs/   # 200 RGB images (.jpg)
BIPED/train/gt/     # 200 edge maps (.png, same stem)
BIPED/test/imgs/    # 50 RGB images
BIPED/test/gt/      # 50 edge maps

--kaggle downloads xavysp/biped with the Kaggle CLI, run through uvx. It needs a Kaggle API token in ~/.kaggle/kaggle.json, or KAGGLE_USERNAME and KAGGLE_KEY. If you downloaded BIPED yourself, pass the folder with --src-dir; when it contains both BIPED and BIPEDv2, v2 is used.

sh scripts/biped.sh --kaggle
# or: sh scripts/biped.sh --src-dir /path/to/downloaded/BIPED
# optional: --data-root DIR (default: BIPED)

Train on the BIPED train split (writes checkpoints under output/checkpoints unless overridden):

uv run train \
  --train_imgs BIPED/train/imgs \
  --train_gt BIPED/train/gt \
  --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 \
  --dataset BIPED \
  --images BIPED/test/imgs \
  --test_gt BIPED/test/gt \
  --output_dir output/test_biped

Quick smoke test: add -n 10.

BRIND is BSDS500 re-annotated at the edge level by RINDNet for four discontinuity types: reflectance, illumination, normal and depth. The published BRIND set merges them into one edge map per image, 300 train / 200 test. The per-type maps are only available from RINDNet.

BRIND is the default dataset. scripts/brind.sh clones it into data/, where train and test look when no paths are given:

data/train/imgs/   # 300 RGB images (.jpg)
data/train/gt/     # 300 edge maps (.png, same stem)
data/test/imgs/    # 200 RGB images
data/test/gt/      # 200 edge maps
sh scripts/brind.sh
# or, from an existing clone: sh scripts/brind.sh --src-dir /path/to/BRIND
# optional: --data-root DIR (default: data)

Train and test on BRIND:

Clone the mirror at the repo root (e.g. next to this project): BIDS/BSDS500.

git clone https://github.com/BIDS/BSDS500.git

Paths 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 \
  --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 \
  --dataset BSDS500 \
  --images BSDS500/BSDS500/data/images/test \
  --test_gt BSDS500/BSDS500/data/groundTruth/test \
  --gt_format mat \
  --output_dir output/test_bsds500

NYUD v2 is an RGB-D dataset. Edge detection uses the ground truth that Gupta et al. released for its 1449 labelled images: 795 train+val and 654 test. scripts/nyud.sh downloads their release (eccv14-data.tgz, about 900 MB) and writes:

NYUDv2/train/imgs/   # 795 RGB images (img_XXXX.png)
NYUDv2/train/gt/     # ground truth, one BSDS-format groundTruth .mat per image
NYUDv2/test/imgs/    # 654 RGB images
NYUDv2/test/gt/
sh scripts/nyud.sh
# or, from an extracted copy: sh scripts/nyud.sh --src-dir /path/to/eccv14-data
# optional: --data-root DIR (default: NYUDv2)

Train on the NYUD train split (.mat ground truth is detected automatically):

uv run train \
  --train_imgs NYUDv2/train/imgs \
  --train_gt NYUDv2/train/gt \
  --cache_dir cache/nyudv2_train

Use 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 NYUD test split. NYUD results are reported with a matching distance of 0.011, looser than BSDS's 0.0075, because its ground truth is less precisely localised:

uv run test \
  --dataset NYUDv2 \
  --images NYUDv2/test/imgs \
  --test_gt NYUDv2/test/gt \
  --tol 0.011 \
  --output_dir output/test_nyudv2

Quick smoke test: add -n 20.

Walks a single image directory, pairs each image with ground truth by matching filename stems, and scores both the raw map (c_eval) and the NMS-thinned map (s_eval) with the Berkeley boundary benchmark. Without path flags it evaluates BRIND in data/test:

uv run test --model output/checkpoints/final.pt

The protocol follows the Berkeley benchmark (haci/boundary_bench.py). At each of --nthresh thresholds, the saved 8-bit prediction is binarised and thinned (bwmorph(·, 'thin', inf)). It is then matched one-to-one against each annotator within tol × image diagonal pixels. ODS interpolates between thresholds, OIS uses each image's best threshold, and AP is the area under the PR curve resampled at 0.01 recall. Each annotator in a .mat file is matched separately; BSDS500 has about five per image. A PNG ground-truth map counts as a single annotator. Scores from earlier versions of test.py, which used a dilation match, are not comparable.

Flag Default Role
--dataset BRIND Dataset name shown in the report and results.json
--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 pretrained/final.pt Checkpoint
--output_dir output/test Output directory
-n all Cap number of images
--device CUDA, else MPS, else CPU cpu, cuda, or mps
--diagnostics off Save ρ and geometry maps per image under output_dir/diagnostics/
--tol 0.0075 Matching distance as a fraction of the image diagonal (NYUD convention: 0.011)
--nthresh 99 Number of thresholds
--workers min(8, cores) Benchmark processes (inference stays on --device)

Each output_dir/{c_eval,s_eval}/ gets results.json (summary, PR curve, per-image counts) and eval_bdry.txt, eval_bdry_thr.txt, eval_bdry_img.txt in the Berkeley column layout, so standard PR-curve plotting scripts can read them.

Single-image edge detection on the image at the -i path.

# assets/base.png → output/results/
uv run infer -i assets/base.png

# your own image and checkpoint, with diagnostics and learned parameters
uv run infer -i /path/to/images/photo.png \
  --model output/checkpoints/final.pt -d -v
Flag Default Role
-i, --image (required) Path to the input image
--model pretrained/final.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
-v, --verbose off Print learned parameters and timings
--gt_dir none Ground-truth folder (same stem as the image) to colour diagnostics
--gt_format auto png or mat when --gt_dir is set
--shape_theta_bins 12 Orientation bins in the render_theta_bins diagnostic
--device CUDA, else MPS, else CPU cpu, cuda, or mps
联系我们 contact @ memedata.com