Skip to content

Commit 90c0fc4

Browse files
committed
v2.7.1
1 parent b77cf8a commit 90c0fc4

File tree

14 files changed

+917
-5
lines changed

14 files changed

+917
-5
lines changed

src/ltrace/ltrace/image/dlis.py

Lines changed: 582 additions & 0 deletions
Large diffs are not rendered by default.

src/ltrace/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ pint-pandas==0.2
4444
pint==0.19.2
4545
plotly==5.18.0
4646
psutil==5.9.0
47+
pyarrow==20.0.0
4748
pyedt==0.1.5
4849
Pygments==2.18.0
4950
pynrrd==1.0.0

src/modules/CLAHETool/CLAHETool.py

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
import ctk
2+
import os
3+
import qt
4+
import slicer
5+
6+
from ltrace.slicer import helpers
7+
from ltrace.slicer.node_attributes import ImageLogDataSelectable
8+
from ltrace.slicer.ui import hierarchyVolumeInput, numericInput, numberParamInt
9+
from ltrace.slicer_utils import LTracePlugin, LTracePluginWidget, LTracePluginLogic, getResourcePath
10+
from ltrace.utils.recursive_progress import RecursiveProgress
11+
from pathlib import Path
12+
from typing import Callable
13+
14+
import numpy as np
15+
import skimage
16+
17+
try:
18+
from Test.CLAHEToolTest import CLAHEToolTest
19+
except ImportError:
20+
CLAHEToolTest = None # tests not deployed to final version or closed source
21+
22+
23+
class CLAHETool(LTracePlugin):
24+
SETTING_KEY = "CLAHETool"
25+
MODULE_DIR = Path(os.path.dirname(os.path.realpath(__file__)))
26+
27+
def __init__(self, parent):
28+
LTracePlugin.__init__(self, parent)
29+
self.parent.title = "CLAHE Tool"
30+
self.parent.categories = ["Tools", "ImageLog", "Multiscale"]
31+
self.parent.contributors = ["LTrace Geophysics Team"]
32+
self.parent.helpText = (
33+
f"file:///{(getResourcePath('manual') / 'Modules/ImageLog/CLAHETool/CLAHETool.html').as_posix()}"
34+
)
35+
36+
@classmethod
37+
def readme_path(cls):
38+
return str(cls.MODULE_DIR / "README.md")
39+
40+
41+
class CLAHEToolWidget(LTracePluginWidget):
42+
def __init__(self, parent):
43+
LTracePluginWidget.__init__(self, parent)
44+
45+
self.subjectHierarchyNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
46+
self.logic = CLAHEToolLogic()
47+
48+
def setup(self):
49+
LTracePluginWidget.setup(self)
50+
51+
# Input section
52+
inputSection = ctk.ctkCollapsibleButton()
53+
inputSection.collapsed = False
54+
inputSection.text = "Input"
55+
56+
self.inputImage = hierarchyVolumeInput(
57+
onChange=self.onInputNodeChange,
58+
nodeTypes=[
59+
"vtkMRMLVectorVolumeNode",
60+
"vtkMRMLScalarVolumeNode",
61+
],
62+
hasNone=True,
63+
)
64+
self.inputImage.objectName = "inputImage"
65+
self.inputImage.setToolTip("Select the image to be corrected")
66+
67+
inputFormLayout = qt.QFormLayout(inputSection)
68+
inputFormLayout.addRow("Image node:", self.inputImage)
69+
70+
# Parameters sections
71+
parametersSection = ctk.ctkCollapsibleButton()
72+
parametersSection.text = "Parameters"
73+
parametersSection.collapsed = False
74+
75+
parametersLayout = qt.QFormLayout(parametersSection)
76+
77+
self.kernelSizeXInput = numberParamInt(vrange=(2, 2048), value=32, step=2)
78+
self.kernelSizeXInput.objectName = "kernelSizeX"
79+
self.kernelSizeXInput.valueChanged.connect(lambda value: self.onNumericIntChanged("kernelSizeX", value))
80+
self.kernelSizeXInput.setToolTip(
81+
"Defines the shape of contextual regions used in the algorithm. By default, kernel_size is 1/8 of image height by 1/8 of its width."
82+
)
83+
84+
parametersLayout.addRow("Kernel Size X:", self.kernelSizeXInput)
85+
86+
self.kernelSizeYInput = numberParamInt(vrange=(2, 2048), value=32, step=2)
87+
self.kernelSizeYInput.objectName = "kernelSizeY"
88+
self.kernelSizeYInput.valueChanged.connect(lambda value: self.onNumericIntChanged("kernelSizeY", value))
89+
self.kernelSizeYInput.setToolTip(
90+
"Defines the shape of contextual regions used in the algorithm. By default, kernel_size is 1/8 of image height by 1/8 of its width."
91+
)
92+
parametersLayout.addRow("Kernel Size Y:", self.kernelSizeYInput)
93+
94+
clipLimitInput = numericInput(value=0.01, onChange=lambda value: self.onNumericChanged("clipLimit", value))
95+
clipLimitInput.objectName = "clipLimit"
96+
clipLimitInput.setToolTip("Clipping limit, normalized between 0 and 1 (higher values give more contrast).")
97+
parametersLayout.addRow("Clip Limit:", clipLimitInput)
98+
99+
nBinsInput = numberParamInt(vrange=(2, 65536), value=256, step=2)
100+
nBinsInput.objectName = "nBinsInput"
101+
nBinsInput.valueChanged.connect(lambda value: self.onNumericIntChanged("nBins", value))
102+
nBinsInput.setToolTip("Number of bins for histogram ('data range').")
103+
parametersLayout.addRow("Number of Bins:", nBinsInput)
104+
105+
# Output section
106+
outputSection = ctk.ctkCollapsibleButton()
107+
outputSection.text = "Output"
108+
outputSection.collapsed = False
109+
110+
self.outputPrefix = qt.QLineEdit()
111+
self.outputPrefix.objectName = "outputPrefix"
112+
self.outputPrefix.setToolTip("Name of the processed image output (in float64 dtype)")
113+
self.outputPrefix.textChanged.connect(self.checkApplyState)
114+
outputFormLayout = qt.QFormLayout(outputSection)
115+
outputFormLayout.addRow("Output prefix:", self.outputPrefix)
116+
117+
# Apply button
118+
self.applyButton = qt.QPushButton("Apply")
119+
self.applyButton.clicked.connect(self.onApplyButtonClicked)
120+
self.applyButton.objectName = "applyButton"
121+
self.applyButton.enabled = False
122+
self.applyButton.setToolTip("Run the azimuth shift correcting tool")
123+
124+
# Progress Bar
125+
self.progressBar = qt.QProgressBar()
126+
self.progressBar.setValue(0)
127+
self.progressBar.hide()
128+
self.progressBar.objectName = "progressBar"
129+
130+
statusLabel = qt.QLabel("Status: ")
131+
self.currentStatusLabel = qt.QLabel("Idle")
132+
statusHBoxLayout = qt.QHBoxLayout()
133+
# statusHBoxLayout.addStretch(1)
134+
statusHBoxLayout.addWidget(statusLabel)
135+
statusHBoxLayout.addWidget(self.currentStatusLabel)
136+
outputFormLayout.addRow(statusHBoxLayout)
137+
138+
# Update layout
139+
self.layout.addWidget(inputSection)
140+
self.layout.addWidget(parametersSection)
141+
self.layout.addWidget(outputSection)
142+
self.layout.addWidget(self.applyButton)
143+
self.layout.addWidget(self.progressBar)
144+
self.layout.addStretch(1)
145+
146+
def onApplyButtonClicked(self):
147+
self.applyButton.enabled = False
148+
self.progressBar.show()
149+
150+
self.currentStatusLabel.setStyleSheet("color: white;")
151+
self.currentStatusLabel.text = "Applying Contrast Limited Adaptive Histogram Equalization (CLAHE)..."
152+
try:
153+
self.logic.apply(
154+
volume_node=self.inputImage.currentNode(),
155+
kernel_size=(
156+
self.logic.model["kernelSizeX"],
157+
self.logic.model["kernelSizeY"],
158+
),
159+
clip_limit=self.logic.model["clipLimit"],
160+
nbins=self.logic.model["nBins"],
161+
prefix=self.outputPrefix.text,
162+
callback=self.progress_callback,
163+
)
164+
except RuntimeError as e:
165+
# logging.error(e)
166+
self.progressBar.setValue(0)
167+
self.currentStatusLabel.setStyleSheet("color: red;")
168+
self.currentStatusLabel.text = "Export failed!"
169+
return
170+
171+
self.progressBar.setValue(100)
172+
self.currentStatusLabel.setStyleSheet("color: green;")
173+
self.currentStatusLabel.text = f"CLAHE completed. Volume {self.outputPrefix.text} created."
174+
175+
self.applyButton.enabled = True
176+
177+
def progress_callback(self, progress: float):
178+
self.progressBar.setValue(progress)
179+
180+
def onInputNodeChange(self, itemId):
181+
volumeNode = self.subjectHierarchyNode.GetItemDataNode(itemId)
182+
if volumeNode:
183+
self.outputPrefix.text = slicer.mrmlScene.GenerateUniqueName(
184+
f"{self.inputImage.currentNode().GetName()}_CLAHE"
185+
)
186+
kx = volumeNode.GetImageData().GetDimensions()[0] / 8
187+
if kx:
188+
self.kernelSizeXInput.setValue(kx)
189+
else:
190+
self.kernelSizeXInput.setValue(2)
191+
ky = volumeNode.GetImageData().GetDimensions()[2] / 8
192+
if ky:
193+
self.kernelSizeYInput.setValue(ky)
194+
else:
195+
self.kernelSizeYInput.setValue(2)
196+
else:
197+
self.outputPrefix.text = ""
198+
199+
def onNumericChanged(self, key, value):
200+
self.logic.model[key] = float(value)
201+
202+
def onNumericIntChanged(self, key, value):
203+
self.logic.model[key] = int(value)
204+
205+
def checkApplyState(self):
206+
if self.inputImage.currentNode() is not None and self.outputPrefix.text.replace(" ", "") != "":
207+
self.applyButton.enabled = True
208+
else:
209+
self.applyButton.enabled = False
210+
211+
212+
def CLAHEModel():
213+
return dict(
214+
inputVolume=None,
215+
kernelSizeX=32,
216+
kernelSizeY=32,
217+
clipLimit=0.01,
218+
nBins=256,
219+
)
220+
221+
222+
class CLAHEToolLogic(LTracePluginLogic):
223+
def __init__(self):
224+
LTracePluginLogic.__init__(self)
225+
self.model = CLAHEModel()
226+
227+
def apply(self, volume_node, kernel_size, clip_limit, nbins, prefix, callback: Callable[[float], None]):
228+
"""
229+
Applies Contrast Limited Adaptive Histogram Equalization (CLAHE) to the given image.
230+
231+
Parameters:
232+
acoustic_imagelog (uint16 numpy array): The input acoustic_imagelog.
233+
kernel_size (int or tuple, optional): Defines the shape of contextual regions used in the algorithm.
234+
clip_limit (float, optional): Clipping limit, normalized between 0 and 1 (higher values give more contrast).
235+
nbins (int, optional): Number of gray bins for histogram (“nbins”).
236+
237+
Returns:
238+
numpy array: CLAHE applied image.
239+
"""
240+
callback(0)
241+
242+
volumeArray = slicer.util.arrayFromVolume(volume_node)
243+
volumeArray = volumeArray.squeeze() # remove the dimension with value 1
244+
245+
# Convert pixels to uint16 type (skimage.exposure.equalize_adapthist expects int type and will convert to 16 bits
246+
# anyway). Note also that we are in the context of image visualization - our color look up tables have 16 bits...
247+
image_dyn_uint16 = volumeArray.astype("uint16")
248+
249+
callback(5)
250+
251+
# Apply CLAHE
252+
img_clahe = skimage.exposure.equalize_adapthist(
253+
image_dyn_uint16, kernel_size=kernel_size, clip_limit=clip_limit, nbins=nbins
254+
)
255+
256+
callback(80)
257+
258+
# Add new volume to the hierarchy
259+
newVolume = slicer.mrmlScene.AddNewNodeByClass(volume_node.GetClassName(), prefix)
260+
newVolume.SetName(slicer.mrmlScene.GenerateUniqueName(newVolume.GetName()))
261+
newVolume.CopyOrientation(volume_node)
262+
newVolume.SetAttribute(ImageLogDataSelectable.name(), ImageLogDataSelectable.TRUE.value)
263+
264+
callback(85)
265+
266+
# Scale the image back to the range
267+
image_converted = (img_clahe * (volumeArray.max() - volumeArray.min())) + volumeArray.min()
268+
269+
final_image = np.zeros((volumeArray.shape[0], 1, volumeArray.shape[1]))
270+
final_image[:, 0, :] = image_converted
271+
slicer.util.updateVolumeFromArray(newVolume, final_image)
272+
273+
callback(90)
274+
275+
helpers.copy_display(volume_node, newVolume)
276+
277+
subjectHierarchyNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
278+
parent = subjectHierarchyNode.GetItemParent(subjectHierarchyNode.GetItemByDataNode(volume_node))
279+
280+
subjectHierarchyNode.SetItemParent(subjectHierarchyNode.GetItemByDataNode(newVolume), parent)
281+
282+
callback(100)

