Skip to content

Add Image.new_like() for custom subclass support#1445

Open
fepegar wants to merge 2 commits into
v1from
1391-image-new-like
Open

Add Image.new_like() for custom subclass support#1445
fepegar wants to merge 2 commits into
v1from
1391-image-new-like

Conversation

@fepegar

@fepegar fepegar commented Apr 1, 2026

Copy link
Copy Markdown
Member

Fixes #1391

Problem

Transforms that create new image instances — specifically Crop (with copy_patch=True) and ToReferenceSpace.from_tensor — reconstruct images by calling the constructor directly:

new_image = type(image)(tensor=..., affine=..., type=..., path=...)

This breaks for any Image subclass whose __init__ has a different signature (e.g., a required history parameter):

class HistoryScalarImage(tio.ScalarImage):
    def __init__(self, tensor, affine, history, **kwargs):
        super().__init__(tensor=tensor, affine=affine, **kwargs)
        self.history = history

img = HistoryScalarImage(torch.rand(1, 10, 10, 10), affine=torch.eye(4), history=[])
subject = tio.Subject(image=img)
tio.Crop(cropping=2)(subject)  # TypeError!

Solution

Add a new_like(tensor, affine=None) factory method to Image. Transforms call this instead of the constructor directly. The base implementation works for all built-in types (Image, ScalarImage, LabelMap). Subclasses with custom __init__ signatures override it:

class HistoryScalarImage(tio.ScalarImage):
    def __init__(self, tensor, affine, history, **kwargs):
        super().__init__(tensor=tensor, affine=affine, **kwargs)
        self.history = history

    def new_like(self, tensor, affine=None):
        return type(self)(
            tensor=tensor,
            affine=affine if affine is not None else self.affine,
            history=self.history,
        )

Scope

Only 2 call sites in the codebase actually reconstruct images via type(image)(...):

  1. Crop._crop_image — when copy_patch=True
  2. build_image_from_reference — used by ToReferenceSpace.from_tensor

These are the only places that needed fixing. Other spatial transforms (Pad, Resample, ToOrientation, Transpose) use in-place mutation (image.set_data() + image.affine = ...), which already preserves the original object's type. Those are left untouched.

Image.__copy__() was also updated to use new_like() for loaded images, so copy.copy() now works for custom subclasses too.

Changes

File Change
src/torchio/data/image.py Add new_like(), rewrite __copy__()
src/torchio/transforms/preprocessing/spatial/crop.py type(image)(...)image.new_like(...)
src/torchio/transforms/preprocessing/spatial/to_reference_space.py class_(...)reference.new_like(...)
tests/transforms/test_custom_image_subclass.py 16 tests including the exact #1391 repro

@codecov

codecov Bot commented Apr 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
tests/transforms/test_custom_image_subclass.py 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Add a new_like(tensor, affine) factory method to Image so that
transforms creating new image instances work with custom subclasses
that have different __init__ signatures. Extra dict keys (metadata
like age, site, etc.) are automatically propagated.

Only the two actual reconstruction call sites are changed:
- Crop._crop_image: type(image)(...) → image.new_like(...)
- build_image_from_reference: class_(...) → reference.new_like(...)

Image.__copy__() now delegates to new_like() for loaded images.

Fixes #1391

Co-authored-by: tcollins-hub <245905921+tcollins-hub@users.noreply.github.com>
Co-authored-by: siddharth10ss <143159776+siddharth10ss@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@fepegar
fepegar force-pushed the 1391-image-new-like branch from c98633d to 23b30cf Compare April 1, 2026 23:08
@github-actions

github-actions Bot commented Apr 1, 2026

Copy link
Copy Markdown
Contributor

📖 Docs Preview

Preview of the documentation for this PR:

🔗 https://smokeshow.helpmanual.io/1z5n220n2y5x2r3o6x2f/

Built from eda78a5

@fepegar

fepegar commented Apr 1, 2026

Copy link
Copy Markdown
Member Author

@tcollins-hub, @siddharth10ss, would this work for you?

Copilot AI review requested due to automatic review settings July 13, 2026 13:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 an Image.new_like() factory method as an extension point so transforms that need to create a new image instance can preserve custom Image subclasses whose __init__ signatures differ from TorchIO’s built-ins.

Changes:

  • Add Image.new_like(tensor, affine=None) and update Image.__copy__() to use it for loaded images.
  • Update Crop (copy_patch=True) and ToReferenceSpace.from_tensor to construct images via new_like() instead of calling constructors directly.
  • Add a dedicated test suite covering custom subclasses and the #1391 reproduction.

Reviewed changes

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

File Description
src/torchio/data/image.py Introduces new_like() and updates copy semantics to support subclassing.
src/torchio/transforms/preprocessing/spatial/crop.py Uses image.new_like(...) when creating cropped copies.
src/torchio/transforms/preprocessing/spatial/to_reference_space.py Uses reference.new_like(...) when building an image from a tensor + reference.
tests/transforms/test_custom_image_subclass.py Adds regression + behavior tests for custom subclasses and new_like().

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

type=image.type,
path=image.path,
)
new_image.path = image.path
Comment thread src/torchio/data/image.py
check_nans=self.check_nans,
reader=self.reader,
)
new_image.path = self.path
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.

Some transforms (e.g., Crop, Pad, CropOrPad) not compatible with custom Image subclasses with different constructors

2 participants