radifox-utils


Nameradifox-utils JSON
Version 1.0.2 PyPI version JSON
download
home_pagehttps://gitlab.com/iacl/radifox-utils
SummaryMedical image utilities provided with RADIFOX
upload_time2023-11-16 01:53:05
maintainer
docs_urlNone
authorIACL
requires_python>=3.7
licenseApache License, 2.0
keywords
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            ## Installation
```bash
pip install radifox-utils
```
This will install the utils package with it's `scipy` dependancy chain.

To install pytorch dependencies to use these functions with pytorch Tensors:
```bash
pip install radifox-utils[pytorch]
```


## Degrade

This is a small library to degrade (blur and downsample) a signal. Here, we
aim to properly model the forward process in signal acquisition.

## Resize

### Sampling step when resizing an image

When resizing a digital image with interpolation, we usually need to sample values at non-integer coordinates. The sampling step, i.e., the separation between two adjacent pixels/voxels to be sampled, then determines the digital resolution in the resulting image. When given a target digital resolution, we usually want our image resizing routines respect the corresponding sampling step. For example, when visualizing a zoomed-in medical image, if the digital resolution calculated from the image header mismatches the sampling step, we will see either a squeezed or stretched image.

There are many Python libraries available to resize an image. However, functions such as `scipy.ndimage.zoom` and `skimage.transform.rescale` do not respect the sampling step. Here we use a very simple experiment to demonstrate this.

```python
import numpy as np
from scipy.ndimage import zoom

sampling_step = 0.7
x = np.arange(6).astype(float)

# Perform a linear interpolation with replication padding
y = zoom(x, 1 / sampling_step, order=1, mode='nearest')
```

With Python version 3.7.6 and Scipy version 1.6.3, we will have y equal to

```
[0, 0.625, 1.25, 1.875, 2.5, 3.125, 3.75, 4.375, 5.]
```

Since our sampling step is chosen as 0.7, we would expect the resulting array to have approximately 0.7 different between two nearby values. However, we get 0.625 in this case. For scikit-image,

```python
import numpy as np
from skimage.transform import rescale

sampling_step = 0.7
x = np.arange(6).astype(float)

# Perform a linear interpolation with replication padding
y = rescale(x, 1 / sampling_step, order=1, mode='edge')
```

With Python version 3.7.6 and sciki-image version 1.18.1, we have y equal to

```
[0, 0.5, 1.16666667, 1.83333333, 2.5, 3.16666667, 3.83333333, 4.5, 5]
```

Here the nearby difference is 0.6666667 (ignore the first and last since they are affected by padding). It is not 0.7 either.

What happens here is that since the resulting image should have an integer number of values, these functions choose to change the sampling step (or the scale, the inverse of the sampling) to accommodate that.

However, if we use the PyTorch function `torch.nn.functional.interpolate`:

```python
import numpy as np
import torch
from torch.nn.functional import interpolate

sampling_step = 0.7
x = torch.arange(6).float()
x = x[None, None, ...] # add batch and channel dim

# Perform a linear interpolation with replication padding
y = interpolate(x, scale_factor=1/sampling_step, mode='linear')
```

With Python version 3.7.6 and PyTorch version 1.8.1, we have

```
[0.0000, 0.5500, 1.2500, 1.9500, 2.6500, 3.3500, 4.0500, 4.7500]
```

Here we have our 0.7 sampling step back (ignore the values that are affected by padding).

### The shift of the resized image

Even if `torch.nn.functional.interpolate` preserves the sampling step, the output field of view (FOV) does not align with the original image, as we can see from the above example. It is usually preferred to have the FOVs center around the same position before and after the interpolation to avoid shifting the contents of the image.

### Our implementation

Here we provide an implementation to both preserve the sampling step and to align up the FOV.

```python
from radifox.utils.resize.scipy import resize

sampling_step = 0.7
x = np.arange(6).astype(float)
ya = resize(x, (sampling_step, ), order=1)
print(ya) # [0.0 0.4 1.1 1.8 2.5 3.2 3.9 4.6 5.0]
```

### Update the Affine Matrix

Some images, such as medical images, come with position and orientation information in addition to the image 
array, usually as an affine matrix. We need to update that with a new origin and scale, based on our resized 
voxels. Our `update_affine` function assumes a homogeneous affine matrix as a numpy array.

```python
import numpy as np
from radifox.utils.resize.affine import update_affine

original_affine = np.array([
    [1, 0, 0, 0],
    [0, 1, 0, 0],
    [0, 0, 1, 0],
    [0, 0, 0, 1],
])
new_affine = update_affine(original_affine, [1, 1, 4])
```

This function will update the affine based on the new voxel size, taking into account any existing rotation, 
scale, translation or shear.

```python
np.array([[1. , 0. , 0. , 0. ],
          [0. , 1. , 0. , 0. ],
          [0. , 0. , 4. , 1.5],
          [0. , 0. , 0. , 1. ]])
```

For a more complex example, we can create a different example matrix with existing values.