src/modules/CLAHETool/README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# CLAHE Tool
2+
This module applies the Contrast Limit Adaptive Histogram Equalization (CLAHE)
3+
4+
## Methods
5+
"an algorithm for local contrast enhancement, that uses histograms computed over different tile regions of the image. Local details can therefore be enhanced even in regions that are darker or lighter than most of the image." - scikit-image equalize_adapthist documentation
6+
7+
## Inputs
8+
1. __Image Node__: Image Log volume to be processed.
9+
10+
## Parameters
11+
1. __Kernel Size__: "Defines the shape of contextual regions used in the algorithm. By default, kernel_size is 1/8 of image height by 1/8 of its width."
12+
2. __Clip Limit__: "Clipping limit, normalized between 0 and 1 (higher values give more contrast)."
13+
3. __Number of Bins__: "Number of bins for histogram ('data range')."
14+
## Outputs
15+
1. __Output prefix__: Equalized image with float64 dtype.
Lines changed: 1 addition & 0 deletions
Loading

src/modules/ImageLogEnv/ImageLogEnv.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ def setupEnvironment(self):
5555
relatedModules["QualityIndicator"],
5656
relatedModules["HeterogeneityIndex"],
5757
relatedModules["AzimuthShiftTool"],
58+
relatedModules["CLAHETool"],
5859
]
5960

6061
if hasattr(slicer.modules, "eccentricity"):

src/modules/ImageLogExport/ImageLogExport.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def setup(self):
4747
self.versions["base"] = (ImageLogExportBaseWidget(), "Base")
4848
self.versions["extended"] = (ImageLogExportExtendedWidget(), "Extended")
4949

50-
if slicer_is_in_developer_mode():
50+
if slicer_is_in_developer_mode() and self.versions["extended"][0] is not None:
5151
mainTab = qt.QTabWidget()
5252
for _, (widget, name) in self.versions.items():
5353
if widget is None:

src/modules/MultiscaleEnv/MultiscaleEnv.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@ def setupEnvironment(self):
5252
],
5353
),
5454
# ImageLog Modules
55-
("Image Log Pre-Processing", ["ImageLogCropVolume", "AzimuthShiftTool", "SpiralFilter", "ImageLogInpaint"]),
55+
(
56+
"Image Log Pre-Processing",
57+
["ImageLogCropVolume", "AzimuthShiftTool", "SpiralFilter", "ImageLogInpaint", "CLAHETool"],
58+
),
5659
# Volumes Modules
5760
(
5861
"Volumes Pre-Processing",

src/modules/StreamlinedSegmentation/StreamlinedSegmentation.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ def __init__(self, parent):
6363
self.editor = None
6464
self.__tag = None
6565
self.__lastUsedEffect = None
66-
self.destroyed.connect(self.cleanup)
6766

6867
def setup(self):
6968
LTracePluginWidget.setup(self)

src/modules/TableFilter/TableFilter.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,8 @@ def setupUI(self):
335335

336336
def __applyComboBoxFilter(self):
337337
"""Force qMRMLNodeComboBox nodes filter to handle hidden nodes correctly."""
338-
self.referenceNodeSelector.sortFilterProxyModel().invalidateFilter()
338+
if hasattr(self.referenceNodeSelector, "sortFilterProxyModel"):
339+
self.referenceNodeSelector.sortFilterProxyModel().invalidateFilter()
339340

340341
def onFilterListItemSlectionChanged(self):
341342
self.filterEditButton.enabled = len(self.filterList.selectedItems()) > 0

0 commit comments

Comments
 (0)