Skip to content

image/label transformations#146

Merged
HelenaLC merged 9 commits into
mainfrom
trans
Apr 6, 2026
Merged

image/label transformations#146
HelenaLC merged 9 commits into
mainfrom
trans

Conversation

@HelenaLC
Copy link
Copy Markdown
Owner

@HelenaLC HelenaLC commented Apr 6, 2026

No description provided.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds support for applying translation() to sdArray objects (covering both ImageArray and LabelArray) and introduces test coverage for image/label translations.

Changes:

  • Implement translation,sdArray,numeric-method to pad/shift array-backed SpatialData elements.
  • Add testthat tests validating translation behavior for images, labels, and multiscale labels.
  • Update roxygen-generated docs and NAMESPACE imports to support the new method.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
R/trans.R Implements translation() for sdArray (image/label arrays), including multiscale handling.
tests/testthat/test-trans.R Adds tests for translation() on images, labels, and multiscale labels.
NAMESPACE Imports DelayedArray::cbind / DelayedArray::rbind to support the implementation.
man/trans.Rd Documents the new S4 method alias/usage for translation,sdArray,numeric.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread NAMESPACE
Comment on lines +93 to +94
importFrom(DelayedArray,cbind)
importFrom(DelayedArray,rbind)
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Importing cbind/rbind from DelayedArray into the package namespace can mask base cbind/rbind and affect other code paths in this package that call cbind() (e.g., data.frame/DFrame binding). Consider removing these importFrom() entries and instead calling DelayedArray::cbind() / DelayedArray::rbind() (or DelayedArray::bindCOLS/bindROWS) explicitly at the call sites that need DelayedArray support.

Suggested change
importFrom(DelayedArray,cbind)
importFrom(DelayedArray,rbind)

Copilot uses AI. Check for mistakes.
Comment thread R/trans.R
Comment on lines +82 to +88
if (length(dim(y)) == 2) {
n <- NULL
} else {
t <- t[-1]
n <- dim(x)[1]
y <- aperm(y, c(2,3,1))
}
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For 3D arrays you drop the first component of t (t <- t[-1]) and never use it. If a caller passes a non-zero translation for the first dimension, it will be silently ignored (and dim(y) will not match the requested translation). Either enforce t[1] == 0 when length(dim(y)) > 2, or implement translation along that dimension as well.