```python
from transforms3d.affines import compose
from transforms3d.euler import euler2mat

T = np.array([14.0, 3.8, -39.1])
R = euler2mat(np.pi/16, -np.pi/16, np.pi/16)
Z = np.array([0.8, 0.8, 1.0])
S = np.array([0.0, 0.0, 0.0])

original_affine = compose(T, R, Z, S)
new_affine = update_affine(original_affine, [1, 1, 3])
```

We can see that our affine has now adjusted not just the 2 values as before, but has adjusted all of the origin 
values and the axis 2 column to reflect existing rotations and scales.

```python
new_affine - original_affine
# array([[ 0. ,  0. , -0.29921,  0.14960],
#        [ 0. ,  0. , -0.45734,  0.22867],
#        [ 0. ,  0. ,  1.92388,  0.96194],
#        [ 0. ,  0. ,  0.     ,  0.     ]])
```

            

Raw data

            {
    "_id": null,
    "home_page": "https://gitlab.com/iacl/radifox-utils",
    "name": "radifox-utils",
    "maintainer": "",
    "docs_url": null,
    "requires_python": ">=3.7",
    "maintainer_email": "",
    "keywords": "",
    "author": "IACL",
    "author_email": "iaclsoftware@jhu.edu",
    "download_url": "https://files.pythonhosted.org/packages/88/4a/8e571fc9d2576b8dd24bdc0c02a7f329fea6d1d556f8ddbc7000532e696f/radifox-utils-1.0.2.tar.gz",
    "platform": null,
    "description": "## Installation\n```bash\npip install radifox-utils\n```\nThis will install the utils package with it's `scipy` dependancy chain.\n\nTo install pytorch dependencies to use these functions with pytorch Tensors:\n```bash\npip install radifox-utils[pytorch]\n```\n\n\n## Degrade\n\nThis is a small library to degrade (blur and downsample) a signal. Here, we\naim to properly model the forward process in signal acquisition.\n\n## Resize\n\n### Sampling step when resizing an image\n\nWhen resizing a digital image with interpolation, we usually need to sample values at non-integer coordinates. The sampling step, i.e., the separation between two adjacent pixels/voxels to be sampled, then determines the digital resolution in the resulting image. When given a target digital resolution, we usually want our image resizing routines respect the corresponding sampling step. For example, when visualizing a zoomed-in medical image, if the digital resolution calculated from the image header mismatches the sampling step, we will see either a squeezed or stretched image.\n\nThere are many Python libraries available to resize an image. However, functions such as `scipy.ndimage.zoom` and `skimage.transform.rescale` do not respect the sampling step. Here we use a very simple experiment to demonstrate this.\n\n```python\nimport numpy as np\nfrom scipy.ndimage import zoom\n\nsampling_step = 0.7\nx = np.arange(6).astype(float)\n\n# Perform a linear interpolation with replication padding\ny = zoom(x, 1 / sampling_step, order=1, mode='nearest')\n```\n\nWith Python version 3.7.6 and Scipy version 1.6.3, we will have y equal to\n\n```\n[0, 0.625, 1.25, 1.875, 2.5, 3.125, 3.75, 4.375, 5.]\n```\n\nSince our sampling step is chosen as 0.7, we would expect the resulting array to have approximately 0.7 different between two nearby values. However, we get 0.625 in this case. For scikit-image,\n\n```python\nimport numpy as np\nfrom skimage.transform import rescale\n\nsampling_step = 0.7\nx = np.arange(6).astype(float)\n\n# Perform a linear interpolation with replication padding\ny = rescale(x, 1 / sampling_step, order=1, mode='edge')\n```\n\nWith Python version 3.7.6 and sciki-image version 1.18.1, we have y equal to\n\n```\n[0, 0.5, 1.16666667, 1.83333333, 2.5, 3.16666667, 3.83333333, 4.5, 5]\n```\n\nHere the nearby difference is 0.6666667 (ignore the first and last since they are affected by padding). It is not 0.7 either.\n\nWhat happens here is that since the resulting image should have an integer number of values, these functions choose to change the sampling step (or the scale, the inverse of the sampling) to accommodate that.\n\nHowever, if we use the PyTorch function `torch.nn.functional.interpolate`:\n\n```python\nimport numpy as np\nimport torch\nfrom torch.nn.functional import interpolate\n\nsampling_step = 0.7\nx = torch.arange(6).float()\nx = x[None, None, ...] # add batch and channel dim\n\n# Perform a linear interpolation with replication padding\ny = interpolate(x, scale_factor=1/sampling_step, mode='linear')\n```\n\nWith Python version 3.7.6 and PyTorch version 1.8.1, we have\n\n```\n[0.0000, 0.5500, 1.2500, 1.9500, 2.6500, 3.3500, 4.0500, 4.7500]\n```\n\nHere we have our 0.7 sampling step back (ignore the values that are affected by padding).\n\n### The shift of the resized image\n\nEven if `torch.nn.functional.interpolate` preserves the sampling step, the output field of view (FOV) does not align with the original image, as we can see from the above example. It is usually preferred to have the FOVs center around the same position before and after the interpolation to avoid shifting the contents of the image.\n\n### Our implementation\n\nHere we provide an implementation to both preserve the sampling step and to align up the FOV.\n\n```python\nfrom radifox.utils.resize.scipy import resize\n\nsampling_step = 0.7\nx = np.arange(6).astype(float)\nya = resize(x, (sampling_step, ), order=1)\nprint(ya) # [0.0 0.4 1.1 1.8 2.5 3.2 3.9 4.6 5.0]\n```\n\n### Update the Affine Matrix\n\nSome images, such as medical images, come with position and orientation information in addition to the image \narray, usually as an affine matrix. We need to update that with a new origin and scale, based on our resized \nvoxels. Our `update_affine` function assumes a homogeneous affine matrix as a numpy array.\n\n```python\nimport numpy as np\nfrom radifox.utils.resize.affine import update_affine\n\noriginal_affine = np.array([\n    [1, 0, 0, 0],\n    [0, 1, 0, 0],\n    [0, 0, 1, 0],\n    [0, 0, 0, 1],\n])\nnew_affine = update_affine(original_affine, [1, 1, 4])\n```\n\nThis function will update the affine based on the new voxel size, taking into account any existing rotation, \nscale, translation or shear.\n\n```python\nnp.array([[1. , 0. , 0. , 0. ],\n          [0. , 1. , 0. , 0. ],\n          [0. , 0. , 4. , 1.5],\n          [0. , 0. , 0. , 1. ]])\n```\n\nFor a more complex example, we can create a different example matrix with existing values.\n\n```python\nfrom transforms3d.affines import compose\nfrom transforms3d.euler import euler2mat\n\nT = np.array([14.0, 3.8, -39.1])\nR = euler2mat(np.pi/16, -np.pi/16, np.pi/16)\nZ = np.array([0.8, 0.8, 1.0])\nS = np.array([0.0, 0.0, 0.0])\n\noriginal_affine = compose(T, R, Z, S)\nnew_affine = update_affine(original_affine, [1, 1, 3])\n```\n\nWe can see that our affine has now adjusted not just the 2 values as before, but has adjusted all of the origin \nvalues and the axis 2 column to reflect existing rotations and scales.\n\n```python\nnew_affine - original_affine\n# array([[ 0. ,  0. , -0.29921,  0.14960],\n#        [ 0. ,  0. , -0.45734,  0.22867],\n#        [ 0. ,  0. ,  1.92388,  0.96194],\n#        [ 0. ,  0. ,  0.     ,  0.     ]])\n```\n",
    "bugtrack_url": null,
    "license": "Apache License, 2.0",
    "summary": "Medical image utilities provided with RADIFOX",
    "version": "1.0.2",
    "project_urls": {
        "Homepage": "https://gitlab.com/iacl/radifox-utils"
    },
    "split_keywords": [],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "52d53fd279a40ea242041b3aaa5dd624b0873b88910342821d9e65fdd2e1dd72",
                "md5": "84c99f3df16d0fd175140d23ae162651",
                "sha256": "9d8778329a98c6e1312c27982b8b37c7b55ef92fc379a31e83e515e66c46e355"
            },
            "downloads": -1,
            "filename": "radifox_utils-1.0.2-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "84c99f3df16d0fd175140d23ae162651",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.7",
            "size": 24050,
            "upload_time": "2023-11-16T01:53:04",
            "upload_time_iso_8601": "2023-11-16T01:53:04.742120Z",
            "url": "https://files.pythonhosted.org/packages/52/d5/3fd279a40ea242041b3aaa5dd624b0873b88910342821d9e65fdd2e1dd72/radifox_utils-1.0.2-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "884a8e571fc9d2576b8dd24bdc0c02a7f329fea6d1d556f8ddbc7000532e696f",
                "md5": "71e7874d13e91307620f5e04ddb3fc08",
                "sha256": "58aedce9d52471dd4dedef7c1c22c5e8d60eabb37ba9d0300a3c9fd0c5ade764"
            },
            "downloads": -1,
            "filename": "radifox-utils-1.0.2.tar.gz",
            "has_sig": false,
            "md5_digest": "71e7874d13e91307620f5e04ddb3fc08",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.7",
            "size": 20525,
            "upload_time": "2023-11-16T01:53:05",
            "upload_time_iso_8601": "2023-11-16T01:53:05.863150Z",
            "url": "https://files.pythonhosted.org/packages/88/4a/8e571fc9d2576b8dd24bdc0c02a7f329fea6d1d556f8ddbc7000532e696f/radifox-utils-1.0.2.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2023-11-16 01:53:05",
    "github": false,
    "gitlab": true,
    "bitbucket": false,
    "codeberg": false,
    "gitlab_user": "iacl",
    "gitlab_project": "radifox-utils",
    "lcname": "radifox-utils"
}
        
Elapsed time: 0.14784s