Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@
^src/symbols\.rds$
^doc$
^Meta$
^references$
^CRAN-SUBMISSION$
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,9 @@ docs/
/doc/
/Meta/
docs

# Reference papers used during development. We don't hold redistribution
# licenses for these, so PDFs do not go in git. The README that
# documents the directory's purpose is the only tracked file.
/references/*
!/references/README.md
63 changes: 41 additions & 22 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,20 @@

## Package responsibility

`thinr` provides binary image thinning (skeletonization) algorithms — Zhang-Suen, Guo-Hall, and (in v0.2) Lee and K3M — behind a single dispatching API. `thinImage()` is a signature-compatible drop-in for `EBImage::thinImage()` so callers can switch by changing the namespace prefix only. Per ADR-007 this is the **one public CRAN package** in the figureextract ecosystem; LGPL-3 is chosen for EBImage compatibility.
`thinr` provides binary image thinning (skeletonization) algorithms — Zhang-Suen, Guo-Hall, Lee (2-D), K3M, the parallel form commonly attributed to Hilditch, OPTA / SPTA, and Holt — behind a single dispatching API. Also provides the medial axis transform (Blum 1967) and a fast distance transform (Felzenszwalb-Huttenlocher 2012; classic two-pass sweep). `thinImage()` is a signature-compatible drop-in for `EBImage::thinImage()` so callers can switch by changing the namespace prefix only. Per ADR-007 this is the **one public CRAN package** in the figureextract ecosystem; LGPL-3 is chosen for EBImage compatibility.

## Current state

- **Slice:** 0 — Infrastructure
- **Version:** 0.1.0
- **Status:** initial release. Zhang-Suen and Guo-Hall fully implemented in Rcpp; Lee and K3M error informatively pending v0.2 implementation. Tests pass; vignette + README + NEWS in place; GitHub Actions configured; CRAN submission prepared.
- **Version:** 0.2.0 (release-prep branch)
- **Status:** CRAN-prep release. Seven thinning algorithms fully implemented in Rcpp, all verified against original papers (or the Lam-Lee-Suen 1992 survey for Hilditch's parallel form). Medial axis and distance transform shipped as standalone exported utilities. Tests pass; lintr clean; R CMD check --as-cran clean (0/0/2 NOTEs, both expected). GitHub Actions: R-CMD-check (matrix), pkgdown, test-coverage, lint, pr-commands.
- **Recent shipments:**
- v0.1.0 skeleton with Rcpp setup, 2 algorithms, 17 passing tests, vignette, README, NEWS, GitHub Actions CI, pkgdown. (2026-05-16)
- Dropped `stentiford` (folk misattribution; the 1983 paper is preprocessing not thinning) and `pavlidis` (current implementation didn't match the contour-following algorithm of the 1980 paper). The dropped algorithms are not implemented in any major image-processing library (scikit-image, OpenCV, MATLAB, ImageJ, mahotas); per the package's "widely used elsewhere" inclusion criterion, removing them keeps the package focused. (2026-05-20)
- Algorithm verification pass against papers in `references/`: K3M lookup tables corrected against Saeed et al. 2010; OPTA rewritten per survey's safe-point expression; Holt rewritten per the original CACM paper (correcting a survey transcription error in the middle clause). (2026-05-20)
- Tier-1 + Tier-2 algorithm expansion: Hilditch, OPTA, Holt; plus medial_axis() and distance_transform(). (2026-05-16)
- CRAN reviewer feedback addressed (function-name quotes + DOIs). (2026-05-16)
- v0.2.0 stub-replacement: Lee 2-D and K3M fully implemented. (2026-05-16)
- v0.1.0 skeleton with Rcpp setup, 2 algorithms, vignette, README, NEWS, GitHub Actions CI, pkgdown. (2026-05-16)

## Coding rules

Expand All @@ -25,36 +30,50 @@
## Testing conventions

- **Framework:** `testthat` 3rd edition with the `describe`/`it` BDD style (legible test reports for a public package).
- **Coverage:** every algorithm has tests for: a solid-shape skeleton, idempotence, empty input, and topology preservation on a representative shape.
- **Stubs:** Lee and K3M have tests that verify their stubs error with the documented message. When the implementations land in v0.2, the same test files extend with full behavioral tests.
- **Acceptance:** `R CMD check --as-cran` clean (0 errors, 0 warnings, NOTEs acceptable for "new submission" only).
- **Coverage:** every algorithm in `methods <- c(...)` (currently nine) is exercised against the same property suite (solid square thins, line collapse, idempotence, empty input, isolated-pixel preservation, ring topology). New methods are added to the vector and inherit all tests automatically.
- **`distance_transform` and `medial_axis`** each have their own test files (`tests/testthat/test-distance-transform.R`, `test-medial-axis.R`).
- **Acceptance:** `R CMD check --as-cran` clean (0 errors, 0 warnings, NOTEs acceptable for "new submission" and the Ubuntu system compilation-flags note).

## Module boundaries

- `src/zhang_suen.cpp` — Zhang-Suen (1984). Fully implemented.
- `src/guo_hall.cpp` — Guo-Hall (1989). Fully implemented.
- `src/lee.cpp` — Lee (1994). Stub; v0.2.
- `src/k3m.cpp` — K3M (Saeed et al. 2010). Stub; v0.2.
- `src/RcppExports.cpp` — auto-generated; do not edit (regenerated by `Rcpp::compileAttributes()`).
- `R/thin.R` — `thin()` dispatching function and the `as_binary_matrix()` / `restore_storage()` coercion helpers.
- `R/thin_image.R` — `thinImage()` drop-in.
- `R/thinr-package.R` — package-level Roxygen doc.
- `R/RcppExports.R` — auto-generated; do not edit.
C++ sources in `src/`:

- `thinr_common.h` — shared inline helpers (`crossing_number`, `neighbour_count`, `is_border_4`).
- `zhang_suen.cpp` — Zhang & Suen (1984).
- `guo_hall.cpp` — Guo & Hall (1989).
- `lee.cpp` — Lee, Kashyap & Chu (1994), 2-D adaptation.
- `k3m.cpp` — Saeed et al. (2010); paper lookup tables reproduced verbatim.
- `hilditch.cpp` — parallel form commonly attributed to Hilditch (1969); the actual implementation follows the Rutovitz-style R1–R4 conditions as documented in Lam, Lee & Suen (1992).
- `opta.cpp` — Naccache & Shinghal (1984), Safe Point Thinning Algorithm; boolean safe-point expression follows the survey.
- `holt.cpp` — Holt, Stewart, Clint & Perrott (1987); condition H follows the original CACM paper (a survey transcription error was caught and corrected against the original).
- `distance_transform.h` + `distance_transform.cpp` — Felzenszwalb-Huttenlocher 2012 squared Euclidean + Rosenfeld-Pfaltz two-pass sweep for L1 and L∞.
- `medial_axis.cpp` — ridge detection on the squared Euclidean DT.
- `RcppExports.cpp` — auto-generated; do not edit (regenerated by `Rcpp::compileAttributes()`).

R sources in `R/`:

- `thin.R` — `thin()` dispatching function and the `as_binary_matrix()` / `restore_storage()` coercion helpers.
- `thin_image.R` — `thinImage()` drop-in.
- `distance_transform.R` — exported wrapper for `.distance_transform_cpp`.
- `medial_axis.R` — exported wrapper for `.medial_axis_cpp`.
- `thinr-package.R` — package-level Roxygen doc.
- `RcppExports.R` — auto-generated; do not edit.

## Public API surface

- Exported: `thin()`, `thinImage()`.
- Exported: `thin()`, `thinImage()`, `medial_axis()`, `distance_transform()`.
- Internal: `as_binary_matrix()`, `restore_storage()` (helpers; not exported).

## Extension points

When implementing Lee and K3M in v0.2:
To add a new thinning algorithm:

1. Replace the stub body in the corresponding `src/<name>.cpp`.
1. Add `src/<algorithm>.cpp` exporting `.<algorithm>_cpp(IntegerMatrix, int)`. Use the helpers in `thinr_common.h`.
2. Run `Rcpp::compileAttributes(".")` so `R/RcppExports.R` is regenerated.
3. Replace the v0.1 stub test in `tests/testthat/test-thin.R` with full behavioral tests.
4. Update `NEWS.md` and bump the version.
5. Update this CLAUDE.md's status section and the v0.2 entry of NEWS.
3. Add the method name to the `match.arg` list and the `switch()` in `R/thin.R`.
4. Add the method to the `methods` vector at the top of `tests/testthat/test-thin.R` — all property tests apply automatically.
5. Update `NEWS.md`, the algorithms table in `README.md`, the algorithms section in `R/thinr-package.R`, and the algorithms table in `vignettes/choosing-a-method.Rmd`.
6. Add the published reference to `DESCRIPTION` Description field if it has a DOI.

## Documentation requirements

Expand Down
21 changes: 16 additions & 5 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,22 @@ Authors@R:
comment = c(ORCID = "0000-0002-5759-428X",
affiliation = "Human Predictions, LLC"))
Description: Thinning (skeletonization) algorithms for binary raster
images. Provides four algorithms behind a single dispatching
function: Zhang-Suen, Guo-Hall, a 2-D adaptation of Lee, and K3M.
The drop-in 'thinImage()' matches the signature of
'EBImage::thinImage()' so existing code can switch parsers without
changes. The wider 'thin()' API selects the algorithm by name.
images. Provides seven algorithms behind a single dispatching
function: Zhang-Suen (Zhang and Suen 1984)
<doi:10.1145/357994.358023>, Guo-Hall (Guo and Hall 1989)
<doi:10.1145/62065.62074>, a 2-D adaptation of Lee
(Lee, Kashyap, and Chu 1994) <doi:10.1006/cgip.1994.1042>, K3M
(Saeed, Tabedzki, Rybnik, and Adamski 2010)
<doi:10.2478/v10006-010-0024-4>, the parallel form commonly
attributed to Hilditch (1969, in 'Machine Intelligence 4'),
OPTA / SPTA (Naccache and Shinghal 1984), and Holt and colleagues
(1987) <doi:10.1145/12527.12531>. Also provides the medial axis
transform (Blum 1967) and a distance transform implementation
following Felzenszwalb and Huttenlocher (2012)
<doi:10.4086/toc.2012.v008a019>. The drop-in thinImage() matches
the signature of thinImage() in the 'EBImage' package on
Bioconductor so existing code can switch parsers without changes.
The wider thin() API selects the algorithm by name.
License: LGPL-3
Encoding: UTF-8
Depends:
Expand Down
2 changes: 2 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Generated by roxygen2: do not edit by hand

export(distance_transform)
export(medial_axis)
export(thin)
export(thinImage)
importFrom(Rcpp,sourceCpp)
Expand Down
38 changes: 1 addition & 37 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,39 +1,3 @@
# thinr 0.2.0

All four algorithms are now implemented.

## New

- `lee` — 2-D adaptation of Lee, Kashyap & Chu (1994). Four directional sub-iterations (N / E / S / W boundary) with crossing-number Euler-invariance check and endpoint preservation.
- `k3m` — Saeed et al. (2010). Six-phase iterative thinning: phases 1–5 remove border pixels under progressively permissive lookup tables, plus a final phase 0 cleanup sweep with the strictest table. Includes the crossing-number topology guard and endpoint preservation.

## Tests

- Test suite expanded from 17 to 38 assertions. Each of the six core properties (skeleton size, horizontal-line collapse, idempotence, all-background invariance, isolated-pixel preservation, ring-topology preservation) is exercised across all four methods.

## Notes

- The K3M lookup tables (`A1` through `A5`) in `src/k3m.cpp` are reconstructed from the algorithm's published description. The algorithm produces topology-preserving, one-pixel-wide skeletons on the test corpus; reviewers familiar with the paper are invited to verify the table contents against the original publication and submit corrections.
- 3-D support (the original motivation for Lee's algorithm) is still not implemented; arrays with more than two dimensions are explicitly rejected. The 2-D Lee adaptation is what ships here.

# thinr 0.1.0

Initial release.

## Algorithms

- `zhang_suen` — Zhang & Suen (1984). Full implementation.
- `guo_hall` — Guo & Hall (1989). Full implementation.
- `lee` — Lee (1994). Stub; planned for v0.2.
- `k3m` — Saeed et al. (2010). Stub; planned for v0.2.

## API

- `thin(image, method)` — main dispatching function.
- `thinImage(x)` — drop-in replacement for `EBImage::thinImage()`. Uses Zhang-Suen.
- Accepts logical, integer, and numeric input matrices; preserves storage mode in the return.

## Known limitations

- 2-D matrix inputs only; higher-dimensional arrays are not yet supported.
- Lee and K3M are stubs that error with a clear message. Two algorithms are enough to validate the package API and to provide an immediate drop-in replacement for `EBImage::thinImage()`; the other two follow in v0.2 once the underlying implementations are written.
Initial CRAN release.
20 changes: 20 additions & 0 deletions R/RcppExports.R
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
# Generated by using Rcpp::compileAttributes() -> do not edit by hand
# Generator token: 10BE3573-1514-4C36-9D1C-5A225CD40393

.distance_transform_cpp <- function(img, metric) {
.Call(`_thinr_distance_transform_cpp`, img, metric)
}

.guo_hall_cpp <- function(img, max_iter) {
.Call(`_thinr_guo_hall_cpp`, img, max_iter)
}

.hilditch_cpp <- function(img, max_iter) {
.Call(`_thinr_hilditch_cpp`, img, max_iter)
}

.holt_cpp <- function(img, max_iter) {
.Call(`_thinr_holt_cpp`, img, max_iter)
}

.k3m_cpp <- function(img, max_iter) {
.Call(`_thinr_k3m_cpp`, img, max_iter)
}
Expand All @@ -13,6 +25,14 @@
.Call(`_thinr_lee_cpp`, img, max_iter)
}

.medial_axis_cpp <- function(img) {
.Call(`_thinr_medial_axis_cpp`, img)
}

.opta_cpp <- function(img, max_iter) {
.Call(`_thinr_opta_cpp`, img, max_iter)
}

.zhang_suen_cpp <- function(img, max_iter) {
.Call(`_thinr_zhang_suen_cpp`, img, max_iter)
}
Expand Down
47 changes: 47 additions & 0 deletions R/distance_transform.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#' Distance transform of a binary image
#'
#' Compute the distance from each foreground pixel to the nearest
#' background pixel, under one of three standard metrics.
#'
#' @param image A binary image: a matrix where non-zero values are
#' foreground and zero values are background. Logical, integer, and
#' numeric inputs are accepted.
#' @param metric Distance metric. One of:
#' * `"euclidean"` (default) — exact L2 distance, via
#' Felzenszwalb & Huttenlocher (2012) linear-time separable
#' algorithm.
#' * `"manhattan"` — L1 distance via two-pass forward + backward
#' sweep (Rosenfeld & Pfaltz 1968).
#' * `"chessboard"` — L_infinity (Chebyshev) distance via the same
#' two-pass sweep with 8-connected propagation.
#'
#' @return A numeric matrix of the same shape as `image`. Background
#' pixels are 0; foreground pixels carry their distance to the
#' nearest background pixel.
#'
#' @examples
#' # A 5x5 image with a single background pixel in the corner.
#' m <- matrix(1L, nrow = 5, ncol = 5)
#' m[1, 1] <- 0L
#' distance_transform(m, metric = "manhattan")
#' distance_transform(m, metric = "chessboard")
#' round(distance_transform(m, metric = "euclidean"), 3)
#'
#' @references
#' Felzenszwalb, P. F., & Huttenlocher, D. P. (2012). Distance
#' transforms of sampled functions. *Theory of Computing*, 8(19),
#' 415-428. \doi{10.4086/toc.2012.v008a019}
#'
#' Rosenfeld, A., & Pfaltz, J. L. (1968). Distance functions on
#' digital pictures. *Pattern Recognition*, 1(1), 33-61.
#' \doi{10.1016/0031-3203(68)90013-7}
#'
#' @export
distance_transform <- function(image,
metric = c("euclidean", "manhattan",
"chessboard")) {
metric <- match.arg(metric)
mat <- as_binary_matrix(image)
code <- switch(metric, euclidean = 0L, manhattan = 1L, chessboard = 2L)
.distance_transform_cpp(mat, code)
}
59 changes: 59 additions & 0 deletions R/medial_axis.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#' Medial axis transform
#'
#' Return the medial axis of a binary image: the locus of foreground
#' pixels that are local maxima of the (squared) Euclidean distance
#' transform in at least one of the four principal directions
#' (horizontal, vertical, NW-SE diagonal, NE-SW diagonal). Each
#' skeleton pixel carries width information via the distance value at
#' that point.
#'
#' This is different from [thin()]: classical thinning algorithms
#' produce a connected, 1-pixel-wide skeleton without width
#' information. The medial axis transform (Blum 1967) produces a
#' skeleton **with** width information, useful for shape analysis
#' where local thickness matters.
#'
#' @param image A binary image: a matrix where non-zero values are
#' foreground and zero values are background. Logical, integer, and
#' numeric inputs are accepted.
#' @param return_distance Logical. If `FALSE` (default), return only
#' the binary skeleton in the same storage mode as `image`. If
#' `TRUE`, return a list with elements `skeleton` (the binary
#' skeleton) and `distance` (a numeric matrix of Euclidean
#' distances from each foreground pixel to the nearest background
#' pixel, with 0 on background pixels).
#'
#' @return Either a matrix (when `return_distance = FALSE`) or a
#' `list(skeleton, distance)` (when `return_distance = TRUE`).
#'
#' @examples
#' # A 7x9 solid rectangle: the medial axis is the middle row.
#' m <- matrix(0L, nrow = 7, ncol = 9)
#' m[3:5, 3:7] <- 1L
#' medial_axis(m)
#'
#' # Returning width information alongside the skeleton.
#' result <- medial_axis(m, return_distance = TRUE)
#' result$skeleton
#' round(result$distance, 3)
#'
#' @references
#' Blum, H. (1967). A transformation for extracting new descriptors
#' of shape. In *Models for the Perception of Speech and Visual Form*
#' (pp. 362-380). MIT Press.
#'
#' Felzenszwalb, P. F., & Huttenlocher, D. P. (2012). Distance
#' transforms of sampled functions. *Theory of Computing*, 8(19),
#' 415-428. \doi{10.4086/toc.2012.v008a019}
#'
#' @export
medial_axis <- function(image, return_distance = FALSE) {
mat <- as_binary_matrix(image)
result <- .medial_axis_cpp(mat)
skeleton <- restore_storage(result$skeleton, image)
if (isTRUE(return_distance)) {
list(skeleton = skeleton, distance = result$distance)
} else {
skeleton
}
}
24 changes: 16 additions & 8 deletions R/thin.R
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@
#' matrix; arrays with more than two dimensions are not yet supported.
#' @param method Algorithm to use. One of `"zhang_suen"` (default,
#' matches `EBImage::thinImage`), `"guo_hall"`, `"lee"` (2-D
#' adaptation of Lee, Kashyap & Chu 1994), or `"k3m"` (Saeed et al.
#' 2010). See `vignette("choosing-a-method")` for guidance on which to
#' pick.
#' adaptation of Lee, Kashyap & Chu 1994), `"k3m"` (Saeed et al.
#' 2010), `"hilditch"` (Hilditch 1969), `"opta"` (Naccache &
#' Shinghal 1984), or `"holt"` (Holt et al. 1987).
#' See `vignette("choosing-a-method")` for guidance on which to pick.
#' @param max_iter Maximum number of passes. Default 1000. Real binary
#' images of typical sizes converge well under 50 passes; the limit is
#' a safety bound against pathological inputs.
Expand All @@ -30,17 +31,24 @@
#' nrow = 5, byrow = TRUE)
#' thin(m, method = "zhang_suen")
#' thin(m, method = "guo_hall")
#' thin(m, method = "hilditch")
#'
#' @export
thin <- function(image, method = c("zhang_suen", "guo_hall", "lee", "k3m"),
thin <- function(image,
method = c("zhang_suen", "guo_hall", "lee", "k3m",
"hilditch", "opta", "holt"),
max_iter = 1000L) {
method <- match.arg(method)
mat <- as_binary_matrix(image)
iter <- as.integer(max_iter)
out <- switch(method,
zhang_suen = .zhang_suen_cpp(mat, as.integer(max_iter)),
guo_hall = .guo_hall_cpp(mat, as.integer(max_iter)),
lee = .lee_cpp(mat, as.integer(max_iter)),
k3m = .k3m_cpp(mat, as.integer(max_iter))
zhang_suen = .zhang_suen_cpp(mat, iter),
guo_hall = .guo_hall_cpp(mat, iter),
lee = .lee_cpp(mat, iter),
k3m = .k3m_cpp(mat, iter),
hilditch = .hilditch_cpp(mat, iter),
opta = .opta_cpp(mat, iter),
holt = .holt_cpp(mat, iter)
)
restore_storage(out, image)
}
Expand Down
Loading
Loading