Copilot uses AI. Check for mistakes.
Comment thread R/trans.R Outdated
Comment on lines +91 to +96
z <- DelayedArray(array(0, d))
y <- if (t[2] > 0) cbind(z, y) else cbind(y, z)
}
if (t[1] != 0) {
d <- c(abs(t[1]), ncol(y), n)
z <- DelayedArray(array(0, d))
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DelayedArray(array(0, d)) allocates a dense in-memory array for the padding block. For large images/labels and/or large translations this can cause significant memory use and slowdowns. Prefer a lazy constant/zero array (e.g., S4Arrays::ConstantArray(0, dim=d) / an equivalent DelayedArray/S4Arrays constructor) so the padding remains delayed.

Suggested change
z <- DelayedArray(array(0, d))
y <- if (t[2] > 0) cbind(z, y) else cbind(y, z)
}
if (t[1] != 0) {
d <- c(abs(t[1]), ncol(y), n)
z <- DelayedArray(array(0, d))
z <- S4Arrays::ConstantArray(0, dim=d)
y <- if (t[2] > 0) cbind(z, y) else cbind(y, z)
}
if (t[1] != 0) {
d <- c(abs(t[1]), ncol(y), n)
z <- S4Arrays::ConstantArray(0, dim=d)

Copilot uses AI. Check for mistakes.
Comment thread R/trans.R
Comment on lines +69 to +74
if (length(ys) == 1) {
ts <- list(t)
} else {
ds <- vapply(ys, ncol, integer(1))
sf <- c(1, ds[-1]/ds[-length(ds)])
ts <- lapply(cumprod(sf), `*`, t)
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When x is multiscale, ts is computed by multiplying t by scale factors derived from ncol(). This can yield non-integer translations due to non-integer scale ratios and/or floating-point rounding (e.g., 0.5 * t becoming 31.999999), which will later be used as array dimensions via abs(t[1/2]) and can error. Consider validating/rounding each scaled translation (and failing with a clear error if it’s not exactly integer) before using it to construct d.

Suggested change
if (length(ys) == 1) {
ts <- list(t)
} else {
ds <- vapply(ys, ncol, integer(1))
sf <- c(1, ds[-1]/ds[-length(ds)])
ts <- lapply(cumprod(sf), `*`, t)
validate_translation <- \(tt, scale) {
rr <- round(tt)
tol <- sqrt(.Machine$double.eps)
if (!all(is.finite(tt)) || any(abs(tt - rr) > tol)) {
stop(
sprintf(
"translation becomes non-integer at multiscale level %d after scaling; got %s",
scale, paste(tt, collapse=", ")),
call.=FALSE)
}
as.integer(rr)
}
if (length(ys) == 1) {
ts <- list(as.integer(round(t)))
} else {
ds <- vapply(ys, ncol, integer(1))
sf <- c(1, ds[-1]/ds[-length(ds)])
ts <- lapply(seq_along(sf), \(k) {
validate_translation(cumprod(sf)[k] * t, k)
})

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +23
# row
t <- c(0,n <- sample(77, 1),0)
y <- translation(x, t)
expect_equal(dim(y), dim(x)+t)
expect_is(data(y), "DelayedArray")
expect_true(sum(data(y)[,seq_len(n),]) == 0)
# col
t <- c(0,0,n <- sample(77, 1))
y <- translation(x, t)
expect_equal(dim(y), dim(x)+t)
expect_is(data(y), "DelayedArray")
expect_true(sum(data(y)[,,seq_len(n)]) == 0)
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests use sample() to pick translation sizes. Even with a small range, randomness can make failures harder to reproduce and can introduce variability in runtime. Consider using fixed translation values (or setting a local seed inside each test_that) so the tests are fully deterministic.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread R/trans.R
Comment on lines +65 to +66
length(t) == length(dim(x)),
t == round(t), all(is.finite(t)))
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stopifnot(t == round(t)) will always fail when t has length > 1 because stopifnot() expects each argument to be a single TRUE; a logical vector is not isTRUE(). This makes translation(sdArray, numeric) unusable for the intended 2D/3D cases. Use a scalar check like all(t == round(t)) (and keep the finiteness check scalar as well).

Suggested change
length(t) == length(dim(x)),
t == round(t), all(is.finite(t)))
length(t) == length(dim(x)),
all(t == round(t)),
all(is.finite(t)))

Copilot uses AI. Check for mistakes.
Comment thread tests/testthat/test-trans.R Outdated
Comment on lines +69 to +74
replicate(10, \() {
n <- f(); m <- f()
y <- translation(x, c(n,m))
expect_identical(x$x, y$x+n)
expect_identical(x$y, y$y+m)
expect_identical(x[,i], y[i])
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The replicate() call is not executing the expectations: replicate(10, \() { ... }) replicates the function object rather than running the body. Additionally, inside the body the translation assertions are currently reversed (expect_identical(x$x, y$x+n) should compare y$x to x$x + n) and y[i] subsets rows rather than the intended columns (y[, i]). As written this test will either do nothing (because the expectations never run) or fail once corrected to execute.

Suggested change
replicate(10, \() {
n <- f(); m <- f()
y <- translation(x, c(n,m))
expect_identical(x$x, y$x+n)
expect_identical(x$y, y$y+m)
expect_identical(x[,i], y[i])
replicate(10, {
n <- f(); m <- f()
y <- translation(x, c(n,m))
expect_identical(y$x, x$x+n)
expect_identical(y$y, x$y+m)
expect_identical(x[,i], y[,i])

Copilot uses AI. Check for mistakes.
@HelenaLC HelenaLC requested a review from Copilot April 6, 2026 13:23
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread R/trans.R
Comment on lines +126 to +127
mutate(a=x*R[1,1], b=y*R[1,2]) |>
mutate(c=x*R[2,1], d=y*R[2,2]) |>
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.R() now returns the transpose of the usual CCW rotation matrix (suitable for right-multiplying row vectors, as used in ShapeFrame rotation via xy %*% .R(...)). However, PointFrame rotation computes x' = x*R[1,1] + y*R[1,2] / y' = x*R[2,1] + y*R[2,2] (left-multiplying column vectors), which makes the rotation direction opposite to ShapeFrame and the “counter-clockwise” comment. Please make the convention consistent (e.g., transpose R in the PointFrame method or adjust .R()/call sites) so the same angle rotates points in the same direction across element types.

Suggested change
mutate(a=x*R[1,1], b=y*R[1,2]) |>
mutate(c=x*R[2,1], d=y*R[2,2]) |>
mutate(a=x*R[1,1], b=y*R[2,1]) |>
mutate(c=x*R[1,2], d=y*R[2,2]) |>

Copilot uses AI. Check for mistakes.
@HelenaLC HelenaLC merged commit a2cf633 into main Apr 6, 2026
3 checks passed
@HelenaLC HelenaLC deleted the trans branch April 24, 2026 13:48
@Artur-man Artur-man mentioned this pull request May 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants