pumpia-uniform-phantom 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pumpia_uniform_phantom-0.0.1/.gitignore +118 -0
- pumpia_uniform_phantom-0.0.1/LICENSE +28 -0
- pumpia_uniform_phantom-0.0.1/PKG-INFO +61 -0
- pumpia_uniform_phantom-0.0.1/README.md +32 -0
- pumpia_uniform_phantom-0.0.1/pumpia_uniform_phantom/__init__.py +0 -0
- pumpia_uniform_phantom-0.0.1/pumpia_uniform_phantom/modules/__init__.py +0 -0
- pumpia_uniform_phantom-0.0.1/pumpia_uniform_phantom/modules/sub_snr.py +192 -0
- pumpia_uniform_phantom-0.0.1/pumpia_uniform_phantom/modules/uniformity.py +86 -0
- pumpia_uniform_phantom-0.0.1/pumpia_uniform_phantom/repeat/__init__.py +0 -0
- pumpia_uniform_phantom-0.0.1/pumpia_uniform_phantom/repeat/collection.py +66 -0
- pumpia_uniform_phantom-0.0.1/pumpia_uniform_phantom/scripts/__init__.py +0 -0
- pumpia_uniform_phantom-0.0.1/pumpia_uniform_phantom/scripts/run_uniform_rpt.py +9 -0
- pumpia_uniform_phantom-0.0.1/pyproject.toml +45 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Editor temporary/working/backup files #
|
|
2
|
+
#########################################
|
|
3
|
+
.#*
|
|
4
|
+
[#]*#
|
|
5
|
+
*~
|
|
6
|
+
*$
|
|
7
|
+
*.bak
|
|
8
|
+
*.diff
|
|
9
|
+
.idea/
|
|
10
|
+
*.iml
|
|
11
|
+
*.ipr
|
|
12
|
+
*.iws
|
|
13
|
+
*.org
|
|
14
|
+
.project
|
|
15
|
+
pmip
|
|
16
|
+
*.rej
|
|
17
|
+
.settings/
|
|
18
|
+
.*.sw[nop]
|
|
19
|
+
.sw[nop]
|
|
20
|
+
*.tmp
|
|
21
|
+
*.vim
|
|
22
|
+
.vscode
|
|
23
|
+
tags
|
|
24
|
+
cscope.out
|
|
25
|
+
# gnu global
|
|
26
|
+
GPATH
|
|
27
|
+
GRTAGS
|
|
28
|
+
GSYMS
|
|
29
|
+
GTAGS
|
|
30
|
+
.cache
|
|
31
|
+
.mypy_cache/
|
|
32
|
+
|
|
33
|
+
# Compiled source #
|
|
34
|
+
###################
|
|
35
|
+
*.a
|
|
36
|
+
*.com
|
|
37
|
+
*.class
|
|
38
|
+
*.dll
|
|
39
|
+
*.exe
|
|
40
|
+
*.o
|
|
41
|
+
*.o.d
|
|
42
|
+
*.py[ocd]
|
|
43
|
+
*.so
|
|
44
|
+
*.mod
|
|
45
|
+
*.dist
|
|
46
|
+
|
|
47
|
+
# Packages #
|
|
48
|
+
############
|
|
49
|
+
# it's better to unpack these files and commit the raw source
|
|
50
|
+
# git has its own built in compression methods
|
|
51
|
+
*.7z
|
|
52
|
+
*.bz2
|
|
53
|
+
*.bzip2
|
|
54
|
+
*.dmg
|
|
55
|
+
*.gz
|
|
56
|
+
*.iso
|
|
57
|
+
*.jar
|
|
58
|
+
*.rar
|
|
59
|
+
*.tar
|
|
60
|
+
*.tbz2
|
|
61
|
+
*.tgz
|
|
62
|
+
*.zip
|
|
63
|
+
|
|
64
|
+
# Python files #
|
|
65
|
+
################
|
|
66
|
+
# meson build/installation directories
|
|
67
|
+
build
|
|
68
|
+
build-install
|
|
69
|
+
# meson python output
|
|
70
|
+
.mesonpy-native-file.ini
|
|
71
|
+
# sphinx build directory
|
|
72
|
+
_build
|
|
73
|
+
# dist directory is where sdist/wheel end up
|
|
74
|
+
dist
|
|
75
|
+
doc/build
|
|
76
|
+
doc/docenv
|
|
77
|
+
doc/cdoc/build
|
|
78
|
+
# Egg metadata
|
|
79
|
+
*.egg-info
|
|
80
|
+
# The shelf plugin uses this dir
|
|
81
|
+
./.shelf
|
|
82
|
+
.cache
|
|
83
|
+
pip-wheel-metadata
|
|
84
|
+
.python-version
|
|
85
|
+
|
|
86
|
+
# Logs and databases #
|
|
87
|
+
######################
|
|
88
|
+
*.log
|
|
89
|
+
*.sql
|
|
90
|
+
*.sqlite
|
|
91
|
+
*nuitka-crash-report.xml
|
|
92
|
+
|
|
93
|
+
# Patches #
|
|
94
|
+
###########
|
|
95
|
+
*.patch
|
|
96
|
+
*.diff
|
|
97
|
+
|
|
98
|
+
# OS generated files #
|
|
99
|
+
######################
|
|
100
|
+
.DS_Store*
|
|
101
|
+
.VolumeIcon.icns
|
|
102
|
+
.fseventsd
|
|
103
|
+
Icon?
|
|
104
|
+
.gdb_history
|
|
105
|
+
ehthumbs.db
|
|
106
|
+
Thumbs.db
|
|
107
|
+
.directory
|
|
108
|
+
|
|
109
|
+
# pytest generated files #
|
|
110
|
+
##########################
|
|
111
|
+
/.pytest_cache
|
|
112
|
+
|
|
113
|
+
# doc build generated files #
|
|
114
|
+
#############################
|
|
115
|
+
doc/source/savefig/
|
|
116
|
+
doc/source/**/generated/
|
|
117
|
+
doc/source/release/notes-towncrier.rst
|
|
118
|
+
doc/source/.jupyterlite.doit.db
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025, Principle Five
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pumpia-uniform-phantom
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Uniform MRI phantom analysis based using the PumpIA framework.
|
|
5
|
+
Project-URL: Homepage, https://www.principlefive.org.uk/
|
|
6
|
+
Project-URL: Documentation, https://github.com/Principle-Five/pumpia-uniform-phantom
|
|
7
|
+
Project-URL: Source, https://github.com/Principle-Five/pumpia-uniform-phantom
|
|
8
|
+
Project-URL: Issues, https://github.com/Principle-Five/pumpia-uniform-phantom/issues
|
|
9
|
+
Author: Zack Ravetz et al.
|
|
10
|
+
License-Expression: BSD-3-Clause
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
21
|
+
Classifier: Topic :: Software Development
|
|
22
|
+
Requires-Python: >=3.12
|
|
23
|
+
Requires-Dist: matplotlib
|
|
24
|
+
Requires-Dist: numpy
|
|
25
|
+
Requires-Dist: pumpia==0.6.*,>=0.6.7
|
|
26
|
+
Requires-Dist: pylibjpeg[all]
|
|
27
|
+
Requires-Dist: scipy
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# Introduction
|
|
31
|
+
This repository contains code to analyse a uniform phantom using the [PumpIA](https://github.com/Principle-Five/pumpia) framework.
|
|
32
|
+
It uses the subtraction SNR method and therefore expects a repeat image, however the uniformity module will run with a single image.
|
|
33
|
+
|
|
34
|
+
It is currently not validated and is provided as is, see the license for more information.
|
|
35
|
+
|
|
36
|
+
The collection contains the following tests:
|
|
37
|
+
- SNR
|
|
38
|
+
- Uniformity
|
|
39
|
+
|
|
40
|
+
# Usage
|
|
41
|
+
|
|
42
|
+
Users should make themselves familiar with the [PumpIA user interface](https://principle-five.github.io/pumpia/usage/user_interface.html)
|
|
43
|
+
|
|
44
|
+
To run the collection:
|
|
45
|
+
1. Clone the repository
|
|
46
|
+
2. Use an environment manager to install the requirements from `requirements.txt` or install the requirements using the command `pip install -r requirements.txt` when in the repository directory
|
|
47
|
+
3. Run the `run_uniform_rpt_collection.py` script
|
|
48
|
+
|
|
49
|
+
To use the collection:
|
|
50
|
+
1. Load the folder with the relevant images
|
|
51
|
+
2. Drag and drop the series containing the relevant images into the viewer of the `Main` tab
|
|
52
|
+
3. Generate the ROIs and run analysis
|
|
53
|
+
4. Correct the context if required (see below)
|
|
54
|
+
- Re-run generating ROIs
|
|
55
|
+
5. Move any ROIs as required, this should be done through their relevant modules.
|
|
56
|
+
- Re-run analysis
|
|
57
|
+
6. Copy the results in the relevant format. Horizontal is tab separated, vertical is new line separated.
|
|
58
|
+
|
|
59
|
+
## Correcting Context
|
|
60
|
+
|
|
61
|
+
The context used for this collection is the Auto Phantom Context Manager provided with PumpIA, see the [relevant documentation](https://principle-five.github.io/pumpia/usage/user_interface.html) for this.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Introduction
|
|
2
|
+
This repository contains code to analyse a uniform phantom using the [PumpIA](https://github.com/Principle-Five/pumpia) framework.
|
|
3
|
+
It uses the subtraction SNR method and therefore expects a repeat image, however the uniformity module will run with a single image.
|
|
4
|
+
|
|
5
|
+
It is currently not validated and is provided as is, see the license for more information.
|
|
6
|
+
|
|
7
|
+
The collection contains the following tests:
|
|
8
|
+
- SNR
|
|
9
|
+
- Uniformity
|
|
10
|
+
|
|
11
|
+
# Usage
|
|
12
|
+
|
|
13
|
+
Users should make themselves familiar with the [PumpIA user interface](https://principle-five.github.io/pumpia/usage/user_interface.html)
|
|
14
|
+
|
|
15
|
+
To run the collection:
|
|
16
|
+
1. Clone the repository
|
|
17
|
+
2. Use an environment manager to install the requirements from `requirements.txt` or install the requirements using the command `pip install -r requirements.txt` when in the repository directory
|
|
18
|
+
3. Run the `run_uniform_rpt_collection.py` script
|
|
19
|
+
|
|
20
|
+
To use the collection:
|
|
21
|
+
1. Load the folder with the relevant images
|
|
22
|
+
2. Drag and drop the series containing the relevant images into the viewer of the `Main` tab
|
|
23
|
+
3. Generate the ROIs and run analysis
|
|
24
|
+
4. Correct the context if required (see below)
|
|
25
|
+
- Re-run generating ROIs
|
|
26
|
+
5. Move any ROIs as required, this should be done through their relevant modules.
|
|
27
|
+
- Re-run analysis
|
|
28
|
+
6. Copy the results in the relevant format. Horizontal is tab separated, vertical is new line separated.
|
|
29
|
+
|
|
30
|
+
## Correcting Context
|
|
31
|
+
|
|
32
|
+
The context used for this collection is the Auto Phantom Context Manager provided with PumpIA, see the [relevant documentation](https://principle-five.github.io/pumpia/usage/user_interface.html) for this.
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Subtraction SNR module for uniform phantom
|
|
3
|
+
"""
|
|
4
|
+
import math
|
|
5
|
+
import numpy as np
|
|
6
|
+
import matplotlib.pyplot as plt
|
|
7
|
+
|
|
8
|
+
from pumpia.module_handling.modules import PhantomModule
|
|
9
|
+
from pumpia.module_handling.fields.roi_fields import GeneralROIField
|
|
10
|
+
from pumpia.module_handling.fields.viewer_fields import MonochromeDicomViewerField
|
|
11
|
+
from pumpia.module_handling.fields.simple import (PercField,
|
|
12
|
+
FloatField,
|
|
13
|
+
BoolField)
|
|
14
|
+
from pumpia.image_handling.roi_structures import EllipseROI, RectangleROI
|
|
15
|
+
from pumpia.file_handling.dicom_structures import Series, Instance
|
|
16
|
+
from pumpia.file_handling.dicom_tags import MRTags
|
|
17
|
+
from pumpia.module_handling.context import PhantomContext
|
|
18
|
+
from pumpia.widgets.context_managers import AutoPhantomManager
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SubSNR(PhantomModule):
|
|
22
|
+
"""
|
|
23
|
+
Module for subtraction method SNR on uniform phantom.
|
|
24
|
+
"""
|
|
25
|
+
context_manager = AutoPhantomManager()
|
|
26
|
+
show_draw_rois_button = True
|
|
27
|
+
show_analyse_button = True
|
|
28
|
+
title = "Subtraction SNR"
|
|
29
|
+
|
|
30
|
+
viewer1 = MonochromeDicomViewerField(row=0, column=0)
|
|
31
|
+
viewer2 = MonochromeDicomViewerField(row=0, column=1, allow_changing_rois=False)
|
|
32
|
+
|
|
33
|
+
size = PercField(70, verbose_name="Size (%)")
|
|
34
|
+
ref_bandwidth = FloatField(1, verbose_name="Reference Bandwidth (Hz/px)")
|
|
35
|
+
bw_cor_bool = BoolField(verbose_name="Bandwidth Correction")
|
|
36
|
+
pix_size_bool = BoolField(verbose_name="Pixel Size Correction")
|
|
37
|
+
avg_cor_bool = BoolField(verbose_name="Averages Correction")
|
|
38
|
+
pe_cor_bool = BoolField(verbose_name="Phase Encode Correction")
|
|
39
|
+
|
|
40
|
+
im_bw = FloatField(verbose_name="Image Bandwidth (Hz/px)", read_only=True)
|
|
41
|
+
pixel_size_cor = FloatField(verbose_name="Pixel Size Correction", read_only=True)
|
|
42
|
+
pe_cor = FloatField(verbose_name="Phase Encode Correction", read_only=True)
|
|
43
|
+
avg_cor = FloatField(verbose_name="Averages Correction", read_only=True)
|
|
44
|
+
signal = FloatField(read_only=True)
|
|
45
|
+
noise = FloatField(read_only=True)
|
|
46
|
+
snr = FloatField(verbose_name="SNR", read_only=True)
|
|
47
|
+
cor_snr = FloatField(verbose_name="Corrected SNR", read_only=True)
|
|
48
|
+
|
|
49
|
+
signal_roi1 = GeneralROIField("SNR ROI1", default_type="ROI rectangle")
|
|
50
|
+
signal_roi2 = GeneralROIField("SNR ROI2", allow_manual_draw=False)
|
|
51
|
+
|
|
52
|
+
def draw_rois(self, context: PhantomContext, batch: bool = False) -> None:
|
|
53
|
+
if isinstance(self.viewer1.image, (Instance, Series)):
|
|
54
|
+
factor = self.size / 100
|
|
55
|
+
if context.shape == "rectangle":
|
|
56
|
+
xmin = round(context.xcent - (factor * context.x_length / 2))
|
|
57
|
+
xmax = round(context.xcent + (factor * context.x_length / 2))
|
|
58
|
+
ymin = round(context.ycent - (factor * context.y_length / 2))
|
|
59
|
+
ymax = round(context.ycent + (factor * context.y_length / 2))
|
|
60
|
+
self.signal_roi1.register_roi(RectangleROI(self.viewer1.image,
|
|
61
|
+
xmin,
|
|
62
|
+
ymin,
|
|
63
|
+
xmax - xmin,
|
|
64
|
+
ymax - ymin,
|
|
65
|
+
slice_num=self.viewer1.current_slice))
|
|
66
|
+
else:
|
|
67
|
+
a = round(factor * context.x_length / 2)
|
|
68
|
+
b = round(factor * context.y_length / 2)
|
|
69
|
+
self.signal_roi1.register_roi(EllipseROI(self.viewer1.image,
|
|
70
|
+
round(context.xcent),
|
|
71
|
+
round(context.ycent),
|
|
72
|
+
a,
|
|
73
|
+
b,
|
|
74
|
+
slice_num=self.viewer1.current_slice))
|
|
75
|
+
|
|
76
|
+
def post_roi_register(self, roi_input: GeneralROIField):
|
|
77
|
+
if (roi_input == self.signal_roi1
|
|
78
|
+
and self.signal_roi1.roi is not None):
|
|
79
|
+
if self.manager is not None:
|
|
80
|
+
self.manager.add_roi(self.signal_roi1.roi)
|
|
81
|
+
|
|
82
|
+
if isinstance(self.viewer2.image, (Instance, Series)):
|
|
83
|
+
image = self.viewer2.image
|
|
84
|
+
roi = self.signal_roi1.roi.copy_to_image(image,
|
|
85
|
+
image.current_slice,
|
|
86
|
+
"SNR ROI",
|
|
87
|
+
True)
|
|
88
|
+
self.signal_roi2.register_roi(roi)
|
|
89
|
+
if (self.signal_roi2.roi is not None
|
|
90
|
+
and self.manager is not None):
|
|
91
|
+
self.manager.add_roi(self.signal_roi2.roi)
|
|
92
|
+
|
|
93
|
+
def link_rois_viewers(self):
|
|
94
|
+
self.signal_roi1.viewer = self.viewer1
|
|
95
|
+
self.signal_roi2.viewer = self.viewer2
|
|
96
|
+
|
|
97
|
+
def analyse(self, batch: bool = False):
|
|
98
|
+
if self.signal_roi1.roi is not None and self.signal_roi2.roi is not None:
|
|
99
|
+
roi1 = self.signal_roi1.roi
|
|
100
|
+
roi2 = self.signal_roi2.roi
|
|
101
|
+
roi_sum = roi1.pixel_values + roi2.pixel_values
|
|
102
|
+
roi_sub = np.array(roi1.pixel_values) - np.array(roi2.pixel_values)
|
|
103
|
+
sum_roi = np.mean(roi_sum)
|
|
104
|
+
if isinstance(sum_roi, float):
|
|
105
|
+
self.signal = sum_roi
|
|
106
|
+
roi_noise = np.std(roi_sub) / math.sqrt(2)
|
|
107
|
+
if isinstance(roi_noise, float):
|
|
108
|
+
self.noise = roi_noise
|
|
109
|
+
snr = sum_roi / roi_noise
|
|
110
|
+
if isinstance(snr, float):
|
|
111
|
+
self.snr = snr
|
|
112
|
+
|
|
113
|
+
cor_snr = snr
|
|
114
|
+
|
|
115
|
+
image = roi1.image
|
|
116
|
+
if isinstance(image, Instance):
|
|
117
|
+
px_cor = 1
|
|
118
|
+
bw_cor = 1
|
|
119
|
+
avg_cor = 1
|
|
120
|
+
pe_cor = 1
|
|
121
|
+
|
|
122
|
+
if (self.pix_size_bool
|
|
123
|
+
and not (image.slice_thickness is None
|
|
124
|
+
or image.pixel_spacing is None)):
|
|
125
|
+
pix_size = image.slice_thickness, *image.pixel_spacing
|
|
126
|
+
self.logger.info("pixel size = %s", pix_size)
|
|
127
|
+
px_cor = 1 / math.prod(pix_size)
|
|
128
|
+
self.pixel_size_cor = px_cor
|
|
129
|
+
|
|
130
|
+
if self.bw_cor_bool:
|
|
131
|
+
ref_bw = self.ref_bandwidth
|
|
132
|
+
try:
|
|
133
|
+
im_bw = image.get_tag(MRTags.PixelBandwidth)
|
|
134
|
+
try:
|
|
135
|
+
im_bw = float(im_bw) # type: ignore
|
|
136
|
+
except (ValueError, TypeError):
|
|
137
|
+
im_bw = ref_bw
|
|
138
|
+
except KeyError:
|
|
139
|
+
im_bw = ref_bw
|
|
140
|
+
self.im_bw = im_bw
|
|
141
|
+
bw_cor = math.sqrt(im_bw / ref_bw)
|
|
142
|
+
|
|
143
|
+
if self.avg_cor_bool:
|
|
144
|
+
try:
|
|
145
|
+
im_av = image.get_tag(MRTags.NumberOfAverages)
|
|
146
|
+
try:
|
|
147
|
+
im_av = float(im_av) # type: ignore
|
|
148
|
+
except (ValueError, TypeError):
|
|
149
|
+
im_av = 1
|
|
150
|
+
except KeyError:
|
|
151
|
+
im_av = 1
|
|
152
|
+
avg_cor = 1 / math.sqrt(im_av)
|
|
153
|
+
self.avg_cor = avg_cor
|
|
154
|
+
|
|
155
|
+
if self.pe_cor_bool:
|
|
156
|
+
try:
|
|
157
|
+
im_pe = float(
|
|
158
|
+
image.get_tag(MRTags.NumberOfPhaseEncodingSteps)) # type: ignore
|
|
159
|
+
except (KeyError, ValueError, TypeError):
|
|
160
|
+
im_pe = 1
|
|
161
|
+
|
|
162
|
+
if im_pe == 1:
|
|
163
|
+
try:
|
|
164
|
+
if image.get_tag(MRTags.InPlanePhaseEncodingDirection) == "ROW":
|
|
165
|
+
num = int(
|
|
166
|
+
image.get_tag(MRTags.Rows)) # type: ignore
|
|
167
|
+
else:
|
|
168
|
+
num = int(
|
|
169
|
+
image.get_tag(MRTags.Columns)) # type: ignore
|
|
170
|
+
im_pe = float(
|
|
171
|
+
image.get_tag(MRTags.PercentSampling)) * num # type: ignore
|
|
172
|
+
except (KeyError, ValueError, TypeError):
|
|
173
|
+
im_pe = 1
|
|
174
|
+
|
|
175
|
+
pe_cor = 1 / math.sqrt(im_pe)
|
|
176
|
+
self.pe_cor = pe_cor
|
|
177
|
+
|
|
178
|
+
cor_snr = snr * px_cor * bw_cor * avg_cor * pe_cor
|
|
179
|
+
|
|
180
|
+
self.cor_snr = float(cor_snr)
|
|
181
|
+
|
|
182
|
+
def load_commands(self):
|
|
183
|
+
self.register_command("Show Subtraction Image", self.show_sub_image)
|
|
184
|
+
|
|
185
|
+
def show_sub_image(self):
|
|
186
|
+
if self.signal_roi1.roi is not None and self.signal_roi2.roi is not None:
|
|
187
|
+
roi1 = self.signal_roi1.roi
|
|
188
|
+
roi2 = self.signal_roi2.roi
|
|
189
|
+
sub_array = roi1.image.array[0] - roi2.image.array[0]
|
|
190
|
+
plt.imshow(sub_array, cmap='grey')
|
|
191
|
+
plt.colorbar()
|
|
192
|
+
plt.show()
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Integral uniformity module for uniform phantom
|
|
3
|
+
"""
|
|
4
|
+
import numpy as np
|
|
5
|
+
from scipy.signal import convolve2d
|
|
6
|
+
|
|
7
|
+
from pumpia.module_handling.modules import PhantomModule
|
|
8
|
+
from pumpia.module_handling.fields.roi_fields import GeneralROIField
|
|
9
|
+
from pumpia.module_handling.fields.viewer_fields import MonochromeDicomViewerField
|
|
10
|
+
from pumpia.module_handling.fields.simple import (PercField,
|
|
11
|
+
BoolField,
|
|
12
|
+
FloatField)
|
|
13
|
+
from pumpia.image_handling.roi_structures import EllipseROI, RectangleROI
|
|
14
|
+
from pumpia.file_handling.dicom_structures import Series, Instance
|
|
15
|
+
from pumpia.module_handling.context import PhantomContext
|
|
16
|
+
from pumpia.widgets.context_managers import AutoPhantomManager
|
|
17
|
+
|
|
18
|
+
LOW_PASS_KERNEL = np.array([[1, 2, 1], [2, 4, 2], [1, 2, 1]]) / 16
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Uniformity(PhantomModule):
|
|
22
|
+
"""
|
|
23
|
+
Integral uniformity module for uniform phantom.
|
|
24
|
+
"""
|
|
25
|
+
context_manager = AutoPhantomManager()
|
|
26
|
+
show_draw_rois_button = True
|
|
27
|
+
show_analyse_button = True
|
|
28
|
+
title = "Uniformity"
|
|
29
|
+
|
|
30
|
+
viewer = MonochromeDicomViewerField(row=0, column=0)
|
|
31
|
+
|
|
32
|
+
size = PercField(70, verbose_name="Size (%)")
|
|
33
|
+
kernel_bool = BoolField(verbose_name="Apply Low Pass Kernel")
|
|
34
|
+
|
|
35
|
+
uniformity = FloatField(verbose_name="Uniformity (%)", read_only=True)
|
|
36
|
+
|
|
37
|
+
uniformity_roi = GeneralROIField("Uniformity ROI", default_type="ROI rectangle")
|
|
38
|
+
|
|
39
|
+
def draw_rois(self, context: PhantomContext, batch: bool = False) -> None:
|
|
40
|
+
if isinstance(self.viewer.image, (Instance, Series)):
|
|
41
|
+
factor = self.size / 100
|
|
42
|
+
if context.shape == "rectangle":
|
|
43
|
+
xmin = round(context.xcent - (factor * context.x_length / 2))
|
|
44
|
+
xmax = round(context.xcent + (factor * context.x_length / 2))
|
|
45
|
+
ymin = round(context.ycent - (factor * context.y_length / 2))
|
|
46
|
+
ymax = round(context.ycent + (factor * context.y_length / 2))
|
|
47
|
+
self.uniformity_roi.register_roi(RectangleROI(self.viewer.image,
|
|
48
|
+
xmin,
|
|
49
|
+
ymin,
|
|
50
|
+
xmax - xmin,
|
|
51
|
+
ymax - ymin,
|
|
52
|
+
slice_num=self.viewer.current_slice))
|
|
53
|
+
else:
|
|
54
|
+
a = round(factor * context.x_length / 2)
|
|
55
|
+
b = round(factor * context.y_length / 2)
|
|
56
|
+
self.uniformity_roi.register_roi(EllipseROI(self.viewer.image,
|
|
57
|
+
round(context.xcent),
|
|
58
|
+
round(context.ycent),
|
|
59
|
+
a,
|
|
60
|
+
b,
|
|
61
|
+
slice_num=self.viewer.current_slice))
|
|
62
|
+
|
|
63
|
+
def post_roi_register(self, roi_input: GeneralROIField):
|
|
64
|
+
if (roi_input == self.uniformity_roi
|
|
65
|
+
and self.uniformity_roi.roi is not None
|
|
66
|
+
and self.manager is not None):
|
|
67
|
+
self.manager.add_roi(self.uniformity_roi.roi)
|
|
68
|
+
|
|
69
|
+
def link_rois_viewers(self):
|
|
70
|
+
self.uniformity_roi.viewer = self.viewer
|
|
71
|
+
|
|
72
|
+
def analyse(self, batch: bool = False):
|
|
73
|
+
if self.uniformity_roi.roi is not None:
|
|
74
|
+
roi = self.uniformity_roi.roi
|
|
75
|
+
if self.kernel_bool:
|
|
76
|
+
array = roi.image.array[0]
|
|
77
|
+
array = convolve2d(array, LOW_PASS_KERNEL, mode="same")
|
|
78
|
+
mask = roi.mask
|
|
79
|
+
pixel_values = list(array[mask])
|
|
80
|
+
else:
|
|
81
|
+
pixel_values = roi.pixel_values
|
|
82
|
+
|
|
83
|
+
max_val = max(pixel_values)
|
|
84
|
+
min_val = min(pixel_values)
|
|
85
|
+
uniformity = 100 * (1 - ((max_val - min_val) / (max_val + min_val))) # type: ignore
|
|
86
|
+
self.uniformity = uniformity
|
|
File without changes
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Collection for uniform phantom with repeat images.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pumpia.module_handling.collections import ModuleGroup, BaseCollection
|
|
6
|
+
from pumpia.module_handling.fields.windows import FieldWindow
|
|
7
|
+
from pumpia.module_handling.fields.groups import FieldGroup
|
|
8
|
+
from pumpia.module_handling.fields.viewer_fields import MonochromeDicomViewerField
|
|
9
|
+
from pumpia.widgets.viewers import MonochromeDicomViewer
|
|
10
|
+
from pumpia.widgets.context_managers import AutoPhantomManager
|
|
11
|
+
|
|
12
|
+
from pumpia_uniform_phantom.modules.sub_snr import SubSNR
|
|
13
|
+
from pumpia_uniform_phantom.modules.uniformity import Uniformity
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class RepeatImagesCollection(BaseCollection):
|
|
17
|
+
"""
|
|
18
|
+
Collection for uniform phantom with repeated scans.
|
|
19
|
+
"""
|
|
20
|
+
context_manager = AutoPhantomManager()
|
|
21
|
+
title = "Uniform Phantom Repeat Collection"
|
|
22
|
+
|
|
23
|
+
viewer1 = MonochromeDicomViewerField(row=0, column=0)
|
|
24
|
+
viewer2 = MonochromeDicomViewerField(row=0, column=1)
|
|
25
|
+
|
|
26
|
+
snr = SubSNR(verbose_name="SNR")
|
|
27
|
+
|
|
28
|
+
uniformity1 = Uniformity(verbose_name="Uniformity")
|
|
29
|
+
uniformity2 = Uniformity(verbose_name="Uniformity")
|
|
30
|
+
|
|
31
|
+
snr_output = FieldWindow(snr.fields.signal,
|
|
32
|
+
snr.fields.noise,
|
|
33
|
+
snr.fields.snr,
|
|
34
|
+
snr.fields.cor_snr,
|
|
35
|
+
verbose_name="SNR Output")
|
|
36
|
+
image1_output = FieldWindow(uniformity1.fields.uniformity, verbose_name="Image 1 Results")
|
|
37
|
+
image2_output = FieldWindow(uniformity2.fields.uniformity, verbose_name="Image 2 Results")
|
|
38
|
+
full_results = FieldWindow(snr.fields.signal,
|
|
39
|
+
snr.fields.noise,
|
|
40
|
+
snr.fields.snr,
|
|
41
|
+
snr.fields.cor_snr,
|
|
42
|
+
uniformity1.fields.uniformity,
|
|
43
|
+
uniformity2.fields.uniformity,
|
|
44
|
+
field_names=[None,
|
|
45
|
+
None,
|
|
46
|
+
None,
|
|
47
|
+
None,
|
|
48
|
+
"Image 1 Uniformity",
|
|
49
|
+
"Image 2 Uniformity"])
|
|
50
|
+
|
|
51
|
+
size_group = FieldGroup(uniformity1.fields.size, uniformity2.fields.size)
|
|
52
|
+
kernel_group = FieldGroup(uniformity1.fields.kernel_bool, uniformity2.fields.kernel_bool)
|
|
53
|
+
|
|
54
|
+
uniformity_window = ModuleGroup(uniformity1, uniformity2, verbose_name="Uniformity")
|
|
55
|
+
|
|
56
|
+
def on_image_load(self, viewer: MonochromeDicomViewer) -> None:
|
|
57
|
+
if viewer is self.viewer1:
|
|
58
|
+
if self.viewer1.image is not None:
|
|
59
|
+
image = self.viewer1.image
|
|
60
|
+
self.snr.viewer1.load_image(image)
|
|
61
|
+
self.uniformity1.viewer.load_image(image)
|
|
62
|
+
elif viewer is self.viewer2:
|
|
63
|
+
if self.viewer2.image is not None:
|
|
64
|
+
image = self.viewer2.image
|
|
65
|
+
self.snr.viewer2.load_image(image)
|
|
66
|
+
self.uniformity2.viewer.load_image(image)
|
|
File without changes
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pumpia-uniform-phantom"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
authors = [{ name = "Zack Ravetz et al." }]
|
|
9
|
+
description = "Uniform MRI phantom analysis based using the PumpIA framework."
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
requires-python = ">=3.12"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"pumpia == 0.6.*, >=0.6.7",
|
|
14
|
+
"scipy",
|
|
15
|
+
"numpy",
|
|
16
|
+
"pylibjpeg[all]",
|
|
17
|
+
"matplotlib",
|
|
18
|
+
]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
22
|
+
'Programming Language :: Python :: 3.12',
|
|
23
|
+
'Programming Language :: Python :: 3.13',
|
|
24
|
+
'Programming Language :: Python :: 3.14',
|
|
25
|
+
"Intended Audience :: Science/Research",
|
|
26
|
+
'License :: OSI Approved :: BSD License',
|
|
27
|
+
'Topic :: Software Development',
|
|
28
|
+
'Topic :: Scientific/Engineering',
|
|
29
|
+
'Topic :: Scientific/Engineering :: Image Processing',
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
license = "BSD-3-Clause"
|
|
33
|
+
license-files = ["LICENSE"]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://www.principlefive.org.uk/"
|
|
37
|
+
Documentation = "https://github.com/Principle-Five/pumpia-uniform-phantom"
|
|
38
|
+
Source = "https://github.com/Principle-Five/pumpia-uniform-phantom"
|
|
39
|
+
Issues = "https://github.com/Principle-Five/pumpia-uniform-phantom/issues"
|
|
40
|
+
|
|
41
|
+
[tool.hatch.build.targets.sdist]
|
|
42
|
+
include = ["pumpia_uniform_phantom*"]
|
|
43
|
+
|
|
44
|
+
[project.gui-scripts]
|
|
45
|
+
pumpia-uniform-phantom = "pumpia_uniform_phantom.scripts.run_uniform_rpt:run_repeat_uniform"
|