fepegar / torchio

Medical imaging toolkit for deep learning
http://www.torchio.org
Apache License 2.0
2.04k stars 238 forks source link

Cannot copy subclass of Subject with keyword arguments #1181

Open c-winder opened 2 months ago

c-winder commented 2 months ago

Is there an existing issue for this?

Bug summary

A subclass of Subject with keyword arguments cannot be copied with copy.copy() which is used in transform.py. Raises TypeError: A subject without images cannot be created.

Code for reproduction

import copy
import torchio as tio

class Subject(tio.Subject):

    def __init__(self, name: str, **kwargs):
        kwargs['name'] = name
        super().__init__(**kwargs)

colin = tio.datasets.Colin27()
subject = Subject(name='colin', **colin)
copy.copy(subject)

Actual outcome

Raises TypeError: A subject without images cannot be created.

The subclass cannot be copied as _subject_copy_helper in subject.py calls new = new_subj_cls(result_dict).

In the above example, result_dict = {'name:'colin', 'brain': ...} which means that name=result_dict on subclass recreation. This is the only parameter the subclass can see and hence it contains no images.

This can be fixed by changing _subject_copy_helper, to unpack result_dict as below, although I'm unsure if this is the best way of implementing the fix.

import copy
import torchio as tio
from typing import Any
from typing import Callable
from typing import Dict

class Subject(tio.Subject):

    def __init__(self, name: str, **kwargs):
        kwargs['name'] = name
        super().__init__(**kwargs)

    def __copy__(self):
        return _subject_copy_helper(self, type(self))

def _subject_copy_helper(
    old_obj: Subject,
    new_subj_cls: Callable[[Dict[str, Any]], Subject],
):
    result_dict = {}
    for key, value in old_obj.items():
        if isinstance(value, tio.Image):
            value = copy.copy(value)
        else:
            value = copy.deepcopy(value)
        result_dict[key] = value

    new = new_subj_cls(**result_dict)
    new.applied_transforms = old_obj.applied_transforms[:]
    return new

colin = tio.datasets.Colin27()
subject = Subject(name='colin', **colin)
copy.copy(subject)

Error messages

Traceback (most recent call last):
  File "D:\Programs\Anaconda\envs\body-comp\lib\site-packages\IPython\core\interactiveshell.py", line 3526, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-2-b4528fe5d1e0>", line 1, in <module>
    runfile('F:\\body-comp\\Playground\\test3.py', wdir='F:\\body-comp\\Playground')
  File "D:\Programs\PyCharm 2022.2\plugins\python\helpers\pydev\_pydev_bundle\pydev_umd.py", line 197, in runfile
    pydev_imports.execfile(filename, global_vars, local_vars)  # execute the script
  File "D:\Programs\PyCharm 2022.2\plugins\python\helpers\pydev\_pydev_imps\_pydev_execfile.py", line 18, in execfile
    exec(compile(contents+"\n", file, 'exec'), glob, loc)
  File "F:\body-comp\Playground\test3.py", line 17, in <module>
    copy.copy(broken_subject)
  File "D:\Programs\Anaconda\envs\body-comp\lib\copy.py", line 84, in copy
    return copier(x)
  File "D:\Programs\Anaconda\envs\body-comp\lib\site-packages\torchio\data\subject.py", line 75, in __copy__
    return _subject_copy_helper(self, type(self))
  File "D:\Programs\Anaconda\envs\body-comp\lib\site-packages\torchio\data\subject.py", line 445, in _subject_copy_helper
    new = new_subj_cls(result_dict)
  File "F:\body-comp\Playground\test3.py", line 11, in __init__
    super().__init__(**kwargs)
  File "D:\Programs\Anaconda\envs\body-comp\lib\site-packages\torchio\data\subject.py", line 62, in __init__
    self._parse_images(self.get_images(intensity_only=False))
  File "D:\Programs\Anaconda\envs\body-comp\lib\site-packages\torchio\data\subject.py", line 101, in _parse_images
    raise TypeError('A subject without images cannot be created')
TypeError: A subject without images cannot be created

Expected outcome

Subclass should copy correctly.

System info

Platform:   Windows-10-10.0.19045-SP0
TorchIO:    0.19.7
PyTorch:    2.0.1
SimpleITK:  2.3.1 (ITK 5.3)
NumPy:      1.26.3
Python:     3.10.13 | packaged by Anaconda, Inc. | (main, Sep 11 2023, 13:24:38) [MSC v.1916 64 bit (AMD64)]
fepegar commented 2 months ago

Hi, @c-winder. Thanks for reporting. Would you be willing to contribute with a PR? We can start by adding your snippet in a unit test, and your fix would make the new test pass.

c-winder commented 1 month ago

Sorry for the delay @fepegar, I've raised a PR which fixes the bug and extends test_copy_subclass.