diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
index a90aa3c..f077b5e 100644
--- a/.git-blame-ignore-revs
+++ b/.git-blame-ignore-revs
@@ -2,4 +2,6 @@ ff20d1e5015789dcee10b16174c8bd1bda5c15e9
38c0dd5ee7395dcbb477ab96fc5a1b521bcb51e3
9c54d2588091da84b8844ba36acc4c5a6903fc4f
af43fef670327a7fde0d17664c94599f107613ff
-20d4aef214e6a3d0ba828bd50ffe334a1c4f7111
\ No newline at end of file
+20d4aef214e6a3d0ba828bd50ffe334a1c4f7111
+7890d40663d42e82ce8d2386c105c8502c6442c4
+f6085708912c611e0533feb66757eff42a62ce11
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index a28987d..c148b5a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -149,6 +149,7 @@ _deps
!/ltrace/ltrace/assets/**/*
/tools/deploy/Assets/Tesseract-OCR/
/tools/deploy/Resources/manual/
+/tools/deploy/GeoSlicerManual/site/
# Cython build files
*.c
diff --git a/.gitmodules b/.gitmodules
index 6ce9408..b5c9537 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -4,3 +4,9 @@
[submodule "src/submodules/pnflow"]
path = src/submodules/pnflow
url = git@github.com:ltracegeo/pnflow.git
+[submodule "src/submodules/py_pore_flow"]
+ path = src/submodules/py_pore_flow
+ url = git@bitbucket.org:ltrace/py_pore_flow.git
+[submodule "src/submodules/pyflowsolver"]
+ path = src/submodules/pyflowsolver
+ url = git@github.com:Arenhart/PyFlowSolver.git
diff --git a/CTestConfig.cmake b/CTestConfig.cmake
deleted file mode 100644
index 30db31e..0000000
--- a/CTestConfig.cmake
+++ /dev/null
@@ -1,7 +0,0 @@
-set(CTEST_PROJECT_NAME "SlicerLTrace")
-set(CTEST_NIGHTLY_START_TIME "3:00:00 UTC")
-
-set(CTEST_DROP_METHOD "http")
-set(CTEST_DROP_SITE "slicer.cdash.org")
-set(CTEST_DROP_LOCATION "/submit.php?project=SlicerPreview")
-set(CTEST_DROP_SITE_CDASH TRUE)
diff --git a/pyproject.toml b/pyproject.toml
index 54f3853..8b2f92e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,11 +1,11 @@
[build-system]
requires = [
- "setuptools==59.8.0",
+ "setuptools==60.2.0",
"wheel==0.37.0",
"Cython==0.29.28",
"vswhere==1.3.0",
"patch==1.16",
- "packaging==21.2"
+ "packaging==23.2"
]
[tool.autopep8]
@@ -49,6 +49,7 @@ authorized_licenses = [
"3-Clause BSD",
"Apache 2.0",
"Apache License 2.0",
+ "Apache License, Version 2.0",
"Apache Software",
"BSD",
"FreeBSD",
@@ -76,5 +77,5 @@ unauthorized_licenses = [
# The packages below are listed as "Other/Proprietary"
# They use Intel licenses that allow distribution
mkl = ">=2022.2.1"
-tbb = ">=2021.12.0"
+tbb = ">=2021.9.0"
intel-openmp = ">=2022.2.1"
diff --git a/src/ltrace/ltrace/algorithms/measurements.py b/src/ltrace/ltrace/algorithms/measurements.py
index 7293498..10f8763 100644
--- a/src/ltrace/ltrace/algorithms/measurements.py
+++ b/src/ltrace/ltrace/algorithms/measurements.py
@@ -1,9 +1,10 @@
+import logging
import math
from collections import namedtuple
from multiprocessing import Process, Queue, Value
from queue import Empty
from threading import Thread
-from typing import List
+from typing import List, Callable
import cv2
import numba as nb
@@ -52,6 +53,14 @@
CLASS_LABEL_SUFFIX = ["[label]", "[ID]", "[CID]"]
+GENERIC_PROPERTIES = [
+ "area (m²)",
+ "insc diam (mm)",
+ "azimuth (°)",
+ "Circularity",
+ "Perimeter over Area (1/mm)",
+]
+
def get_pore_size_class_label_field(fields):
for suffix in ["_label", *CLASS_LABEL_SUFFIX]:
@@ -434,7 +443,7 @@ def object_consumer(operator, queue, results, visited):
results.put(None)
-def _executor_task(func: object, tasks: Queue, sender: Queue):
+def _executor_task(func: Callable, tasks: Queue, sender: Queue):
processed = 0
valid_results = []
while True:
@@ -470,7 +479,7 @@ def _separator_task(im: np.ndarray, tasks: Queue, pool_size: int):
tasks.put_nowait((-1, None))
-def calculate_statistics_on_segments(im: np.ndarray, operator: object, callback=None) -> None:
+def calculate_statistics_on_segments(im: np.ndarray, operator: object, callback=None):
from timeit import default_timer as timer
tstart = timer()
@@ -757,19 +766,26 @@ def __init__(self, im, spacing, direction, size_filter=0):
else:
self.__anglesFromReferenceDirection = mock
- def __call__(self, label, pointsInRAS):
- pointsInRAS = blobers(pointsInRAS, r=self.radius)
- if pointsInRAS.shape[0] < 3:
+ def __call__(self, label, pointsInRasBorder):
+ # deprecated API
+ return self.calculate(label, pointsInRasBorder)
+
+ def calculate(self, label, pointsInRasBorder):
+ pointsInRasBorder = blobers(pointsInRasBorder, r=self.radius)
+ if pointsInRasBorder.shape[0] < 3:
return None
- voxelCount = len(pointsInRAS)
+ return self.strict_calculate(label, pointsInRasBorder)
+
+ def strict_calculate(self, label, pointsInRasBorder):
+ voxelCount = len(pointsInRasBorder)
area_mm = voxelCount * self.voxel_size
try:
- chull = ConvexHull(pointsInRAS)
+ chull = ConvexHull(pointsInRasBorder)
perimeter = chull.area # yes, for 2D, the area is the perimeter
- diameter, major_angle, min_feret, minor_angle, _ = rotating_calipers(pointsInRAS[chull.vertices])
+ diameter, major_angle, min_feret, minor_angle, _ = rotating_calipers(pointsInRasBorder[chull.vertices])
eccen = np.sqrt(1 - min_feret / diameter)
elong = np.sqrt(diameter / min_feret)
@@ -935,11 +951,11 @@ def zero_origin(vector):
def get_angle_between(v1, v2):
"""Returns the angle in radians between vectors 'v1' and 'v2'::
- >>> angle_between((1, 0, 0), (0, 1, 0))
+ angle_between((1, 0, 0), (0, 1, 0))
1.5707963267948966
- >>> angle_between((1, 0, 0), (1, 0, 0))
+ angle_between((1, 0, 0), (1, 0, 0))
0.0
- >>> angle_between((1, 0, 0), (-1, 0, 0))
+ angle_between((1, 0, 0), (-1, 0, 0))
3.141592653589793
"""
v1_u = unit_vector(zero_origin(v1))
@@ -1022,17 +1038,21 @@ def randomize_colors(im, keep_vals=[0]):
neither will they be in the output.
Examples
--------
- >>> import scipy as sp
- >>> sp.random.seed(0)
- >>> im = sp.random.randint(low=0, high=5, size=[4, 4])
- >>> print(im)
- [[4 0 3 3]
+
+ import scipy as sp
+ sp.random.seed(0)
+ im = sp.random.randint(low=0, high=5, size=[4, 4])
+ print(im)
+
+ $ [[4 0 3 3]
[3 1 3 2]
[4 0 0 4]
[2 1 0 1]]
- >>> im_rand = randomize_colors(im)
- >>> print(im_rand)
- [[2 0 4 4]
+
+ im_rand = randomize_colors(im)
+ print(im_rand)
+
+ [[2 0 4 4]
[4 1 4 3]
[2 0 0 2]
[3 1 0 1]]
@@ -1105,11 +1125,12 @@ def sidewall_sample_instance_properties(instance_mask, spacing):
return properties
-def generic_instance_properties(instance_mask, spacing, shape=None, offset=None):
+def generic_instance_properties(instance_mask, selected_measurements, spacing, shape=None, offset=None):
"""
Calculates the instance generic properties.
:param instance_mask: the binary 2D array containing the instance to be evaluated
+ :param selected_measurements: binary array that controls which measurement to calculate
:param spacing: the spacing from the related volume
:return: a dictionary containing the generic properties of the instance
"""
@@ -1119,44 +1140,75 @@ def generic_instance_properties(instance_mask, spacing, shape=None, offset=None)
if offset is None:
offset = (0, 0)
+ properties = {}
+ for index in range(len(selected_measurements)):
+ if selected_measurements[index]:
+ properties[GENERIC_PROPERTIES[index]] = -1
- properties = {"area (m²)": -1, "insc diam (mm)": -1, "azimuth (°)": -1}
contours, _ = cv2.findContours(instance_mask.astype(np.uint8), cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)
markAreaInPixels = np.count_nonzero(instance_mask)
if markAreaInPixels <= 0:
raise ValueError("Detected Label with invalid area.")
- try:
- pixelArea = spacing[0] * spacing[1]
- markAreaInMillimeters = markAreaInPixels * pixelArea
- markAreaInMeters = markAreaInMillimeters / (10**6)
- properties["area (m²)"] = np.round(markAreaInMeters, 8)
- except Exception as e:
- raise ValueError("Area calculation failed.")
-
- try:
- if any(instance_mask[:, 0] & instance_mask[:, -1]):
- concatenated_mask = np.concatenate([instance_mask, instance_mask], axis=1)
- padded_mask = np.pad(concatenated_mask, pad_width=1, mode="constant", constant_values=0)
- else:
- padded_mask = np.pad(instance_mask, pad_width=1, mode="constant", constant_values=0)
- dt = ndimage.distance_transform_edt(padded_mask, sampling=spacing)
- max_radius = instance_mask.shape[1] * spacing[1] / 2
- radius = np.max(dt, initial=0, where=dt < max_radius)
- diameter = 2 * radius
- properties["insc diam (mm)"] = diameter
- except Exception as e:
- raise ValueError("Diameter calculation failed.")
+ if GENERIC_PROPERTIES[0] in properties.keys():
+ try:
+ pixelArea = spacing[0] * spacing[1]
+ markAreaInMillimeters = markAreaInPixels * pixelArea
+ markAreaInMeters = markAreaInMillimeters / (10**6)
+ properties["area (m²)"] = np.round(markAreaInMeters, 8)
+ except Exception as e:
+ properties["area (m²)"] = np.nan
+ logging.warning("Area calculation failed.")
+
+ if GENERIC_PROPERTIES[1] in properties.keys():
+ try:
+ if any(instance_mask[:, 0] & instance_mask[:, -1]):
+ concatenated_mask = np.concatenate([instance_mask, instance_mask], axis=1)
+ padded_mask = np.pad(concatenated_mask, pad_width=1, mode="constant", constant_values=0)
+ else:
+ padded_mask = np.pad(instance_mask, pad_width=1, mode="constant", constant_values=0)
+ dt = ndimage.distance_transform_edt(padded_mask, sampling=spacing)
+ max_radius = instance_mask.shape[1] * spacing[1] / 2
+ radius = np.max(dt, initial=0, where=dt < max_radius)
+ diameter = 2 * radius
+ properties["insc diam (mm)"] = diameter
+ except Exception as e:
+ properties["insc diam (mm)"] = np.nan
+ logging.warning("Diameter calculation failed.")
+
+ if GENERIC_PROPERTIES[2] in properties.keys():
+ try:
+ markContour = max(contours, key=cv2.contourArea)
+ moments = cv2.moments(markContour)
+ center_x = int(np.round(moments["m10"] / moments["m00"])) + offset[1]
+ azimuth_in_degrees = 360 * center_x / (shape[1] - 1)
+ properties["azimuth (°)"] = int(np.round(azimuth_in_degrees))
+ except Exception as e:
+ properties["azimuth (°)"] = np.nan
+ logging.warning("Azimuth calculation failed.")
+
+ if GENERIC_PROPERTIES[3] in properties.keys():
+ try:
+ countourPerimeter = cv2.arcLength(contours[0], True)
+ countourArea = cv2.contourArea(contours[0])
+ properties[GENERIC_PROPERTIES[3]] = 4 * np.pi * countourArea / countourPerimeter**2
+ except Exception as e:
+ properties[GENERIC_PROPERTIES[3]] = np.nan
+ logging.warning("Circularity calculation failed.")
+
+ if GENERIC_PROPERTIES[4] in properties.keys():
+ try:
+ rescaledContour = contours[0]
+ rescaledContour[:, :, 0] = rescaledContour[:, :, 0] * spacing[0]
+ rescaledContour[:, :, 1] = rescaledContour[:, :, 1] * spacing[1]
- try:
- markContour = max(contours, key=cv2.contourArea)
- moments = cv2.moments(markContour)
- center_x = int(np.round(moments["m10"] / moments["m00"])) + offset[1]
- azimuth_in_degrees = 360 * center_x / (shape[1] - 1)
- properties["azimuth (°)"] = int(np.round(azimuth_in_degrees))
- except Exception as e:
- raise ValueError("Azimuth calculation failed.")
+ rescaledPerimeter = cv2.arcLength(rescaledContour, True)
+ rescaledArea = cv2.contourArea(rescaledContour)
+ properties[GENERIC_PROPERTIES[4]] = rescaledPerimeter / rescaledArea
+ except Exception as e:
+ properties[GENERIC_PROPERTIES[4]] = np.nan
+ logging.warning("Perimeter over Area calculation failed.")
return properties
@@ -1178,7 +1230,7 @@ def crop_to_content(image, padding=0):
return instanceMask, indices, offset
-def instancesPropertiesDataFrame(labelMap):
+def instancesPropertiesDataFrame(labelMap, selectedMeasurements=[1, 1, 1]):
propertiesList = []
array = slicer.util.arrayFromVolume(labelMap)
labels = np.unique(array)
@@ -1213,7 +1265,7 @@ def instancesPropertiesDataFrame(labelMap):
try:
instanceProperties = generic_instance_properties(
- instanceMask, inverted_2d_spacing, arraySliceCopy.shape, offset
+ instanceMask, selectedMeasurements, inverted_2d_spacing, arraySliceCopy.shape, offset
)
except ValueError as err:
# logging.debug(f"{err}\n{traceback.print_exc()}") # hide this error from the user for while; it's not critical; we must handle logging filters;
@@ -1222,9 +1274,11 @@ def instancesPropertiesDataFrame(labelMap):
instanceProperties["depth (m)"] = instance_depth(labelMap, label)
instanceProperties["label"] = label
propertiesList.append(instanceProperties)
- propertiesDataFrame = pd.DataFrame(
- propertiesList, columns=["depth (m)", "area (m²)", "label", "azimuth (°)", "insc diam (mm)"]
- )
+
+ measurementColumns = [
+ GENERIC_PROPERTIES[index] for index in range(len(selectedMeasurements)) if selectedMeasurements[index]
+ ]
+ propertiesDataFrame = pd.DataFrame(propertiesList, columns=["depth (m)", "label"] + measurementColumns)
return propertiesDataFrame
diff --git a/src/ltrace/ltrace/flow/framework.py b/src/ltrace/ltrace/flow/framework.py
index b297a57..780377f 100644
--- a/src/ltrace/ltrace/flow/framework.py
+++ b/src/ltrace/ltrace/flow/framework.py
@@ -49,16 +49,10 @@ def __init__(self):
flowLayout = qt.QHBoxLayout(self)
self.backButton = qt.QPushButton("\u2190 Back")
self.backButton.setFixedHeight(40)
- self.skipButton = qt.QPushButton("Skip \u21e2")
+ self.skipButton = qt.QPushButton("Skip \u21d2")
self.skipButton.setFixedHeight(40)
self.nextButton = qt.QPushButton("Next \u2192")
- self.nextButton.setStyleSheet(
- f"""
- QPushButton:enabled {{
- background-color: {"#003265" if helpers.themeIsDark() else "#8eb3ff"};
- }}
- """
- )
+ self.nextButton.setProperty("class", "actionButtonBackground")
self.nextButton.setFixedHeight(40)
flowLayout.addWidget(self.backButton, 1)
@@ -89,12 +83,12 @@ def __init__(self):
QListWidget::item:selected {{
padding-left: 10px;
font-weight: bold;
- background-color: {'#242e38' if isDark else '#d9ebff'};
- border-left: 6px solid #36a0fe;
+ background-color: {'#37403A' if isDark else '#d9ebff'};
+ border-left: 6px solid #26C252;
color: {'#ffffff' if isDark else '#000000'};
}}
QListWidget::item:hover {{
- background-color: {'#242e38' if isDark else '#d9ebff'};
+ background-color: {'#26C252' if isDark else '#d9ebff'};
}}
"""
)
diff --git a/src/ltrace/ltrace/flow/thin_section.py b/src/ltrace/ltrace/flow/thin_section.py
index 4d7dcb5..b30fb12 100644
--- a/src/ltrace/ltrace/flow/thin_section.py
+++ b/src/ltrace/ltrace/flow/thin_section.py
@@ -1,3 +1,5 @@
+import sys
+
from ltrace.flow.framework import (
FlowWidget,
FlowState,
@@ -8,19 +10,24 @@
onSegmentEditorEnter,
onSegmentEditorExit,
)
-from QEMSCANLoader import QEMSCANLoaderLogic, QEMSCANLoaderWidget, Callback
-from SegmentInspector import SegmentInspectorLogic
-from ThinSectionLoader import ThinSectionLoaderLogic, ThinSectionLoaderWidget
+
from ltrace import assets_utils as assets
from ltrace.slicer import helpers, widgets
from ltrace.slicer.widget.global_progress_bar import LocalProgressBar
from ltrace.slicer.widget.pixel_size_editor import PixelSizeEditor
from ltrace.units import global_unit_registry as ureg
+from ltrace.utils.callback import Callback
from ltrace.utils.ProgressBarProc import ProgressBarProc
-import Segmenter
+
import ctk
import qt
import slicer
+import importlib
+
+
+Segmenter = helpers.LazyLoad("Segmenter")
+QEMSCANLoader = helpers.LazyLoad("QEMSCANLoader")
+ThinSectionLoader = helpers.LazyLoad("ThinSectionLoader")
class Load(FlowStep):
@@ -79,12 +86,12 @@ def exit(self):
def next(self):
with ProgressBarProc() as pb:
- logic = ThinSectionLoaderLogic()
+ logic = slicer.util.getModuleLogic("ThinSectionLoader")
path = self.ppFileSelector.currentPath
pb.setMessage("Loading PP image")
pb.setProgress(0)
- params = ThinSectionLoaderWidget.LoadParameters(path)
+ params = ThinSectionLoader.ThinSectionLoaderWidget.LoadParameters(path)
imageInfo = logic.load(params)
pp = imageInfo["node"]
@@ -98,7 +105,7 @@ def next(self):
pb.setMessage("Loading PX image")
pb.setProgress(50)
path = self.pxFileSelector.currentPath
- params = ThinSectionLoaderWidget.LoadParameters(path, automaticImageSpacing=False)
+ params = ThinSectionLoader.ThinSectionLoaderWidget.LoadParameters(path, automaticImageSpacing=False)
imageInfo = logic.load(params)
px = imageInfo["node"]
@@ -225,7 +232,7 @@ def setup(self):
moduleWidget = slicer.modules.thinsectionregistration.createNewWidgetRepresentation()
widget = moduleWidget.self()
widget.imagesFrame.visible = False
- widget.buttonsFrame.visible = False
+ widget.applyCancelButtons.visible = False
return moduleWidget
def enter(self):
@@ -565,10 +572,23 @@ def setup(self):
self.segmentSelector.targetBox.hide()
layout.addRow(self.segmentSelector)
+ progressLayout = qt.QHBoxLayout()
self.progressBar = LocalProgressBar()
- layout.addRow(self.progressBar)
+ progressLayout.addWidget(self.progressBar, 1)
+
+ self.cancelButton = qt.QPushButton("Cancel")
+ self.cancelButton.visible = False
+ self.cancelButton.setToolTip("Cancel the auto-labeling.")
+ self.cancelButton.setFixedHeight(30)
+ progressLayout.addWidget(self.cancelButton, 0)
+
+ layout.addRow(progressLayout)
self.segmentSelector.segmentSelectionChanged.connect(self.onSegmentSelected)
+ self.cancelButton.clicked.connect(self.onCancel)
+
+ self.logic = slicer.util.getModuleLogic("SegmentInspector")
+ self.logic.inspector_process_finished.connect(self.onFinish)
return widget
@@ -604,7 +624,7 @@ def next(self):
if method == self.WATERSHED:
params = {
"method": "snow",
- "sigma": 0.0423,
+ "sigma": 0.005,
"d_min_filter": 5.0,
"size_min_threshold": 0.0,
"direction": [],
@@ -614,7 +634,6 @@ def next(self):
elif method == self.SEPARATE_OBJECTS:
params = {"method": "islands", "size_min_threshold": 0.0, "direction": []}
- self.logic = SegmentInspectorLogic()
self.cliNode = self.logic.runSelectedMethod(
mainNode,
segments=self.segmentSelector.getSelectedSegments(),
@@ -626,9 +645,9 @@ def next(self):
)
if self.cliNode is None:
return
- self.logic.inspector_process_finished.connect(self.onFinish)
self.progressBar.setCommandLineModuleNode(self.cliNode)
self.nav.setButtonsState(self.BACK_ON_STATE, self.SKIP_OFF_STATE, self.NEXT_IN_PROGRESS_STATE)
+ self.cancelButton.visible = True
def onFinish(self):
if self.cliNode.GetStatusString() == "Completed":
@@ -637,7 +656,11 @@ def onFinish(self):
parent = shNode.GetItemParent(shNode.GetItemByDataNode(self.state.labelmap))
shNode.SetItemParent(parent, self.state.dir)
self.onSegmentSelected(self.segmentSelector.getSelectedSegments())
+ self.cancelButton.visible = False
self.nav.next()
+ else:
+ self.onSegmentSelected(self.segmentSelector.getSelectedSegments())
+ self.cancelButton.visible = False
def onSegmentSelected(self, segmentList):
if segmentList:
@@ -645,6 +668,10 @@ def onSegmentSelected(self, segmentList):
else:
self.nav.setButtonsState(self.BACK_ON_STATE, self.SKIP_OFF_STATE, self.NEXT_OFF_STATE)
+ def onCancel(self):
+ if self.cliNode is not None:
+ self.cliNode.Cancel()
+
class LabelEditor(FlowStep):
HELP = """
@@ -677,8 +704,7 @@ def setup(self):
widget.input_collapsible.visible = False
widget.output_collapsible.visible = False
- widget.save_button.visible = False
- widget.cancel_button.visible = False
+ widget.applyCancelButtons.visible = False
widget.labelmapGenerated = self.onFinish
widget.throat_analysis_checkbox.setChecked(False)
@@ -777,7 +803,7 @@ def setup(self):
self.imageSpacingLineEdit.setToolTip("Pixel size in millimeters")
layout.addRow("Pixel size (mm):", self.imageSpacingLineEdit)
- self.logic = QEMSCANLoaderLogic()
+ self.logic = slicer.util.getModuleLogic("QEMSCANLoader")
return widget
@@ -790,7 +816,7 @@ def exit(self):
def next(self):
with ProgressBarProc() as pb:
- loadParameters = QEMSCANLoaderWidget.LoadParameters(
+ loadParameters = QEMSCANLoader.QEMSCANLoaderWidget.LoadParameters(
callback=Callback(on_update=lambda message, percent, processEvents=True: pb.nextStep(percent, message)),
lookupColorTableNode=None,
fillMissing=True,
diff --git a/src/ltrace/ltrace/image/io.py b/src/ltrace/ltrace/image/io.py
index 23c5d33..7ee3a24 100644
--- a/src/ltrace/ltrace/image/io.py
+++ b/src/ltrace/ltrace/image/io.py
@@ -1,10 +1,10 @@
import os
-import pathlib
import cv2
from natsort import natsorted
import numpy as np
from PIL import Image, ExifTags
+from pathlib import Path
import slicer
@@ -20,6 +20,7 @@ def load_from_files(dirpath: str):
def volume_from_image(filepath):
new_node = slicer.util.loadVolume(str(filepath), properties={"singleFile": True})
+ new_node.SetName(Path(filepath).stem)
with Image.open(filepath) as image:
if image.format == "PNG":
diff --git a/src/ltrace/ltrace/image/las.py b/src/ltrace/ltrace/image/las.py
index 7bdc767..07d5eb2 100644
--- a/src/ltrace/ltrace/image/las.py
+++ b/src/ltrace/ltrace/image/las.py
@@ -1,21 +1,14 @@
+import datetime
import numpy as np
-import os
-import lasio
-import slicer
-from ltrace.slicer.helpers import (
- getVolumeNullValue,
- arrayFromVisibleSegmentsBinaryLabelmap,
-)
-from ImageLogExportLib.ImageLogCSV import _arrayPartsFromNode
-from pathlib import Path
import pandas as pd
-import datetime
-import re
import logging
-from dataclasses import dataclass
-from DLISImportLib.DLISImportLogic import WELL_NAME_TAG, UNITS_TAG
-
import ltrace.image.lasGeologFile as lasGeologFile
+import re
+import slicer
+
+from ltrace.constants import DLISImportConst
+from ltrace.image.optimized_transforms import ANP_880_2022_DEFAULT_NULL_VALUE
+from ltrace.slicer.helpers import getVolumeNullValue, arrayFromVisibleSegmentsBinaryLabelmap, arrayPartsFromNode
def retrieve_depth_curve(node_list):
@@ -126,7 +119,7 @@ def add_curve(lasfile, las_data, las_info):
data = las_data["data"].squeeze()
- data[data == las_info["null_value"]] = -999.25 # TODO MUSA-75 Update invalid data handling policy
+ data[data == las_info["null_value"]] = ANP_880_2022_DEFAULT_NULL_VALUE
if data.ndim > 1:
if data.ndim == 1:
@@ -153,7 +146,7 @@ def _extract_las_data_from_node(node):
las_data["step"] = spacing[2]
las_data["origin"] = origin[2]
elif isinstance(node, slicer.vtkMRMLTableNode):
- depths, las_data["data"] = _arrayPartsFromNode(node)
+ depths, las_data["data"] = arrayPartsFromNode(node)
las_data["step"] = depths[1] - depths[0]
las_data["origin"] = depths[0]
else:
@@ -173,15 +166,15 @@ def extract_las_info_from_node(node):
units_from_name = units_search.group(1) if units_search else "NONE"
units = units_from_name
- if node.GetAttribute(UNITS_TAG) is not None:
- units = node.GetAttribute(UNITS_TAG)
+ if node.GetAttribute(DLISImportConst.UNITS_TAG) is not None:
+ units = node.GetAttribute(DLISImportConst.UNITS_TAG)
if units_from_name == "NONE":
logging.info(
- f"Node name ({node.GetName()}) doesn't include its units ({node.GetAttribute(UNITS_TAG)}) in it."
+ f"Node name ({node.GetName()}) doesn't include its units ({node.GetAttribute(DLISImportConst.UNITS_TAG)}) in it."
)
- elif node.GetAttribute(UNITS_TAG) != units_from_name:
+ elif node.GetAttribute(DLISImportConst.UNITS_TAG) != units_from_name:
logging.warning(
- f"Units informed in {node.GetName()} ({node.GetAttribute(UNITS_TAG)}) metadata are different from the units implied by the node name ({units_from_name}). {node.GetAttribute(UNITS_TAG)} will be considered as the units."
+ f"Units informed in {node.GetName()} ({node.GetAttribute(DLISImportConst.UNITS_TAG)}) metadata are different from the units implied by the node name ({units_from_name}). {node.GetAttribute(DLISImportConst.UNITS_TAG)} will be considered as the units."
)
else:
units = units_from_name
@@ -192,15 +185,15 @@ def extract_las_info_from_node(node):
# well name
well_from_node_name = node.GetName().split("_")[0] if len(node.GetName().split("_")) > 1 else ""
- if node.GetAttribute(WELL_NAME_TAG) is not None:
- las_info["well_name"] = node.GetAttribute(WELL_NAME_TAG)
+ if node.GetAttribute(DLISImportConst.WELL_NAME_TAG) is not None:
+ las_info["well_name"] = node.GetAttribute(DLISImportConst.WELL_NAME_TAG)
if well_from_node_name == "":
logging.info(
- f"Node name ({node.GetName()}) doesn't have the well name ({node.GetAttribute(WELL_NAME_TAG)}) prepended to it."
+ f"Node name ({node.GetName()}) doesn't have the well name ({node.GetAttribute(DLISImportConst.WELL_NAME_TAG)}) prepended to it."
)
- elif node.GetAttribute(WELL_NAME_TAG) != well_from_node_name:
+ elif node.GetAttribute(DLISImportConst.WELL_NAME_TAG) != well_from_node_name:
logging.warning(
- f"Well name informed in {node.GetName()} ({node.GetAttribute(WELL_NAME_TAG)}) metadata is different from the well name implied by the node name ({well_from_node_name}). {node.GetAttribute(WELL_NAME_TAG)} will be considered as the well name."
+ f"Well name informed in {node.GetName()} ({node.GetAttribute(DLISImportConst.WELL_NAME_TAG)}) metadata is different from the well name implied by the node name ({well_from_node_name}). {node.GetAttribute(DLISImportConst.WELL_NAME_TAG)} will be considered as the well name."
)
else:
las_info["well_name"] = well_from_node_name
@@ -209,10 +202,10 @@ def extract_las_info_from_node(node):
las_info["data_name"] = node.GetName().replace("[" + units + "]", "")
new_data_name = las_info["data_name"]
- if node.GetAttribute(WELL_NAME_TAG):
- new_data_name = las_info["data_name"].replace(node.GetAttribute(WELL_NAME_TAG) + "_", "")
+ if node.GetAttribute(DLISImportConst.WELL_NAME_TAG):
+ new_data_name = las_info["data_name"].replace(node.GetAttribute(DLISImportConst.WELL_NAME_TAG) + "_", "")
if len(new_data_name) == 0:
- new_data_name = node.GetAttribute(WELL_NAME_TAG)
+ new_data_name = node.GetAttribute(DLISImportConst.WELL_NAME_TAG)
las_info["data_name"] = new_data_name
las_info["null_value"] = (
diff --git a/src/ltrace/ltrace/image/optimized_transforms.py b/src/ltrace/ltrace/image/optimized_transforms.py
index c78f35f..a81ef73 100644
--- a/src/ltrace/ltrace/image/optimized_transforms.py
+++ b/src/ltrace/ltrace/image/optimized_transforms.py
@@ -7,11 +7,12 @@
import porespy
-DEFAULT_NULL_VALUE = set((-999.25, -9999.00, -9999.25))
+DEFAULT_NULL_VALUES = set((-999.25, -9999.00, -9999.25))
+ANP_880_2022_DEFAULT_NULL_VALUE = -999.25
@nb.jit(nopython=True)
-def trimPointSearch(data, nullvalue=DEFAULT_NULL_VALUE, reverse=False):
+def trimPointSearch(data, nullvalue=DEFAULT_NULL_VALUES, reverse=False):
end_ptr = len(data)
indexing = range(end_ptr) if not reverse else range(end_ptr - 1, 0, -1)
for i in indexing:
@@ -22,7 +23,7 @@ def trimPointSearch(data, nullvalue=DEFAULT_NULL_VALUE, reverse=False):
@nb.jit(nopython=True)
-def trimPointSearch2d(data, nullvalue=DEFAULT_NULL_VALUE, reverse=False):
+def trimPointSearch2d(data, nullvalue=DEFAULT_NULL_VALUES, reverse=False):
end_ptr = len(data)
rows = range(end_ptr) if not reverse else range(end_ptr - 1, 0, -1)
for row in rows:
@@ -160,21 +161,21 @@ def min_max(arr):
return local_min, local_max
-def handle_null_values(image, nullValue: set):
+def handle_null_values(image, nullValues: set):
offset = 0.000001
# for each null value in null value set, replace it with nan
working_image = image.astype(np.float32)
- for v in nullValue:
+ for v in nullValues:
working_image[working_image == v] = np.nan
- # if no values were replaced, return a dummy null value
+ # if no values were replaced, return our standard invalid value
if not np.isnan(np.min(working_image)):
- return list(nullValue)[0]
+ return ANP_880_2022_DEFAULT_NULL_VALUE
tempArrayMaxValue = np.nan
tempArrayMinValue = np.nan
- newNullValue = list(nullValue)[0]
+ newNullValue = ANP_880_2022_DEFAULT_NULL_VALUE
try:
tempArrayMaxValue = np.nanmax(working_image)
tempArrayMinValue = np.nanmin(working_image)
diff --git a/src/ltrace/ltrace/pore_networks/functions.py b/src/ltrace/ltrace/pore_networks/functions.py
index 140420a..1ef9246 100644
--- a/src/ltrace/ltrace/pore_networks/functions.py
+++ b/src/ltrace/ltrace/pore_networks/functions.py
@@ -496,7 +496,7 @@ def _get_paired_throats_table(geo_pore):
if not geo_throat:
qt.QMessageBox.information(
- slicer.util.mainWindow(), "Table parsing failed", "No throats table found in pores table folder."
+ slicer.modules.AppContextInstance.mainWindow, "Table parsing failed", "No throats table found in pores table folder."
)
return False
return geo_throat
diff --git a/src/ltrace/ltrace/pore_networks/functions_simulation.py b/src/ltrace/ltrace/pore_networks/functions_simulation.py
index 6c9b793..a50ccc9 100644
--- a/src/ltrace/ltrace/pore_networks/functions_simulation.py
+++ b/src/ltrace/ltrace/pore_networks/functions_simulation.py
@@ -1,11 +1,12 @@
import os
import csv
-
import numpy as np
import openpnm
+
from pypardiso import spsolve
from scipy.sparse import csr_matrix
from numba import njit, prange
+from pyflowsolver import NetworkManager, DarcySolver
def manual_valvatne_blunt(pore_network):
@@ -336,66 +337,48 @@ def single_phase_permeability(
elif solver == "pyflowsolver":
conn = perm.network["throat.conns"].astype(np.int32)
cond = perm.network["throat.conductance"].astype(np.float64)
- r = _get_sparse_system(conn, cond, inlets, outlets)
- sparse_val, sparse_col_idx, sparse_row_ptr, b, mid_to_total_indexes = r
- if preconditioner == "inverse_diagonal":
- P_val, P_col_idx, P_row_ptr = _get_diagonal_preconditioner(
- A_val=sparse_val,
- A_col_idx=sparse_col_idx,
- A_row_ptr=sparse_row_ptr,
- threads=1,
- )
- else:
- raise Exception
- x, error, iterations = _solve_pcg(
- sparse_val,
- sparse_col_idx,
- sparse_row_ptr,
- P_val,
- P_col_idx,
- P_row_ptr,
- b,
- max_iterations=sparse_val.size**2, # sqrt(n) for n x n system
- target_error=target_error, # 1.0e-6
- X0=np.zeros(b.size, dtype=np.float64),
- threads=1,
+ network_manager = NetworkManager(
+ conn=conn,
+ cond=cond,
+ inlets=inlets,
+ outlets=outlets,
)
+ network_manager.generate_sparse_system()
+ a_sparse_array, b_array = network_manager.get_sparse_system()
+
+ darcy_solver = DarcySolver()
+ darcy_solver.set_linear_system(a_sparse_array, b_array)
+ darcy_solver.generate_preconditioner(preconditioner)
+ x, error, iterations = darcy_solver.solve_pcg()
+
+ pressure = network_manager.get_pressure_list(x)
- pressure = np.zeros(inlets.size, dtype=np.float64)
- pressure[mid_to_total_indexes] = x * np.float64(101325)
- for i in range(inlets.size):
- if inlets[i] == 1:
- pressure[i] = np.float64(101325)
- elif outlets[i] == 1:
- pressure[i] = np.float64(0)
elif solver == "pypardiso":
conn = perm.network["throat.conns"].astype(np.int32)
cond = perm.network["throat.conductance"].astype(np.float64)
- r = _get_sparse_system(conn, cond, inlets, outlets)
- sparse_val, sparse_col_idx, sparse_row_ptr, b, mid_to_total_indexes = r
- A = csr_matrix(
+ network_manager = NetworkManager(
+ conn=conn,
+ cond=cond,
+ inlets=inlets,
+ outlets=outlets,
+ )
+ network_manager.generate_sparse_system()
+ a_sparse_array, b_array = network_manager.get_sparse_system()
+ A_csr = csr_matrix(
(
- sparse_val,
- sparse_col_idx,
- np.append(sparse_row_ptr, sparse_val.size),
+ a_sparse_array["val"],
+ a_sparse_array["col_idx"],
+ np.append(a_sparse_array["row_ptr"], a_sparse_array["val"].size),
)
)
- x = spsolve(A, b)
- pressure = np.zeros(inlets.size, dtype=np.float64)
- pressure[mid_to_total_indexes] = x * np.float64(101325)
- for i in range(inlets.size):
- if inlets[i] == 1:
- pressure[i] = np.float64(101325)
- elif outlets[i] == 1:
- pressure[i] = np.float64(0)
+ x = spsolve(A_csr, b_array)
+ pressure = network_manager.get_pressure_list(x)
project = perm.project
pore_dict = {}
throat_dict = {}
for l in range(len(project)):
for p in project[l].props():
- # if slicer_is_in_developer_mode():
- # print(p, type(project[l][p]), project[l][p])
prop_array = project[l][p]
if prop_array.ndim == 1:
if p[:4] == "pore":
@@ -420,133 +403,6 @@ def single_phase_permeability(
return (perm, pore_dict, throat_dict)
-@njit
-def _get_sparse_system(conn, cond, inlets, outlets):
- # network must have only connected pores
- # conn array(n, 2)
-
- inlets *= np.int32(1) - outlets
- border = inlets + outlets
-
- n_p_total = inlets.size
- n_p_in = inlets.sum()
- n_p_out = outlets.sum()
- n_p_mid = n_p_total - n_p_in - n_p_out
- n_t = cond.size
-
- mid_to_total_indexes = np.zeros((n_p_mid), dtype=np.int32)
- total_to_mid_indexes = np.zeros((n_p_total), dtype=np.int32)
-
- pore_index_filled = 0
- for p in range(n_p_total):
- if border[p] == 0:
- mid_to_total_indexes[pore_index_filled] = p
- total_to_mid_indexes[p] = pore_index_filled
- pore_index_filled += 1
- if pore_index_filled != mid_to_total_indexes.size:
- raise Exception
-
- sparse_row_counter = np.zeros((n_p_mid), dtype=np.int32)
- n_mid_t = 0
- for t in range(n_t):
- i = conn[t, 0]
- j = conn[t, 1]
- if (border[i] == 0) and (border[j] == 0):
- n_mid_t += 1
- sparse_row_counter[total_to_mid_indexes[i]] += 1
- sparse_row_counter[total_to_mid_indexes[j]] += 1
-
- sparse_val = np.zeros((n_p_mid + 2 * n_mid_t), dtype=np.float64)
- sparse_col_idx = np.ones((sparse_val.size), dtype=np.int32) * -1
- sparse_row_ptr = np.zeros((n_p_mid), dtype=np.int32)
- sparse_col_idx[0] = 0
- for i in range(1, n_p_mid):
- sparse_row_ptr[i] = sparse_row_ptr[i - 1] + sparse_row_counter[i - 1] + 1
- sparse_col_idx[sparse_row_ptr[i]] = i
-
- b = np.zeros(n_p_mid, dtype=np.float64)
-
- for t in range(n_t):
- conn_0 = conn[t, 0]
- conn_1 = conn[t, 1]
- conductance = cond[t]
-
- for i, j in ((conn_0, conn_1), (conn_1, conn_0)):
- if border[i] == 1:
- continue
- i_mid = total_to_mid_indexes[i]
- row_ptr_start = sparse_row_ptr[i_mid]
- sparse_val[row_ptr_start] -= conductance
- # print(i, j)
-
- if inlets[j] == 1:
- b[i_mid] -= conductance
- elif border[j] == 0:
- j_mid = total_to_mid_indexes[j]
- # print("mid: ", i_mid, j_mid)
- # target column is j_mid
- # first check if column is already occupied
- if (i_mid + 1) < sparse_row_ptr.size:
- row_ptr_end = sparse_row_ptr[i_mid + 1]
- else:
- row_ptr_end = sparse_val.size
-
- found = False
- for linear_index in range(row_ptr_start, row_ptr_end):
- if sparse_col_idx[linear_index] == j_mid:
- sparse_val[linear_index] += conductance
- found = True
- break
- if not found:
- local_index = sparse_row_counter[i_mid]
- linear_index = row_ptr_start + local_index
- if (local_index <= 0) or (sparse_val[linear_index] != 0):
- raise Exception
- sparse_col_idx[linear_index] = j_mid
- sparse_val[linear_index] = conductance
- sparse_row_counter[i_mid] -= 1
- found = True
- # print(found, linear_index, sparse_val[linear_index])
- if not found:
- raise Exception
-
- # print(sparse_val.sum())
- # sparse cleanup
-
- sparse_val_dirty = sparse_val.copy()
- sparse_col_idx_dirty = sparse_col_idx.copy()
- sparse_row_ptr_dirty = sparse_row_ptr.copy()
-
- nulls_counter = 0
- for i in range(sparse_row_ptr.size):
- row_ptr_start = sparse_row_ptr[i]
- if (i + 1) < sparse_row_ptr.size:
- row_ptr_stop = sparse_row_ptr[i + 1]
- else:
- row_ptr_stop = sparse_val.size
- nulls = (sparse_col_idx[row_ptr_start:row_ptr_stop] == -1).sum()
- filled = row_ptr_stop - row_ptr_start - nulls
- sort_index = np.argsort(sparse_col_idx[row_ptr_start:row_ptr_stop])
- sorted_vals = sparse_val[row_ptr_start:row_ptr_stop][sort_index]
- sorted_col_idx = sparse_col_idx[row_ptr_start:row_ptr_stop][sort_index]
- compacted_start = row_ptr_start - nulls_counter
- compacted_end = compacted_start + filled
- sparse_val[compacted_start:compacted_end] = sorted_vals[nulls:]
- sparse_col_idx[compacted_start:compacted_end] = sorted_col_idx[nulls:]
- nulls_counter += nulls
- sparse_row_ptr[i] -= nulls_counter
- # print()
- # print(sorted_vals)
- # print(sparse_col_idx)
- # print(sparse_row_ptr)
- if nulls > 0:
- sparse_val = sparse_val[:-nulls_counter]
- sparse_col_idx = sparse_col_idx[:-nulls_counter]
-
- # return sparse_val, sparse_col_idx, sparse_row_ptr, b, mid_to_total_indexes, sparse_val_dirty, sparse_col_idx_dirty, sparse_row_ptr_dirty
- return sparse_val, sparse_col_idx, sparse_row_ptr, b, mid_to_total_indexes
-
-
def get_flow_rate(pn_pores, pn_throats):
inlet_flow_total = np.float64(0.0)
outlet_flow_total = np.float64(0.0)
@@ -591,323 +447,3 @@ def get_flow_rate(pn_pores, pn_throats):
flow_rate = (outlet_flow_total + inlet_flow_total) / 2
return flow_rate
-
-
-##### Temporary, delete later and import module
-
-
-@njit
-def _solve_cg(
- A_val,
- A_col_idx,
- A_row_ptr,
- b,
- max_iterations, # sqrt(n) for n x n system
- target_error, # 1.0e-6
- X0,
- threads,
-):
- # Reference: https://repository.lsu.edu/cgi/viewcontent.cgi?article=1254&context=honors_etd
-
- x = X0.copy()
- r = b.copy()
- m = np.empty(1, dtype=np.float64)
- m[0] = _square_sum_vector(r, threads) # f(x:vector) = x'*x
- m_last = np.empty(1, dtype=np.float64)
- p = r.copy()
- alpha = np.empty(1, dtype=np.float64)
- beta = np.empty(1, dtype=np.float64)
- iteration = 0
- for _ in range(max_iterations):
- iteration += 1
- alpha[0] = m[0] / _scalar_product(
- p,
- A_val,
- A_col_idx,
- A_row_ptr,
- threads,
- ) # scalar_product = p'*A*p
- _add_product(x, alpha[0], p, threads) # f(x: vector, y: scalar, z:vector): x += y * z
- # _subtract_product_of_product(
- # r,
- # alpha[0],
- # A_val,
- # A_col_idx,
- # A_row_ptr,
- # p,
- # threads,
- # ) # f(x:vector, y:scalar, z:array, k:vector): x -= y * z * k
- r[:] = _recalc_residuals_jit(A_val, A_col_idx, A_row_ptr, b, x)
- m_last[0] = m[0]
- m[0] = _square_sum_vector(r, threads)
- beta[0] = m[0] / m_last[0]
- _multiply_and_add(
- p,
- r,
- beta[0],
- threads,
- ) # f(x:vector, y:vector, z:scalar): x = y + z * x
- error = np.sqrt(_square_sum_vector(r, threads) / _square_sum_vector(b, threads))
- if error <= target_error:
- return x, error, iteration
-
- return x, error, iteration
-
-
-@njit(parallel=True)
-def _square_sum_vector(v, threads):
- # f(v:vector) = v'*v
- partial_sum = np.zeros(threads, dtype=np.float64)
- n = v.size
-
- for w in prange(threads):
- thread_start = w * n // threads
- thread_end = (w + 1) * n // threads
- for i in range(thread_start, thread_end):
- partial_sum[w] += v[i] ** 2
- return partial_sum.sum()
-
-
-@njit(parallel=True)
-def _scalar_product(
- v,
- A_val,
- A_col_idx,
- A_row_ptr,
- threads,
-):
- # f(v: vector[n], A:array[n, n]) = x'*A*x
- partial_sum = np.zeros(threads, dtype=np.float64)
- n = v.size
-
- for w in prange(threads):
- thread_start = w * n // threads
- thread_end = (w + 1) * n // threads
- for i in range(thread_start, thread_end):
- A_start = A_row_ptr[i]
- if (i + 1) < n:
- A_stop = A_row_ptr[i + 1]
- else:
- A_stop = A_val.size
- for A_linear_index in range(A_start, A_stop):
- j = A_col_idx[A_linear_index]
- partial_sum[w] += v[i] * A_val[A_linear_index] * v[j]
- return partial_sum.sum()
-
-
-@njit(parallel=True)
-def _add_product(v, x, u, threads):
- # f(v: vector, x: scalar, u:vector): v += x * u
- n = v.size
-
- for w in prange(threads):
- thread_start = w * n // threads
- thread_end = (w + 1) * n // threads
- for i in range(thread_start, thread_end):
- v[i] += x * u[i]
-
-
-@njit(parallel=True)
-def _recalc_residuals_jit(r, val, col_idx, row_ptr, condensed_b, X):
- # residuals = np.zeros(condensed_b.size, dtype=np.float64)
- r[:] = condensed_b
-
- for row in range(row_ptr.size):
- start = row_ptr[row]
- if row < (row_ptr.size - 1):
- stop = row_ptr[row + 1]
- else:
- stop = val.size
-
- for index in range(start, stop):
- v = val[index]
- column = col_idx[index]
- r[row] -= v * X[column]
-
-
-@njit(parallel=True)
-def _subtract_product_of_product(
- v,
- x,
- A_val,
- A_col_idx,
- A_row_ptr,
- u,
- threads,
-):
- # f(v:vector, x:scalar, A:array, u:vector): v -= x * A * u
- n = v.size
-
- for w in prange(threads):
- thread_start = w * n // threads
- thread_end = (w + 1) * n // threads
- for i in range(thread_start, thread_end):
- A_start = A_row_ptr[i]
- if (i + 1) < n:
- A_stop = A_row_ptr[i + 1]
- else:
- A_stop = A_val.size
- for A_linear_index in range(A_start, A_stop):
- j = A_col_idx[A_linear_index]
- v[i] -= x * A_val[A_linear_index] * u[j]
-
-
-@njit(parallel=True)
-def _multiply_and_add(
- v,
- u,
- x,
- threads,
-):
- # f(v:vector, u:vector, x:scalar): v = u + x * v
- n = v.size
-
- for w in prange(threads):
- thread_start = w * n // threads
- thread_end = (w + 1) * n // threads
- for i in range(thread_start, thread_end):
- v[i] = u[i] + x * v[i]
-
-
-@njit
-def _get_diagonal_preconditioner(
- A_val,
- A_col_idx,
- A_row_ptr,
- threads,
-): # f(v, A): v = A*v
- diagonal_n = A_row_ptr.size
- P_val = np.empty(diagonal_n, dtype=np.float64)
- P_col_idx = np.arange(diagonal_n, dtype=np.int32)
- P_row_ptr = np.arange(diagonal_n, dtype=np.int32)
- for row in range(diagonal_n):
- start = A_row_ptr[row]
- if row < (A_row_ptr.size - 1):
- stop = A_row_ptr[row + 1]
- else:
- stop = A_val.size
-
- for linear_index in range(start, stop):
- column = A_col_idx[linear_index]
- if column == row:
- v = A_val[linear_index]
- P_val[row] = 1 / v
- return P_val, P_col_idx, P_row_ptr
-
-
-@njit
-def _solve_pcg(
- A_val,
- A_col_idx,
- A_row_ptr,
- P_val,
- P_col_idx,
- P_row_ptr,
- b,
- max_iterations, # sqrt(n) for n x n system
- target_error, # 1.0e-6
- X0,
- threads,
-):
- # Reference: https://repository.lsu.edu/cgi/viewcontent.cgi?article=1254&context=honors_etd
-
- x = X0.copy()
- r = b.copy()
- m = np.empty(1, dtype=np.float64)
- m[0] = _scalar_product(
- r,
- P_val,
- P_col_idx,
- P_row_ptr,
- threads,
- ) # scalar_product = p'*A*p
- m_last = np.empty(1, dtype=np.float64)
- p = r.copy()
- p[:] = _vector_array_multiply(
- p,
- P_val,
- P_col_idx,
- P_row_ptr,
- threads,
- ) # f(v, A): v = A*v
- alpha = np.empty(1, dtype=np.float64)
- beta = np.empty(1, dtype=np.float64)
- iteration = 0
- for _ in range(max_iterations):
- iteration += 1
- alpha[0] = m[0] / _scalar_product(
- p,
- A_val,
- A_col_idx,
- A_row_ptr,
- threads,
- ) # scalar_product = p'*A*p
- _add_product(x, alpha[0], p, threads) # f(x: vector, y: scalar, z:vector): x += y * z
- _recalc_residuals_jit(r, A_val, A_col_idx, A_row_ptr, b, x)
- m_last[0] = m[0]
- m[0] = _scalar_product(
- r,
- P_val,
- P_col_idx,
- P_row_ptr,
- threads,
- ) # scalar_product = p'*A*p
- beta[0] = m[0] / m_last[0]
- p[:] = _multiply_array_and_add(
- p,
- r,
- beta[0],
- P_val,
- P_col_idx,
- P_row_ptr,
- threads,
- ) # f(x:vector, y:vector, z:scalar, A:array): x = A * y + z * x
- error = np.sqrt(_square_sum_vector(r, threads) / _square_sum_vector(b, threads))
- if error <= target_error:
- return x, error, iteration
-
- return x, error, iteration
-
-
-@njit #
-def _vector_array_multiply(
- p,
- val,
- col_idx,
- row_ptr,
- threads,
-): # f(v, A): v = A*v
- new_p = np.zeros_like(p)
- for row in range(row_ptr.size):
- start = row_ptr[row]
- if row < (row_ptr.size - 1):
- stop = row_ptr[row + 1]
- else:
- stop = val.size
-
- for index in range(start, stop):
- v = val[index]
- column = col_idx[index]
- new_p[row] += v * p[column]
- return new_p
-
-
-@njit(parallel=True)
-def _multiply_array_and_add(
- u,
- v,
- x,
- val,
- col_idx,
- row_ptr,
- threads,
-): # f(u:vector, v:vector, x:scalar, A:array): x = A * v + x * u
- new_p = _vector_array_multiply(
- v,
- val,
- col_idx,
- row_ptr,
- threads,
- )
- new_p += x * u
- return new_p
diff --git a/src/ltrace/ltrace/pore_networks/pnflow_parameter_defs.py b/src/ltrace/ltrace/pore_networks/pnflow_parameter_defs.py
index bb25069..e3a6766 100644
--- a/src/ltrace/ltrace/pore_networks/pnflow_parameter_defs.py
+++ b/src/ltrace/ltrace/pore_networks/pnflow_parameter_defs.py
@@ -71,6 +71,12 @@
"false_value": "F",
"default_values": {"left": False, "right": True},
},
+ "seed": {
+ "display_name": "Simulation seed",
+ "layout": "options",
+ "dtype": "singleint",
+ "default_value": 0,
+ },
"calc_box_lower_boundary": {
"display_name": "Lower box boundary",
"layout": "options",
@@ -173,6 +179,14 @@
"false_value": "F",
"default_value": False,
},
+ "create_ca_distributions": {
+ "display_name": "Create CA distribution nodes",
+ "layout": "options",
+ "dtype": "singlecheckbox",
+ "true_value": "T",
+ "false_value": "F",
+ "default_value": False,
+ },
"keep_temporary": {
"display_name": "Keep temporary files",
"layout": "options",
diff --git a/src/ltrace/ltrace/pore_networks/visualization_model.py b/src/ltrace/ltrace/pore_networks/visualization_model.py
index 57c2526..e6d7175 100644
--- a/src/ltrace/ltrace/pore_networks/visualization_model.py
+++ b/src/ltrace/ltrace/pore_networks/visualization_model.py
@@ -13,7 +13,7 @@
def visualize_vtu(
- unstructured_grid,
+ filepath,
cycle,
scale_factor=10**3,
pore_scale=2000,
@@ -30,6 +30,10 @@ def visualize_vtu(
Must be 'w' for wetting phase (usually waater) injection cycles and 'o'
for non-wetting phase (usually oil) injection cycles
"""
+ reader = vtk.vtkXMLUnstructuredGridReader()
+ reader.SetFileName(filepath)
+ reader.Update()
+ unstructured_grid = reader.GetOutput()
sphere_theta_resolution = 8
sphere_phi_resolution = 8
@@ -169,18 +173,13 @@ def visualize_vtu(
return pressure, merger
-def generate_model_variable_scalar(temp_folder, min_saturation_delta=0.005, is_multiscale=False):
+def generate_model_variable_scalar(temp_folder, is_multiscale=False):
file_names = sorted([i for i in os.listdir(temp_folder) if i[-4:] == ".vtu"])
pressures = []
- reader = vtk.vtkXMLUnstructuredGridReader()
-
base_filepath = os.path.join(temp_folder, file_names[0])
- reader.SetFileName(base_filepath)
- reader.Update()
- mesh = reader.GetOutput()
pressure, pore_mesh = visualize_vtu(
- mesh, create_model=False, cycle=file_names[0][2].lower(), normalize_radius=is_multiscale
+ base_filepath, create_model=False, cycle=file_names[0][2].lower(), normalize_radius=is_multiscale
)
point_data = pore_mesh.GetOutput().GetPointData()
pressures.append(pressure)
@@ -193,16 +192,13 @@ def generate_model_variable_scalar(temp_folder, min_saturation_delta=0.005, is_m
for data_point, file_name in enumerate(file_names[1:], start=1):
filepath = os.path.join(temp_folder, file_name)
- reader.SetFileName(filepath)
- reader.Update()
- mesh = reader.GetOutput()
pressure, poly_data = visualize_vtu(
- mesh, cycle=file_name[2].lower(), create_model=False, normalize_radius=is_multiscale
+ filepath, cycle=file_name[2].lower(), create_model=False, normalize_radius=is_multiscale
)
saturation = poly_data.GetOutput().GetPointData().GetArray("saturation")
new_array = vtk.util.numpy_support.vtk_to_numpy(saturation)
- if np.mean(np.abs(new_array - previous_array)) >= min_saturation_delta:
+ if data_point == 1 or np.mean(np.abs(new_array - previous_array)) != 0.0:
saturation.SetName(f"saturation_{(i:=i+1)}")
point_data.AddArray(saturation)
pressures.append(pressure)
@@ -235,7 +231,7 @@ def generate_model_variable_scalar(temp_folder, min_saturation_delta=0.005, is_m
return extract.GetOutputDataObject(0), saturation_steps
-def _model_elements_from_grid(
+def _unstructured_grid_to_dict(
unstructured_grid,
cycle,
scale_factor=10**3,
@@ -289,7 +285,6 @@ def _model_elements_from_grid(
# y_min -= y_length * linear_size_reduction
# y_max += y_length * linear_size_reduction
- throat_id_list = np.empty(n_cells, dtype=np.int64)
throat_index_list = np.empty(n_cells, dtype=np.int64)
neighbors_id_list = np.empty((n_cells, 2), dtype=np.int64)
throat_radius_list = np.empty(n_cells, dtype=np.float64)
@@ -321,10 +316,8 @@ def _model_elements_from_grid(
):
continue
- throat_id = unstructured_grid.GetCell(i).GetPointIds().GetId(2)
throat_radius = unstructured_grid.GetCellData().GetArray("RRR").GetComponent(i, 0)
- throat_id_list[throat_count] = throat_id
throat_index_list[throat_count] = i
neighbors_id_list[throat_count] = (left_pore_id, right_pore_id)
throat_radius_list[throat_count] = throat_radius
@@ -418,16 +411,47 @@ def _model_elements_from_grid(
throats = {}
for i in range(throat_count):
throat_index = throat_index_list[i]
- throat_id = throat_id_list[i]
left_pore_id, right_pore_id = neighbors_id_list[i]
throats[throat_index] = {
"first_conn": pore_mapper[left_pore_id],
"second_conn": pore_mapper[right_pore_id],
"radius": throat_radius_list[i],
- "Sw": unstructured_grid.GetPointData().GetArray("Sw").GetComponent(throat_id, 0),
"Sw_cell": unstructured_grid.GetCellData().GetArray("Sw").GetComponent(throat_index, 0),
}
+ return pores, throats, arrows, volume_side
+
+
+def _model_elements_from_grid(
+ unstructured_grid,
+ cycle,
+ scale_factor=10**3,
+ pore_scale=2000,
+ throat_scale=20,
+ arrow_scale=0.2,
+ axis="x",
+ normalize_radius=False,
+ **kwargs,
+):
+ """Model elements from unstructured grid
+
+ Args:
+ unstructured_grid (vtkUnstructuredGrid): unstructured_grid
+ cycle (char): "w" for water injection, "o" for oil injection
+ scale_factor (float): Scales entire network
+ pore_scale (float): Scales pore sizes
+ throat_scale (float): Scale throat sizes
+ arrow_scale (float): Scale arrow sizes
+ axis (char): axis
+ normalize_radius (bool): If true, ignore throats and pores scale factors and normalize their size by the grid volume
+
+ Returns:
+ dict: model elements data
+ """
+ pores, throats, arrows, volume_side = _unstructured_grid_to_dict(
+ unstructured_grid, cycle, scale_factor, pore_scale, throat_scale, arrow_scale, axis, normalize_radius
+ )
+
coordinates = vtk.vtkPoints()
radii = vtk.vtkFloatArray()
radii.SetName("radius")
diff --git a/src/ltrace/ltrace/remote/hosts/ssh/widget.py b/src/ltrace/ltrace/remote/hosts/ssh/widget.py
index b0908e0..f3baf72 100644
--- a/src/ltrace/ltrace/remote/hosts/ssh/widget.py
+++ b/src/ltrace/ltrace/remote/hosts/ssh/widget.py
@@ -124,6 +124,8 @@ def _onAddPublicKeyClicked(self) -> None:
self.rsa_key = Path(fileDialog.selectedFiles()[0])
self.keyLabel.setText(f" - {self.rsa_key}")
+ fileDialog.deleteLater()
+
def getRSAKeyPathString(self) -> str:
return str(self.rsa_key) if self.rsa_key else None
diff --git a/src/ltrace/ltrace/screenshot/Screenshot.py b/src/ltrace/ltrace/screenshot/Screenshot.py
index 8761b10..c599065 100644
--- a/src/ltrace/ltrace/screenshot/Screenshot.py
+++ b/src/ltrace/ltrace/screenshot/Screenshot.py
@@ -1,7 +1,15 @@
+from pathlib import Path
+
import qt
import slicer
import vtk
-from pathlib import Path
+import pyqtgraph.exporters
+import pyqtgraph as pg
+
+from ltrace.slicer_utils import getResourcePath
+
+GRAPHIC_VIEW_DATA = "GraphicViewData"
+SLICE_VIEW_DATA = "SliceViewData"
def _captureRenderWindow(renderWindow, isTransparent, fileName):
@@ -65,6 +73,13 @@ def _captureSliceView(sliceName, isTransparent, fileName):
renderWindow.SetAlphaBitPlanes(alphaBitPlanes)
+def _captureGraphView(viewIdentifier, fileName):
+ plotItem = slicer.util.getModuleWidget("ImageLogData").getGraphicViewPlotItem(viewIdentifier)
+ if isinstance(plotItem, pg.PlotItem):
+ exporter = pg.exporters.ImageExporter(plotItem.scene())
+ exporter.export(fileName)
+
+
class ScreenshotWidget(qt.QDialog):
VIEW_OPTION = ["Red", "Green", "Yellow"]
THREED_VIEW_OPTION = "3D View"
@@ -75,17 +90,23 @@ class ScreenshotWidget(qt.QDialog):
IS_TRANSPARENT_SETTINGS_KEY = "/".join((SETTINGS_NAME, "isTransparent"))
SAVE_DIRECTORY_SETTINGS_KEY = "/".join((SETTINGS_NAME, "saveDirectory"))
- def __init__(self, icon):
+ ICON = "ScreenShot.png"
+
+ def __init__(self):
super().__init__(
0,
qt.Qt.WindowSystemMenuHint | qt.Qt.WindowTitleHint | qt.Qt.WindowCloseButtonHint,
)
- self.iconPath = icon
- self.setWindowTitle("Screenshot")
+ self.setWindowTitle("ScreenShot")
self.setAttribute(qt.Qt.WA_DeleteOnClose)
self.saved_lines = []
+ try:
+ self.imagelogViews = slicer.util.getModuleWidget("ImageLogData").getVisibleViews()
+ except:
+ self.imagelogViews = None
+
self.setup()
def setup(self):
@@ -93,7 +114,16 @@ def setup(self):
# View option
self.viewCombobox = qt.QComboBox()
- options = (self.THREED_VIEW_OPTION,) + slicer.app.layoutManager().sliceViewNames()
+
+ imagelogOptions = []
+ if self.imagelogViews:
+ self.imagelogValidIds = []
+ for key, value in self.imagelogViews.items():
+ if value["type"] in [GRAPHIC_VIEW_DATA, SLICE_VIEW_DATA]:
+ imagelogOptions.append(value["name"])
+ self.imagelogValidIds.append(key)
+
+ options = [self.THREED_VIEW_OPTION] + self.VIEW_OPTION + imagelogOptions
for option in options:
self.viewCombobox.addItem(option)
self.viewCombobox.currentText = slicer.app.settings().value(self.VIEW_SETTINGS_KEY, self.THREED_VIEW_OPTION)
@@ -103,12 +133,17 @@ def setup(self):
layout.addRow("Transparent background:", self.transparentCheck)
# Annotations options
+ self.annotationsOptionsWidget = qt.QWidget()
+ annotationsOptionsLayout = qt.QFormLayout(self.annotationsOptionsWidget)
+ annotationsOptionsLayout.setContentsMargins(0, 0, 0, 0)
+
hbox_line = qt.QHBoxLayout()
+ hbox_line.setSpacing(10)
line_left = qt.QFrame()
line_left.setFrameShape(qt.QFrame.HLine)
line_left.setFrameShadow(qt.QFrame.Sunken)
hbox_line.addWidget(line_left)
- text_label = qt.QLabel(" Add Annotations")
+ text_label = qt.QLabel("Add Annotations")
hbox_line.addWidget(text_label)
line_right = qt.QFrame()
line_right.setFrameShape(qt.QFrame.HLine)
@@ -117,10 +152,10 @@ def setup(self):
line.setFrameShape(qt.QFrame.HLine)
line.setFrameShadow(qt.QFrame.Sunken)
hbox_line.addWidget(line_right)
- layout.addRow(hbox_line)
+ annotationsOptionsLayout.addRow(hbox_line)
self.input = qt.QLineEdit("Click Add")
- layout.addRow("Annotation:", self.input)
+ annotationsOptionsLayout.addRow("Annotation:", self.input)
hbox = qt.QHBoxLayout()
self.radio_left = qt.QRadioButton("Left")
@@ -130,14 +165,14 @@ def setup(self):
hbox.addWidget(self.radio_left)
hbox.addWidget(self.radio_center)
hbox.addWidget(self.radio_right)
- layout.addRow("Text Position:", hbox)
+ annotationsOptionsLayout.addRow("Text Position:", hbox)
self.fontSizeSlider = slicer.qMRMLSliderWidget()
self.fontSizeSlider.maximum = 100
self.fontSizeSlider.minimum = 12
self.fontSizeSlider.value = 15
self.fontSizeSlider.singleStep = 1
- layout.addRow("Font Size:", self.fontSizeSlider)
+ annotationsOptionsLayout.addRow("Font Size:", self.fontSizeSlider)
textButtons = qt.QHBoxLayout()
addTextButton = qt.QPushButton("Add")
@@ -147,8 +182,9 @@ def setup(self):
textButtons.addWidget(addTextButton)
textButtons.addWidget(removeTextButton)
textButtons.setAlignment(qt.Qt.AlignCenter)
- layout.addRow(textButtons)
+ annotationsOptionsLayout.addRow(textButtons)
+ layout.addRow(self.annotationsOptionsWidget)
layout.addRow(line)
layout.addRow(qt.QFrame())
@@ -160,7 +196,7 @@ def setup(self):
buttonBox.rejected.connect(self.onReject)
layout.addRow(buttonBox)
- self.viewCombobox.currentIndexChanged.connect(lambda font: self.clearAll())
+ self.viewCombobox.currentIndexChanged.connect(self.viewIndexChange)
self.radio_left.clicked.connect(lambda: self.clearAll())
self.radio_center.clicked.connect(lambda: self.clearAll())
self.radio_right.clicked.connect(lambda: self.clearAll())
@@ -168,7 +204,7 @@ def setup(self):
addTextButton.clicked.connect(self.addText)
removeTextButton.clicked.connect(self.removeText)
- self.setWindowIcon(qt.QIcon(self.iconPath))
+ self.setWindowIcon(qt.QIcon(getResourcePath("Icons") / self.ICON))
def addText(self):
input_text = self.input.text
@@ -182,6 +218,15 @@ def removeText(self):
del self.saved_lines[-1]
self.renderText()
+ def viewIndexChange(self, index):
+ self.annotationsOptionsWidget.setEnabled(True)
+ if index > 3:
+ id = self.imagelogValidIds[index - 4]
+ if self.imagelogViews[id]["type"] == GRAPHIC_VIEW_DATA:
+ self.annotationsOptionsWidget.setEnabled(False)
+
+ self.clearAll()
+
def clearAll(self):
textColor = (1.0, 1.0, 1.0)
for viewName in self.VIEW_OPTION:
@@ -192,12 +237,19 @@ def clearAll(self):
def renderText(self):
textColor = (1.0, 1.0, 1.0)
viewName = self.viewCombobox.currentText
+ viewIndex = self.viewCombobox.currentIndex
text = self.get_saved_text()
- if viewName == self.THREED_VIEW_OPTION:
+ if viewIndex == 0: # 3D VIEW
self.renderIn3DView(text, textColor)
- else:
+
+ elif viewIndex in [1, 2, 3]: # RED, GREEN or YELLOW Camera
self.renderInView(text, viewName, textColor)
+ else: # ImageLog Views
+ id = self.imagelogValidIds[viewIndex - 4]
+ if self.imagelogViews[id]["type"] == SLICE_VIEW_DATA:
+ self.renderInView(text, f"ImageLogSliceView{id}", textColor)
+
def renderInView(self, text, viewName, textColor):
lm = slicer.app.layoutManager()
view = lm.sliceWidget(viewName).sliceView()
@@ -244,6 +296,7 @@ def get_saved_text(self):
def onSaveAs(self):
viewName = self.viewCombobox.currentText
+ viewIndex = self.viewCombobox.currentIndex
isTransparent = self.transparentCheck.checked
directory = slicer.app.settings().value(
self.SAVE_DIRECTORY_SETTINGS_KEY,
@@ -259,11 +312,19 @@ def onSaveAs(self):
slicer.app.settings().setValue(self.IS_TRANSPARENT_SETTINGS_KEY, str(isTransparent))
slicer.app.settings().setValue(self.SAVE_DIRECTORY_SETTINGS_KEY, directory)
- if viewName == self.THREED_VIEW_OPTION:
+ if viewIndex == 0: # 3D VIEW
_capture3DView(isTransparent, fileName)
- else:
+
+ elif viewIndex in [1, 2, 3]: # RED, GREEN or YELLOW Camera
_captureSliceView(viewName, isTransparent, fileName)
+ else: # ImageLog Views
+ id = self.imagelogValidIds[viewIndex - 4]
+ if self.imagelogViews[id]["type"] == GRAPHIC_VIEW_DATA:
+ _captureGraphView(id, fileName)
+ else:
+ _captureSliceView(f"ImageLogSliceView{id}", isTransparent, fileName)
+
self.accept()
def onReject(self):
diff --git a/tools/deploy/GeoSlicerManual/docs/Core CT/corect.md b/src/ltrace/ltrace/slicer/about/__init__.py
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Core CT/corect.md
rename to src/ltrace/ltrace/slicer/about/__init__.py
diff --git a/src/ltrace/ltrace/slicer/about/about_dialog.py b/src/ltrace/ltrace/slicer/about/about_dialog.py
new file mode 100644
index 0000000..ad8a903
--- /dev/null
+++ b/src/ltrace/ltrace/slicer/about/about_dialog.py
@@ -0,0 +1,67 @@
+import qt
+import slicer
+
+from ltrace.slicer.app import getApplicationVersion
+from ltrace.slicer_utils import getResourcePath
+
+
+class AboutDialog(qt.QDialog):
+ def __init__(self, parent, *args, **kwargs) -> None:
+ super().__init__(parent)
+
+ self.setWindowTitle("About GeoSlicer")
+ self.setWindowFlags(self.windowFlags() & ~qt.Qt.WindowContextHelpButtonHint)
+ self.setWindowIcon(qt.QIcon((getResourcePath("Icons") / "GeoSlicer.ico").as_posix()))
+ self.setFixedSize(600, 430)
+ self.setupUi()
+ self.setObjectName("GeoSlicer About Dialog")
+
+ def setupUi(self) -> None:
+ layout = qt.QVBoxLayout(self)
+
+ pixMap = qt.QPixmap((getResourcePath("Icons") / "GeoSlicerLogo.png").as_posix())
+ pixMap = pixMap.scaled(
+ pixMap.width() // 10, pixMap.height() // 10, qt.Qt.KeepAspectRatio, qt.Qt.SmoothTransformation
+ )
+ logoLabel = qt.QLabel()
+ logoLabel.setPixmap(pixMap)
+
+ textBrowser = qt.QTextBrowser()
+ textBrowser.setFixedSize(340, 370)
+ textBrowser.setOpenExternalLinks(True)
+
+ textBrowser.setFontPointSize(25)
+ textBrowser.append("GeoSlicer")
+ textBrowser.setFontPointSize(11)
+ textBrowser.append("")
+ textBrowser.append(getApplicationVersion())
+ textBrowser.append("")
+ textBrowser.insertHtml(slicer.modules.AppContextInstance.getAboutGeoSlicer())
+
+ aboutQtButton = qt.QPushButton("About Qt")
+ aboutQtButton.setAutoDefault(False)
+ closeButton = qt.QPushButton("Close")
+ closeButton.setAutoDefault(False)
+
+ horizontalLayout = qt.QHBoxLayout()
+ horizontalLayout.addWidget(logoLabel, 1, qt.Qt.AlignCenter)
+ horizontalLayout.addWidget(textBrowser, 1, qt.Qt.AlignRight)
+
+ buttonsLayout = qt.QHBoxLayout()
+ buttonsLayout.addStretch(1)
+ buttonsLayout.addWidget(aboutQtButton)
+ buttonsLayout.addWidget(closeButton)
+
+ layout.addLayout(horizontalLayout)
+ layout.addLayout(buttonsLayout)
+ self.setLayout(layout)
+
+ # connections
+ aboutQtButton.clicked.connect(self.__onAboutQtButtonClicked)
+ closeButton.clicked.connect(self.__onCloseButtonClicked)
+
+ def __onAboutQtButtonClicked(self):
+ slicer.app.aboutQt()
+
+ def __onCloseButtonClicked(self):
+ self.close()
diff --git a/src/ltrace/ltrace/slicer/app/__init__.py b/src/ltrace/ltrace/slicer/app/__init__.py
new file mode 100644
index 0000000..05a741c
--- /dev/null
+++ b/src/ltrace/ltrace/slicer/app/__init__.py
@@ -0,0 +1,86 @@
+import json
+from pathlib import Path
+from typing import Dict
+
+import slicer
+import vtk
+import os
+
+from datetime import datetime
+
+
+def parseApplicationVersion(data: Dict) -> str:
+ geoslicerVersion = data["GEOSLICER_VERSION"]
+ geoslicerHash = data["GEOSLICER_HASH"]
+ geoslicerHashDirty = data["GEOSLICER_HASH_DIRTY"]
+ geoslicerBuildTime = datetime.strptime(data["GEOSLICER_BUILD_TIME"], "%Y-%m-%d %H:%M:%S.%f")
+
+ versionString = geoslicerVersion
+ if not geoslicerVersion:
+ hash_ = geoslicerHash[:8] + "*" if geoslicerHashDirty else ""
+ date = geoslicerBuildTime.strftime("%Y-%m-%d")
+ versionString = "{} {}".format(hash_, date)
+
+ return versionString
+
+
+def getApplicationVersion():
+ return slicer.modules.AppContextInstance.appVersionString
+
+
+def getApplicationInfo(key):
+ return slicer.modules.AppContextInstance.appData.get(key, None)
+
+
+def updateWindowTitle(versionString):
+ """Updates main window's title according to the current project"""
+
+ projectString = "Untitled project"
+ projectURL = slicer.mrmlScene.GetURL()
+ if projectURL != "":
+ projectString = os.path.dirname(projectURL)
+
+ windowTitle = "GeoSlicer {} - {} [*]".format(versionString, projectString)
+ slicer.modules.AppContextInstance.mainWindow.setWindowTitle(windowTitle)
+
+
+def tryDetectProjectDataType():
+
+ nodes = slicer.util.getNodesByClass("vtkMRMLVolumeNode")
+ if len(nodes) == 0:
+ return False
+
+ sceneTree = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
+ rootItemID = sceneTree.GetSceneItemID()
+
+ children = vtk.vtkIdList()
+ sceneTree.GetItemChildren(rootItemID, children)
+
+ for i in reversed(range(children.GetNumberOfIds())):
+ child = children.GetId(i)
+ name = sceneTree.GetItemName(child)
+ print("name", name)
+ if "Thin Section" in name:
+ return "Thin Section"
+ elif "Micro CT" in name:
+ return "Volumes"
+ elif "Multicore" in name:
+ return "Core"
+ elif "Well Logs" in name:
+ return "Well Logs"
+ elif "Multiscale" in name:
+ return "Multiscale"
+
+ return None
+
+
+def getJsonData() -> Dict:
+ appName = slicer.app.applicationName
+ appHome = Path(slicer.app.slicerHome)
+ appSlicerVersion = f"{slicer.app.majorVersion}.{slicer.app.minorVersion}"
+ scriptedModulesDir = appHome / "lib" / f"{appName}-{appSlicerVersion}" / "qt-scripted-modules"
+ appJson = scriptedModulesDir / "Resources" / "json" / "GeoSlicer.json"
+ with open(appJson, "r") as file:
+ jsonData = json.load(file)
+
+ return jsonData
diff --git a/src/ltrace/ltrace/slicer/app/custom_3dview.py b/src/ltrace/ltrace/slicer/app/custom_3dview.py
new file mode 100644
index 0000000..caad18e
--- /dev/null
+++ b/src/ltrace/ltrace/slicer/app/custom_3dview.py
@@ -0,0 +1,27 @@
+import slicer
+import qt
+
+from ltrace.slicer.helpers import themeIsDark
+
+
+def customize_3d_view():
+ """ "
+ modified by: Gabriel Muller
+ commit 4dee811d6ef94128a37e85d5747a50f0bf5f5acb
+ * PL-1170 3D view settings
+ """
+ viewWidget = slicer.app.layoutManager().threeDWidget(0)
+ viewNode = viewWidget.mrmlViewNode()
+ if themeIsDark():
+ viewNode.SetBackgroundColor(0, 0, 0)
+ viewNode.SetBackgroundColor2(0, 0, 0)
+ # Hiding the purple 3D boundary box
+ viewNode.SetBoxVisible(False)
+
+ viewNode.SetAxisLabelsVisible(False)
+ viewNode.SetOrientationMarkerType(slicer.vtkMRMLViewNode.OrientationMarkerTypeAxes)
+
+ orientationMenu = viewWidget.findChild(qt.QMenu, "orientationMarkerMenu")
+ for action in orientationMenu.actions():
+ if action.text in ["Cube", "Human"]:
+ orientationMenu.removeAction(action)
diff --git a/src/ltrace/ltrace/slicer/color_map_customizer.py b/src/ltrace/ltrace/slicer/app/custom_colormaps.py
similarity index 100%
rename from src/ltrace/ltrace/slicer/color_map_customizer.py
rename to src/ltrace/ltrace/slicer/app/custom_colormaps.py
diff --git a/src/ltrace/ltrace/slicer/app/drawer.py b/src/ltrace/ltrace/slicer/app/drawer.py
new file mode 100644
index 0000000..c22a935
--- /dev/null
+++ b/src/ltrace/ltrace/slicer/app/drawer.py
@@ -0,0 +1,44 @@
+import slicer
+import qt
+
+from ltrace.slicer.helpers import svgToQIcon
+from ltrace.slicer_utils import getResourcePath
+
+
+class ExpandDataDrawer:
+
+ APP_NAME = slicer.app.applicationName
+
+ def __init__(self, drawer: qt.QWidget):
+
+ self.closeIcon = svgToQIcon(getResourcePath("Icons") / "IconSet-dark" / "PanelRightClose.svg")
+ self.openIcon = svgToQIcon(getResourcePath("Icons") / "IconSet-dark" / "PanelRightOpen.svg")
+ self.__actionButton = None
+ self.__drawer = drawer
+
+ self.__drawer.setFeatures(qt.QDockWidget.DockWidgetFloatable | qt.QDockWidget.DockWidgetMovable)
+
+ def widget(self):
+ return self.__drawer
+
+ def setAction(self, action):
+ self.__actionButton = action
+
+ def show(self, index=0):
+ self.__actionButton.setIcon(self.closeIcon)
+ self.__actionButton.setToolTip("Collapse Data")
+ self.__drawer.setCurrentWidget(index)
+ self.__drawer.visible = True
+ slicer.app.userSettings().setValue(f"{ExpandDataDrawer.APP_NAME}/RighDrawerVisible", True)
+
+ def hide(self):
+ self.__actionButton.setIcon(self.openIcon)
+ self.__actionButton.setToolTip("Expand Data")
+ self.__drawer.visible = False
+ slicer.app.userSettings().setValue(f"{ExpandDataDrawer.APP_NAME}/RighDrawerVisible", False)
+
+ def __call__(self, *args, **kargs):
+ if self.__drawer.visible:
+ self.hide()
+ else:
+ self.show()
diff --git a/src/ltrace/ltrace/slicer/app/layouts.py b/src/ltrace/ltrace/slicer/app/layouts.py
new file mode 100644
index 0000000..876abda
--- /dev/null
+++ b/src/ltrace/ltrace/slicer/app/layouts.py
@@ -0,0 +1,19 @@
+import slicer
+import qt
+
+
+def customLayout(layoutID, layoutXML, name, iconPath):
+ layoutManager = slicer.app.layoutManager()
+ layoutManager.layoutLogic().GetLayoutNode().AddLayoutDescription(layoutID, layoutXML)
+
+ # Add button to layout selector toolbar for this custom layout
+ viewToolBar = slicer.modules.AppContextInstance.mainWindow.findChild("QToolBar", "ViewToolBar")
+ layoutMenu = viewToolBar.widgetForAction(viewToolBar.actions()[0]).menu()
+ layoutSwitchActionParent = layoutMenu
+ layoutSwitchAction = layoutSwitchActionParent.addAction(name) # add inside layout list
+ layoutSwitchAction.setData(layoutID)
+ layoutSwitchAction.setIcon(qt.QIcon(str(iconPath)))
+ layoutSwitchAction.connect(
+ "triggered()",
+ lambda layoutId=layoutID: slicer.app.layoutManager().setLayout(layoutId),
+ )
diff --git a/src/ltrace/ltrace/slicer/app/onboard.py b/src/ltrace/ltrace/slicer/app/onboard.py
new file mode 100644
index 0000000..c1dfaf7
--- /dev/null
+++ b/src/ltrace/ltrace/slicer/app/onboard.py
@@ -0,0 +1,278 @@
+import logging
+from dataclasses import dataclass
+from pathlib import Path
+
+import qt
+import slicer
+
+from ltrace.slicer.module_utils import loadModules
+from ltrace.slicer_utils import getResourcePath
+from ltrace.utils.ProgressBarProc import ProgressBarProc
+from ltrace.slicer.app import tryDetectProjectDataType
+
+
+class ui_IntroToolButton(qt.QToolButton):
+ def __init__(self, text: str, moduleName: str, icon: str, parent=None) -> None:
+ super().__init__(parent)
+
+ self.__updateStyleSheet()
+ self.__moduleName = moduleName
+ self.objectName = f"{moduleName} Tool Button"
+ iconWidget = qt.QIcon(icon.as_posix())
+ self.setToolButtonStyle(qt.Qt.ToolButtonTextUnderIcon)
+ self.setIcon(iconWidget)
+ self.setText(text)
+ self.setIconSize(qt.QSize(60, 60))
+ self.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding)
+ #
+ # self.clicked.connect(self._action)
+
+ def __updateStyleSheet(self) -> None:
+ self.setStyleSheet(
+ "QToolButton {\
+ background-color: transparent;\
+ border: none;\
+ padding-top: 8px;\
+ padding-bottom: 8px;\
+ }\
+ QToolButton:hover {\
+ background-color: gray;\
+ border-radius: 3px;\
+ }\
+ QToolButton:pressed {\
+ background-color: #6B6B6B;\
+ }"
+ )
+
+ def _action(self):
+ try:
+ if self.__moduleName is not None:
+ slicer.util.selectModule(self.__moduleName)
+
+ except Exception as error:
+ logging.debug(f"Error in {self.__moduleName} shortcut: {error}.")
+
+
+# TODO move to widgets
+def ui_LineSeparator():
+ line = qt.QFrame()
+ line.setFrameShape(qt.QFrame.HLine)
+ line.setFrameShadow(qt.QFrame.Sunken)
+ return line
+
+
+def ui_LineSeparatorWithText(text):
+ linetagged = qt.QFrame()
+ layout = qt.QHBoxLayout(linetagged)
+
+ lineBefore = ui_LineSeparator()
+ lineBefore.setSizePolicy(qt.QSizePolicy.MinimumExpanding, qt.QSizePolicy.Fixed)
+ lineAfter = ui_LineSeparator()
+ lineAfter.setSizePolicy(qt.QSizePolicy.MinimumExpanding, qt.QSizePolicy.Fixed)
+
+ label = qt.QLabel(text)
+ label.setStyleSheet("color: gray; margin: 8px;")
+ label.setSizePolicy(qt.QSizePolicy.Minimum, qt.QSizePolicy.Preferred)
+
+ layout.addWidget(lineBefore)
+ layout.addWidget(label)
+ layout.addWidget(lineAfter)
+
+ return linetagged
+
+
+class ui_DataLoaderSelectorDialog(qt.QDialog):
+ signalEnvironmentClicked = qt.Signal(object)
+
+ def __init__(self, modules, parent=None) -> None:
+ super().__init__(parent)
+
+ self.projectOptionVisible = True
+
+ self.setWindowIcon(qt.QIcon((getResourcePath("Icons") / "GeoSlicer.ico").as_posix()))
+
+ layout = qt.QVBoxLayout(self)
+ layout.setContentsMargins(16, 16, 16, 16)
+
+ self.setMinimumWidth(400)
+ self.setWindowTitle("Open Image")
+
+ self.projectOptionFrame = qt.QFrame()
+ layoutProjectOption = qt.QVBoxLayout(self.projectOptionFrame)
+
+ openProjectButton = qt.QPushButton("Open .mrml Project")
+ openProjectButton.clicked.connect(self.openProject)
+ openProjectButton.setSizePolicy(qt.QSizePolicy.MinimumExpanding, qt.QSizePolicy.Fixed)
+ openProjectButton.setProperty("class", "actionButtonBackground")
+
+ OrLine = ui_LineSeparatorWithText("Or")
+
+ layoutProjectOption.addWidget(openProjectButton)
+ layoutProjectOption.addWidget(OrLine)
+ layoutProjectOption.setContentsMargins(0, 0, 0, 0)
+
+ introMsgLabel = qt.QLabel("Choose a data type to start")
+ lineFrame = ui_LineSeparator()
+ buttonsFrame = qt.QFrame()
+ buttonsGridLayout = qt.QGridLayout(buttonsFrame)
+
+ self.helpBoxTextEdit = qt.QTextEdit()
+ self.helpBoxTextEdit.setReadOnly(True)
+ self.helpBoxTextEdit.setFixedHeight(64)
+
+ for i, module in enumerate(modules):
+ button = ui_IntroToolButton(text=module.displayName, moduleName=module.moduleName, icon=module.icon)
+ button.installEventFilter(self)
+ button.setToolTip(f"Load {module.displayName} data")
+ buttonsGridLayout.addWidget(button, i // 3, i % 3)
+ button.clicked.connect(lambda _, m=module: self.handleSignalEmit(m))
+
+ layout.addWidget(self.projectOptionFrame)
+ layout.addWidget(introMsgLabel)
+ layout.addWidget(lineFrame)
+ layout.addWidget(buttonsFrame)
+ layout.addWidget(self.helpBoxTextEdit)
+
+ self.projectOptionFrame.visible = self.projectOptionVisible
+
+ def handleSignalEmit(self, module):
+ self.signalEnvironmentClicked.emit(module)
+
+ def eventFilter(self, obj, event):
+ if event.type() == qt.QEvent.Enter:
+ self.helpBoxTextEdit.setText(obj.toolTip)
+ elif event.type() == qt.QEvent.Leave:
+ self.helpBoxTextEdit.clear()
+ return False
+
+ def openProject(self):
+ selected = slicer.modules.AppContextInstance.projectEventsLogic.loadScene()
+ if not selected:
+ return
+
+ category = tryDetectProjectDataType()
+ if category:
+ self.close()
+ loaderInfo: LoaderInfo = LOADERS[category]
+ self.handleSignalEmit(loaderInfo)
+ else:
+ self.showOnlyDataTypes()
+
+ def showOnlyDataTypes(self):
+ self.projectOptionFrame.visible = False
+
+
+@dataclass
+class LoaderInfo:
+ displayName: str
+ moduleName: str
+ icon: Path
+ category: str = None
+ environment: str = None
+
+
+LOADERS = {
+ li.displayName: li
+ for li in [
+ LoaderInfo(
+ displayName="Volumes",
+ moduleName="MicroCTLoader",
+ icon=getResourcePath("Icons") / "MicroCT3D.png",
+ category="MicroCT",
+ environment="MicroCTEnv",
+ ),
+ LoaderInfo(
+ displayName="Thin Section",
+ moduleName="ThinSectionLoader",
+ icon=getResourcePath("Icons") / "ThinSection.png",
+ category="Thin Section",
+ environment="ThinSectionEnv",
+ ),
+ LoaderInfo(
+ displayName="Well Logs",
+ moduleName="ImageLogData",
+ icon=getResourcePath("Icons") / "ImageLog.png",
+ category="ImageLog",
+ environment="ImageLogEnv",
+ ),
+ LoaderInfo(
+ displayName="Core",
+ moduleName="Multicore",
+ icon=getResourcePath("Icons") / "CoreEnv.png",
+ category="Core",
+ environment="CoreEnv",
+ ),
+ LoaderInfo(
+ displayName="Multiscale",
+ moduleName="CustomizedData",
+ icon=getResourcePath("Icons") / "MultiscaleIcon.png",
+ category="Multiscale",
+ environment="MultiscaleEnv",
+ ),
+ LoaderInfo(
+ displayName="NetCDF",
+ moduleName="NetCDFLoader",
+ icon=getResourcePath("Icons") / "NetCDF.png",
+ category="MicroCT",
+ environment="MicroCTEnv",
+ ),
+ ]
+}
+
+
+def loadEnvironment(toolbar, environmentInfo):
+ groups = slicer.modules.AppContextInstance.modules.groups
+ with ProgressBarProc() as pb:
+ related = groups[environmentInfo.category]
+ loadModules(related, permanent=False, favorite=False)
+
+ try:
+ toolbar.clear() # clear before add
+ module = getattr(slicer.modules, f"{environmentInfo.environment}Instance")
+ module.environment.modulesToolbar = toolbar # APP_TOOLBARS["ModuleToolBar"]
+ module.environment.setCategory(environmentInfo.category)
+ module.environment.setupEnvironment()
+ module.environment.enter()
+
+ # TODO this is ugly, move to class/module
+ sval = slicer.app.userSettings().value(f"{slicer.app.applicationName}/LeftDrawerVisible", True)
+ if slicer.util.toBool(sval):
+ widget = toolbar.widgetForAction(toolbar.actions()[0])
+ if widget.toolButtonStyle != qt.Qt.ToolButtonTextBesideIcon:
+ mainToolBar = slicer.util.mainWindow().findChild(qt.QToolBar, "MainToolBar")
+ mainToolBar.actions()[0].trigger()
+
+ except AttributeError as e:
+ import traceback
+
+ traceback.print_exc()
+ logging.error(f"Error setting up environment {environmentInfo.environment}: {e}")
+
+ slicer.util.selectModule(environmentInfo.moduleName)
+
+ moduleSelectorToolbar = slicer.util.mainWindow().moduleSelector()
+ envSelectorButton = moduleSelectorToolbar.findChild(qt.QToolButton, "environment Selector Menu")
+ envSelectorButton.setText(environmentInfo.displayName)
+ envSelectorButton.setIcon(qt.QIcon(environmentInfo.icon))
+ envSelectorButton.setToolButtonStyle(qt.Qt.ToolButtonTextBesideIcon)
+
+
+def loadEnvironmentByName(toolbar, displayName):
+ module = LOADERS[displayName]
+ loadEnvironment(toolbar, module)
+
+
+def showDataLoaders(toolbar):
+ def threadWrapper(*args):
+ loadEnvironment(*args)
+ welcomeDialog.signalEnvironmentClicked.disconnect()
+ welcomeDialog.close()
+
+ welcomeDialog = ui_DataLoaderSelectorDialog(
+ [LOADERS[category] for category in LOADERS],
+ parent=slicer.util.mainWindow(),
+ )
+
+ welcomeDialog.signalEnvironmentClicked.connect(lambda envInfo: threadWrapper(toolbar, envInfo))
+
+ welcomeDialog.exec_()
diff --git a/src/ltrace/ltrace/slicer/application_observables.py b/src/ltrace/ltrace/slicer/application_observables.py
index c85d573..f2757e4 100644
--- a/src/ltrace/ltrace/slicer/application_observables.py
+++ b/src/ltrace/ltrace/slicer/application_observables.py
@@ -8,3 +8,4 @@ class ApplicationObservables(qt.QObject):
applicationLoadFinished = qt.Signal()
aboutToQuit = qt.Signal()
moduleWidgetEnter = qt.Signal(object)
+ environmentChanged = qt.Signal()
diff --git a/src/ltrace/ltrace/slicer/biaep/thumbnail_downloader.py b/src/ltrace/ltrace/slicer/biaep/thumbnail_downloader.py
index 2fb2b27..ede6d0d 100644
--- a/src/ltrace/ltrace/slicer/biaep/thumbnail_downloader.py
+++ b/src/ltrace/ltrace/slicer/biaep/thumbnail_downloader.py
@@ -151,7 +151,7 @@ def _download_thumbnails(
end = idx + self._batch_size
batch_df = input_df[start:end].reset_index(drop=True)
batch_df_with_cache = self.get_thumbnails_from_cache(df=batch_df)
- batch_df_with_cache.dropna(inplace=True, subset=[THUMBNAIL_FILE_PATH_LABEL])
+ batch_df_with_cache = batch_df_with_cache.dropna(subset=[THUMBNAIL_FILE_PATH_LABEL])
batch_df_without_cache = (
pd.merge(batch_df, batch_df_with_cache, on=list(batch_df.columns), how="outer", indicator=True)
.query("_merge != 'both'")
@@ -270,7 +270,6 @@ def get_thumbnails_from_cache(self, df: pd.DataFrame) -> pd.DataFrame:
data_df = pd.DataFrame.from_dict(data)
# join data to related columns from the input DataFrame
output_df = pd.merge(df, data_df, on="name", how="left")
- # output_df.dropna(inplace=True, subset=[THUMBNAIL_FILE_PATH_LABEL])
return output_df
def _get_thumbnail_file_name_pattern(self, name: str, extension: str = None) -> str:
diff --git a/src/ltrace/ltrace/slicer/cli_queue.py b/src/ltrace/ltrace/slicer/cli_queue.py
index 5c4ec33..7ffcf96 100644
--- a/src/ltrace/ltrace/slicer/cli_queue.py
+++ b/src/ltrace/ltrace/slicer/cli_queue.py
@@ -8,21 +8,39 @@
from ltrace.slicer.widget.global_progress_bar import LocalProgressBar
-CliNodeInformation = namedtuple("CliNodeInformation", ["node", "module", "parameters", "modified_callback"])
+CliNodeInformation = namedtuple(
+ "CliNodeInformation", ["node", "module", "parameters", "modified_callback", "progress_text"]
+)
class CliQueue(qt.QObject):
+ signal_queue_successful = qt.Signal()
+ signal_queue_failed = qt.Signal()
+ signal_queue_cancelled = qt.Signal()
signal_queue_finished = qt.Signal()
- def __init__(self, cli_node=None, quick_start=False, progress_bar: LocalProgressBar = None):
+ def __init__(
+ self,
+ cli_node=None,
+ quick_start=False,
+ update_display=True,
+ progress_bar: LocalProgressBar = None,
+ progress_label: qt.QLabel = None,
+ ):
super(qt.QObject, self).__init__()
self.__nodes = []
self.__queue = []
self.__running = False
- self.__update_display = True
+ self.__update_display = update_display
self.__progress_bar = progress_bar
+ self.__progress_label = progress_label
self.__current_observer_handlers = list()
+ self.__current_node_idx = 0
+ self.__total_nodes = 0
+ self.__current_node_info = None
+ self.__error_message = None
+
if cli_node is not None:
if isinstance(cli_node, list):
for node in cli_node:
@@ -30,6 +48,9 @@ def __init__(self, cli_node=None, quick_start=False, progress_bar: LocalProgress
else:
self.add_cli_node(cli_node)
+ if self.__progress_label is not None:
+ self.__progress_label.setVisible(False)
+
if quick_start:
self.run()
@@ -48,7 +69,7 @@ def run(self):
logging.debug("Starting running CLI nodes process. Total in queue: {}".format(len(self.__queue)))
self.__run_next_node_in_queue()
- def stop(self):
+ def stop(self, cancelled=False):
if not self.__running:
return
@@ -62,8 +83,20 @@ def stop(self):
self.__queue.clear()
self.__nodes.clear()
self.__running = False
+ if self.__progress_label is not None:
+ self.__progress_label.setVisible(False)
+
+ if self.__error_message is None:
+ if not cancelled:
+ self.signal_queue_successful.emit()
+ else:
+ self.signal_queue_cancelled.emit()
+ else:
+ self.signal_queue_failed.emit()
self.signal_queue_finished.emit()
+ self.__current_node_info = None
+
def add_cli_node(self, cli_node: CliNodeInformation):
if not isinstance(cli_node, CliNodeInformation):
raise RuntimeError(
@@ -71,8 +104,11 @@ def add_cli_node(self, cli_node: CliNodeInformation):
type(cli_node)
)
)
+
+ self.__total_nodes += 1
if self.__running:
self.__queue.append(cli_node)
+ self.update_progress_label(self.__queue[0])
logging.debug(
"Creating cli node {} and adding it to queue. Total in queue: {}".format(
cli_node.node.GetID(), len(self.__queue)
@@ -82,53 +118,73 @@ def add_cli_node(self, cli_node: CliNodeInformation):
self.__nodes.append(cli_node)
logging.debug("Creating cli node {}".format(cli_node.node.GetID()))
- def create_cli_node(self, module, parameters=None, modified_callback=None):
+ def create_cli_node(self, module, parameters=None, modified_callback=None, progress_text="Running"):
node = slicer.cli.createNode(module, parameters)
self.add_cli_node(
- CliNodeInformation(node=node, module=module, parameters=parameters, modified_callback=modified_callback)
+ CliNodeInformation(
+ node=node,
+ module=module,
+ parameters=parameters,
+ modified_callback=modified_callback,
+ progress_text=progress_text,
+ )
)
def __clear_observers(self):
for obj, handler in self.__current_observer_handlers:
logging.debug("Removing observer {} from cli node {}".format(handler, obj.GetID()))
- qt.QTimer.singleShot(10, lambda x=handler: obj.RemoveObserver(x))
+ obj.RemoveObserver(handler)
self.__current_observer_handlers.clear()
def __on_modified_event(self, caller, event):
- if caller is None and not self.__running and caller.IsBusy():
+ if caller is None or self.__current_node_info is None:
+ return
+
+ if self.__current_node_info.modified_callback:
+ self.__current_node_info.modified_callback(caller, event, self.__current_node_info.parameters)
+
+ if not self.__running or caller.IsBusy():
return
- if (
- caller.GetStatus() == slicer.vtkMRMLCommandLineModuleNode.Completed
- or caller.GetStatus() == slicer.vtkMRMLCommandLineModuleNode.CompletedWithErrors
- ):
- for queued_node_info in self.__queue[:]:
- if caller is queued_node_info.node:
- self.__queue.remove(queued_node_info)
- logging.debug(
- "cli node {} run finished! Removing it from queue!".format(queued_node_info.node.GetID())
- )
+ if caller.GetStatus() == slicer.vtkMRMLCommandLineModuleNode.Completed:
+ logging.debug(
+ "cli node {} run finished! Removing it from queue!".format(self.__current_node_info.node.GetID())
+ )
+ self.__queue.remove(self.__current_node_info)
if len(self.__queue) > 0:
self.__run_next_node_in_queue()
else:
self.stop()
+ elif caller.GetStatus() == slicer.vtkMRMLCommandLineModuleNode.CompletedWithErrors:
+ logging.error("error running cli node {}!".format(self.__current_node_info.node.GetID()))
+ self.__error_message = (
+ f"{self.__current_node_info.progress_text}:\n{caller.GetErrorText().strip().splitlines()[-1]}"
+ )
+ self.stop()
+
+ elif caller.GetStatus() == slicer.vtkMRMLCommandLineModuleNode.Cancelled:
+ self.stop(cancelled=True)
+
def __run_next_node_in_queue(self):
if len(self.__queue) <= 0 or not self.__running:
self.stop()
return
- node_info = self.__queue[0]
- node = node_info.node
- module = node_info.module
+ self.__clear_observers()
+ self.__current_node_info = self.__queue[0]
+ node = self.__current_node_info.node
+ module = self.__current_node_info.module
if node.IsBusy():
logging.debug("Node {} is busy! Skipping it!".format(node.GetID()))
- self.__queue.remove(node_info)
+ self.__queue.remove(self.__current_node_info)
+ self.__current_node_info = None
self.__run_next_node_in_queue()
return
+ self.__current_node_idx += 1
logging.debug("Running next node: {}! Total node in queue: {}".format(node.GetID(), len(self.__queue)))
logic = module.logic()
logic.SetDeleteTemporaryFiles(True)
@@ -137,15 +193,29 @@ def __run_next_node_in_queue(self):
modified_handler = node.AddObserver("ModifiedEvent", self.__on_modified_event)
self.__current_observer_handlers.append((node, modified_handler))
logging.debug("Creating default observer {} for node {}".format(modified_handler, node.GetID()))
- if node_info.modified_callback is not None:
- modified_callback_handler = node.AddObserver(
- "ModifiedEvent",
- lambda caller, event, config=node_info.parameters: node_info.modified_callback(caller, event, config),
- )
- logging.debug("Creating custom observer {} for node {}".format(modified_callback_handler, node.GetID()))
- self.__current_observer_handlers.append((node, modified_callback_handler))
if self.__progress_bar is not None:
self.__progress_bar.setCommandLineModuleNode(node)
+ if self.__progress_label is not None and self.__total_nodes > 1:
+ self.update_progress_label(self.__current_node_info)
def is_running(self):
return self.__running
+
+ def get_error_message(self):
+ return self.__error_message
+
+ def get_current_node(self):
+ return self.__current_node_info.node
+
+ def update_progress_label(self, node_info):
+ if self.__progress_label is None:
+ return
+ self.__progress_label.setText(f"Step {self.__current_node_idx}/{self.__total_nodes}: {node_info.progress_text}")
+ self.__progress_label.setVisible(True)
+
+ # Compatibility with vtkMRMLCommandLineModuleNode:
+ def IsBusy(self):
+ return self.is_running()
+
+ def Cancel(self):
+ return self.stop(cancelled=True)
diff --git a/src/ltrace/ltrace/slicer/cli_utils.py b/src/ltrace/ltrace/slicer/cli_utils.py
index 2729221..af522e0 100644
--- a/src/ltrace/ltrace/slicer/cli_utils.py
+++ b/src/ltrace/ltrace/slicer/cli_utils.py
@@ -38,6 +38,8 @@ def _readVectorFrom(volumeFile):
vtkArray = numpy_support.numpy_to_vtk(imageArray.ravel(), deep=True)
vtkArray.SetNumberOfComponents(3)
+ if imageArray.ndim == 3:
+ imageArray = imageArray.reshape(1, *imageArray.shape)
vtkImage.SetDimensions(imageArray.shape[-2::-1])
vtkImage.GetPointData().SetScalars(vtkArray)
diff --git a/src/ltrace/ltrace/slicer/custom_export_to_file.py b/src/ltrace/ltrace/slicer/custom_export_to_file.py
index 6707949..37f4118 100644
--- a/src/ltrace/ltrace/slicer/custom_export_to_file.py
+++ b/src/ltrace/ltrace/slicer/custom_export_to_file.py
@@ -8,14 +8,7 @@
Env = NodeEnvironment
-def _select_tab(tab_widget, label):
- for i in range(tab_widget.count):
- if tab_widget.tabText(i) == label:
- tab_widget.setCurrentIndex(i)
- return tab_widget.widget(i)
-
-
-def _detect_node_env(node, current_env):
+def _detectNodeEnv(node, current_env):
if node is None:
return None
if node.IsA("vtkMRMLTableNode") or node.IsA("vtkMRMLSegmentationNode"):
@@ -33,45 +26,31 @@ def _detect_node_env(node, current_env):
return None
-def _export_node_as(selected_item_id, env):
+def _exportNodeAs(selectedItemId, env):
if env is None:
slicer.util.warningDisplay(
"Can't export selection. Make sure you have selected a single image, or try using the 'Data > Export' tab of your environment."
)
return
- select_module = slicer.util.mainWindow().moduleSelector().selectModule
+ selectModule = slicer.modules.AppContextInstance.mainWindow.moduleSelector().selectModule
if env == Env.THIN_SECTION:
- select_module("ThinSectionEnv")
- widget = slicer.modules.ThinSectionEnvWidget
-
- data_widget = _select_tab(widget.mainTab, "Data")
- export_widget = _select_tab(data_widget, "Export").self()
-
- export_widget.subjectHierarchyTreeView.setCurrentItem(selected_item_id)
+ selectModule("ThinSectionExport")
+ widget = slicer.modules.ThinSectionExportWidget
+ widget.subjectHierarchyTreeView.setCurrentItem(selectedItemId)
return
if env == Env.IMAGE_LOG:
- select_module("ImageLogEnv")
- widget = slicer.modules.ImageLogEnvWidget
-
- data_widget = _select_tab(widget.mainTab, "Data")
- export_widget = _select_tab(data_widget, "Export").self()
- export_widget.subjectHierarchyTreeView.setCurrentItem(selected_item_id)
+ selectModule("ImageLogExport")
+ widget = slicer.modules.ImageLogExportWidget
+ widget.subjectHierarchyTreeView.setCurrentItem(selectedItemId)
return
if env == Env.CORE:
- select_module("CoreEnv")
- widget = slicer.modules.CoreEnvWidget
+ selectModule("MulticoreExport")
+ widget = slicer.modules.MulticoreExportWidget
- data_widget = _select_tab(widget.mainTab, "Data")
- export_widget = _select_tab(data_widget, "Export").self()
-
- export_widget.subjectHierarchyTreeView.setCurrentItem(selected_item_id)
+ widget.subjectHierarchyTreeView.setCurrentItem(selectedItemId)
return
if env == Env.MICRO_CT:
- select_module("MicroCTEnv")
- widget = slicer.modules.MicroCTEnvWidget
-
- data_widget = _select_tab(widget.mainTab, "Data")
- export_widget = _select_tab(data_widget, "Export").self()
+ selectModule("MicroCTExport")
return
@@ -85,29 +64,30 @@ def _export_folder_as_netcdf(folder_id):
def _save_folder_as_netcdf(folder_id):
widget = SaveNetcdfWidget()
- widget.setFolder(folder_id)
widget.show()
+ widget.setFolder(folder_id)
-def _export_selected_node():
+def _exportSelectedNode():
sh = slicer.mrmlScene.GetSubjectHierarchyNode()
- plugin_handler = slicer.qSlicerSubjectHierarchyPluginHandler().instance()
- selected_item_id = plugin_handler.currentItem()
+ pluginHandler = slicer.qSlicerSubjectHierarchyPluginHandler().instance()
+ selectedItemId = pluginHandler.currentItem()
- if sh.GetItemOwnerPluginName(selected_item_id) == "Folder":
- if sh.GetItemAttribute(selected_item_id, "netcdf_path"):
- _save_folder_as_netcdf(selected_item_id)
+ if sh.GetItemOwnerPluginName(selectedItemId) == "Folder":
+ if sh.GetItemAttribute(selectedItemId, "netcdf_path"):
+ _save_folder_as_netcdf(selectedItemId)
else:
- _export_folder_as_netcdf(selected_item_id)
+ _export_folder_as_netcdf(selectedItemId)
return
- node = sh.GetItemDataNode(selected_item_id)
- detected_env = _detect_node_env(node, getCurrentEnvironment())
- _export_node_as(selected_item_id, detected_env)
+
+ node = sh.GetItemDataNode(selectedItemId)
+ detectedEnv = _detectNodeEnv(node, getCurrentEnvironment())
+ _exportNodeAs(selectedItemId, detectedEnv)
-def customize_export_to_file():
- plugin_handler = slicer.qSlicerSubjectHierarchyPluginHandler().instance()
- export_plugin = plugin_handler.pluginByName("Export")
- export_action = export_plugin.findChild(qt.QAction)
- export_action.triggered.disconnect()
- export_action.triggered.connect(_export_selected_node)
+def customizeExportToFile():
+ pluginHandler = slicer.qSlicerSubjectHierarchyPluginHandler().instance()
+ exportPlugin = pluginHandler.pluginByName("Export")
+ exportAction = exportPlugin.findChild(qt.QAction)
+ exportAction.triggered.disconnect()
+ exportAction.triggered.connect(_exportSelectedNode)
diff --git a/src/ltrace/ltrace/slicer/custom_main_window_event_filter.py b/src/ltrace/ltrace/slicer/custom_main_window_event_filter.py
index 5b5f166..d72e95f 100644
--- a/src/ltrace/ltrace/slicer/custom_main_window_event_filter.py
+++ b/src/ltrace/ltrace/slicer/custom_main_window_event_filter.py
@@ -18,10 +18,12 @@ def eventFilter(self, object, event):
"""Qt eventFilter method overload.
Please read reference for more information: https://doc.qt.io/archives/qt-4.8/eventsandfilters.html
"""
- appObservables = ApplicationObservables()
- mainWindow = slicer.util.mainWindow()
+
if event.type() == qt.QEvent.Close:
+ appObservables = ApplicationObservables()
+ mainWindow = slicer.modules.AppContextInstance.mainWindow
isModified = mainWindow.isWindowModified()
+
if isModified is False:
event.accept()
appObservables.aboutToQuit.emit()
@@ -49,9 +51,6 @@ def eventFilter(self, object, event):
event.ignore()
return True
else: # SaveStatus.FAILED options and SaveStatus.IN_PROGRESS
- slicer.util.errorDisplay(
- "Failed to save the project. Please, check the application's logs and try again."
- )
event.ignore()
return True
elif messageBox.clickedButton() == exitButton:
diff --git a/src/ltrace/ltrace/slicer/debounce_caller.py b/src/ltrace/ltrace/slicer/debounce_caller.py
new file mode 100644
index 0000000..90ca3b0
--- /dev/null
+++ b/src/ltrace/ltrace/slicer/debounce_caller.py
@@ -0,0 +1,74 @@
+import logging
+import qt
+
+from typing import Union
+
+
+class DebounceCaller:
+ """
+ Wrapper for qt.Signal emits and method calls with debouncing effect,
+ emiting the signal/calling the method only one time after a given interval.
+ """
+
+ def __init__(
+ self,
+ parent: Union[qt.QWidget, qt.QObject],
+ signal: qt.Signal = None,
+ callback=None,
+ intervalMs: int = 500,
+ qtTimer=qt.QTimer,
+ ) -> None:
+ assert signal is not None or callback is not None, "Use either signal or callback."
+ assert parent is not None, "Invalid parent reference."
+
+ self.__signal = signal
+ self.__callback = callback
+ self.__useSignal = signal is not None
+ self.timer = qtTimer(parent)
+ self.timer.setSingleShot(True)
+ self.timer.setInterval(intervalMs)
+ self.timer.timeout.connect(self.__onTimeout)
+ self.timer.stop()
+ self.__args = None
+ self.__kwargs = None
+
+ def emit(self, *args, **kwargs) -> None:
+ self.__args = args
+ self.__kwargs = kwargs
+
+ try:
+ if self.timer.isActive():
+ self.timer.stop()
+
+ self.timer.start()
+ except ValueError: # timer or parent has been deleted
+ pass
+
+ def __call__(self, *args, **kwargs):
+ self.emit(*args, **kwargs)
+
+ def __onTimeout(self) -> None:
+ try:
+ if self.__useSignal:
+ self.__signal.emit(*self.__args, **self.__kwargs)
+ else:
+ if self.__args is None and self.__kwargs is None:
+ self.__callback()
+ elif self.__kwargs is None:
+ self.__callback(*self.__args)
+ elif self.__args is None:
+ self.__callback(**self.__kwargs)
+ else:
+ self.__callback(*self.__args, **self.__kwargs)
+ except Exception as error:
+ logging.info(f"Failed to execute the call: {error}")
+
+ self.__args = None
+ self.__kwargs = None
+
+ def stop(self):
+ try:
+ if self.timer.isActive():
+ self.timer.stop()
+ except ValueError: # timer or parent has been deleted
+ pass
diff --git a/src/ltrace/ltrace/slicer/directorylistwidget.py b/src/ltrace/ltrace/slicer/directorylistwidget.py
index dfb7a70..8e5f6c9 100644
--- a/src/ltrace/ltrace/slicer/directorylistwidget.py
+++ b/src/ltrace/ltrace/slicer/directorylistwidget.py
@@ -63,3 +63,5 @@ def addDirectories(self):
for path in natsorted(paths):
if len(path):
self.ui.pathList.addDirectory(path)
+
+ file_dialog.delete()
diff --git a/src/ltrace/ltrace/slicer/export.py b/src/ltrace/ltrace/slicer/export.py
new file mode 100644
index 0000000..cd1d337
--- /dev/null
+++ b/src/ltrace/ltrace/slicer/export.py
@@ -0,0 +1,312 @@
+import csv
+import os
+from pathlib import Path
+import re
+
+import cv2
+import numpy as np
+import slicer.util
+import vtk
+
+from ltrace.slicer.helpers import (
+ extent2size,
+ getSourceVolume,
+ export_las_from_histogram_in_depth_data,
+ createTemporaryNode,
+ removeTemporaryNodes,
+ safe_convert_array,
+)
+from ltrace.slicer.node_attributes import TableDataOrientation
+from ltrace.units import global_unit_registry as ureg, SLICER_LENGTH_UNIT
+
+SCALAR_VOLUME_FORMAT_RAW = 0
+SCALAR_VOLUME_FORMAT_TIF = 1
+
+IMAGE_FORMAT_TIF = 0
+IMAGE_FORMAT_PNG = 1
+
+LABEL_MAP_FORMAT_RAW = 0
+LABEL_MAP_FORMAT_TIF = 1
+LABEL_MAP_FORMAT_PNG = 2
+
+SEGMENTATION_FORMAT_RAW = 0
+SEGMENTATION_FORMAT_TIF = 1
+SEGMENTATION_FORMAT_PNG = 2
+
+TABLE_FORMAT_CSV = 0
+TABLE_FORMAT_LAS = 1
+
+
+def _rawPath(node, name=None, imageType=None):
+ """Creates path for node according to standard nomenclature.
+ See https://ltrace.atlassian.net/browse/PL-532
+ """
+ inferredName = node.GetName()
+ if isinstance(node, slicer.vtkMRMLSegmentationNode):
+ # Use the master volume to find out the extent
+ master = getSourceVolume(node)
+ if master:
+ inferredName = master.GetName()
+ imageData = master.GetImageData()
+ spacing = master.GetMinSpacing()
+ else:
+ # Segmentation has no master volume, so we merge the segments
+ imageData = slicer.vtkOrientedImageData()
+ node.GenerateMergedLabelmapForAllSegments(imageData, slicer.vtkSegmentation.EXTENT_UNION_OF_SEGMENTS)
+ spacing = min(imageData.GetSpacing())
+ inferredImageType = "LABELS"
+ elif isinstance(node, slicer.vtkMRMLLabelMapVolumeNode):
+ imageData = node.GetImageData()
+ inferredImageType = "LABELS"
+ spacing = node.GetMinSpacing()
+ elif isinstance(node, slicer.vtkMRMLScalarVolumeNode):
+ imageData = node.GetImageData()
+ size = imageData.GetScalarSize()
+ if size == 1:
+ inferredImageType = "LABELS"
+ elif size == 2:
+ inferredImageType = "CT"
+ elif size >= 4:
+ inferredImageType = "FLOAT"
+ spacing = node.GetMinSpacing()
+
+ name = name or inferredName
+ imageType = imageType or inferredImageType
+ parts = [name, imageType]
+
+ dimensions = extent2size(imageData.GetExtent())
+ parts += [str(dim).rjust(4, "0") for dim in dimensions]
+
+ mmToNm = 10**6
+ spacingNm = int(spacing * mmToNm)
+ parts.append(str(spacingNm).rjust(5, "0") + "nm.raw")
+
+ return Path("_".join(parts))
+
+
+def _getItemsSubitemsIds(items):
+ subjectHierarchyNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
+ nodesIds = []
+ numberOfIds = items.GetNumberOfIds()
+ if numberOfIds == 0:
+ return []
+ for i in range(numberOfIds):
+ itemId = items.GetId(i)
+ if itemId == 3: # when not selecting any item, it supposes entire scene, which we don't want
+ return []
+ nodesIds.append(itemId)
+ itemChildren = vtk.vtkIdList()
+ subjectHierarchyNode.GetItemChildren(itemId, itemChildren, True) # recursive
+ for j in range(itemChildren.GetNumberOfIds()):
+ childrenItemId = itemChildren.GetId(j)
+ nodesIds.append(childrenItemId)
+ return list(set(nodesIds)) # removing duplicate items
+
+
+def _createImageArrayForLabelMapAndSegmentation(labelMapNode):
+ array = slicer.util.arrayFromVolume(labelMapNode).copy()
+
+ if 1 not in array.shape: # if the label map is not 2D
+ print("Export 3D images to TIFF or PNG format is not supported yet.")
+ return None
+
+ arrayShape = np.array(array.shape)
+ imageDimensions = arrayShape[arrayShape > 1]
+ array = array.reshape(imageDimensions).astype(np.uint8)
+
+ # Converting to RGB
+ imageArray = cv2.cvtColor(array, cv2.COLOR_GRAY2RGB)
+
+ colorNode = labelMapNode.GetDisplayNode().GetColorNode()
+ colorCSV = []
+ for i in range(1, colorNode.GetNumberOfColors()):
+ color = np.zeros(4)
+ colorNode.GetColor(i, color)
+ rgbColor = (color * 255).round().astype(int)[:-1]
+ colorLocations = np.where(
+ np.logical_and(imageArray[:, :, 0] == i, imageArray[:, :, 1] == i, imageArray[:, :, 2] == i)
+ )
+ imageArray[colorLocations] = rgbColor[::-1]
+ if len(colorLocations[0]) > 0:
+ colorCSV.append(colorNode.GetColorName(i) + "," + ",".join(str(e) for e in rgbColor))
+
+ return imageArray, colorCSV
+
+
+def exportNodeAsImage(nodeName, dataArray, imageFormat, rootPath, nodePath, colorTable=None):
+ path = rootPath / nodePath
+ path.mkdir(parents=True, exist_ok=True)
+ cv2.imwrite(str(path / Path(nodeName + imageFormat)), dataArray)
+ if colorTable is not None:
+ with open(str(path / Path(nodeName + ".csv")), mode="w", newline="") as csvFile:
+ writer = csv.writer(csvFile, delimiter="\n")
+ writer.writerow(colorTable)
+
+
+def _exportTableAsCsv(node, rootPath, nodePath):
+ csvRows = []
+
+ # Column names
+ csvRow = []
+ for i in range(node.GetNumberOfColumns()):
+ csvRow.append(node.GetColumnName(i))
+ csvRows.append(",".join(str(s) for s in csvRow))
+
+ # Values
+ for i in range(node.GetNumberOfRows()):
+ csvRow = []
+ for j in range(node.GetNumberOfColumns()):
+ value = node.GetCellText(i, j)
+ if j == 0 and "DEPTH" in node.GetColumnName(j):
+ value = (float(value) * SLICER_LENGTH_UNIT).m_as(ureg.meter)
+ if isinstance(value, float):
+ value = np.format_float_positional(value, trim="0", precision=6)
+ csvRow.append(value)
+ csvRows.append(",".join(str(s) for s in csvRow))
+
+ path = rootPath / nodePath
+ adequatedNodeName = re.sub(r"[\\/*.<>ç?:]", "_", node.GetName()) # avoiding characters not suitable for file name
+ path.mkdir(parents=True, exist_ok=True)
+ with open(str(path / Path(adequatedNodeName + ".csv")), mode="w", newline="") as csvFile:
+ writer = csv.writer(csvFile, delimiter="\n")
+ writer.writerow(csvRows)
+
+
+def _exportTableAsLas(self, node, rootPath, nodePath):
+ table_data_orientation_attribute = node.GetAttribute(TableDataOrientation.name())
+ if table_data_orientation_attribute is None or table_data_orientation_attribute != str(
+ TableDataOrientation.ROW.value
+ ):
+ raise RuntimeError("The selected table doesn't match the pattern necessary for this export type.")
+
+ path = rootPath / nodePath
+ path.mkdir(parents=True, exist_ok=True)
+ file_path = os.path.join(path, node.GetName() + ".las")
+
+ df = slicer.util.dataframeFromTable(node)
+ status = export_las_from_histogram_in_depth_data(df=df, file_path=file_path)
+ if not status:
+ raise RuntimeError("Unable to export the LAS file. Please check the logs for more information.")
+
+
+def getLabelMapLabelsCSV(labelMapNode, withColor=False):
+ colorNode = labelMapNode.GetDisplayNode().GetColorNode()
+ labelsCSV = []
+ for i in range(1, colorNode.GetNumberOfColors()):
+ label = f"{colorNode.GetColorName(i)},{i}"
+ if withColor:
+ color = [0] * 4
+ colorNode.GetColor(i, color)
+ label += ",#%02x%02x%02x" % tuple(int(ch * 255) for ch in color[:3])
+ labelsCSV.append(label)
+ return labelsCSV
+
+
+def getDataNodes(itemsIds, exportableTypes):
+ subjectHierarchyNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
+ itemsIds = _getItemsSubitemsIds(itemsIds)
+ dataNodes = []
+ for itemId in itemsIds:
+ dataNode = subjectHierarchyNode.GetItemDataNode(itemId)
+ if dataNode is not None and type(dataNode) in exportableTypes:
+ dataNodes.append(dataNode)
+ return dataNodes
+
+
+def exportScalarVolume(node, rootPath, nodePath, format, name=None, imageType=None, imageDtype=None):
+ name = name or node.GetName()
+ array = slicer.util.arrayFromVolume(node)
+ if format == SCALAR_VOLUME_FORMAT_RAW:
+ path = rootPath / nodePath
+ path.mkdir(parents=True, exist_ok=True)
+ if imageDtype:
+ array = safe_convert_array(array, imageDtype)
+ array.tofile(str(path / _rawPath(node, name, imageType)))
+ elif format == SCALAR_VOLUME_FORMAT_TIF:
+ path = rootPath / nodePath / Path(f"{name}_{imageType}.tif")
+
+ dtype = imageDtype or array.dtype
+ if dtype not in [np.uint8, np.uint16, np.int8, np.int16]:
+ # Slicer supports float 32 TIFF, but not integer 32 types, or 64 bit types
+ dtype = np.float32
+
+ array = safe_convert_array(array, dtype)
+ node = createTemporaryNode(slicer.vtkMRMLScalarVolumeNode, "converted")
+ slicer.util.updateVolumeFromArray(node, array)
+
+ success = slicer.util.saveNode(node, str(path))
+ removeTemporaryNodes()
+
+ if not success:
+ slicer.util.errorDisplay(f"Failed to save node {name} to {path}")
+ return
+
+
+def exportImage(node, rootPath, nodePath, format):
+ array = slicer.util.arrayFromVolume(node)
+ imageArray = cv2.cvtColor(array[0, :, :, :], cv2.COLOR_BGR2RGB)
+ if format == IMAGE_FORMAT_TIF:
+ exportNodeAsImage(node.GetName(), imageArray, ".tif", rootPath, nodePath)
+ elif format == IMAGE_FORMAT_PNG:
+ exportNodeAsImage(node.GetName(), imageArray, ".png", rootPath, nodePath)
+
+
+def exportLabelMap(node, rootPath, nodePath, format, name=None, imageType=None, imageDtype=np.uint8):
+ name = name or node.GetName()
+ if format == LABEL_MAP_FORMAT_RAW:
+ array = slicer.util.arrayFromVolume(node)
+ path = rootPath / nodePath
+ path.mkdir(parents=True, exist_ok=True)
+ rawPath = path / _rawPath(node, name, imageType)
+ array.astype(imageDtype).tofile(str(rawPath))
+ colorCSV = getLabelMapLabelsCSV(node)
+ csvPath = rawPath.with_suffix(".csv")
+ with open(str(csvPath), mode="w", newline="") as csvFile:
+ writer = csv.writer(csvFile, delimiter="\n")
+ writer.writerow(colorCSV)
+ else:
+ imageArrayAndColorCSV = _createImageArrayForLabelMapAndSegmentation(node)
+ if imageArrayAndColorCSV is not None:
+ imageArray, colorCSV = imageArrayAndColorCSV
+ imageArray = safe_convert_array(imageArray, imageDtype)
+ if format == LABEL_MAP_FORMAT_TIF:
+ exportNodeAsImage(name, imageArray, ".tif", rootPath, nodePath, colorTable=colorCSV)
+ elif format == LABEL_MAP_FORMAT_PNG:
+ exportNodeAsImage(name, imageArray, ".png", rootPath, nodePath, colorTable=colorCSV)
+
+
+def exportSegmentation(node, rootPath, nodePath, format, name=None, imageType=None):
+ name = name or node.GetName()
+ labelMapVolumeNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLLabelMapVolumeNode")
+ slicer.modules.segmentations.logic().ExportAllSegmentsToLabelmapNode(
+ node, labelMapVolumeNode, slicer.vtkSegmentation.EXTENT_REFERENCE_GEOMETRY
+ )
+ if format == SEGMENTATION_FORMAT_RAW:
+ array = slicer.util.arrayFromVolume(labelMapVolumeNode)
+ path = rootPath / nodePath
+ path.mkdir(parents=True, exist_ok=True)
+ rawPath = path / _rawPath(node, name, imageType)
+ array.astype(np.uint8).tofile(str(rawPath))
+ colorCSV = getLabelMapLabelsCSV(labelMapVolumeNode)
+ csvPath = rawPath.with_suffix(".csv")
+ with open(str(csvPath), mode="w", newline="") as csvFile:
+ writer = csv.writer(csvFile, delimiter="\n")
+ writer.writerow(colorCSV)
+ else:
+ imageArrayAndColorCSV = _createImageArrayForLabelMapAndSegmentation(labelMapVolumeNode)
+ if imageArrayAndColorCSV is not None:
+ imageArray, colorCSV = imageArrayAndColorCSV
+ if format == SEGMENTATION_FORMAT_TIF:
+ exportNodeAsImage(name, imageArray, ".tif", rootPath, nodePath, colorTable=colorCSV)
+ elif format == SEGMENTATION_FORMAT_PNG:
+ exportNodeAsImage(name, imageArray, ".png", rootPath, nodePath, colorTable=colorCSV)
+ slicer.mrmlScene.RemoveNode(labelMapVolumeNode)
+
+
+def exportTable(node, rootPath, nodePath, format):
+ if format == TABLE_FORMAT_CSV:
+ _exportTableAsCsv(node, rootPath, nodePath)
+ elif format == TABLE_FORMAT_LAS:
+ _exportTableAsLas(node, rootPath, nodePath)
+ else:
+ raise RuntimeError(f"{format} export table format not implemented.")
diff --git a/src/ltrace/ltrace/slicer/graph_data.py b/src/ltrace/ltrace/slicer/graph_data.py
index 18358d5..9c55793 100644
--- a/src/ltrace/ltrace/slicer/graph_data.py
+++ b/src/ltrace/ltrace/slicer/graph_data.py
@@ -13,7 +13,7 @@
from ltrace.slicer.helpers import tryGetNode
from pyqtgraph import QtCore
from ltrace.slicer import data_utils as dutils
-from ltrace.slicer_utils import DebounceSignal
+from ltrace.slicer.debounce_caller import DebounceCaller
TEXT_SYMBOLS = {
"● Circle": "o",
@@ -236,7 +236,7 @@ def __init__(
# destroyed signal only works with a lambda
self.destroyed.connect(lambda: self._cleanUp())
- self.signalModifiedDebouncer = DebounceSignal(parent=self, signal=self.signalModified, qtTimer=QtCore.QTimer)
+ self.signalModifiedDebouncer = DebounceCaller(parent=self, signal=self.signalModified, qtTimer=QtCore.QTimer)
def __eq__(self, other):
if not isinstance(other, GraphData):
@@ -318,11 +318,7 @@ def handleLegacyPoreSizeClassVersions(columns, columnData):
return columns[idx], None
except ValueError:
labelColumnName = f"pore_size_class[label]"
- newLabelColumnData = columnData.replace(
- PORE_SIZE_CATEGORIES,
- np.arange(0, len(PORE_SIZE_CATEGORIES), 1),
- inplace=False,
- )
+ newLabelColumnData = columnData.replace(PORE_SIZE_CATEGORIES, np.arange(0, len(PORE_SIZE_CATEGORIES), 1))
return labelColumnName, newLabelColumnData
@@ -376,7 +372,7 @@ def __del__(self):
@property
def node(self):
- return tryGetNode(self.__node)
+ return tryGetNode(self.__nodeId)
def __parseData(self, dataNode: slicer.vtkMRMLNode):
"""Internal parser function. Currently only supporting Table's nodes.
diff --git a/src/ltrace/ltrace/slicer/helpers.py b/src/ltrace/ltrace/slicer/helpers.py
index ca95101..6fd492f 100644
--- a/src/ltrace/ltrace/slicer/helpers.py
+++ b/src/ltrace/ltrace/slicer/helpers.py
@@ -1,10 +1,9 @@
###########################################################
# WARNING: DO NOT IMPORT UI MODULES LIKE QT, CTK GLOBALLY #
###########################################################
-import sys
-import lasio
import cv2
import enum
+import importlib
import lasio
import logging
import numpy as np
@@ -14,7 +13,9 @@
import psutil
import re
import slicer
+import sys
import stat
+import tempfile
import time
import vtk
@@ -24,7 +25,6 @@
NodeTemporarity,
LosslessAttribute,
)
-from ltrace.wrappers import timeit
from pathlib import Path
from skimage.segmentation import relabel_sequential
@@ -268,10 +268,15 @@ def bounds2size(extent):
def getCurrentEnvironment():
- envName = slicer.util.selectedModule()
+ try:
+ envName = slicer.modules.AppContextInstance.modules.currentWorkingDataType[1]
+ except Exception as error:
+ return None
+
for env in NodeEnvironment:
if env.value == envName:
return env
+
return None
@@ -349,7 +354,8 @@ def createTemporaryVolumeNode(
"""
valid_name = slicer.mrmlScene.GenerateUniqueName(name) if uniqueName else name
tempNode = slicer.mrmlScene.AddNewNodeByClass(cls.__name__, valid_name)
- tempNode.CreateDefaultDisplayNodes()
+ if hasattr(tempNode, "CreateDefaultDisplayNodes"):
+ tempNode.CreateDefaultDisplayNodes()
if hidden:
tempNode.SetHideFromEditors(True)
@@ -1445,7 +1451,7 @@ def openModuleHelp(module):
else:
return
- mainWindow = slicer.util.mainWindow()
+ mainWindow = slicer.modules.AppContextInstance.mainWindow
mainWindow.moduleSelector().selectModule(module.parent.name)
modulePanel = mainWindow.findChild(slicer.qSlicerModulePanel, "ModulePanel")
helpCollapsibleButton = modulePanel.findChild(ctk.ctkCollapsibleButton, "HelpCollapsibleButton")
@@ -1534,87 +1540,32 @@ def arrayPartsFromNode(node: slicer.vtkMRMLNode) -> tuple[np.ndarray, np.ndarray
return depthColumn, values
-def redactAnonymize(
- node: slicer.vtkMRMLNode,
- messDepths: bool,
- messData: bool,
- newName="DummyName",
- newWellName="DummyWell",
-):
- """Anonymizes data (and optionally edits numerical values) to meet NDA criteria".
-
- Well name is replaced, node name is replaced, depths can be offset by a random value. And (not working yet) numerical data can be shuffled.
- No noise is added to the data so to keep the standard null entries and to preserve the numerical range.
- """
+def themeIsDark():
+ import qt
- raise NotImplementedError("redactAnonymize not fully implemented.")
+ palette = slicer.app.palette()
+ bg_color = palette.color(qt.QPalette.Background)
+ fg_color = palette.color(qt.QPalette.WindowText)
+ return fg_color.value() > bg_color.value()
- # messData not working yet
- if messData:
- messData = False
- node.SetAttribute("WellName", newWellName)
+def svgToQIcon(iconPath):
+ import qt
- if newWellName:
- node.SetName(f"{newWellName}_{newName}")
- else:
- node.SetName(f"{newName}")
-
- if isinstance(node, slicer.vtkMRMLTableNode):
- df = None # dutils.tableNodeToDataFrame(node)
- df_inner = df.values
- depths = df_inner[0:, 0]
- dfTo = None # dutils.tableNodeToDataFrame(node)
- if messDepths:
- offset = 100000.0 + (np.random.random_sample() * 200000.0)
- depths += offset
- dfTo.iloc[0:, 0] = pd.Series(depths)
- dutils.dataFrameToTableNode(dfTo, node)
- if messData: # not working yet
- values = df_inner[0:, 1:]
- values = np.squeeze(values)
- np.random.shuffle(values)
- values = values.transpose()
- np.random.shuffle(values)
- values = values.transpose()
- dfTo.iloc[0:, 1] = pd.Series(values) # doesn't work for multidimensional arrays
- dutils.dataFrameToTableNode(dfTo, node)
- else:
- values = []
- spacing = []
- origin = []
- if isinstance(node, slicer.vtkMRMLSegmentationNode):
- values, spacing, origin = arrayFromVisibleSegmentsBinaryLabelmap(node)
- else:
- values = slicer.util.arrayFromVolume(node)
- origin = node.GetOrigin()
-
- if messDepths:
- new_origin = [
- origin[0],
- origin[1],
- origin[2] - 100000.0 - (np.random.random_sample() * 200000.0),
- ]
- else:
- new_origin = origin
- node.SetOrigin(new_origin)
+ with open(iconPath, "r", encoding="utf-8") as src:
+ svg_content = src.read()
- if messData:
- values = np.squeeze(values)
- np.random.shuffle(values)
- values = values.transpose()
- np.random.shuffle(values)
- values = values.transpose()
- slicer.util.updateVolumeFromArray(node, values)
+ hex = "#e1e1e1" if themeIsDark() else "#333333"
+ updated_svg_content = re.sub(r'stroke="[^"]+"', f'stroke="{hex}"', svg_content)
+ with tempfile.NamedTemporaryFile(suffix=".svg", delete=False, mode="w", encoding="utf-8") as src:
+ src.write(updated_svg_content)
+ tmpIconPath = Path(src.name)
-def themeIsDark():
- import qt
+ icon = qt.QIcon(tmpIconPath.as_posix())
+ tmpIconPath.unlink()
- palette = slicer.app.palette()
- bg_color = palette.color(qt.QPalette.Background)
- fg_color = palette.color(qt.QPalette.WindowText)
- return fg_color.value() > bg_color.value()
+ return icon
def numberArrayToLabelArray(array: np.ndarray) -> np.ndarray:
@@ -2022,36 +1973,32 @@ class GitImportError(Exception):
pass
-def import_git():
- try:
- import git
+def install_git_module(remote, collection=False):
+ from ltrace.slicer.app import getApplicationInfo
- return git
- except Exception as e:
- raise GitImportError() from e
+ modules_parent_dir = get_scripted_modules_path().parent
+ third_party_dir = modules_parent_dir / "qt-scripted-external-modules"
+ repo = clone_or_update_repo(remote, third_party_dir, branch="master", collection=collection)
-def install_git_module(remote):
- from ltrace.slicer_utils import base_version
+ return repo
- geoslicer_version = base_version()
-
- modules_folders = (
- *(os.path.dirname(slicer.app.launcherExecutableFilePath).split("/")),
- *(("lib\\" + geoslicer_version + "\\qt-scripted-modules").split("\\")),
- )
- modules_path = os.path.join(modules_folders[0], os.sep, *modules_folders[1:])
-
- json_folders = (
- *(os.path.dirname(slicer.app.launcherExecutableFilePath).split("/")),
- *(("lib\\" + geoslicer_version + "\\qt-scripted-modules\\Resources\\json\\WelcomeGeoSlicer.json").split("\\")),
- )
- json_path = os.path.join(json_folders[0], os.sep, *json_folders[1:])
-
- new_module_name = remote.split("/")[-1].split(".")[0]
- new_module_path = os.path.join(modules_path, new_module_name)
- _ = import_git().Repo.clone_from(remote, new_module_path, env={"GIT_SSL_NO_VERIFY": "1"})
- config_module_paths(new_module_name, new_module_path, json_path)
+ # modules_folders = (
+ # *(os.path.dirname(slicer.app.launcherExecutableFilePath).split("/")),
+ # *(("lib\\" + geoslicer_version + "\\qt-scripted-modules").split("\\")),
+ # )
+ # modules_path = os.path.join(modules_folders[0], os.sep, *modules_folders[1:])
+ #
+ # json_folders = (
+ # *(os.path.dirname(slicer.app.launcherExecutableFilePath).split("/")),
+ # *(("lib\\" + geoslicer_version + "\\qt-scripted-modules\\Resources\\json\\WelcomeGeoSlicer.json").split("\\")),
+ # )
+ # json_path = os.path.join(json_folders[0], os.sep, *json_folders[1:])
+ #
+ # new_module_name = remote.split("/")[-1].split(".")[0]
+ # new_module_path = os.path.join(modules_path, new_module_name)
+ # _ = import_git().Repo.clone_from(remote, new_module_path, env={"GIT_SSL_NO_VERIFY": "1"})
+ # config_module_paths(new_module_name, new_module_path, json_path)
def config_module_paths(new_module_name, new_module_path, json_path):
@@ -2224,7 +2171,6 @@ def safe_convert_array(array, dtype):
array = array.astype(dtype)
return array
- return typeStr
def isImageFile(filePath: Union[Path, str]) -> bool:
@@ -2254,7 +2200,7 @@ class WatchSignal:
Example:
>>> with WatchSignal(signal=slicer.mrmlScene.EndImportEvent, timeout_ms=10000):
>>> doSomething()
- >>> slicer.mrmlScene.Clear(0) # Close project
+ >>> ProjectManager().close() # Close project
"""
def __init__(self, signal: slicer.vtkMRMLScene.SceneEventType, timeout_ms: int = 2000) -> None:
@@ -2366,3 +2312,62 @@ def hex2Rgb(hex: str, normalize=True) -> Tuple:
lv = len(hex)
rgb = tuple(int(hex[i : i + lv // 3], 16) / normalizeValue for i in range(0, lv, lv // 3))
return rgb
+
+
+class LazyLoad:
+ def __init__(self, moduleName):
+ self.moduleName = moduleName
+ self.module = None
+
+ def __getattr__(self, name):
+ if not self.module:
+ moduleInfo = slicer.modules.AppContextInstance.modules.availableModules[self.moduleName]
+ sys.path.append(moduleInfo.searchPath)
+ self.module = importlib.import_module(self.moduleName)
+ return getattr(self.module, name)
+
+
+def checkUniqueNames(nodes):
+ nodeNames = set()
+ for node in nodes:
+ if node.GetName() in nodeNames:
+ node.SetName(slicer.mrmlScene.GenerateUniqueName(node.GetName()))
+ nodeNames.add(node.GetName())
+
+
+def arrayPartsFromNode(node: slicer.vtkMRMLNode) -> tuple[np.ndarray, np.ndarray]:
+ mmToM = 0.001
+ if isinstance(node, slicer.vtkMRMLScalarVolumeNode):
+ values = slicer.util.arrayFromVolume(node).copy().squeeze()
+ if values.ndim != 2:
+ raise ValueError(f"Node has dimension {values.ndim}, expected 2.")
+
+ bounds = [0] * 6
+ node.GetBounds(bounds)
+ ymax = -bounds[4] * mmToM
+ ymin = -bounds[5] * mmToM
+ spacing = node.GetSpacing()[2] * mmToM
+ depthColumn = np.arange(ymin, ymax - spacing / 2, spacing)
+
+ ijkToRas = np.zeros([3, 3])
+ node.GetIJKToRASDirections(ijkToRas)
+ if ijkToRas[0][0] > 0:
+ values = np.flip(values, axis=0)
+ if ijkToRas[1][1] > 0:
+ values = np.flip(values, axis=1)
+ if ijkToRas[2][2] > 0:
+ values = np.flip(values, axis=2)
+ elif isinstance(node, slicer.vtkMRMLTableNode):
+ if node.GetAttribute("table_type") == "histogram_in_depth":
+ df = slicer.util.dataframeFromTable(node)
+ df_columns = df.columns
+ depthColumn = df[df_columns[0]].to_numpy() * mmToM
+ values = df[df_columns[1:]].to_numpy()
+ else:
+ values = slicer.util.arrayFromTableColumn(node, node.GetColumnName(1))
+ depthColumn = slicer.util.arrayFromTableColumn(node, node.GetColumnName(0)) * mmToM
+ if depthColumn[0] > depthColumn[-1]:
+ depthColumn = np.flipud(depthColumn)
+ values = np.flipud(values)
+
+ return depthColumn, values
diff --git a/tools/deploy/GeoSlicerManual/docs/Thin Section/thinsection.md b/src/ltrace/ltrace/slicer/image_log/__init__.py
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Thin Section/thinsection.md
rename to src/ltrace/ltrace/slicer/image_log/__init__.py
diff --git a/src/modules/DLISImport/DLISImportLib/DLISImportLogic.py b/src/ltrace/ltrace/slicer/image_log/import_logic.py
similarity index 95%
rename from src/modules/DLISImport/DLISImportLib/DLISImportLogic.py
rename to src/ltrace/ltrace/slicer/image_log/import_logic.py
index 8fae532..b5fdd30 100644
--- a/src/modules/DLISImport/DLISImportLib/DLISImportLogic.py
+++ b/src/ltrace/ltrace/slicer/image_log/import_logic.py
@@ -1,17 +1,18 @@
import logging
import re
-from collections import namedtuple
-from pathlib import Path
import vtk
-
import lasio
import numpy as np
import pandas as pd
import slicer
+
+
+from collections import namedtuple
+from pathlib import Path
from dlisio import dlis as dlisio
from pandas.errors import ParserError
-
-from ltrace.image.optimized_transforms import DEFAULT_NULL_VALUE, handle_null_values
+from ltrace.constants import DLISImportConst
+from ltrace.image.optimized_transforms import DEFAULT_NULL_VALUES, handle_null_values, ANP_880_2022_DEFAULT_NULL_VALUE
from ltrace.lmath.filtering import DistributionFilter
from ltrace.ocr import parse_pdf
from ltrace.slicer import helpers
@@ -26,14 +27,15 @@
from ltrace.units import global_unit_registry as ureg
from ltrace.file_utils import read_csv
-SCALAR_VOLUME_TYPE = "ScalarVolumeType"
-WELL_PROFILE_TAG = "WellProfile"
-NULL_VALUE_TAG = "NullValue"
-LOGICAL_FILE_TAG = "LogicalFile"
-FRAME_TAG = "Frame"
-WELL_NAME_TAG = "WellName"
-UNITS_TAG = "Units"
-DEPTH_LABEL = "DEPTH"
+SCALAR_VOLUME_TYPE = DLISImportConst.SCALAR_VOLUME_TYPE
+WELL_PROFILE_TAG = DLISImportConst.WELL_PROFILE_TAG
+NULL_VALUE_TAG = DLISImportConst.NULL_VALUE_TAG
+LOGICAL_FILE_TAG = DLISImportConst.LOGICAL_FILE_TAG
+FRAME_TAG = DLISImportConst.FRAME_TAG
+WELL_NAME_TAG = DLISImportConst.WELL_NAME_TAG
+UNITS_TAG = DLISImportConst.UNITS_TAG
+DEPTH_LABEL = DLISImportConst.DEPTH_LABEL
+
CURVES_NAME = ["T2_DIST", "T2DIST", "T1DIST"]
ChannelMetadata = namedtuple(
"ChannelMetadata",
@@ -53,10 +55,10 @@
class DLISLoader(object):
- def __init__(self, filepath):
+ def __init__(self, filepath, nulls=set):
self.filepath = filepath
self.logical_files = dlisio.load(str(self.filepath))
- self.null_value = DEFAULT_NULL_VALUE
+ self.null_value = nulls
def load_volumes(self, curves, stepCallback, appFolder, nullValue, well_diameter_mm, well_name):
return load_volumes(curves, stepCallback, appFolder, nullValue, well_diameter_mm, well_name)
@@ -156,10 +158,21 @@ def load_data(self, file_path, mnemonic_and_files):
class LASLoader(object):
- def __init__(self, filepath):
+ def __init__(self, filepath, nulls=set):
self.filepath = filepath
- self.logical_files = lasio.read(str(self.filepath))
- self.null_value = set([self.logical_files.well.NULL.value])
+
+ # Some invalid data will be susbtituted by nan (also other malformed data) by lasio
+ invalid_values_handling_options = ["(null)", "NA", "INF", "IO", "IND"]
+
+ self.logical_files = lasio.read(str(self.filepath), null_policy=invalid_values_handling_options)
+
+ # Finally, the null values substitutions will be left to GeoSlicer's handle_null_values function
+ self.null_value = nulls
+ if self.logical_files.well.NULL.value:
+ self.null_value.add(
+ self.logical_files.well.NULL.value
+ ) # Ensuring that self.logical_files.well.NULL.value is in the substitution list
+
self.stacked_curves_pattern = re.compile(r"(.+)[\[|(|{]([0-9]+)[\]|)|}]")
def load_volumes(self, curves, stepCallback, appFolder, nullValue, well_diameter_mm=None, well_name=None):
@@ -314,13 +327,13 @@ def load_data(self, file_path, mnemonic_and_files):
class CSVLoader(object):
- def __init__(self, filepath):
+ def __init__(self, filepath, nulls=set):
self.filepath = filepath
self.curve_depth = None
self.curve_name = None
self.filename = None
self.db = {}
- self.null_value = DEFAULT_NULL_VALUE
+ self.null_value = nulls
self.loaded_as_image = False
@staticmethod
@@ -386,7 +399,7 @@ def load_metadata(self):
if secondRowHasUnits:
units = df.iloc()[0]
df = df.drop(0)
- df.reset_index(drop=True, inplace=True)
+ df = df.reset_index(drop=True)
depthUnit = units[0]
else:
pattern = re.compile(r"\((.+)\)")
@@ -758,9 +771,6 @@ def add_volume_from_data(
if is_labelmap:
name = f"{name}_LabelMap"
- if verify_repeated_file(name, root_id, folder, frame):
- return None, None # IF a TDEP exists into this frame, ignore a new one
-
if is_labelmap:
volume_node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLLabelMapVolumeNode")
data = helpers.numberArrayToLabelArray(data)
@@ -774,7 +784,7 @@ def add_volume_from_data(
name = name.strip() # remove trailing spaces
volume_node.SetName(slicer.mrmlScene.GenerateUniqueName(name))
volume_node.SetAttribute(SCALAR_VOLUME_TYPE, WELL_PROFILE_TAG)
- volume_node.SetAttribute(NULL_VALUE_TAG, str(nullValue))
+ volume_node.SetAttribute(NULL_VALUE_TAG, str(nullValue)) # default set at handle_null_values
volume_node.SetAttribute(WELL_NAME_TAG, well_name)
volume_units = units if units != "" else None
@@ -806,14 +816,14 @@ def add_volume_from_data(
return volume_node, volume_item_id
-def get_loader(file_path):
+def get_loader(file_path, null_values=""):
ext = Path(file_path).suffix.lower()
if ext == ".las":
- return LASLoader(file_path)
+ return LASLoader(file_path, null_values)
if ext == ".dlis":
- return DLISLoader(file_path)
+ return DLISLoader(file_path, null_values)
if ext == ".csv" or ext == ".pdf":
- return CSVLoader(file_path)
+ return CSVLoader(file_path, null_values)
raise NotImplementedError(f'Handler for "{ext}" not implemented.')
@@ -882,7 +892,7 @@ def create_depth_curves_table(
df.columns = list(curves.keys())
if null_value is not None:
- df.replace(null_value, np.nan, inplace=True)
+ df = df.replace(null_value, np.nan)
# Assert data frame has DEPTH as first column
depth_column_index = df.columns.get_loc(DEPTH_LABEL)
@@ -959,7 +969,7 @@ def create_depth_curves_table_from_image(
df = pd.DataFrame(curves[name].astype(float))
if null_value is not None:
- df.replace(null_value, np.nan, inplace=True)
+ df = df.replace(null_value, np.nan)
# Assert data frame has DEPTH as first column
if curves[DEPTH_LABEL].size > 0:
diff --git a/src/modules/DLISImport/DLISImportLib/DLISImportWidget.py b/src/ltrace/ltrace/slicer/image_log/import_widget.py
similarity index 82%
rename from src/modules/DLISImport/DLISImportLib/DLISImportWidget.py
rename to src/ltrace/ltrace/slicer/image_log/import_widget.py
index 893e3ac..8795a63 100644
--- a/src/modules/DLISImport/DLISImportLib/DLISImportWidget.py
+++ b/src/ltrace/ltrace/slicer/image_log/import_widget.py
@@ -1,9 +1,17 @@
import time
import qt, ctk, slicer
-from .DLISTableViewer import DLISTableViewer
-from .DLISImportLogic import DLISLoader, LASLoader, CSVLoader, blank_fn, get_loader, LoaderError, ImageLogImportError
-from ltrace.image.optimized_transforms import DEFAULT_NULL_VALUE
+from .table_viewer_widget import ImageLogTableViewer
+from .import_logic import (
+ DLISLoader,
+ LASLoader,
+ CSVLoader,
+ blank_fn,
+ get_loader,
+ LoaderError,
+ ImageLogImportError,
+)
+from ltrace.image.optimized_transforms import DEFAULT_NULL_VALUES
from ltrace.slicer import ui, widgets
from ltrace.utils.ProgressBarProc import ProgressBarProc
from ltrace.slicer import helpers
@@ -18,7 +26,7 @@ def __init__(self, parent=None):
self.loadClicked = blank_fn
- self.tableView = DLISTableViewer()
+ self.tableView = ImageLogTableViewer()
self.tableView.setMinimumHeight(500)
self.tableView.loadClicked = self.onLoadClicked
@@ -56,9 +64,19 @@ def buildFileInputWidget(self):
self.ioFileInputLineEdit.setCurrentPath("")
self.ioFileInputLineEdit.findChild("QLineEdit").enabled = False
+ nullsLayout = qt.QHBoxLayout()
self.nullValuesListText = qt.QLineEdit()
- self.nullValuesListText.text = str(DEFAULT_NULL_VALUE)[1:-1]
+ self.nullValuesListText.setObjectName("Null Values List Text")
+ self.nullValuesListText.text = str(DEFAULT_NULL_VALUES)[1:-1]
self.nullValuesListText.textChanged.connect(lambda: self.setNullValuesFieldState(widgets.InputState.OK))
+ nullsLayout.addWidget(self.nullValuesListText)
+
+ self.resetNullValuesListButton = qt.QPushButton("Reset list")
+ self.resetNullValuesListButton.setObjectName("Reset Null Values List Button")
+ self.resetNullValuesListButton.clicked.connect(
+ lambda: self.nullValuesListText.setText(str(DEFAULT_NULL_VALUES)[1:-1])
+ )
+ nullsLayout.addWidget(self.resetNullValuesListButton)
self.wellDiameter = ui.floatParam("")
self.wellDiameter.setObjectName("Well Diameter Input")
@@ -67,18 +85,29 @@ def buildFileInputWidget(self):
wellDiameterLabel = qt.QLabel("Well diameter (inches):")
formLayout.addRow("Well log file:", self.ioFileInputLineEdit)
- formLayout.addRow("Null values list:", self.nullValuesListText)
+ formLayout.addRow("Null values list:", nullsLayout)
formLayout.addRow(wellDiameterLabel, self.wellDiameter)
def onPathChanged(filepath):
with ProgressBarProc() as progressBar:
- progressBar.nextStep(5, "Starting to load metadata...")
- self.dataLoader = get_loader(filepath)
+ progressBar.nextStep(5, "Loading metadata...")
+
progressBar.nextStep(80, "Setting context variables...")
nullvalues = set(self.nullValuesListText.text.split(","))
- nullvalues = set(map(float, nullvalues))
+
+ if self.nullValuesListText.text.split(",")[0]:
+ nullvalues = set(map(float, nullvalues))
+ else: # an empty nullValuesListText will give us a 1-element set ([""]). Reinitializing the set here
+ nullvalues = set()
+
+ self.dataLoader = get_loader(filepath, nullvalues)
+
nullvalues.union(self.dataLoader.null_value)
- self.nullValuesListText.text = str(nullvalues)[1:-1]
+
+ if len(nullvalues):
+ self.nullValuesListText.text = str(nullvalues)[1:-1]
+ else:
+ self.nullValuesListText.text = ""
try:
progressBar.nextStep(90, "Showing metadata...")
diff --git a/src/modules/DLISImport/DLISImportLib/DLISTableViewer.py b/src/ltrace/ltrace/slicer/image_log/table_viewer_widget.py
similarity index 99%
rename from src/modules/DLISImport/DLISImportLib/DLISTableViewer.py
rename to src/ltrace/ltrace/slicer/image_log/table_viewer_widget.py
index 2b93b01..d5851c2 100644
--- a/src/modules/DLISImport/DLISImportLib/DLISTableViewer.py
+++ b/src/ltrace/ltrace/slicer/image_log/table_viewer_widget.py
@@ -1,7 +1,7 @@
import qt
-from .DLISImportLogic import ChannelMetadata
-from .DLISImportLogic import checkMnemonicChanged
+from .import_logic import ChannelMetadata
+from .import_logic import checkMnemonicChanged
from ltrace.slicer import ui
@@ -17,7 +17,7 @@ def buildCheckBoxCell(checked):
return checkBoxWidget
-class DLISTableViewer(qt.QWidget):
+class ImageLogTableViewer(qt.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
diff --git a/src/ltrace/ltrace/slicer/module_info.py b/src/ltrace/ltrace/slicer/module_info.py
new file mode 100644
index 0000000..e30d3f6
--- /dev/null
+++ b/src/ltrace/ltrace/slicer/module_info.py
@@ -0,0 +1,136 @@
+import ast
+import os
+
+
+class StopTraversal(Exception):
+ pass
+
+
+class CategoryVisitor(ast.NodeVisitor):
+ def __init__(self, expectedClassName):
+ self.categories = None
+ self.hidden = False
+ self.name = expectedClassName
+
+ def visit_ClassDef(self, node):
+ if node.name == self.name:
+ if "LTracePlugin" in [base.id for base in node.bases]:
+ self.generic_visit(node) # Visit the children of the class
+
+ def visit_Assign(self, node):
+ # Check if the assignment is to `self.parent.categories`
+ if isinstance(node.targets[0], ast.Attribute):
+ attr = node.targets[0]
+ if (
+ isinstance(attr.value, ast.Attribute)
+ and isinstance(attr.value.value, ast.Name)
+ and attr.value.value.id == "self"
+ and attr.value.attr == "parent"
+ ):
+ if attr.attr == "categories" and isinstance(node.value, ast.List):
+ # Extract the value (should be a list in this case)
+ self.categories = [elt.s for elt in node.value.elts]
+ elif attr.attr == "hidden" and isinstance(node.value, ast.Constant):
+ # Extract the value (should be a boolean in this case)
+ self.hidden = node.value.value
+
+ # Halt the traversal only when both categories and hidden are found
+ if self.categories is not None and self.hidden is not False:
+ raise StopTraversal
+
+ self.generic_visit(node)
+
+
+# =============================================================================
+#
+# _ui_CreateComponentDialog
+#
+# =============================================================================
+# =============================================================================
+#
+# ModuleInfo
+#
+# =============================================================================
+class ModuleInfo:
+ # ---------------------------------------------------------------------------
+ def __init__(self, path, key=None, categories=None, hidden=False):
+ self.path = path
+ self.searchPath = os.path.dirname(path)
+ self.categories = categories or []
+ self.hidden = hidden
+
+ if key is None:
+ self.key = os.path.splitext(os.path.basename(path))[0]
+ else:
+ self.key = key
+
+ # ---------------------------------------------------------------------------
+ def __repr__(self):
+ return "ModuleInfo(key=%(key)r, path=%(path)r, hidden=%(hidden)r" % self.__dict__
+
+ # ---------------------------------------------------------------------------
+ def __str__(self):
+ return self.path
+
+ # ---------------------------------------------------------------------------
+ @staticmethod
+ def findModules(path, depth):
+ result = []
+ if os.path.isfile(path):
+ entries = [path]
+ elif os.path.isdir(path):
+ entries = [os.path.join(path, entry) for entry in os.listdir(path)]
+ # If the folder contains __init__.py, it means that this folder
+ # is not a Slicer module but an embedded Python library that a module will load.
+ if any(entry.endswith("__init__.py") for entry in entries):
+ entries = []
+ else:
+ # not a file or folder
+ return result
+
+ if depth > 0:
+ for entry in filter(os.path.isdir, entries):
+ result += ModuleInfo.findModules(entry, depth - 1)
+
+ for entry in filter(os.path.isfile, entries):
+ if not entry.endswith(".py"):
+ continue
+
+ if os.path.basename(entry) == "__init__.py":
+ continue
+
+ # Criteria for a Slicer module to have a module class
+ # that has the same name as the filename and its base class is LTracePlugin.
+
+ if entry.endswith("CLI.py") and os.path.dirname(entry).endswith("CLI"):
+ # Dirty but works / TODO move it to a function specialized for CLI
+ result.append(ModuleInfo(entry, categories=["CLI"], hidden=False))
+ continue
+
+ try:
+ # Find all class definitions
+ with open(entry) as entry_file:
+ tree = ast.parse(entry_file.read())
+
+ filename = os.path.basename(entry)
+ expectedClassName = os.path.splitext(filename)[0]
+ visitor = CategoryVisitor(expectedClassName)
+ try:
+ visitor.visit(tree)
+ except StopTraversal:
+ pass
+
+ if visitor.categories:
+ minfo = ModuleInfo(entry, categories=visitor.categories, hidden=visitor.hidden)
+ result.append(minfo)
+
+ except:
+ # Error while processing the file (e.g., syntax error),
+ # it cannot be a Slicer module.
+ pass
+
+ # We have the option to identify scripted CLI modules, such as by examining the existence of a
+ # compatible module descriptor XML file. However, this type of module is relatively uncommon, so
+ # the decision was made not to invest in implementing this feature.
+
+ return result
diff --git a/src/ltrace/ltrace/slicer/module_utils.py b/src/ltrace/ltrace/slicer/module_utils.py
new file mode 100644
index 0000000..716a6e9
--- /dev/null
+++ b/src/ltrace/ltrace/slicer/module_utils.py
@@ -0,0 +1,179 @@
+import logging
+
+from pathlib import Path
+
+import slicer, qt
+
+from ltrace.slicer.module_info import ModuleInfo
+
+
+def fetchAsList(settings, key) -> list:
+ # Return a settings value as a list (even if empty or a single value)
+
+ value = settings.value(key)
+
+ if isinstance(value, str):
+ return [value]
+
+ return [] if value is None else value
+
+
+def loadModule(module: ModuleInfo):
+ factory = slicer.app.moduleManager().factoryManager()
+
+ if factory.isLoaded(module.key):
+ return
+
+ factory.registerModule(qt.QFileInfo(str(module.path)))
+ if not factory.isRegistered(module.key):
+ logging.warning(f"Failed to register module {module.key}")
+ return False
+
+ if not factory.loadModules([module.key]):
+ logging.error(f"Failed to load module {module.key}")
+
+ return True
+
+
+def loadModules(modules, permanent=False, favorite=False):
+ """
+ Loads a module in the Slicer factory while Slicer is running
+ """
+ # Determine which modules in above are not already loaded
+ factory = slicer.app.moduleManager().factoryManager()
+
+ # Add module(s) to permanent search paths, if requested
+ settings = slicer.app.revisionUserSettings()
+ searchPaths = [Path(fp) for fp in fetchAsList(settings, "Modules/AdditionalPaths")]
+ npaths = len(searchPaths)
+
+ modulesToLoad = []
+
+ for myModule in modules:
+ if factory.isLoaded(myModule.key):
+ logging.info(f"Module {myModule.key} already loaded")
+ continue
+
+ if permanent:
+ rawPath = Path(myModule.searchPath)
+
+ if rawPath not in searchPaths:
+ searchPaths.append(rawPath)
+
+ # Register requested module(s)
+ factory.registerModule(qt.QFileInfo(str(myModule.path)))
+
+ if not factory.isRegistered(myModule.key):
+ logging.warning(f"Failed to register module {myModule.key}")
+ continue
+
+ modulesToLoad.append(myModule.key)
+
+ if not factory.loadModules(modulesToLoad):
+ logging.error(f"Failed to load some module(s)")
+ return
+
+ if len(searchPaths) > npaths:
+ settings.setValue("Modules/AdditionalPaths", [str(p) for p in searchPaths])
+
+ for myModule in modules:
+ myModule.loaded = factory.isLoaded(myModule.key)
+ logging.info(f"Module {myModule.key} loaded")
+
+ # Instantiate and load requested module(s)
+ # if len(modules) != len(modulesToLoad):
+ # slicer.util.errorDisplay("The module factory manager reported an error. \
+ # One or more of the requested module(s) and/or \
+ # dependencies thereof may not have been loaded.")
+
+ if favorite and len(modulesToLoad) > 0:
+ favorites = [*slicer.app.userSettings().value("Modules/FavoriteModules"), *modulesToLoad]
+ slicer.app.userSettings().setValue("Modules/FavoriteModules", favorites)
+
+
+def fetchModulesFrom(path, depth=1):
+ if path is None:
+ return {}
+
+ try:
+ if path.suffix == ".git":
+ # Clone or update the repository
+ from ltrace.slicer_utils import base_version
+
+ geoslicer_version = base_version()
+ dest = Path(slicer.app.slicerHome) / "lib" / f"GeoSlicer-{geoslicer_version}" / "qt-scripted-extern-modules"
+ dest.mkdir(parents=True, exist_ok=True)
+ path = clone_or_update_repo(path, dest, branch="master")
+
+ # Get list of modules in specified path
+ modules = ModuleInfo.findModules(path, depth)
+
+ candidates = {m.key: m for m in modules}
+ return candidates
+ except RuntimeError as re:
+ logging.warning(repr(re))
+ except Exception as e:
+ logging.warning(f"Failed to load modules: {e}")
+
+ return {}
+
+
+def mapByCategory(modules):
+ groupedModulesByCategories = {}
+ for module in modules:
+ if module.key == "CustomizedGradientAnisotropicDiffusion":
+ pass
+ for category in module.categories:
+ if category not in groupedModulesByCategories:
+ groupedModulesByCategories[category] = []
+ groupedModulesByCategories[category].append(module)
+
+ return groupedModulesByCategories
+
+
+def clone_or_update_repo(remote_url: Path, destination_dir: Path, branch: str = "master") -> None:
+ """
+ Clone the repository from `remote_url` into `destination_dir`.
+ If the repository already exists, update it by pulling the latest changes.
+
+ Args:
+ remote_url (str): URL of the remote repository.
+ destination_dir (str | Path): Path where the repository should be cloned or updated.
+
+ Raises:
+ ValueError: If the existing repository in `destination_dir` has a different remote URL.
+
+ Returns:
+ str: A message indicating the action taken.
+ """
+ import os
+
+ os.environ["GIT_PYTHON_REFRESH"] = "quiet"
+ import git
+
+ try:
+ remote_repo = remote_url.split("/")[-1].split(".")[0]
+
+ destination_dir = destination_dir / remote_repo
+
+ if destination_dir.exists():
+ # If the directory exists, open the repo and check the remote URL
+ repo = git.Repo(destination_dir)
+ if repo.remotes.origin.url != remote_url:
+ raise ValueError(f"Directory exists but points to a different repository: {repo.remotes.origin.url}")
+
+ # Pull the latest changes if the remote URL matches
+ try:
+ repo.remotes.origin.pull(branch).raise_if_error()
+ except git.GitCommandError as e:
+ repo.remotes.origin.pull("main").raise_if_error()
+
+ logging.info(f"Updated '{branch}' branch in repository at '{destination_dir}'.")
+ else:
+ # Clone the repository if the directory does not exist
+ git.Repo.clone_from(remote_url, destination_dir, env={"GIT_SSL_NO_VERIFY": "1"})
+ logging.info(f"Cloned repository '{remote_repo}' into '{destination_dir}'.")
+ except git.GitCommandError as e:
+ raise RuntimeError(f"Failed to fetch {remote_url}")
+
+ return destination_dir
diff --git a/src/ltrace/ltrace/slicer/modules_help_menu.py b/src/ltrace/ltrace/slicer/modules_help_menu.py
index eefaec3..b23dd4b 100644
--- a/src/ltrace/ltrace/slicer/modules_help_menu.py
+++ b/src/ltrace/ltrace/slicer/modules_help_menu.py
@@ -95,8 +95,15 @@ def __getEnvironments(self):
clsObject=None,
modules=[],
)
+ multiScale = EnvModule(
+ cls=slicer.modules.multiscaleenv,
+ name=slicer.modules.multiscaleenv.title,
+ tag="Multiscale",
+ clsObject=None,
+ modules=[],
+ )
- return [core, imageLog, microCt, thinSection]
+ return [core, imageLog, microCt, thinSection, multiScale]
except AttributeError:
return []
diff --git a/src/ltrace/ltrace/slicer/netcdf.py b/src/ltrace/ltrace/slicer/netcdf.py
index 8a9a916..b96c3fc 100644
--- a/src/ltrace/ltrace/slicer/netcdf.py
+++ b/src/ltrace/ltrace/slicer/netcdf.py
@@ -3,9 +3,13 @@
import logging
import xarray as xr
import logging
+import numpy as np
-from PIL import ImageColor
+from dataclasses import dataclass
from pathlib import Path
+from PIL import ImageColor
+from ltrace.readers import microtom
+from ltrace.slicer import export
from ltrace.slicer.helpers import (
autoDetectColumnType,
create_color_table,
@@ -13,10 +17,24 @@
makeTemporaryNodePermanent,
removeTemporaryNodes,
updateSegmentationFromLabelMap,
+ getSourceVolume,
+ safe_convert_array,
+ checkUniqueNames,
)
+from ltrace.utils.callback import Callback
+from scipy import ndimage
from typing import List, Tuple
-from ltrace.readers import microtom
+MIN_CHUNKING_SIZE_BYTES = 2**33 # 8 GiB
+CHUNK_SIZE_BYTES = 2**21 # 2 MiB
+
+
+@dataclass
+class DataArrayTransform:
+ ijk_to_ras: np.ndarray
+ transform: np.ndarray
+ ras_min: np.ndarray
+ ras_max: np.ndarray
def _crop_value(array: xr.DataArray, value: int):
@@ -179,3 +197,310 @@ def import_dataset(dataset, images="all"):
makeTemporaryNodePermanent(node, show=True)
autoDetectColumnType(node)
yield node
+
+
+def _segmentation_to_label_map(segmentation: slicer.vtkMRMLSegmentationNode) -> slicer.vtkMRMLLabelMapVolumeNode:
+ labelMapVolumeNode = createTemporaryVolumeNode(slicer.vtkMRMLLabelMapVolumeNode, segmentation.GetName())
+ slicer.modules.segmentations.logic().ExportAllSegmentsToLabelmapNode(
+ segmentation, labelMapVolumeNode, slicer.vtkSegmentation.EXTENT_REFERENCE_GEOMETRY
+ )
+ return labelMapVolumeNode
+
+
+def _node_to_data_array(
+ node: slicer.vtkMRMLScalarVolumeNode, dim_names: List[str], dtype=None
+) -> Tuple[slicer.vtkMRMLScalarVolumeNode, xr.DataArray]:
+ attrs = {}
+ if isinstance(node, slicer.vtkMRMLSegmentationNode):
+ node_name = node.GetName()
+ node = _segmentation_to_label_map(node)
+
+ if node.GetImageData().GetPointData().GetScalars() is None:
+ return f"Could not export {node_name}: segmentation is empty"
+
+ attrs["type"] = "segmentation"
+ elif isinstance(node, slicer.vtkMRMLLabelMapVolumeNode):
+ attrs["type"] = "labelmap"
+ elif not isinstance(node, slicer.vtkMRMLScalarVolumeNode):
+ raise ValueError(f"Unsupported node type: {type(node)}")
+
+ if isinstance(node, slicer.vtkMRMLLabelMapVolumeNode):
+ attrs["labels"] = ["Name,Index,Color"] + export.getLabelMapLabelsCSV(node, withColor=True)
+
+ array = slicer.util.arrayFromVolume(node)
+ if dtype:
+ array = safe_convert_array(array, dtype)
+ dims = dim_names[: array.ndim]
+ return node, xr.DataArray(array, dims=dims, attrs=attrs)
+
+
+def _recommended_chunksizes(img):
+ chunk_size = round((CHUNK_SIZE_BYTES // img.dtype.itemsize) ** (1 / 3))
+ if (
+ img.nbytes >= MIN_CHUNKING_SIZE_BYTES
+ and img.ndim >= 3
+ and all(size >= chunk_size * 4 for size in img.shape[:3])
+ ):
+ return (chunk_size,) * 3
+ return None
+
+
+def _add_color(transform: xr.DataArray) -> xr.DataArray:
+ assert transform.shape == (4, 4)
+ ret = np.insert(transform, 3, 0, axis=0)
+ ret = np.insert(ret, 3, 0, axis=1)
+ ret[3, 3] = 1
+ return ret
+
+
+def _get_transform(node: slicer.vtkMRMLScalarVolumeNode, array_shape: np.ndarray) -> DataArrayTransform:
+ default_transform = np.array(
+ [
+ [-1, 0, 0, 0],
+ [0, -1, 0, 0],
+ [0, 0, 1, 0],
+ [0, 0, 0, 1],
+ ]
+ )
+
+ transform = vtk.vtkMatrix4x4()
+ node.GetIJKToRASMatrix(transform)
+ transform = _vtk_4x4_to_numpy(transform)
+ ijk_to_ras = transform.copy()
+
+ transform = transform @ default_transform
+ transform[:2, 3] *= -1
+
+ # Convert XYZ to ZYX
+ transform[:3, :3] = np.flip(transform[:3, :3], axis=(0, 1))
+ transform[:3, 3] = np.flip(transform[:3, 3], axis=0)
+
+ pre_shape = np.array(array_shape[:3]) - 1
+
+ # Corners of a unit cube
+ unit_corners = [
+ [0, 0, 0],
+ [0, 0, 1],
+ [0, 1, 0],
+ [0, 1, 1],
+ [1, 0, 0],
+ [1, 0, 1],
+ [1, 1, 0],
+ [1, 1, 1],
+ ]
+ ijk_corners = unit_corners * pre_shape
+ ras_corners = np.array([(transform @ np.concatenate([ijk, [1]]))[:3] for ijk in ijk_corners])
+
+ ras_min, ras_max = ras_corners.min(axis=0), ras_corners.max(axis=0)
+
+ if len(array_shape) == 4:
+ transform = _add_color(transform)
+
+ return DataArrayTransform(ijk_to_ras, transform, ras_min, ras_max)
+
+
+def _vtk_4x4_to_numpy(matrix_vtk):
+ matrix_np = np.empty(16)
+ matrix_vtk.DeepCopy(matrix_np, matrix_vtk)
+ return matrix_np.reshape(4, 4)
+
+
+def _get_dataset_main_dims(dataset):
+ # Uses a heuristic to find likely zyxc dimensions of a dataset
+ for var in dataset:
+ array = dataset[var]
+ dims = array.dims
+ if len(dims) >= 3:
+ return dims[:4]
+
+
+def exportNetcdf(
+ exportPath,
+ dataNodes,
+ referenceItem=None,
+ single_coords=False,
+ use_compression=False,
+ callback=None,
+ nodeNames=None,
+ nodeDtypes=None,
+ save_in_place=False,
+):
+ if not dataNodes:
+ raise ValueError("No images selected.")
+
+ if callback is None:
+ callback = Callback(on_update=lambda *args, **kwargs: None)
+
+ if single_coords and not save_in_place:
+ if not referenceItem:
+ raise ValueError("No reference node selected.")
+ if referenceItem not in dataNodes:
+ raise ValueError("Reference image must be in the list of images to export.")
+
+ warnings = []
+
+ callback.on_update("Starting…", 0)
+ if not nodeNames:
+ checkUniqueNames(dataNodes)
+
+ arrays = {}
+ coords = {}
+ nodeNames = nodeNames or [node.GetName() for node in dataNodes]
+ nodeDtypes = nodeDtypes or [None] * len(dataNodes)
+
+ id_to_name_map = {node.GetID(): name for node, name in zip(dataNodes, nodeNames)}
+
+ if save_in_place:
+ existing_dataset = xr.load_dataset(exportPath)
+ existing_dims = _get_dataset_main_dims(existing_dataset)
+
+ for node, dtype in zip(dataNodes, nodeDtypes):
+ name = id_to_name_map[node.GetID()]
+ if save_in_place and name in existing_dataset:
+ continue
+ is_ref = node == referenceItem
+
+ if save_in_place:
+ dims = existing_dims
+ elif single_coords or name == "microtom":
+ dims = list("zyxc")
+ else:
+ dims = [f"{d}_{name}" for d in "zyx"] + ["c"]
+
+ source_node = getSourceVolume(node)
+
+ result = _node_to_data_array(node, dims, dtype)
+ if isinstance(result, str):
+ warnings.append(result)
+ continue
+ node, data_array = result
+
+ if source_node:
+ try:
+ source_name = id_to_name_map[source_node.GetID()]
+ data_array.attrs["reference"] = source_name
+ except KeyError:
+ pass
+
+ if is_ref:
+ referenceItem = node
+ if data_array.ndim == 4:
+ coords["c"] = ["r", "g", "b"]
+ arrays[name] = (data_array, _get_transform(node, data_array.shape), node)
+
+ if single_coords:
+ if save_in_place:
+ ref_spacing = []
+ ras_min = []
+ output_shape = []
+ for dim in existing_dims[:3]:
+ dim_coords = existing_dataset.coords[dim]
+ if len(dim_coords) > 1:
+ ref_spacing.append((dim_coords[1] - dim_coords[0]).item())
+ else:
+ ref_spacing.append(1)
+ ras_min.append(dim_coords[0].item())
+ output_shape.append(len(dim_coords))
+ else:
+ ref_spacing = np.array(referenceItem.GetSpacing())[::-1]
+ ras_min = np.array([tr.ras_min for _, tr, _ in arrays.values()]).min(axis=0)
+ ras_max = np.array([tr.ras_max for _, tr, _ in arrays.values()]).max(axis=0)
+ output_shape = np.ceil((ras_max - ras_min) / ref_spacing).astype(int) + 1
+
+ output_transform_no_color = np.array(
+ [
+ [ref_spacing[0], 0, 0, ras_min[0]],
+ [0, ref_spacing[1], 0, ras_min[1]],
+ [0, 0, ref_spacing[2], ras_min[2]],
+ [0, 0, 0, 1],
+ ]
+ )
+
+ output_transform_with_color = _add_color(output_transform_no_color)
+
+ if not arrays:
+ raise ValueError("No images to export.\n" + "\n".join(warnings))
+
+ progress_range = np.arange(5, 90, 85 / len(arrays))
+ data_arrays = {}
+ for (name, (data_array, transform, _)), progress in zip(arrays.items(), progress_range):
+ callback.on_update(f'Processing "{name}"…', round(progress))
+
+ array = data_array.data
+ attrs = data_array.attrs
+
+ if single_coords:
+ input_transform = transform.transform
+ output_transform = output_transform_no_color if data_array.ndim == 3 else output_transform_with_color
+ output_to_input = np.linalg.inv(input_transform) @ output_transform
+
+ fill_value = 0 if "labels" in data_array.attrs else 255
+ shape = output_shape.copy()
+ if data_array.ndim == 4:
+ shape = np.append(shape, 3)
+
+ # Transform interpolation does not work on dimensions with size 1
+ for i in range(3):
+ if data_array.shape[i] == 1:
+ output_to_input[i, :] = 0
+ output_to_input[:, i] = 0
+ output_to_input[i, i] = 1
+
+ identity = np.eye(output_to_input.shape[0])
+ if np.allclose(output_to_input, identity):
+ if data_array.shape != tuple(shape):
+ pads = []
+ for small, large in zip(data_array.shape, shape):
+ diff = large - small
+ pads.append((0, diff))
+ array = np.pad(array, pads, mode="constant", constant_values=fill_value)
+
+ else:
+ array = ndimage.affine_transform(
+ data_array, output_to_input, output_shape=shape, order=0, cval=fill_value, mode="grid-constant"
+ )
+ else:
+ attrs["transform"] = transform.ijk_to_ras.flatten().tolist()
+
+ if "reference" in attrs and attrs["reference"] not in arrays:
+ del attrs["reference"]
+
+ new_data_array = xr.DataArray(array, dims=data_array.dims, attrs=attrs)
+ data_arrays[name] = new_data_array
+
+ removeTemporaryNodes()
+ callback.on_update("Exporting to NetCDF…", 90)
+
+ if save_in_place:
+ for dim in existing_dims[:3]:
+ coords[dim] = existing_dataset.coords[dim]
+ elif single_coords:
+ for min_, spacing, size, dim in zip(ras_min, ref_spacing, output_shape, "zyx"):
+ max_ = min_ + spacing * (size - 1)
+ coords[dim] = np.linspace(min_, max_, size)
+ else:
+ for name, data_array in data_arrays.items():
+ node = arrays[name][2]
+ origin_zyx = list(node.GetOrigin()[::-1])
+ origin_zyx[1] *= -1
+ origin_zyx[2] *= -1
+ spacing_zyx = node.GetSpacing()[::-1]
+ for origin, spacing, size, dim in zip(origin_zyx, spacing_zyx, data_array.shape, "zyx"):
+ coord_name = f"{dim}_{name}" if name != "microtom" else dim
+ coords[coord_name] = np.linspace(origin, origin + spacing * (size - 1), size)
+ dataset = xr.Dataset(data_arrays, coords=coords)
+
+ if save_in_place:
+ for var in dataset:
+ existing_dataset[var] = dataset[var]
+ dataset = existing_dataset
+
+ encoding = {}
+ for var in dataset:
+ img = dataset[var]
+ encoding[var] = {"zlib": use_compression, "chunksizes": _recommended_chunksizes(img)}
+
+ dataset.attrs["geoslicer_version"] = slicer.app.applicationVersion
+ dataset.to_netcdf(exportPath, encoding=encoding, format="NETCDF4")
+
+ return warnings
diff --git a/src/ltrace/ltrace/slicer/node_attributes.py b/src/ltrace/ltrace/slicer/node_attributes.py
index 87d72cc..9163120 100644
--- a/src/ltrace/ltrace/slicer/node_attributes.py
+++ b/src/ltrace/ltrace/slicer/node_attributes.py
@@ -54,6 +54,7 @@ class NodeEnvironment(NodeAttributeValue):
THIN_SECTION = "ThinSectionEnv"
CHARTS = "Charts"
LABEL_MAP_EDITOR = "LabelMapEditor"
+ MULTISCALE = "MultiscaleEnv"
class DataOrigin(NodeAttributeValue):
diff --git a/src/ltrace/ltrace/slicer/nodes/custom_behavior_image_node.py b/src/ltrace/ltrace/slicer/nodes/custom_behavior_image_node.py
index aa4b12e..a323c36 100644
--- a/src/ltrace/ltrace/slicer/nodes/custom_behavior_image_node.py
+++ b/src/ltrace/ltrace/slicer/nodes/custom_behavior_image_node.py
@@ -2,6 +2,7 @@
from .defs import TriggerEvent
from ltrace.slicer.node_attributes import LosslessAttribute
from ltrace.slicer.helpers import isImageFile
+from ltrace.slicer import export
from pathvalidate import sanitize_filepath
from pathlib import Path
from typing import Union
@@ -126,8 +127,7 @@ def _exportImageWrapper(self, node: slicer.vtkMRMLNode, destinationImageFile: Un
array = slicer.util.arrayFromVolume(node)
imageArray = cv2.cvtColor(array[0, :, :, :], cv2.COLOR_BGR2RGB)
- exportLogic = slicer.modules.export.widgetRepresentation().self().logic
- exportLogic.exportNodeAsImage(
+ export.exportNodeAsImage(
nodeName=destinationImageFile.stem,
dataArray=imageArray,
imageFormat=destinationImageFile.suffix,
diff --git a/src/ltrace/ltrace/slicer/project_manager.py b/src/ltrace/ltrace/slicer/project_manager.py
index 82d552b..ded65a3 100644
--- a/src/ltrace/ltrace/slicer/project_manager.py
+++ b/src/ltrace/ltrace/slicer/project_manager.py
@@ -6,17 +6,31 @@
import re
import shutil
import traceback
+import psutil
+import pandas as pd
+from dataclasses import dataclass
from ltrace.constants import SaveStatus
-from ltrace.slicer.helpers import bounds2size, singleton
+from ltrace.slicer.helpers import bounds2size, singleton, getSourceVolume
from ltrace.slicer.nodes.custom_behavior_node_manager import CustomBehaviorNodeManager
from ltrace.slicer.nodes.defs import TriggerEvent
from ltrace.slicer.node_observer import NodeObserver
from pathlib import Path
from pathvalidate import sanitize_filepath, is_valid_filename
-from typing import List
+from typing import List, Union
+from humanize import naturalsize
DEFAULT_PROPERTIES = {"useCompression": 0}
+FILE_SIZE_EXTRA_MARGIN = 1.1
+
+
+@dataclass
+class SliceViewConfiguration:
+ background: str = None
+ foreground: str = None
+ label: str = None
+ foregroundOpacity: float = None
+ labelOpacity: float = None
class HierarchyVisibilityManager:
@@ -61,10 +75,9 @@ def __on_node_modified(self, caller, event):
self.__last_visibility = caller.GetVisibility()
-def BUGFIX_handle_copy_suffix_on_cloned_nodes(storage_node) -> str:
+def handleCopySuffixOnClonedNodes(storageNode: slicer.vtkMRMLStorageNode) -> str:
"""Handle the extra copy at the end of the file name.
This is a bug fix for the method AddDefaultStorageNode, that probably generate the wrong name internally
- (TODO check this).
Args:
filename (str): the file name to be fixed.
@@ -72,7 +85,7 @@ def BUGFIX_handle_copy_suffix_on_cloned_nodes(storage_node) -> str:
Returns:
str: the fixed file name.
"""
- filename = storage_node.GetFileName()
+ filename = storageNode.GetFileName()
if not filename:
return ""
@@ -84,22 +97,11 @@ def BUGFIX_handle_copy_suffix_on_cloned_nodes(storage_node) -> str:
if found > 0:
filepath = Path(filename[:-found])
filename = str(filepath.with_name(f"{filepath.stem} Copy{filepath.suffix}"))
- storage_node.SetFileName(filename)
+ storageNode.SetFileName(filename)
return filename
-def generate_unique_node_name(name: str, dirpath: Path):
- count = 0
- for file in dirpath.iterdir():
- if file.is_file() and file.name.startswith(name):
- count += 1
-
- unique_name = f"{name} ({count})" if count > 0 else name
-
- return unique_name
-
-
@singleton
class ProjectManager(qt.QObject):
"""Class to handle the 'project concept' from Geoslicer.
@@ -108,128 +110,315 @@ class ProjectManager(qt.QObject):
projectChangedSignal = qt.Signal(None)
- def __init__(self, folder_icon_path, *args, **kwargs):
+ def __init__(self, folderIconPath, *args, **kwargs):
super().__init__(*args, **kwargs)
- self.__node_observers = list()
- self.__folder_icon_path = folder_icon_path
- self.__slices_shown = False
- self.custom_behavior_node_manager = CustomBehaviorNodeManager()
- self.__config_compression_mode()
-
- def save(self, project_url, internal_call=False, *args, **kwargs):
+ self.__nodeObservers = list()
+ self.__folderIconPath = folderIconPath
+ self.__slicesShown = False
+ self.__customBehaviorNodeManager = CustomBehaviorNodeManager()
+ self.__configCompressionMode()
+ self.__endCloseSceneObserverHandler = None
+ self.__modifiedEventObserverHandler = None
+ self.__endLoadEventObserverHandler = None
+ self.__startLoadEventObserverHandler = None
+
+ def save(self, projectUrl, internalCall=False, *args, **kwargs):
"""Handles project' saving process. It saves the nodes that has modifications and
are related to the current scene, and then, saves the scene.
Args:
- project_url (str): the scene filepath (.mrml).
+ projectUrl (str): the scene filepath (.mrml).
Returns:
bool: True if save process was successful, otherwise False.
"""
- if not internal_call:
- self.custom_behavior_node_manager.triggerEvent = TriggerEvent.SAVE
+ if not internalCall:
+ self.__customBehaviorNodeManager.triggerEvent = TriggerEvent.SAVE
slicer.mrmlScene.StartState(slicer.mrmlScene.SaveState)
status = SaveStatus.IN_PROGRESS
- root_dir_before_save = slicer.mrmlScene.GetRootDirectory()
- self.__config_compression_mode(*args, **kwargs)
- if os.path.isdir(project_url):
- slicer.mrmlScene.SetRootDirectory(project_url)
-
- # Save Scene
- if not self.__save_scene(project_url, *args, **kwargs):
- slicer.mrmlScene.SetRootDirectory(root_dir_before_save)
- status = SaveStatus.FAILED
- else:
- slicer.mrmlScene.SetRootDirectory(os.path.dirname(project_url))
- # Save nodes
- nodeSavingStatus = self.__save_nodes(*args, **kwargs)
+ if not self.__validateProjectIsWritable(projectUrl):
+ slicer.mrmlScene.EndState(slicer.mrmlScene.SaveState)
+ return SaveStatus.FAILED
- # Save Scene
- if not nodeSavingStatus or not self.__save_scene(project_url, *args, **kwargs):
- slicer.mrmlScene.SetRootDirectory(root_dir_before_save)
- status = SaveStatus.FAILED
+ rootDirBeforeSave = slicer.mrmlScene.GetRootDirectory()
+ self.__configCompressionMode(*args, **kwargs)
+ projectUrl = Path(projectUrl).resolve()
+ projectRootUrl = projectUrl.parent if projectUrl.is_file() else projectUrl
+ slicer.mrmlScene.SetRootDirectory(projectRootUrl.as_posix())
+ firstSave = not projectUrl.exists()
- if status == SaveStatus.IN_PROGRESS:
- status = SaveStatus.SUCCEED
+ # Save Scene
+ if not self.__saveNodes(firstSave=firstSave, *args, **kwargs) or not self.__saveScene(
+ projectUrl, *args, **kwargs
+ ):
+ slicer.mrmlScene.SetRootDirectory(rootDirBeforeSave)
+ status = SaveStatus.FAILED
+
+ projectFile = self.__findProjectFile(projectRootUrl.as_posix())
+ if not projectFile:
+ slicer.mrmlScene.SetRootDirectory(rootDirBeforeSave)
+ status = SaveStatus.FAILED
- if not internal_call:
+ if not internalCall:
slicer.mrmlScene.EndState(slicer.mrmlScene.SaveState)
- self.__set_project_modified(status != SaveStatus.SUCCEED)
+ if status != SaveStatus.IN_PROGRESS: # status is FAILED or FAILED_FILE_ALREADY_EXISTS
+ if not internalCall:
+ self.__setProjectModified(True)
+ return status
- return status
+ fileProjectPath = (projectRootUrl / projectFile).resolve()
+
+ parameters = {
+ "fileType": "SceneFile",
+ "fileName": fileProjectPath.as_posix(),
+ }
+ slicer.app.coreIOManager().emitFileSaved(parameters)
+
+ if not internalCall:
+ self.__setProjectModified(False)
+
+ return SaveStatus.SUCCEED
+
+ def __getSliceViewConfiguration(self) -> SliceViewConfiguration:
+ config = SliceViewConfiguration()
+ num = slicer.mrmlScene.GetNumberOfNodesByClass("vtkMRMLSliceCompositeNode")
+ for i in range(num):
+ sliceViewer = slicer.mrmlScene.GetNthNodeByClass(i, "vtkMRMLSliceCompositeNode")
+ config.background = config.background or sliceViewer.GetBackgroundVolumeID()
+ config.foreground = config.foreground or sliceViewer.GetForegroundVolumeID()
+ config.label = config.label or sliceViewer.GetLabelVolumeID()
+ config.foregroundOpacity = config.foregroundOpacity or sliceViewer.GetForegroundOpacity()
+ config.labelOpacity = config.labelOpacity or sliceViewer.GetLabelOpacity()
+
+ return config
+
+ def __setSliceViewConfiguration(self, config: SliceViewConfiguration) -> None:
+ slicer.util.setSliceViewerLayers(
+ background=config.background,
+ foreground=config.foreground,
+ label=config.label,
+ foregroundOpacity=config.foregroundOpacity,
+ labelOpacity=config.labelOpacity,
+ )
- def save_as(self, scene_path, *args, **kwargs):
+ def saveAs(self, scenePath, *args, **kwargs):
"""Handle custom save scene as operation."""
- self.custom_behavior_node_manager.triggerEvent = TriggerEvent.SAVE_AS
+ self.__customBehaviorNodeManager.triggerEvent = TriggerEvent.SAVE_AS
+ sliceViewConfig = self.__getSliceViewConfiguration()
slicer.mrmlScene.StartState(slicer.mrmlScene.SaveState)
status = SaveStatus.IN_PROGRESS
- scene_path = Path(scene_path)
+ scenePath = Path(scenePath)
- if scene_path.is_file():
- slicer.util.errorDisplay(f'Cannot create project directory at "{scene_path}" because it is a file.')
- self.__set_project_modified(True)
+ if scenePath.is_file():
+ slicer.util.errorDisplay(f'Cannot create project directory at "{scenePath}" because it is a file.')
+ self.__setProjectModified(True)
slicer.mrmlScene.EndState(slicer.mrmlScene.SaveState)
+ self.__setSliceViewConfiguration(sliceViewConfig)
return SaveStatus.FAILED_FILE_ALREADY_EXISTS
- try:
- scene_path.mkdir(parents=True, exist_ok=True)
- except Exception as error:
- logging.error("Failed during attempt to create new project directory.")
- self.__set_project_modified(True)
- slicer.mrmlScene.EndState(slicer.mrmlScene.SaveState)
- return SaveStatus.FAILED
-
- status = self.save(str(scene_path), internal_call=True, *args, **kwargs)
+ status = self.save(str(scenePath), internalCall=True, *args, **kwargs)
if status != SaveStatus.SUCCEED and status != SaveStatus.IN_PROGRESS: # CANCELLED or FAILED options
- self.__set_project_modified(True)
+ self.__setProjectModified(True)
slicer.mrmlScene.EndState(slicer.mrmlScene.SaveState)
+ self.__setSliceViewConfiguration(sliceViewConfig)
return status
- self.__configure_project_folder(str(scene_path))
- project_file = self.__find_project_file(str(scene_path))
+ self.__configureProjectFolder(str(scenePath))
+ projectFile = self.__findProjectFile(str(scenePath))
- if project_file is None:
- self.__set_project_modified(True)
+ if projectFile is None:
+ self.__setProjectModified(True)
slicer.mrmlScene.EndState(slicer.mrmlScene.SaveState)
+ self.__setSliceViewConfiguration(sliceViewConfig)
return SaveStatus.FAILED
slicer.mrmlScene.EndState(slicer.mrmlScene.SaveState)
- file_project_path = os.path.join(str(scene_path), project_file)
+ # Maintain event behavior from previous version
+ slicer.mrmlScene.StartState(slicer.mrmlScene.ImportState)
+
+ fileProjectPath = (scenePath / projectFile).resolve()
+ slicer.mrmlScene.SetURL(fileProjectPath.as_posix())
+ status = SaveStatus.SUCCEED
+
+ slicer.mrmlScene.EndState(slicer.mrmlScene.ImportState)
+
+ self.projectChangedSignal.emit()
+ self.__setProjectModified(False)
+ self.__setSliceViewConfiguration(sliceViewConfig)
+
+ return status
+
+ def __getFileSize(self, filePath: str) -> int:
+ """Returns the size of a file in bytes if it exists.
+ If the file does not exist, checks if the path is a directory and returns the total size of all files in that directory.
+ """
+ filePath = Path(filePath)
+ if filePath.is_file():
+ return filePath.stat().st_size
+ elif filePath.is_dir():
+ totalSize = 0
+ for item in list(filePath.iterdir()):
+ if item.is_file():
+ totalSize += item.stat().st_size
+ return totalSize
+ else:
+ if filePath.parent.is_dir():
+ totalSize = 0
+ for item in list(filePath.parent.iterdir()):
+ if item.is_file():
+ totalSize += item.stat().st_size
+
+ return totalSize
+
+ logging.error(f"File or directory not found: {filePath}")
+ return 0
+
+ def __estimateImageDataSize(self, imageData: vtk.vtkImageData) -> int:
+ """returns expected file size of a volume node Image Data"""
+ dimensions = imageData.GetDimensions()
+ dataTypeSize = imageData.GetScalarSize()
+ return dimensions[0] * dimensions[1] * dimensions[2] * dataTypeSize
+
+ def estimateNodeSize(self, node: slicer.vtkMRMLNode) -> int:
+ """returns the expected file size in bytes of a node"""
+ if isinstance(node, slicer.vtkMRMLScalarVolumeNode):
+ estimate = self.__estimateImageDataSize(node.GetImageData())
+
+ elif isinstance(node, slicer.vtkMRMLSegmentationNode):
+ try:
+ binaryLabelMap = node.GetSegmentation().GetNthSegment(0).GetRepresentation("Binary labelmap")
+ except AttributeError: # Invalid segment or representation
+ logging.error("Attempt to estimate size of empty segmentation node failed.")
+ estimate = 0
+ else:
+ estimate = self.__estimateImageDataSize(binaryLabelMap)
+
+ elif isinstance(node, slicer.vtkMRMLTableNode):
+ tableDF = slicer.util.dataframeFromTable(node)
+ tableDF: pd.DataFrame = tableDF.round(6)
+ headerCounts = pd.Series(tableDF.columns.astype(str).str.len())
+ numRows, numColumns = tableDF.shape
+ characterCounts = tableDF.astype(str).apply(lambda x: x.astype(str).str.len())
+ estimate = characterCounts.sum().sum() + numRows * numColumns + headerCounts.sum()
+
+ elif isinstance(node, slicer.vtkMRMLModelNode):
+ try:
+ estimate = node.GetPolyData().GetActualMemorySize() * 1024
+ except AttributeError:
+ estimate = 0
+
+ elif isinstance(node, slicer.vtkMRMLSequenceNode):
+ estimate = 0
+ for item in range(node.GetNumberOfDataNodes()):
+ estimate += self.estimateNodeSize(node.GetNthDataNode(item))
+
+ else:
+ estimate = 0
+
+ return estimate * FILE_SIZE_EXTRA_MARGIN
+
+ def getNodeSize(self, node: slicer.vtkMRMLNode, scenePath: str) -> int:
+ """Returns the size of a node's associated file if it exists and is in scene path,
+ the expected estimate size or 0 if it doesn't exist or type has considerable size."""
+ storageNode = node.GetStorageNode()
+ filePath = None
+ if storageNode:
+ filePath = storageNode.GetFullNameFromFileName()
+ if not Path(filePath).is_relative_to(Path(scenePath).parent):
+ filePath = None
+
+ if filePath:
+ return self.__getFileSize(filePath)
+ else:
+ estimate = self.estimateNodeSize(node)
+ return estimate
+
+ def __listAllStorableNodes(self, scenePath: str) -> int:
+ """Lists all storable nodes and their sizes."""
+ totalSize = 0
+ for node in self.__getNodesToSave():
+ nodeSize = self.getNodeSize(node, scenePath)
+ totalSize += nodeSize
+
+ return totalSize
+
+ def __hasWriteAccess(self, path: Path) -> bool:
+ """Check if the user has write access to the given path."""
+
try:
- self.load(file_project_path)
- except Exception as error:
- status = SaveStatus.FAILED
- logging.error(f"A problem occured during the 'Save As Scene' process: {error}\n{traceback.format_exc()}")
- self.__set_project_modified(True)
+ testFile = path / "test_write.tmp"
+ with open(testFile, "w") as f:
+ f.write("test")
+ testFile.unlink(missing_ok=True)
+ return True
+ except (PermissionError, IOError):
+ return False
+
+ def getWritableStorageInfo(self, userHome: Path) -> int:
+ """Get writable storage information for the user home directory."""
+
+ if self.__hasWriteAccess(userHome):
+ usage = psutil.disk_usage(userHome.as_posix())
+ return usage.free
else:
- status = SaveStatus.SUCCEED
- self.__set_project_modified(False)
- self.projectChangedSignal.emit()
+ logging.error(f"Warning: No write access to {userHome}!")
+ return 0
- return status
+ def __validateProjectIsWritable(self, scenePath: str) -> bool:
+ """Checks if project can be written and if has enough space for expected files sizes"""
+ try:
+ userHomeDirectory = Path.home()
+ hdSize = self.getWritableStorageInfo(userHomeDirectory)
+ sceneSize = self.__listAllStorableNodes(scenePath)
+
+ if hdSize <= sceneSize:
+ logging.error(
+ f"Not enough space in drive ({naturalsize(hdSize)}) for scene ({naturalsize(sceneSize)})."
+ )
+ return False
+
+ except Exception as e:
+ logging.error(repr(e))
+ return False
+
+ return True
- def load(self, project_file_path, internal_call=False) -> None:
+ def load(self, projectFilePath: Union[str, Path], internalCall: bool = False) -> bool:
"""Handle custom load scene operation."""
- if isinstance(project_file_path, Path):
- project_file_path = project_file_path.as_posix()
+ if isinstance(projectFilePath, str):
+ projectFilePath = Path(projectFilePath)
- if project_file_path == slicer.mrmlScene.GetURL():
- return
+ projectFilePath = projectFilePath.resolve()
- if not internal_call:
- self.custom_behavior_node_manager.triggerEvent = TriggerEvent.LOAD
+ if projectFilePath.as_posix() == slicer.mrmlScene.GetURL():
+ return True
- self.__clear_node_observers()
+ if not projectFilePath.exists():
+ logging.error(f"Cannot load project from '{projectFilePath.as_posix()}'. File does not exist.")
+ return False
+
+ if not internalCall:
+ self.__customBehaviorNodeManager.triggerEvent = TriggerEvent.LOAD
+
+ self.__clearNodeObservers()
# Close scene before load a new one
self.close()
- slicer.util.loadScene(project_file_path)
- self.__set_project_modified(False)
+ status = True
+ try:
+ slicer.util.loadScene(projectFilePath.as_posix())
+ except Exception as error:
+ logging.error(f"A problem occured during the 'Load Scene' process: {error}\n{traceback.format_exc()}")
+ status = False
+
+ self.__setProjectModified(False)
+
+ return status
def close(self) -> None:
"""Wrapper method to close the project."""
@@ -238,42 +427,42 @@ def close(self) -> None:
def setup(self):
"""Initialize project's event handlers"""
- self.end_close_scene_observer_handler = slicer.mrmlScene.AddObserver(
- slicer.mrmlScene.EndCloseEvent, self.__on_end_close_scene
+ self.__endCloseSceneObserverHandler = slicer.mrmlScene.AddObserver(
+ slicer.mrmlScene.EndCloseEvent, self.__onEndCloseScene
)
- self.modified_event_observer_handler = slicer.mrmlScene.AddObserver("ModifiedEvent", self.__on_scene_modified)
+ self.__modifiedEventObserverHandler = slicer.mrmlScene.AddObserver("ModifiedEvent", self.__onSceneModified)
self.node_added_observer_handler = slicer.mrmlScene.AddObserver(
- slicer.mrmlScene.NodeAddedEvent, self.__on_node_added
+ slicer.mrmlScene.NodeAddedEvent, self.__onNodeAdded
)
- self.end_load_event_observer_handler = slicer.mrmlScene.AddObserver(
- slicer.mrmlScene.EndImportEvent, self.__on_end_load_scene
+ self.__endLoadEventObserverHandler = slicer.mrmlScene.AddObserver(
+ slicer.mrmlScene.EndImportEvent, self.__onEndLoadScene
)
- self.start_load_event_observer_handler = slicer.mrmlScene.AddObserver(
- slicer.mrmlScene.StartImportEvent, self.__on_start_load_scene
+ self.__startLoadEventObserverHandler = slicer.mrmlScene.AddObserver(
+ slicer.mrmlScene.StartImportEvent, self.__onStartLoadScene
)
- def __on_start_load_scene(self, *args, **kwargs):
+ def __onStartLoadScene(self, *args, **kwargs):
"""Handle slicer' start load scene event."""
- slicer.mrmlScene.RemoveObserver(self.modified_event_observer_handler)
+ slicer.mrmlScene.RemoveObserver(self.__modifiedEventObserverHandler)
- def __on_end_close_scene(self, *args):
+ def __onEndCloseScene(self, *args):
"""Handle slicer' end close scene event."""
- self.__slices_shown = False
+ self.__slicesShown = False
def process():
- self.__set_project_modified(False)
+ self.__setProjectModified(False)
self.projectChangedSignal.emit()
# Add 'process' method to the end of the Qt's events queue.
qt.QTimer.singleShot(0, process)
- def __on_end_load_scene(self, *args, **kwargs):
+ def __onEndLoadScene(self, *args, **kwargs):
"""Handle slicer' end load scene event."""
- slicer.util.mainWindow().setWindowModified(False)
+ slicer.modules.AppContextInstance.mainWindow.setWindowModified(False)
self.projectChangedSignal.emit()
- self.modified_event_observer_handler = slicer.mrmlScene.AddObserver("ModifiedEvent", self.__on_scene_modified)
+ self.__modifiedEventObserverHandler = slicer.mrmlScene.AddObserver("ModifiedEvent", self.__onSceneModified)
# Hide axis labels for legacy projects which may have them.
# This can probably be removed in the future, as it is also done
@@ -281,28 +470,14 @@ def __on_end_load_scene(self, *args, **kwargs):
viewNode = slicer.app.layoutManager().threeDWidget(0).mrmlViewNode()
viewNode.SetAxisLabelsVisible(False)
- # Without this the Image Log segmenter doesn't correctly restore the selected segmentation node
- try:
- image_log_data_logic = slicer.modules.imagelogdata.widgetRepresentation().self().logic
- image_log_data_logic.imageLogSegmenterWidget.self().initializeSavedNodes()
+ self.__clearMaskSettingsOnAllSegmentEditors()
- # Image Log number of views restoration
- if slicer.app.layoutManager().layout >= 15000:
- image_log_data_logic.configurationsNode = None
- image_log_data_logic.loadConfiguration()
-
- except ValueError:
- # Widget has been deleted after test
- pass
-
- self.__clear_mask_settings_on_all_segment_editors()
-
- def __on_scene_modified(self, *args, **kwargs):
+ def __onSceneModified(self, *args, **kwargs):
"""Handle slicer' scene modified event."""
- self.__set_project_modified(True)
+ self.__setProjectModified(True)
@vtk.calldata_type(vtk.VTK_OBJECT)
- def __on_node_added(self, caller, eventId, callData):
+ def __onNodeAdded(self, caller, eventId, callData):
"""Handle slicer' node added to scene event."""
if issubclass(
type(callData),
@@ -312,15 +487,20 @@ def __on_node_added(self, caller, eventId, callData):
slicer.vtkMRMLSubjectHierarchyNode,
slicer.vtkMRMLTransformNode,
slicer.vtkMRMLSliceDisplayNode,
+ slicer.vtkMRMLTableViewNode,
+ slicer.vtkMRMLSegmentEditorNode,
),
):
return
+ if isinstance(callData, slicer.vtkMRMLTableNode) and callData.GetName() == "Default mineral colors":
+ return
+
observer = NodeObserver(node=callData, parent=self)
- observer.modifiedSignal.connect(self.__on_scene_modified)
- observer.removedSignal.connect(self.__on_observed_node_removed)
+ observer.modifiedSignal.connect(self.__onSceneModified)
+ observer.removedSignal.connect(self.__onObservedNodeRemoved)
- self.__node_observers.append(observer)
+ self.__nodeObservers.append(observer)
if isinstance(callData, slicer.vtkMRMLVolumeNode):
@@ -331,7 +511,7 @@ def onVolumeModified(node_observer: NodeObserver, node: slicer.vtkMRMLNode) -> N
timer = qt.QTimer()
def onTimeout():
- self.__on_volume_modified(node)
+ self.__onVolumeModified(node)
timer.timeout.disconnect(onTimeout)
timer.setSingleShot(True)
@@ -347,49 +527,49 @@ def onTimeout():
segmentation_display = callData
HierarchyVisibilityManager(segmentation_display, lambda node: node.GetDisplayableNode())
- def __on_observed_node_removed(self, node_observer: NodeObserver, node: slicer.vtkMRMLNode) -> None:
+ def __onObservedNodeRemoved(self, node_observer: NodeObserver, node: slicer.vtkMRMLNode) -> None:
"""Handle when a node being observed is removed from the scene."""
- if node_observer not in self.__node_observers:
+ if node_observer not in self.__nodeObservers:
return
- self.__node_observers.remove(node_observer)
+ self.__nodeObservers.remove(node_observer)
del node_observer
- def __clear_node_observers(self):
+ def __clearNodeObservers(self) -> None:
"""Clear the node observer's list and remove the observer handlers from each one."""
- for node_observer in self.__node_observers[:]:
+ for node_observer in self.__nodeObservers[:]:
node_observer.clear()
del node_observer
- self.__node_observers.clear()
+ self.__nodeObservers.clear()
- def __configure_project_folder(self, project_path):
+ def __configureProjectFolder(self, projectPath: str) -> None:
"""Applies folder's custom configuration
Args:
- project_path (str): the folder path
+ projectPath (str): the folder path
"""
platform = os.name
if platform == "nt": # Windows:
- self.__create_project_folder_configuration_file(
- project_path=project_path,
+ self.__createProjectFolderConfigurationFile(
+ projectPath=projectPath,
ConfirmFileOp=1,
NoSharing=0,
- IconFile=self.__folder_icon_path,
+ IconFile=self.__folderIconPath,
IconIndex=0,
InfoTip="This is a Geoslicer project folder",
)
elif platform == "posix": # Linux:
- self.__create_project_folder_configuration_file(project_path=project_path, Icon=self.__folder_icon_path)
+ self.__createProjectFolderConfigurationFile(projectPath=projectPath, Icon=self.__folderIconPath)
else:
pass
- def __create_project_folder_configuration_file(self, project_path, *args, **kwargs):
+ def __createProjectFolderConfigurationFile(self, projectPath: str, *args, **kwargs) -> None:
"""Wrapper for creating the file that configures the folder attributes.
Works with the following OS: Windows, Linux
Args:
- project_path (str): the folder path to be configured
+ projectPath (str): the folder path to be configured
Raises:
Exception: Not supported OS.
@@ -398,106 +578,106 @@ def __create_project_folder_configuration_file(self, project_path, *args, **kwar
platform = os.name
if platform == "nt": # Windows
- icon_parameter_label = "IconFile"
- config_file_name = "desktop.ini"
+ iconParameterLabel = "IconFile"
+ configFileName = "desktop.ini"
header = "[.ShellClassInfo]"
- def post_setup(config_file):
+ def post_setup(configFile):
# Add attributes to the config file (Mandatory)
- os.system('attrib +h +s "{}"'.format(config_file))
+ os.system('attrib +h +s "{}"'.format(configFile))
# Add attributes to the directory (Mandatory)
- os.system('attrib +r "{}"'.format(os.path.dirname(config_file)))
+ os.system('attrib +r "{}"'.format(os.path.dirname(configFile)))
elif platform == "posix": # Linux
- icon_parameter_label = "Icon"
- config_file_name = ".directory"
+ iconParameterLabel = "Icon"
+ configFileName = ".directory"
header = "[Desktop Entry]"
- def post_setup(config_file):
+ def post_setup(configFile):
# TODO: test if this works consistently in different Linux systems
- command = f'gio set -t string "{project_path}"'
- command += f' metadata::custom-icon "file://{project_path}/ProjectIcon.ico"'
- command += f' && touch "{project_path}"'
+ command = f'gio set -t string "{projectPath}"'
+ command += f' metadata::custom-icon "file://{projectPath}/ProjectIcon.ico"'
+ command += f' && touch "{projectPath}"'
os.system(command)
else:
raise Exception("Current OS is not supported by this function.")
- if icon_parameter_label in kwargs.keys():
- icon_file = kwargs[icon_parameter_label]
- if os.path.isfile(icon_file):
- shutil.copy2(icon_file, project_path)
- kwargs[icon_parameter_label] = os.path.basename(icon_file)
+ if iconParameterLabel in kwargs.keys():
+ iconFile = kwargs[iconParameterLabel]
+ if os.path.isfile(iconFile):
+ shutil.copy2(iconFile, projectPath)
+ kwargs[iconParameterLabel] = os.path.basename(iconFile)
data = header + "\n"
for k, v in kwargs.items():
data += "{}={}\n".format(k, v)
- config_file = os.path.join(project_path, config_file_name)
+ configFile = os.path.join(projectPath, configFileName)
# Create folder configuration file
- with open(config_file, "w", encoding="utf-8") as file:
+ with open(configFile, "w", encoding="utf-8") as file:
file.write(data)
# Apply post configurations
- post_setup(config_file)
+ post_setup(configFile)
- def __find_project_file(self, project_path: str, extension=".mrml"):
+ def __findProjectFile(self, projectPath: str, extension=".mrml") -> str:
"""Search for project file inside path.
Args:
- project_path (str): The directory to search for the project file
+ projectPath (str): The directory to search for the project file
extension (str, optional): The project file extension. Defaults to ".mrml".
Returns:
str: The project file path if it was found. Otherwise, None
"""
- project_file = None
- for _, _, files in os.walk(project_path):
+ projectFile = None
+ for _, _, files in os.walk(projectPath):
for file in files:
if file.endswith(extension):
- project_file = file
+ projectFile = file
break
- return project_file
+ return projectFile
- def __set_project_modified(self, mode: bool):
+ def __setProjectModified(self, mode: bool) -> None:
"""Handle project modification's events."""
- slicer.util.mainWindow().setWindowModified(mode)
+ slicer.modules.AppContextInstance.mainWindow.setWindowModified(mode)
- def __config_compression_mode(self, *args, **kwargs):
+ def __configCompressionMode(self, *args, **kwargs) -> None:
properties = DEFAULT_PROPERTIES.copy()
- custom_properties = kwargs.get("properties")
- if custom_properties is not None and isinstance(custom_properties, dict):
- properties.update(custom_properties)
+ customProperties = kwargs.get("properties")
+ if customProperties is not None and isinstance(customProperties, dict):
+ properties.update(customProperties)
- use_compression = properties.get("useCompression", 0)
+ useCompression = properties.get("useCompression", 0)
# Add default storage nodes for volume node types
- default_volume_storage_node = slicer.vtkMRMLVolumeArchetypeStorageNode()
- default_volume_storage_node.SetUseCompression(use_compression)
- slicer.mrmlScene.AddDefaultNode(default_volume_storage_node)
+ defaultVolumeStorageNode = slicer.vtkMRMLVolumeArchetypeStorageNode()
+ defaultVolumeStorageNode.SetUseCompression(useCompression)
+ slicer.mrmlScene.AddDefaultNode(defaultVolumeStorageNode)
# Add default storage nodes for segmentation node types
- default_seg_storage_node = slicer.vtkMRMLSegmentationStorageNode()
- default_seg_storage_node.SetUseCompression(use_compression)
- slicer.mrmlScene.AddDefaultNode(default_seg_storage_node)
+ defaultSegStorageNode = slicer.vtkMRMLSegmentationStorageNode()
+ defaultSegStorageNode.SetUseCompression(useCompression)
+ slicer.mrmlScene.AddDefaultNode(defaultSegStorageNode)
- def __get_nodes_to_save(self) -> List[slicer.vtkMRMLNode]:
- nodes_collection = slicer.mrmlScene.GetNodesByClass("vtkMRMLStorableNode")
- nodesCount = nodes_collection.GetNumberOfItems()
+ def __getNodesToSave(self) -> List[slicer.vtkMRMLNode]:
+ nodesCollection = slicer.mrmlScene.GetNodesByClass("vtkMRMLStorableNode")
+ nodesCount = nodesCollection.GetNumberOfItems()
for idx in range(nodesCount):
- node = nodes_collection.GetItemAsObject(idx)
+ node = nodesCollection.GetItemAsObject(idx)
hide = bool(node.GetHideFromEditors())
- save_with_scene = bool(node.GetSaveWithScene())
+ saveWithScene = bool(node.GetSaveWithScene())
- if not save_with_scene:
+ if not saveWithScene:
continue
yield node
- def __save_nodes(self, *args, **kwargs):
+ def __saveNodes(self, firstSave: bool, *args, **kwargs) -> bool:
"""Handle the nodes saving process.
Returns:
@@ -505,65 +685,76 @@ def __save_nodes(self, *args, **kwargs):
"""
status = True
- data_folder = Path(slicer.mrmlScene.GetRootDirectory()) / "Data"
- files_to_delete = set(data_folder.iterdir())
+ rootPath = Path(slicer.mrmlScene.GetRootDirectory())
+ dataFolder = (rootPath / "Data").resolve()
+ if dataFolder.exists():
+ filesToDelete = {path for path in dataFolder.iterdir()}
+ else:
+ filesToDelete = set()
try:
- for node in self.__get_nodes_to_save():
+ for node in self.__getNodesToSave():
if not self.__handle_storable_node(node, *args, **kwargs):
continue
- storage_node = node.GetStorageNode()
- file_path = Path(storage_node.GetFileName()).resolve()
- file_paths = {file_path}
+ storageNode = node.GetStorageNode()
+ filePath = Path(storageNode.GetFileName()).resolve()
+ filePaths = {filePath}
# File name list. Typically used to store a table schema.
- for i in range(storage_node.GetNumberOfFileNames()):
- path = Path(storage_node.GetNthFileName(i)).resolve()
- file_paths.add(path)
- files_to_delete -= file_paths
+ for i in range(storageNode.GetNumberOfFileNames()):
+ path = Path(storageNode.GetNthFileName(i)).resolve()
+ filePaths.add(path)
+ filesToDelete -= filePaths
- file_already_exists = all(path.exists() for path in file_paths)
- if node.GetModifiedSinceRead() is False and file_already_exists:
+ fileAlreadyExists = all(path.exists() for path in filePaths)
+ if node.GetModifiedSinceRead() is False and fileAlreadyExists:
continue
- file_path = str(file_path)
- status = slicer.util.saveNode(node, file_path)
- if not status:
- logging.error(
- "Failed to save {} node's file at the location: {}\n{}".format(
- node.GetName(), file_path, traceback.format_exc()
+ if filePath.exists() or not firstSave:
+ relativeNodeFilePath = filePath.relative_to(rootPath).as_posix()
+ status = slicer.util.saveNode(node, relativeNodeFilePath)
+ if not status:
+ logging.error(
+ "Failed to save {} node's file at the location: {}\n{}".format(
+ node.GetName(), filePath, traceback.format_exc()
+ )
)
- )
- return False
+ return False
+ else:
+ logging.debug("Node {} was saved succesfully at {}".format(node.GetName(), filePath))
+
+ for filePath in filesToDelete:
+ if filePath.is_dir():
+ shutil.rmtree(filePath)
else:
- logging.debug("Node {} was saved succesfully at {}".format(node.GetName(), file_path))
+ filePath.unlink()
+ logging.debug(f"{filePath} was deleted as it is no longer associated to any node.")
- for file_path in files_to_delete:
- file_path.unlink()
- logging.debug(f"File {file_path} was deleted as it is no longer associated to any node.")
except Exception as error:
logging.error(f"A problem has occured during the nodes' save process: {error}\n{traceback.format_exc()}")
return False
return status
- def __get_localized_storage_node(self, node, local_storage_dir):
- storage_node = node.GetStorageNode()
+ def __getLocalizedStorageNode(
+ self, node: slicer.vtkMRMLStorableNode, localStorageDir: Path
+ ) -> slicer.vtkMRMLStorageNode:
+ storageNode = node.GetStorageNode()
# All files should be stored in the Data directory.
# If the node's file name is in another directory, create a new default storage node.
- if storage_node and storage_node.GetFileName():
- file_path = Path(storage_node.GetFileName()).resolve()
- if file_path.parent != local_storage_dir:
- slicer.mrmlScene.RemoveNode(storage_node)
+ if storageNode and storageNode.GetFileName():
+ filePath = Path(storageNode.GetFileName()).resolve()
+ if filePath.parent != localStorageDir:
+ slicer.mrmlScene.RemoveNode(storageNode)
node.AddDefaultStorageNode()
else:
node.AddDefaultStorageNode()
return node.GetStorageNode()
- def __handle_storable_node(self, node, *args, **kwargs):
+ def __handle_storable_node(self, node: slicer.vtkMRMLStorableNode, *args, **kwargs) -> bool:
"""Function that checks if storable node has a valid storage node or if it could be created.
In case of creating a new storage node, it will define its filename.
Otherwise, if it wouldn't be possible to create a new storage node,
@@ -571,7 +762,7 @@ def __handle_storable_node(self, node, *args, **kwargs):
(Probably the scene file's writer will handle it)
Args:
- node (vtk.vtkMRMLStorableNode): the storable node object.
+ node (slicer.vtkMRMLStorableNode): the storable node object.
Returns:
bool: True if node has a valid storage node, otherwise returns False.
@@ -579,43 +770,50 @@ def __handle_storable_node(self, node, *args, **kwargs):
if not hasattr(node, "GetStorageNode"):
return False
- data_folder = Path(slicer.mrmlScene.GetRootDirectory()) / "Data"
+ dataFolder = (Path(slicer.mrmlScene.GetRootDirectory()) / "Data").resolve()
- storage_node = self.__get_localized_storage_node(node, data_folder)
- if not storage_node:
+ storageNode = self.__getLocalizedStorageNode(node, dataFolder)
+ if not storageNode:
return False
- filepath = Path(BUGFIX_handle_copy_suffix_on_cloned_nodes(storage_node))
+ filepath = Path(handleCopySuffixOnClonedNodes(storageNode))
if not (filepath and is_valid_filename(filepath.name)):
- filename = generate_unique_node_name(node.GetName(), data_folder)
- ext = storage_node.GetDefaultWriteFileExtension()
- storage_node.SetFileName(str(data_folder / filename) + f".{ext}")
+ tempFileName = storageNode.GetTempFileName()
+ if tempFileName and Path(tempFileName).is_relative_to(dataFolder):
+ storageNode.SetFileName(tempFileName)
+ else:
+ ext = storageNode.GetDefaultWriteFileExtension()
+ filePath = f"{(dataFolder / node.GetID()).as_posix()}.{ext}"
+ storageNode.SetFileName(filePath)
# Define compression mode
properties = DEFAULT_PROPERTIES.copy()
- custom_properties = kwargs.get("properties")
- if custom_properties is not None and isinstance(custom_properties, dict):
- properties.update(custom_properties)
+ customProperties = kwargs.get("properties")
+ if customProperties is not None and isinstance(customProperties, dict):
+ properties.update(customProperties)
- use_compression = properties.get("useCompression", 0)
- storage_node.SetUseCompression(use_compression)
+ useCompression = properties.get("useCompression", 0)
+ storageNode.SetUseCompression(useCompression)
return True
- def __save_scene(self, project_url, *args, **kwargs):
+ def __saveScene(self, projectUrl: Union[str, Path], *args, **kwargs) -> bool:
"""Handle the nodes saving process.
Args:
- project_url (str): the scene URL string.
+ projectUrl (str): the scene URL string.
Returns:
bool: True if worked succesfully, otherwise returns False.
"""
status = True
+ if isinstance(projectUrl, str):
+ projectUrl = Path(projectUrl).resolve()
+
try:
- status = slicer.util.saveScene(project_url)
+ status = slicer.util.saveScene(projectUrl.as_posix())
except Exception as error:
status = False
logging.error(
@@ -628,21 +826,21 @@ def __save_scene(self, project_url, *args, **kwargs):
return status
- def __on_volume_modified(self, volume: slicer.vtkMRMLNode) -> None:
+ def __onVolumeModified(self, volume: slicer.vtkMRMLNode) -> None:
if volume is None:
return
autoFrameOff = volume.GetAttribute("AutoFrameOff")
autoSliceVisibleOff = volume.GetAttribute("AutoSliceVisibleOff")
if volume.GetImageData():
- if not self.__slices_shown and not slicer.mrmlScene.GetURL() and autoSliceVisibleOff != "true":
+ if not self.__slicesShown and not slicer.mrmlScene.GetURL() and autoSliceVisibleOff != "true":
# Open slice eyes once for a new project
- self.__show_slices_in_3d()
- self.__slices_shown = True
+ self.__showSlicesIn3D()
+ self.__slicesShown = True
if autoFrameOff != "true":
- self.__frame_volume(volume)
+ self.__frameVolume(volume)
- def __show_slices_in_3d(self):
+ def __showSlicesIn3D(self):
if slicer.mrmlScene.GetNumberOfNodesByClass("vtkMRMLScalarVolumeNode") == 0:
return
layoutManager = slicer.app.layoutManager()
@@ -650,7 +848,7 @@ def __show_slices_in_3d(self):
controller = layoutManager.sliceWidget(sliceViewName).sliceController()
controller.setSliceVisible(True)
- def __frame_volume(self, volume):
+ def __frameVolume(self, volume):
"""Reposition camera so it points to the center of the volume and
position it so the volume is reasonably visible.
"""
@@ -668,7 +866,54 @@ def __frame_volume(self, volume):
cam.SetPosition(pos)
camNode.ResetClippingRange()
- def __clear_mask_settings_on_all_segment_editors(self):
+ def __clearMaskSettingsOnAllSegmentEditors(self):
nodes = slicer.util.getNodesByClass("vtkMRMLSegmentEditorNode")
for node in nodes:
node.SourceVolumeIntensityMaskOff()
+
+
+def getAvailableFilename(name: str, dirpath: Path):
+ files = [file for file in dirpath.iterdir() if file.is_file()]
+ fileNames = [file.name for file in files]
+ if name not in fileNames:
+ return name
+
+ def check(base_name, filename):
+ pattern = rf"^({re.escape(base_name)})?\s?(\(\d+\))?$"
+ return re.fullmatch(pattern, filename)
+
+ name, ext = os.path.splitext(name)
+ count = 0
+ suffixCount = 0
+ currentNameHasSuffix = False
+
+ sch = re.search(rf"^(\w+)?\s+?(\(\d+\))?", name)
+ if sch and len(sch.groups()) >= 2:
+ name = sch.group(1)
+ currentNameHasSuffix = True
+ number = int(re.sub(r"[()]", "", sch.group(2)))
+ suffixCount = max(suffixCount, int(number))
+
+ for file in files:
+ fileStem = os.path.splitext(file.name)[0]
+ match = check(name, fileStem)
+ if not match:
+ continue
+
+ pattern = rf"^({re.escape(name)})?\s?(\(\d+\))?$"
+ if sch := re.search(pattern, fileStem):
+ if len(sch.groups()) < 2:
+ continue
+
+ if sch.group(2):
+ number = int(re.sub(r"[()]", "", sch.group(2)))
+ suffixCount = max(suffixCount, int(number))
+
+ count += 1
+
+ if count == 0 and suffixCount == 0 and currentNameHasSuffix is False:
+ unique_name = name + ext
+ else:
+ unique_name = f"{name} ({suffixCount+1}){ext}"
+
+ return unique_name
diff --git a/src/ltrace/ltrace/slicer/tests/ltrace_plugin_test.py b/src/ltrace/ltrace/slicer/tests/ltrace_plugin_test.py
index 5ba75bd..f9e2e06 100644
--- a/src/ltrace/ltrace/slicer/tests/ltrace_plugin_test.py
+++ b/src/ltrace/ltrace/slicer/tests/ltrace_plugin_test.py
@@ -214,6 +214,8 @@ def runTest(self):
if self.__test_state == TestState.CANCELLED and idx != len(test_cases) - 1:
break
+ test.reset()
+
self.setUp(test)
log(
@@ -445,7 +447,7 @@ def __on_timeout(self) -> None:
return
# Clear possible message boxes freezing the operation
- message_boxes = slicer.util.mainWindow().findChildren(qt.QMessageBox)
+ message_boxes = slicer.modules.AppContextInstance.mainWindow.findChildren(qt.QMessageBox)
message_from_boxes = []
for message_box in message_boxes:
if not message_box.visible:
diff --git a/src/ltrace/ltrace/slicer/tests/ltrace_tests_model.py b/src/ltrace/ltrace/slicer/tests/ltrace_tests_model.py
index 4f40dee..7bea9b7 100644
--- a/src/ltrace/ltrace/slicer/tests/ltrace_tests_model.py
+++ b/src/ltrace/ltrace/slicer/tests/ltrace_tests_model.py
@@ -9,6 +9,7 @@
from enum import Enum
from ltrace.slicer.tests.ltrace_plugin_test import LTracePluginTest
from ltrace.slicer.tests.constants import TestState
+from typing import List
class TestsSource(Enum):
@@ -232,16 +233,20 @@ def cancel(self):
self.__cancelling = True
def result(self):
- for test_suite in self.test_suite_list:
- if not test_suite.enabled:
- continue
+ def check_suite(suite_list: List[TestSuiteData]) -> TestState:
+ for test_suite in suite_list:
+ if not test_suite.enabled:
+ continue
+
+ enabled_test_cases = [case for case in test_suite.test_case_data_list if case.enabled is True]
+
+ for case in enabled_test_cases:
+ if case.test_status != TestState.SUCCEED:
+ return TestState.FAILED
- enabled_test_cases = [case for case in test_suite.test_case_data_list if case.enabled is True]
- for case in enabled_test_cases:
- if case.test_status != TestState.SUCCEED:
- return TestState.FAILED
+ return TestState.SUCCEED
- return TestState.SUCCEED
+ return check_suite(self.test_suite_list + self.generate_suite_list)
def __get_all_suites(self, test_source=TestsSource.ANY):
test_suite_list = []
@@ -338,7 +343,7 @@ def __get_generate_methods_data(self, test_class):
def run_tests(self, **kwargs):
self.__is_running = True
shuffle = kwargs.get("shuffle", False)
- test_suite_list = kwargs.get("suite_list")
+ test_suite_list: List[TestSuiteData] = kwargs.get("suite_list")
if not isinstance(test_suite_list, list) or test_suite_list is None:
self.__cancelling = False
diff --git a/src/ltrace/ltrace/slicer/tests/ltrace_tests_widget.py b/src/ltrace/ltrace/slicer/tests/ltrace_tests_widget.py
index 8e1c896..adcc8c7 100644
--- a/src/ltrace/ltrace/slicer/tests/ltrace_tests_widget.py
+++ b/src/ltrace/ltrace/slicer/tests/ltrace_tests_widget.py
@@ -4,429 +4,460 @@
from ltrace.slicer.tests.constants import TestState
from ltrace.slicer.tests.ltrace_tests_model import LTraceTestsModel, TestSuiteData, TestCaseData, TestsSource
-from ltrace.slicer.tests.utils import log
+from ltrace.slicer.tests.utils import log, loadAllModules
from pathlib import Path
+from typing import List, Union
RESOURCES_DIR = Path(__file__).parent / "resources"
-class LogWidgetHandler(logging.Handler):
- """Custom logging handler for showing log in text browser widget."""
+class ASortFilterProxyModel(qt.QSortFilterProxyModel):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.sort(0, qt.Qt.AscendingOrder)
+ self.setSortCaseSensitivity(qt.Qt.CaseInsensitive)
+ self.setFilterCaseSensitivity(qt.Qt.CaseInsensitive)
- def __init__(self, callback, *args, **kwargs):
+ def filterAcceptsRow(self, sourceRow, sourceParent):
+ if self.sourceModel is None:
+ return True
+
+ model = sourceParent.model()
+ if model is not None:
+ parentItem = model.item(sourceParent.row())
+
+ if parentItem.isEnabled():
+ return True
+
+ return qt.QSortFilterProxyModel.filterAcceptsRow(self, sourceRow, sourceParent)
+
+
+class ATreeViewItem(qt.QStandardItem):
+ def __init__(self, data: Union[TestCaseData, TestSuiteData], *args, **kwargs):
super().__init__(*args, **kwargs)
- formatter = logging.Formatter("%(message)s%(end)s")
- self.setFormatter(formatter)
- self.__callback = callback
+ assert data is not None
+ self._testData = None
+ self.testData = data
- def emit(self, record):
- message = self.format(record)
+ @property
+ def testData(self):
+ return self._testData
- if hasattr(self, "end"):
- self.terminator = ""
+ @testData.setter
+ def testData(self, data):
+ if self._testData is not None:
+ self._testData.enablement_changed.disconnect(self.onEnablementChanged)
- if self.__callback is not None:
- self.__callback(message)
+ self._testData = data
+ self._testData.enablement_changed.connect(self.onEnablementChanged)
+ self._setup()
-class TestSuiteDataWidgetItem(qt.QTreeWidgetItem):
- def __init__(self, data: TestSuiteData, *args, **kwargs):
- qt.QTreeWidgetItem.__init__(self, *args, **kwargs)
- qt.QObject.__init__(self, *args, **kwargs)
- self.__test_suite_data = None
- self.__test_case_data_widget_list = []
- self.test_suite_data = data
-
- def __setup(self):
- self.setText(0, self.__test_suite_data.name)
+ def _setup(self):
+ self.setEditable(False)
+ self.setText(self.testData.name)
+ checkState = qt.Qt.Checked if self.testData.enabled else qt.Qt.Unchecked
self.setFlags(self.flags() | qt.Qt.ItemIsUserCheckable | qt.Qt.ItemIsSelectable)
- check_state = qt.Qt.Checked if self.__test_suite_data.enabled else qt.Qt.Unchecked
- self.setCheckState(0, check_state)
+ self.setCheckState(checkState)
- def set_check_box_enabled(self, state: bool):
+ def setCheckBoxEnabled(self, state: bool):
if state:
self.setFlags(self.flags() | qt.Qt.ItemIsEnabled)
else:
self.setFlags(self.flags() & ~qt.Qt.ItemIsEnabled)
- for test_case_widget in self.__test_case_data_widget_list:
- test_case_widget.set_check_box_enabled(state)
+ def onEnablementChanged(self, state: bool):
+ checkState = qt.Qt.Checked if state is True else qt.Qt.Unchecked
+ self.setCheckState(checkState)
- @property
- def test_case_data_widget_list(self):
- return self.__test_case_data_widget_list
- @property
- def test_suite_data(self):
- return self.__test_suite_data
+class ATreeView(qt.QTreeView):
+ def __init__(self, *args, **kwargs) -> None:
+ super().__init__(*args, **kwargs)
+ self.setHeaderHidden(True)
+ self.standardItemModel = qt.QStandardItemModel()
+ self.sortModel = ASortFilterProxyModel(self)
+ self.sortModel.setSourceModel(self.standardItemModel)
+ self.setModel(self.sortModel)
+ self.standardItemModel.itemChanged.connect(self.__onTreeViewItemClicked)
+
+ def clear(self) -> None:
+ self.standardItemModel.clear()
+
+ def setFilterRegularExpression(self, text) -> None:
+ self.sortModel.setFilterRegularExpression(text)
+
+ def appendItem(self, item: ATreeViewItem) -> None:
+ self.standardItemModel.invisibleRootItem().appendRow(item)
+
+ def selectAll(self) -> None:
+ for item in self.__items():
+ item.testData.enabled = True
+
+ def unselectAll(self) -> None:
+ for item in self.__items():
+ item.testData.enabled = False
+
+ def __items(self):
+ totalRowsDisplayed = self.sortModel.rowCount()
+
+ for i in range(totalRowsDisplayed):
+ modelIndex = self.sortModel.index(i, 0)
+ modelIndex2 = self.sortModel.mapToSource(modelIndex)
+ item = self.standardItemModel.itemFromIndex(modelIndex2)
+ if item is None:
+ continue
- @test_suite_data.setter
- def test_suite_data(self, data: TestSuiteData):
- if self.__test_suite_data is not None:
- self.__test_suite_data.enablement_changed.disconnect(self.on_enablement_changed)
+ yield item
- for test_case_widget in self.__test_case_data_widget_list:
- test_case_widget.blockSignals(True)
- del test_case_widget
+ def __onTreeViewItemClicked(self, item):
+ itemCheckState = item.checkState()
- for test_case_data in data.test_case_data_list:
- child_item = TestCaseDataWidgetItem(test_case_data)
- self.__test_case_data_widget_list.append(child_item)
- self.addChild(child_item)
+ if item is None:
+ raise RuntimeError("Selected item data wasn't found in the current list. Please restart the application.")
- self.__test_suite_data = data
- self.__test_suite_data.enablement_changed.connect(self.on_enablement_changed)
- self.__setup()
+ item.testData.enabled = True if itemCheckState == qt.Qt.Checked else False
- def on_enablement_changed(self, state):
- check_state = qt.Qt.Checked if state is True else qt.Qt.Unchecked
- self.setCheckState(0, check_state)
- self.setExpanded(state)
+ def filteredItemsCount(self):
+ return self.sortModel.rowCount()
-class TestCaseDataWidgetItem(qt.QTreeWidgetItem):
+class TestCaseTreeViewItem(ATreeViewItem):
def __init__(self, data: TestCaseData, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.__test_case_data = None
- self.test_case_data = data
+ super().__init__(data, *args, **kwargs)
- def __setup(self):
- self.setText(0, self.__test_case_data.name)
- self.setFlags(self.flags() | qt.Qt.ItemIsUserCheckable | qt.Qt.ItemIsSelectable)
- check_state = qt.Qt.Checked if self.__test_case_data.enabled else qt.Qt.Unchecked
- self.setCheckState(0, check_state)
- self.setToolTip(0, self.__test_case_data.name)
- def set_check_box_enabled(self, state: bool):
- if state:
- self.setFlags(self.flags() | qt.Qt.ItemIsEnabled)
- else:
- self.setFlags(self.flags() & ~qt.Qt.ItemIsEnabled)
+class TestSuiteTreeViewItem(ATreeViewItem):
+ def __init__(self, data: TestSuiteData, *args, **kwargs):
+ super().__init__(data, *args, **kwargs)
- @property
- def test_case_data(self):
- return self.__test_case_data
+ @ATreeViewItem.testData.setter
+ def testData(self, data: TestSuiteData):
+ if self._testData is not None:
+ self._testData.enablement_changed.disconnect(self.onEnablementChanged)
+
+ self.removeRows(0, self.rowCount())
- @test_case_data.setter
- def test_case_data(self, data: TestCaseData):
- if self.__test_case_data is not None:
- self.__test_case_data.enablement_changed.disconnect(self.on_enablement_changed)
+ for testCaseData in data.test_case_data_list:
+ childItem = TestCaseTreeViewItem(testCaseData)
+ self.appendRow(childItem)
- self.__test_case_data = data
- self.__test_case_data.enablement_changed.connect(self.on_enablement_changed)
- self.__setup()
+ self._testData = data
+ self._testData.enablement_changed.connect(self.onEnablementChanged)
+ self._setup()
- def on_enablement_changed(self, state):
- check_state = qt.Qt.Checked if state is True else qt.Qt.Unchecked
- self.setCheckState(0, check_state)
+
+class LogWidgetHandler(logging.Handler):
+ """Custom logging handler for showing log in text browser widget."""
+
+ def __init__(self, callback, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ formatter = logging.Formatter("%(message)s%(end)s")
+ self.setFormatter(formatter)
+ self.__callback = callback
+
+ def emit(self, record):
+ message = self.format(record)
+
+ if hasattr(self, "end"):
+ self.terminator = ""
+
+ if self.__callback is not None:
+ self.__callback(message)
class LTraceTestsWidget(qt.QDialog):
- def __init__(self, parent=None, current_module=None, *args, **kwargs):
+ TEST_TAB = 0
+ GENERATE_TAB = 1
+
+ def __init__(self, parent=None, currentModule=None, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
+ loadAllModules()
self.__model = LTraceTestsModel(test_source=TestsSource.GEOSLICER)
- self.__test_suite_widget_list = []
- self.__generate_suite_widget_list = []
- self.__test_cases_count = 0
- self.__generate_cases_count = 0
- self.setup_ui()
- self.__install_logger_handler()
- self.__populate_tree()
- self.__select_current_module_tests(current_module)
-
- def __select_current_module_tests(self, current_module):
- if current_module is None or not hasattr(self, "tree_widget"):
+ self.__testSuiteItemList: List[TestSuiteTreeViewItem] = []
+ self.__generateSuiteItemList: List[TestCaseTreeViewItem] = []
+ self.__testCasesCount = 0
+ self.__generateCasesCount = 0
+ self.__setupUi()
+ self.__installLoggerHandler()
+ self.__populateTree()
+ self.__selectCurrentModuleTests(currentModule)
+
+ def __selectCurrentModuleTests(self, currentModule):
+ if currentModule is None or not hasattr(self, "testTreeView"):
return
- for test_suite in self.__test_suite_widget_list:
- if current_module not in test_suite.test_suite_data.name:
+ for testSuiteItem in self.__testSuiteItemList:
+ if currentModule not in testSuiteItem.testData.name:
continue
- test_suite.test_suite_data.enabled = True
- self.tree_widget_tests.scrollToItem(test_suite)
+ testSuiteItem.testData.enabled = True
+ self.__testTreeView.scrollTo(testSuiteItem.index())
break
- def setup_ui(self):
+ def __setupUi(self):
self.setMinimumSize(1080, 720)
self.setWindowTitle("GeoSlicer Test GUI")
self.setWindowFlags(self.windowFlags() & ~qt.Qt.WindowContextHelpButtonHint | qt.Qt.WindowMinMaxButtonsHint)
# Options layout
- ## Test sources checkbox
- self.__geoslicer_test_source_check_box = qt.QCheckBox("GeoSlicer")
- self.__slicer_test_source_check_box = qt.QCheckBox("Slicer")
- self.__slicer_test_source_check_box.setEnabled(False) # Disable it because its not functional yet
- test_source_layout = qt.QHBoxLayout()
- test_source_layout.addWidget(qt.QLabel("Source:"))
- test_source_layout.addSpacing(10)
- test_source_layout.addWidget(self.__geoslicer_test_source_check_box)
- test_source_layout.addWidget(self.__slicer_test_source_check_box)
- test_source_layout.addStretch()
+ self.__searchLineEdit = qt.QLineEdit()
+ self.__searchLineEdit.setPlaceholderText("Search")
## Select/Unselect tests cases buttons
- self.__select_all_items_button = qt.QPushButton("Select all")
- self.__unselect_all_items_button = qt.QPushButton("Unselect all")
- self.__total_tests_label = qt.QLabel(f"{self.test_cases_count}")
-
- select_buttons_layout = qt.QHBoxLayout()
- select_buttons_layout.addWidget(qt.QLabel("Total tests:"))
- select_buttons_layout.addSpacing(5)
- select_buttons_layout.addWidget(self.__total_tests_label)
- select_buttons_layout.addStretch()
- select_buttons_layout.addWidget(self.__select_all_items_button)
- select_buttons_layout.addWidget(self.__unselect_all_items_button)
-
- ## Checkboxes default values
- self.__geoslicer_test_source_check_box.setChecked(True)
- self.__slicer_test_source_check_box.setChecked(False)
+ self.__selectAllItemsButton = qt.QPushButton("Select all")
+ self.__unselectAllItemsButton = qt.QPushButton("Unselect all")
+ self.__totalTestsLabel = qt.QLabel(f"{self.testCasesCount}")
+
+ selectButtonsLayout = qt.QHBoxLayout()
+ selectButtonsLayout.addWidget(qt.QLabel("Total tests:"))
+ selectButtonsLayout.addSpacing(5)
+ selectButtonsLayout.addWidget(self.__totalTestsLabel)
+ selectButtonsLayout.addStretch()
+ selectButtonsLayout.addWidget(self.__selectAllItemsButton)
+ selectButtonsLayout.addWidget(self.__unselectAllItemsButton)
## Tree widget of tests
- self.tree_widget_tests = qt.QTreeWidget()
- self.tree_widget_tests.setColumnCount(1)
- self.tree_widget_tests.setHeaderHidden(True)
+ self.__testTreeView = ATreeView()
+ self.__generateTreeView = ATreeView()
- ## Tree widget of template generation
- self.tree_widget_generate = qt.QTreeWidget()
- self.tree_widget_generate.setColumnCount(1)
- self.tree_widget_generate.setHeaderHidden(True)
-
- self.tree_widget_tabs = qt.QTabWidget()
- self.tree_widget_tabs.addTab(self.tree_widget_tests, "Tests")
- self.tree_widget_tabs.addTab(self.tree_widget_generate, "Generate")
- self.tree_widget_tabs.currentChanged.connect(self.__on_tab_changed)
+ self.__treeWidgetTabs = qt.QTabWidget()
+ self.__treeWidgetTabs.addTab(self.__testTreeView, "Tests")
+ self.__treeWidgetTabs.addTab(self.__generateTreeView, "Generate")
+ self.__treeWidgetTabs.setEnabled(True)
## Options group box
- options_form_layout = qt.QFormLayout()
- options_form_layout.setLabelAlignment(qt.Qt.AlignRight)
+ optionsFormLayout = qt.QFormLayout()
+ optionsFormLayout.setLabelAlignment(qt.Qt.AlignRight)
- self.__shuffle_check_box = qt.QCheckBox()
- self.__break_on_failure_check_box = qt.QCheckBox()
- self.__clear_scene_after_tests_check_box = qt.QCheckBox()
+ self.__shuffleCheckBox = qt.QCheckBox()
+ self.__breakOnFailureCheckBox = qt.QCheckBox()
+ self.__clearSceneAfterTestsCheckBox = qt.QCheckBox()
- self.__shuffle_check_box.setToolTip("Run tests cases in random order if activated.")
- self.__break_on_failure_check_box.setToolTip("Stop test run after an failure if activated")
- self.__clear_scene_after_tests_check_box.setToolTip(
+ self.__shuffleCheckBox.setToolTip("Run tests cases in random order if activated.")
+ self.__breakOnFailureCheckBox.setToolTip("Stop test run after an failure if activated")
+ self.__clearSceneAfterTestsCheckBox.setToolTip(
"Clear scene data after the last test run if activated. "
+ "When inactivated, it helps to analyse scene data when a failure occurs. "
+ "Works best with 'break on failure' option."
)
- self.__shuffle_check_box.setChecked(qt.Qt.Checked)
- self.__break_on_failure_check_box.setChecked(qt.Qt.Unchecked)
- self.__clear_scene_after_tests_check_box.setChecked(qt.Qt.Checked)
+ self.__shuffleCheckBox.setChecked(qt.Qt.Checked)
+ self.__breakOnFailureCheckBox.setChecked(qt.Qt.Unchecked)
+ self.__clearSceneAfterTestsCheckBox.setChecked(qt.Qt.Checked)
- options_form_layout.addRow("Shuffle", self.__shuffle_check_box)
- options_form_layout.addRow("Break on failure", self.__break_on_failure_check_box)
- options_form_layout.addRow("Clear scene after test", self.__clear_scene_after_tests_check_box)
+ optionsFormLayout.addRow("Shuffle", self.__shuffleCheckBox)
+ optionsFormLayout.addRow("Break on failure", self.__breakOnFailureCheckBox)
+ optionsFormLayout.addRow("Clear scene after test", self.__clearSceneAfterTestsCheckBox)
- options_group_box = qt.QGroupBox("Options")
- options_group_box.setAlignment(qt.Qt.AlignCenter)
- options_group_box.setLayout(options_form_layout)
+ optionsGroupBox = qt.QGroupBox("Options")
+ optionsGroupBox.setAlignment(qt.Qt.AlignHCenter)
+ optionsGroupBox.setLayout(optionsFormLayout)
- options_layout = qt.QVBoxLayout()
- options_layout.addLayout(test_source_layout, 1)
- options_layout.addLayout(select_buttons_layout, 1)
- options_layout.addWidget(self.tree_widget_tabs, 4)
- options_layout.addWidget(options_group_box, 1)
+ optionsLayout = qt.QVBoxLayout()
+ optionsLayout.addLayout(selectButtonsLayout, 1)
+ optionsLayout.addWidget(self.__searchLineEdit, 4)
+ optionsLayout.addWidget(self.__treeWidgetTabs, 4)
+ optionsLayout.addWidget(optionsGroupBox, 1)
# Logging layout
- self.__log_text_browser = qt.QTextBrowser()
- logging_layout = qt.QVBoxLayout()
- logging_layout.addWidget(self.__log_text_browser)
- logging_group_box = qt.QGroupBox("Logs")
- logging_group_box.setAlignment(qt.Qt.AlignCenter)
- logging_group_box.setLayout(logging_layout)
+ self.__logTextBrowser = qt.QTextBrowser()
+ loggingLayout = qt.QVBoxLayout()
+ loggingLayout.addWidget(self.__logTextBrowser)
+ loggingGroupBox = qt.QGroupBox("Logs")
+ loggingGroupBox.setAlignment(qt.Qt.AlignCenter)
+ loggingGroupBox.setLayout(loggingLayout)
# Run layout
## Progress bar
- self.__progress_bar = qt.QProgressBar()
- self.__progress_bar.setVisible(False)
+ self.__progressBar = qt.QProgressBar()
+ self.__progressBar.setVisible(False)
- progress_bar_layout = qt.QVBoxLayout()
- progress_bar_layout.addWidget(self.__progress_bar)
+ progressBarLayout = qt.QVBoxLayout()
+ progressBarLayout.addWidget(self.__progressBar)
## Run & Cancel button
- self.__run_button = qt.QPushButton("Run")
- self.__run_button.setIcon(qt.QIcon(str(RESOURCES_DIR / "play_button.png")))
+ self.__runButton = qt.QPushButton("Run")
+ self.__runButton.setIcon(qt.QIcon(str(RESOURCES_DIR / "play_button.png")))
- self.__cancel_button = qt.QPushButton("Cancel")
- self.__cancel_button.setIcon(qt.QIcon(str(RESOURCES_DIR / "stop_button.png")))
- self.__cancel_button.setVisible(False)
+ self.__cancelButton = qt.QPushButton("Cancel")
+ self.__cancelButton.setIcon(qt.QIcon(str(RESOURCES_DIR / "stop_button.png")))
+ self.__cancelButton.setVisible(False)
- run_cancel_button_layout = qt.QVBoxLayout()
- run_cancel_button_layout.addWidget(self.__run_button)
- run_cancel_button_layout.addWidget(self.__cancel_button)
+ runCancelButtonLayout = qt.QVBoxLayout()
+ runCancelButtonLayout.addWidget(self.__runButton)
+ runCancelButtonLayout.addWidget(self.__cancelButton)
# Main layout
layout = qt.QGridLayout()
- layout.addLayout(options_layout, 0, 0, 4, 2)
- layout.addWidget(logging_group_box, 0, 2, 4, 4)
- layout.addLayout(progress_bar_layout, 5, 0, 1, 6)
- layout.addLayout(run_cancel_button_layout, 6, 2, 1, 2)
+ layout.addLayout(optionsLayout, 0, 0, 4, 2)
+ layout.addWidget(loggingGroupBox, 0, 2, 4, 4)
+ layout.addLayout(progressBarLayout, 5, 0, 1, 6)
+ layout.addLayout(runCancelButtonLayout, 6, 2, 1, 2)
self.setLayout(layout)
# connections
- self.__run_button.clicked.connect(self.__on_run_button_clicked)
- self.__cancel_button.clicked.connect(self.__on_cancel_button_clicked)
- self.tree_widget_tests.itemClicked.connect(self.__on_tree_widget_item_clicked)
- self.tree_widget_generate.itemClicked.connect(self.__on_tree_widget_item_clicked)
- self.__geoslicer_test_source_check_box.stateChanged.connect(self.__on_test_source_checkbox_changed)
- self.__slicer_test_source_check_box.stateChanged.connect(self.__on_test_source_checkbox_changed)
- self.__select_all_items_button.clicked.connect(self.__on_select_all_button_clicked)
- self.__unselect_all_items_button.clicked.connect(self.__on_unselect_all_button_clicked)
-
- def __on_tab_changed(self, state: bool):
- cases_count = self.__test_cases_count if state == 0 else self.__generate_cases_count
- self.__total_tests_label.setText(f"{cases_count}")
-
- def __set_all_tree_widget_item_enable(self, state: bool):
- tab_index = self.tree_widget_tabs.currentIndex
- widget_list = self.__test_suite_widget_list if tab_index == 0 else self.__generate_suite_widget_list
- for test_suite_widget in widget_list:
- test_suite_widget.set_check_box_enabled(state)
-
- def __on_select_all_button_clicked(self, state):
- tab_index = self.tree_widget_tabs.currentIndex
- widget_list = self.__test_suite_widget_list if tab_index == 0 else self.__generate_suite_widget_list
- for test_suite_widget in widget_list:
- test_suite_widget.test_suite_data.enabled = True
-
- def __on_unselect_all_button_clicked(self, state):
- tab_index = self.tree_widget_tabs.currentIndex
- widget_list = self.__test_suite_widget_list if tab_index == 0 else self.__generate_suite_widget_list
- for test_suite_widget in widget_list:
- test_suite_widget.test_suite_data.enabled = False
+ self.__runButton.clicked.connect(self.__onRunButtonClicked)
+ self.__cancelButton.clicked.connect(self.__onCancelButtonClicked)
+ self.__selectAllItemsButton.clicked.connect(self.__onSelectAllButtonClicked)
+ self.__unselectAllItemsButton.clicked.connect(self.__onUnselectAllButtonClicked)
+ self.__searchLineEdit.textChanged.connect(self.__onSearchTextChanged)
+ self.__treeWidgetTabs.currentChanged.connect(self.__onTabChanged)
+
+ # Update search input based on the last stored value
+ lastSearchInput = slicer.app.settings().value("LTraceTestsWidget/LastSearch", "")
+ if lastSearchInput:
+ self.__searchLineEdit.setText(lastSearchInput)
+
+ def __onSearchTextChanged(self, text):
+ self.__testTreeView.setFilterRegularExpression(text)
+ self.__generateTreeView.setFilterRegularExpression(text)
+
+ isValid = self.__testTreeView.filteredItemsCount() > 0 or self.__generateTreeView.filteredItemsCount() > 0
+ searchQuery = text if isValid else ""
+ slicer.app.settings().setValue("LTraceTestsWidget/LastSearch", searchQuery)
+
+ def __onTabChanged(self, state: bool):
+ casesCount = self.__testCasesCount if state == self.TEST_TAB else self.__generateCasesCount
+ self.__totalTestsLabel.setText(f"{casesCount}")
+
+ def __setAllTreeWidgetItemEnable(self, state: bool):
+ tabIndex = self.__treeWidgetTabs.currentIndex
+ suiteItemList = self.__testSuiteItemList if tabIndex == self.TEST_TAB else self.__generateSuiteItemList
+ for testSuiteItem in suiteItemList:
+ testSuiteItem.setCheckBoxEnabled(state)
+
+ def __onSelectAllButtonClicked(self, state):
+ tree = self.__getCurrentTree()
+ tree.selectAll()
+
+ def __onUnselectAllButtonClicked(self, state):
+ tree = self.__getCurrentTree()
+ tree.unselectAll()
+
+ def __getCurrentTree(self) -> ATreeView:
+ return self.__testTreeView if self.__treeWidgetTabs.currentIndex == self.TEST_TAB else self.__generateTreeView
@property
- def test_cases_count(self):
- return self.__test_cases_count
+ def testCasesCount(self):
+ return self.__testCasesCount
- @test_cases_count.setter
- def test_cases_count(self, value):
- self.__test_cases_count = value
- self.__total_tests_label.setText(f"{self.__test_cases_count}")
+ @testCasesCount.setter
+ def testCasesCount(self, value):
+ self.__testCasesCount = value
+ self.__totalTestsLabel.setText(f"{self.__testCasesCount}")
@property
- def generate_cases_count(self):
- return self.__generate_cases_count
+ def generateCasesCount(self):
+ return self.__generateCasesCount
- @generate_cases_count.setter
- def generate_cases_count(self, value):
- self.__generate_cases_count = value
- self.__total_tests_label.setText(f"{self.__generate_cases_count}")
+ @generateCasesCount.setter
+ def generateCasesCount(self, value):
+ self.__generateCasesCount = value
+ self.__totalTestsLabel.setText(f"{self.__generateCasesCount}")
def accept(self):
qt.QDialog.accept(self)
- self.__uninstall_logger_handler()
+ self.uninstallLoggerHandler()
def reject(self):
qt.QDialog.reject(self)
- self.__uninstall_logger_handler()
+ self.uninstallLoggerHandler()
- def __logger_callback(self, message):
- if self.__log_text_browser is None:
+ def loggerCallback(self, message):
+ if self.__logTextBrowser is None:
return
- self.__log_text_browser.insertPlainText(message)
+ self.__logTextBrowser.insertPlainText(message)
- def __install_logger_handler(self):
- self.__logger_handler = LogWidgetHandler(callback=self.__logger_callback)
- logging.getLogger("tests_logger").addHandler(self.__logger_handler)
+ def __installLoggerHandler(self):
+ self.loggerHandler = LogWidgetHandler(callback=self.loggerCallback)
+ logging.getLogger("tests_logger").addHandler(self.loggerHandler)
- def __uninstall_logger_handler(self):
- if not hasattr(self, "__logger_handler") or self.__logger_handler is None:
+ def uninstallLoggerHandler(self):
+ if not hasattr(self, "loggerHandler") or self.loggerHandler is None:
return
- logging.getLogger("tests_logger").removeHandler(self.__logger_handler)
+ logging.getLogger("tests_logger").removeHandler(self.loggerHandler)
- def __populate_tree(self):
- self.tree_widget_tests.clear()
- self.tree_widget_generate.clear()
- self.__test_suite_widget_list.clear()
- self.__generate_suite_widget_list.clear()
- self.test_cases_count = 0
- self.generate_cases_count = 0
+ def __populateTree(self):
+ self.__testTreeView.clear()
+ self.__generateTreeView.clear()
+ self.__testSuiteItemList.clear()
+ self.__generateSuiteItemList.clear()
+ self.testCasesCount = 0
+ self.generateCasesCount = 0
for test_suite in self.__model.test_suite_list:
- test_suite_widget_item = TestSuiteDataWidgetItem(data=test_suite)
- test_suite_widget_item.test_suite_data.test_case_enablement_changed.connect(
- self.__on_test_case_enablement_changed
- )
- self.__test_suite_widget_list.append(test_suite_widget_item)
- self.tree_widget_tests.addTopLevelItem(test_suite_widget_item)
+ testSuiteItem = TestSuiteTreeViewItem(data=test_suite)
+ testSuiteItem.testData.test_case_enablement_changed.connect(self.__onTestCaseEnablementChanged)
+ self.__testSuiteItemList.append(testSuiteItem)
+
+ self.__testTreeView.appendItem(testSuiteItem)
for test_suite in self.__model.generate_suite_list:
- test_suite_widget_item = TestSuiteDataWidgetItem(data=test_suite)
- test_suite_widget_item.test_suite_data.test_case_enablement_changed.connect(
- self.__on_generate_case_enablement_changed
- )
- self.__generate_suite_widget_list.append(test_suite_widget_item)
- self.tree_widget_generate.addTopLevelItem(test_suite_widget_item)
+ testSuiteItem = TestSuiteTreeViewItem(data=test_suite)
+ testSuiteItem.testData.test_case_enablement_changed.connect(self.__onGenerateCaseEnablementChanged)
+ self.__generateSuiteItemList.append(testSuiteItem)
- def __on_test_case_enablement_changed(self, state: bool):
- value = 1 if state else -1
- self.test_cases_count = self.test_cases_count + value
+ self.__generateTreeView.appendItem(testSuiteItem)
- def __on_generate_case_enablement_changed(self, state: bool):
+ def __onTestCaseEnablementChanged(self, state: bool):
value = 1 if state else -1
- self.generate_cases_count = self.generate_cases_count + value
+ self.testCasesCount = self.testCasesCount + value
- def __on_test_source_checkbox_changed(self, state):
- selected_test_source = TestsSource.ANY
- if self.__geoslicer_test_source_check_box.isChecked() and not self.__slicer_test_source_check_box.isChecked():
- selected_test_source = TestsSource.GEOSLICER
- elif not self.__geoslicer_test_source_check_box.isChecked() and self.__slicer_test_source_check_box.isChecked():
- selected_test_source = TestsSource.SLICER
-
- if selected_test_source == self.__model.test_source:
- return
-
- self.__model.test_source = selected_test_source
- self.__populate_tree()
+ def __onGenerateCaseEnablementChanged(self, state: bool):
+ value = 1 if state else -1
+ self.generateCasesCount = self.generateCasesCount + value
- def __on_run_button_clicked(self, state):
+ def __onRunButtonClicked(self, state):
if self.__model.is_running:
return
- selected_tab = self.tree_widget_tabs.currentIndex
+ selectedTab = self.__treeWidgetTabs.currentIndex
+ casesCount = self.testCasesCount if selectedTab == self.TEST_TAB else self.generateCasesCount
- if selected_tab == 0:
- cases_count = self.test_cases_count
- else:
- cases_count = self.generate_cases_count
-
- if cases_count <= 0:
+ if casesCount <= 0:
slicer.util.infoDisplay("Please select a test.", parent=self)
return
- self.__log_text_browser.clear()
+ self.__logTextBrowser.clear()
# Disable widgets
- self.__set_all_tree_widget_item_enable(False)
- self.__run_button.setEnabled(False)
+ self.__setAllTreeWidgetItemEnable(False)
+ self.__runButton.setEnabled(False)
qt.QTimer.singleShot(
- 1000, lambda: self.__cancel_button.setEnabled(True)
+ 1000, lambda: self.__cancelButton.setEnabled(True)
) # Avoid missclick after starting the test process.
- self.__select_all_items_button.setEnabled(False)
- self.__unselect_all_items_button.setEnabled(False)
- self.__model.test_case_finished.connect(self.__on_test_case_finished)
+ self.__selectAllItemsButton.setEnabled(False)
+ self.__unselectAllItemsButton.setEnabled(False)
+ self.__model.test_case_finished.connect(self.__onTestCaseFinished)
# Start progress bar
- self.__progress_bar.reset()
- self.__progress_bar.setRange(0, cases_count)
- self.__progress_bar.setValue(0)
- self.__progress_bar.setVisible(True)
+ self.__progressBar.reset()
+ self.__progressBar.setRange(0, casesCount)
+ self.__progressBar.setValue(0)
+ self.__progressBar.setVisible(True)
# Update run/cancel buttons
- self.__cancel_button.setVisible(True)
- self.__run_button.setVisible(False)
+ self.__cancelButton.setVisible(True)
+ self.__runButton.setVisible(False)
+ self.__treeWidgetTabs.setEnabled(False)
+
+ suiteList = self.__model.test_suite_list if selectedTab == self.TEST_TAB else self.__model.generate_suite_list
+ # Disable cases from the other suite list (other tab)
+ otherSuiteList = (
+ self.__model.test_suite_list if selectedTab != self.TEST_TAB else self.__model.generate_suite_list
+ )
+ for suiteData in otherSuiteList:
+ suiteData.enabled = False
# Start running tests
self.__model.run_tests(
- suite_list=self.__model.test_suite_list if selected_tab == 0 else self.__model.generate_suite_list,
- shuffle=self.__shuffle_check_box.isChecked(),
- break_on_failure=self.__break_on_failure_check_box.isChecked(),
- after_clear=self.__clear_scene_after_tests_check_box.isChecked(),
+ suite_list=suiteList,
+ shuffle=self.__shuffleCheckBox.isChecked(),
+ break_on_failure=self.__breakOnFailureCheckBox.isChecked(),
+ after_clear=self.__clearSceneAfterTestsCheckBox.isChecked(),
)
test_process_result = self.__model.result()
@@ -440,65 +471,45 @@ def __on_run_button_clicked(self, state):
)
# User feedback
- for test_suite_data in self.__model.test_suite_list:
+ for test_suite_data in self.__model.test_suite_list + self.__model.generate_suite_list:
if warning_text := test_suite_data.warning_log_text:
log(warning_text)
if failure_text := test_suite_data.failure_log_text:
log(failure_text)
# Reset progress bar
- self.__progress_bar.setValue(cases_count)
- self.__progress_bar.setVisible(False)
+ self.__progressBar.setValue(casesCount)
+ self.__progressBar.setVisible(False)
# Remove connections
- self.__model.test_case_finished.disconnect(self.__on_test_case_finished)
+ self.__model.test_case_finished.disconnect(self.__onTestCaseFinished)
# Re-enable widgets
- self.__set_all_tree_widget_item_enable(True)
- self.__cancel_button.setVisible(False)
- self.__cancel_button.setEnabled(False)
- self.__run_button.setEnabled(True)
- self.__run_button.setVisible(True)
- self.__select_all_items_button.setEnabled(True)
- self.__unselect_all_items_button.setEnabled(True)
-
- def __on_cancel_button_clicked(self, state):
+ self.__setAllTreeWidgetItemEnable(True)
+ self.__cancelButton.setVisible(False)
+ self.__cancelButton.setEnabled(False)
+ self.__runButton.setEnabled(True)
+ self.__runButton.setVisible(True)
+ self.__selectAllItemsButton.setEnabled(True)
+ self.__unselectAllItemsButton.setEnabled(True)
+ self.__treeWidgetTabs.setEnabled(True)
+
+ def __onCancelButtonClicked(self, state):
if not self.__model.is_running:
return
- self.__cancel_button.setEnabled(False)
- self.__cancel_button.setText("Cancelling...")
- self.__model.tests_cancelled.connect(self.__on_test_cancelled)
+ self.__cancelButton.setEnabled(False)
+ self.__cancelButton.setText("Cancelling...")
+ self.__model.tests_cancelled.connect(self.onTestCancelled)
self.__model.cancel()
- def __on_test_cancelled(self):
- self.__cancel_button.setVisible(False)
- self.__cancel_button.setEnabled(False)
- self.__cancel_button.setText("Cancel")
- self.__run_button.setEnabled(True)
- self.__run_button.setVisible(True)
+ def onTestCancelled(self):
+ self.__cancelButton.setVisible(False)
+ self.__cancelButton.setEnabled(False)
+ self.__cancelButton.setText("Cancel")
+ self.__runButton.setEnabled(True)
+ self.__runButton.setVisible(True)
self.__model.tests_cancelled.disconnect()
- def __on_test_case_finished(self, test_suite_data, test_case_data):
- self.__progress_bar.setValue(self.__progress_bar.value + 1)
-
- def __on_tree_widget_item_clicked(self, item, column):
- item_check_state = item.checkState(0)
-
- test_item = None
- for test_suite_widget in self.__test_suite_widget_list + self.__generate_suite_widget_list:
- if test_suite_widget == item:
- test_item = test_suite_widget
- break
- for test_case_widget in test_suite_widget.test_case_data_widget_list:
- if test_case_widget == item:
- test_item = test_case_widget
- break
-
- if test_item is None:
- raise RuntimeError("Selected item data wasn't found in current state. Please restart the application.")
-
- if isinstance(test_item, TestSuiteDataWidgetItem):
- test_item.test_suite_data.enabled = True if item_check_state == qt.Qt.Checked else False
- else:
- test_item.test_case_data.enabled = True if item_check_state == qt.Qt.Checked else False
+ def __onTestCaseFinished(self, testSuiteData, testCaseData):
+ self.__progressBar.setValue(self.__progressBar.value + 1)
diff --git a/src/ltrace/ltrace/slicer/tests/mocks/__init__.py b/src/ltrace/ltrace/slicer/tests/mocks/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/ltrace/ltrace/slicer/tests/mocks/mock_qt_widgets.py b/src/ltrace/ltrace/slicer/tests/mocks/mock_qt_widgets.py
new file mode 100644
index 0000000..bd1c589
--- /dev/null
+++ b/src/ltrace/ltrace/slicer/tests/mocks/mock_qt_widgets.py
@@ -0,0 +1,23 @@
+from typing import List
+from unittest.mock import patch, MagicMock
+
+import contextlib
+
+
+@contextlib.contextmanager
+def mockFileDialog(selectedFiles: List[str] = None, getSaveFileName: str = None, cancel=False) -> None:
+
+ fileDialogClsMock = MagicMock()
+ fileDialogMock = MagicMock()
+ fileDialogMock.selectedFiles.return_value = selectedFiles
+ fileDialogMock.exec.return_value = 0 if cancel else 1
+
+ fileDialogClsMock.return_value = fileDialogMock
+ fileDialogClsMock.getSaveFileName.return_value = getSaveFileName
+
+ patcher = patch("qt.QFileDialog", fileDialogClsMock)
+ patcher.start()
+
+ yield
+
+ patcher.stop()
diff --git a/src/ltrace/ltrace/slicer/tests/test_case.py b/src/ltrace/ltrace/slicer/tests/test_case.py
index 991bfb4..3800ae3 100644
--- a/src/ltrace/ltrace/slicer/tests/test_case.py
+++ b/src/ltrace/ltrace/slicer/tests/test_case.py
@@ -42,6 +42,11 @@ def __generate_name(self, function: Callable) -> None:
def __call__(self, *args, **kwargs) -> None:
self.run()
+ def reset(self):
+ self.status = TestState.NOT_INITIALIZED
+ self.reason = ""
+ self.elapsed_time_sec = 0
+
def run(self) -> None:
self.status = TestState.RUNNING
with self.__exception_hook_patch:
diff --git a/src/ltrace/ltrace/slicer/tests/utils.py b/src/ltrace/ltrace/slicer/tests/utils.py
index d8309c7..d5cefe3 100644
--- a/src/ltrace/ltrace/slicer/tests/utils.py
+++ b/src/ltrace/ltrace/slicer/tests/utils.py
@@ -8,10 +8,11 @@
from ltrace.constants import SaveStatus
from ltrace.slicer.project_manager import ProjectManager
from ltrace.slicer.helpers import make_directory_writable, WatchSignal
+from ltrace.slicer.module_utils import loadModules
from ltrace.utils.string_comparison import StringComparison
from pathlib import Path
from stopit import TimeoutException
-from typing import Union
+from typing import Callable, List, Union
TEST_LOG_FILE_PATH = Path(slicer.app.temporaryPath) / "tests.log"
@@ -62,27 +63,27 @@ def wait(seconds: float) -> None:
raise TimeoutError("Test timeout reached!")
-def wait_cli_to_finish(cli_node, timeout_sec: int = 3600) -> None:
- """Lock thread until respective CLI node is not busy anymore.
+def wait_cli_to_finish(cli_entity, timeout_sec: int = 3600) -> None:
+ """Lock thread until respective CLI node or queue is not busy anymore.
Args:
- cli_node (vtkMRMLCommandLineModuleNode): the CLI node object,
+ cli_entity (slicer.vtkMRMLCommandLineModuleNode or ltrace.slicer.CliQueue): the CLI entity object,
timeout_sec (int, optional): the timeout in seconds. Defaults to 3600 seconds.
"""
- if cli_node is None:
+ if cli_entity is None:
return
start = time.perf_counter()
try:
- while cli_node.IsBusy():
+ while cli_entity.IsBusy():
time.sleep(0.200)
process_events()
if time.perf_counter() - start >= timeout_sec:
- cli_node.Cancel()
+ cli_entity.Cancel()
raise TimeoutError("CLI timeout reached!")
except TimeoutException:
- cli_node.Cancel()
+ cli_entity.Cancel()
raise TimeoutError("Test timeout reached!")
@@ -126,22 +127,27 @@ def load_project(project_file_path, timeout_ms=300000):
if not path.exists():
raise ValueError("Project file not found!")
+ status = False
with WatchSignal(signal=slicer.mrmlScene.EndImportEvent, timeout_ms=timeout_ms):
- try:
- slicer.util.loadScene(path.as_posix())
- except Exception:
- raise RuntimeError(f"Timeout! Failed to load {path.as_posix()} project file!")
+ status = ProjectManager().load(path)
+
+ return status
@contextlib.contextmanager
-def check_for_message_box(message, should_accept=True, timeout_sec=2, buttonTextToClick=None):
+def check_for_message_box(
+ message, should_accept: bool = True, timeout_sec: int = 2, buttonTextToClick: bool = None, closeOthers: bool = True
+):
"""Context manager to handle the displaying of a QMessageBox during a test scenario
Args:
message (str): the expected QMessageBox text
should_accept (bool, optional): Accept if True, otherwise Reject the message box action. Defaults to True.
timeout_sec (int, optional): Timeout to wait for the message box to appear, in seconds. Defaults to 2.
- buttonTextToClick (_type_, optional): The button text to click. Defaults to None.
+ buttonTextToClick (bool, optional): The button text to click. Defaults to None.
+ closeOthers (:bool, optional): Close other visible message boxes.
+ It might help to disable it when chaining multiple contexts to check
+ for a message box. Defaults to True.
Raises:
AttributeError: When the QMessageBox isn't identified.
@@ -156,7 +162,7 @@ def check():
if result[0] is True:
return
- mw = slicer.util.mainWindow()
+ mw = slicer.modules.AppContextInstance.mainWindow
message_boxes = mw.findChildren(qt.QMessageBox)
the_message_box = None
@@ -168,7 +174,9 @@ def check():
# Find possible visible QMessageBox and close it to avoid freezing the test process
other_message_boxes = [msg_box for msg_box in message_boxes if msg_box.visible == True]
for message_box in other_message_boxes:
- message_box.close()
+ logging.debug(f"A different message box had appeared with the text: '{message_box.text}'")
+ if closeOthers:
+ message_box.close()
if time.perf_counter() - start_time < timeout_sec:
timer.start()
@@ -190,6 +198,7 @@ def check():
the_button = button
if the_button is None:
+ logging.error(f"The message box button {buttonTextToClick} doesn't exist in the current context.")
the_message_box.reject()
else:
the_button.click()
@@ -235,10 +244,11 @@ def save_project(project_path: Union[str, Path], timeout_ms=300000, properties=N
elif project_path.is_dir():
shutil.rmtree(project_path, onerror=make_directory_writable)
- project_manager = ProjectManager(folder_icon_path="")
-
+ status = False
with WatchSignal(signal=slicer.mrmlScene.EndImportEvent, timeout_ms=timeout_ms):
- return project_manager.save_as(project_path.parent, properties=properties) == SaveStatus.SUCCEED
+ status = ProjectManager().saveAs(project_path.parent, properties=properties) == SaveStatus.SUCCEED
+
+ return status
def save_current_project(timeout_ms=300000, properties=None) -> bool:
@@ -257,17 +267,17 @@ def save_current_project(timeout_ms=300000, properties=None) -> bool:
if properties is None or not isinstance(properties, dict):
properties = {}
- project_manager = ProjectManager(folder_icon_path="")
-
+ status = False
with WatchSignal(signal=slicer.mrmlScene.EndSaveEvent, timeout_ms=timeout_ms):
- return project_manager.save(url, properties=properties) == SaveStatus.SUCCEED
+ status = ProjectManager().save(url, properties=properties) == SaveStatus.SUCCEED
+
+ return status
def close_project(timeout_ms: int = 300000) -> None:
with WatchSignal(signal=slicer.mrmlScene.EndCloseEvent, timeout_ms=timeout_ms):
- slicer.mrmlScene.Clear(0)
+ ProjectManager().close()
- slicer.mrmlScene.EndState(slicer.mrmlScene.CloseState)
process_events()
@@ -275,3 +285,58 @@ def compare_ignore_line_endings(file1, file2):
with open(file1, "r") as f1:
with open(file2, "r") as f2:
return f1.read().replace("\r\n", "\n") == f2.read().replace("\r\n", "\n")
+
+
+def handleFileDialog(accept: bool = True, directory: Path = None, projectName: str = None) -> None:
+ fileDialog = None
+
+ existentFileDialogs = [widget for widget in slicer.app.topLevelWidgets() if isinstance(widget, qt.QFileDialog)]
+ if len(existentFileDialogs) == 0:
+ raise RuntimeError("File dialog wasn't detected.")
+ elif len(existentFileDialogs) > 1:
+ logging.warning(f"Found {len(existentFileDialogs)} file dialogs. Check if this is the expected behavior.")
+
+ visibleFileDialogs = [widget for widget in existentFileDialogs if widget.visible is True]
+
+ if len(visibleFileDialogs) == 0:
+ raise RuntimeError("A file dialog is instancied but wasn't visible.")
+
+ fileDialog = visibleFileDialogs[0]
+
+ if not accept:
+ fileDialog.reject()
+ return
+
+ wait(1)
+ fileDialog.setDirectory(qt.QDir(directory.as_posix()))
+ fileNameEdit = fileDialog.findChild("QLineEdit")
+ fileNameEdit.setText(projectName)
+ wait(0.2)
+ fileDialog.accept()
+ wait(0.2)
+
+
+def loadEnvironmentByName(displayName):
+ slicer.modules.AppContextInstance.modules.loadEnvironmentByName(
+ slicer.util.mainWindow().findChild("QToolBar", "ModuleToolBar"), displayName
+ )
+
+
+def loadAllModules():
+ groups = slicer.modules.AppContextInstance.modules.groups
+ modules = {obj.key: obj for sublist in groups.values() for obj in sublist}
+ modules = list(modules.values())
+ loadModules(modules, permanent=False, favorite=False)
+
+
+def waitCondition(condition: Callable, timeoutSec: int = 5) -> None:
+ """Sleeps until a condition is met.
+
+ Args:
+ condition (Callable): A function that returns True when the process is finished, otherwise it should returns False
+ timeoutSec (int): The time limit to wait for the condition to happen.
+ """
+ start = time.perf_counter()
+ while time.perf_counter() - start < timeoutSec and not condition():
+ time.sleep(0.2)
+ process_events()
diff --git a/src/ltrace/ltrace/slicer/throat_analysis/throat_analysis.py b/src/ltrace/ltrace/slicer/throat_analysis/throat_analysis.py
index a36120f..1bde753 100644
--- a/src/ltrace/ltrace/slicer/throat_analysis/throat_analysis.py
+++ b/src/ltrace/ltrace/slicer/throat_analysis/throat_analysis.py
@@ -199,12 +199,12 @@ def __generate_statistics(self, boundaries_array, params):
SegmentOperator(operator, volume_operator.ijkToRasOperator),
stepcb=lambda i, total: self.__progress_update_callback(i / total),
)
- df.set_axis(operator.ATTRIBUTES, axis=1, inplace=True)
- df.sort_values(by=["label"], ascending=True, inplace=True)
+ df = df.set_axis(operator.ATTRIBUTES, axis=1)
+ df = df.sort_values(by=["label"], ascending=True)
if df.shape[1] > 1 and df.shape[0] > 0:
- df.dropna(axis=1, how="all", inplace=True) # Remove unused columns
- df.dropna(axis=0, how="any", inplace=True) # Remove unused columns
+ df = df.dropna(axis=1, how="all") # Remove unused columns
+ df = df.dropna(axis=0, how="any") # Remove unused columns
# Relabel array due to report handling
indices = np.array(df.label, copy=True, dtype=int)
@@ -252,7 +252,7 @@ def __generate_ids(self, boundaries_label_image, input_label_image):
id_list = rename_duplicated_ids(id_list)
df = pd.DataFrame({"id": id_list, "label": label_list})
- df.sort_values(by=["label"], ascending=True, inplace=True)
+ df = df.sort_values(by=["label"], ascending=True)
return df["id"].tolist()
diff --git a/src/ltrace/ltrace/slicer/ui.py b/src/ltrace/ltrace/slicer/ui.py
index b67f627..b621a6b 100644
--- a/src/ltrace/ltrace/slicer/ui.py
+++ b/src/ltrace/ltrace/slicer/ui.py
@@ -1,4 +1,3 @@
-import random
import textwrap
from functools import reduce
from pathlib import Path
@@ -12,13 +11,6 @@
from ltrace.slicer.helpers import getSegmentList, createLabelmapInput
from ltrace.slicer.widget.global_progress_bar import LocalProgressBar
-from pathlib import Path
-
-import ctk
-import numpy as np
-
-from ltrace.slicer.widget.customized_pyqtgraph.GraphicsLayoutWidget import GraphicsLayoutWidget
-
def volumeInput(onChange=None, hasNone=False, nodeTypes=None, onActivation=None):
inputSelector = slicer.qMRMLNodeComboBox()
@@ -68,15 +60,22 @@ def hierarchyVolumeInput(
tooltip=None,
defaultText=None,
showSegments=False,
+ allowFolders=False,
):
from ltrace.slicer.widget.hierarchy_volume_input import HierarchyVolumeInput
- widget = HierarchyVolumeInput(onChange or onActivation, hasNone, nodeTypes, defaultText)
+ widget = HierarchyVolumeInput(hasNone, nodeTypes, defaultText, allowFolders)
if tooltip:
widget.setToolTip(tooltip)
if not showSegments:
- widget.setExcludeItemAttributeNamesFilter(("segmentID",))
+ widget.selectorWidget.setExcludeItemAttributeNamesFilter(("segmentID",))
+
+ if onChange is not None:
+ widget.currentItemChanged.connect(onChange)
+
+ if onActivation is not None: # Not implemented
+ pass
return widget
@@ -279,6 +278,47 @@ def ApplyButton(onClick=None, tooltip="", text="Apply", enabled=True, object_nam
return btn
+class ApplyCancelButtons(qt.QWidget):
+ def __init__(
+ self,
+ onApplyClick=None,
+ onCancelClick=None,
+ applyTooltip="Apply",
+ cancelTooltip="Cancel",
+ applyText="Apply",
+ cancelText="Cancel",
+ enabled=True,
+ applyObjectName="Apply Button",
+ cancelObjectName="Cancel Button",
+ parent=None,
+ ):
+ super().__init__(parent)
+
+ # Create Apply button
+ self.applyBtn = ButtonWidget(onApplyClick, applyText, applyTooltip, enabled, applyObjectName)
+ self.applyBtn.setProperty("class", "actionButtonBackground")
+ self.applyBtn.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Minimum)
+
+ # Create Cancel button
+ self.cancelBtn = ButtonWidget(onCancelClick, cancelText, cancelTooltip, enabled, cancelObjectName)
+ self.cancelBtn.setStyleSheet("QPushButton {font-size: 11px; padding: 8px; margin: 0px}")
+ self.cancelBtn.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Minimum)
+
+ # Create layout and add buttons
+ self.buttonLayout = qt.QHBoxLayout(self)
+ self.buttonLayout.addWidget(self.applyBtn)
+ self.buttonLayout.addWidget(self.cancelBtn)
+
+ # Method to enable/disable both buttons at once
+ def setEnabled(self, enabled):
+ self.applyBtn.setEnabled(enabled)
+ self.cancelBtn.setEnabled(enabled)
+
+ @property
+ def text(self):
+ return f"ApplyCancelButtons"
+
+
def CheckBoxLayout(text="", tooltip="", onToggle=None):
hbox = qt.QHBoxLayout()
@@ -691,6 +731,8 @@ def add(self, directories=False):
self.directoryListView.addDirectory(path)
self.addCallback(lastPathString)
+ fileDialog.delete()
+
def numberParam(vrange, value=0.1, step=0.1, decimals=1):
param = qt.QDoubleSpinBox()
diff --git a/src/ltrace/ltrace/slicer/widget/custom_toolbar_buttons.py b/src/ltrace/ltrace/slicer/widget/custom_toolbar_buttons.py
new file mode 100644
index 0000000..905a306
--- /dev/null
+++ b/src/ltrace/ltrace/slicer/widget/custom_toolbar_buttons.py
@@ -0,0 +1,161 @@
+import qt
+import slicer
+
+
+class CustomAction(qt.QWidgetAction):
+ # Define a custom signal that can be connected to any slot
+ clicked = qt.Signal(bool)
+
+ def __init__(self, icon, text, parent=None):
+ super().__init__(parent)
+
+ # Store the icon and text for later use
+ self.icon = icon
+ self.text = text # if " " in text else text + " "
+
+ # Create a custom widget for the action
+ self.widget = qt.QWidget(parent)
+ self.layout = qt.QHBoxLayout()
+ self.layout.setAlignment(qt.Qt.AlignLeft) # Align content to the left initially
+ self.layout.setContentsMargins(0, 2, 0, 2)
+
+ # Create a button that holds the icon and text
+ self.button = qt.QPushButton(parent)
+ self.button.setIcon(self.icon)
+ self.button.setFlat(True) # Make the button look like a label without borders
+ self.label = qt.QLabel(self.text)
+ self.label.visible = False
+
+ # Connect the button's clicked signal to emit the custom clicked signal
+ self.button.clicked.connect(self.clicked)
+
+ # Set up the layout
+ self.layout.addWidget(self.button)
+ self.layout.addWidget(self.label)
+ self.widget.setLayout(self.layout)
+
+ # Set the custom widget as the default widget for this action
+ self.setDefaultWidget(self.widget)
+
+ self.hideName()
+
+ def showName(self):
+ """Switch to text beside icon (left-aligned)."""
+ self.label.visible = True # Show the text
+
+ def hideName(self):
+ """Switch to icon only (centered)."""
+ self.label.visible = False
+ # self.layout.setAlignment(qt.Qt.AlignCenter) # Center the icon
+
+ def toggleName(self):
+ """Toggle between text beside icon and icon only."""
+ if self.label.visible:
+ self.hideName()
+ else:
+ self.showName()
+
+
+class CustomToolButton(qt.QToolButton):
+ def __init__(self, parent):
+ super().__init__(parent)
+
+ def showName(self):
+ """Switch to text beside icon (left-aligned)."""
+ self.setToolButtonStyle(qt.Qt.ToolButtonTextBesideIcon)
+
+ def hideName(self):
+ """Switch to icon only (centered)."""
+ self.setToolButtonStyle(qt.Qt.ToolButtonIconOnly)
+
+ def toggleName(self):
+ """Toggle between text beside icon and icon only."""
+ if self.toolButtonStyle == qt.Qt.ToolButtonTextBesideIcon:
+ self.hideName()
+ else:
+ self.showName()
+
+
+def addAction(module, toolbar, root="", parent=None):
+ if not parent:
+ parent = toolbar
+ m = getattr(slicer.modules, module.key.lower())
+ button = CustomToolButton(parent)
+ action = qt.QAction(m.icon, m.title, parent)
+ action.setToolTip(m.title)
+
+ def selectModule():
+ slicer.util.selectModule(module.key)
+
+ action.triggered.connect(selectModule)
+ button.setDefaultAction(action)
+ toolbar.addWidget(button)
+ return action
+
+
+def addEntry(module, menu, parent=None):
+ if not parent:
+ parent = menu
+ m = getattr(slicer.modules, module.key.lower())
+ action = qt.QAction(m.icon, m.title, parent)
+ action.triggered.connect(lambda _, name=module.key: slicer.util.selectModule(name))
+ # menu.setToolButtonStyle(qt.Qt.ToolButtonTextBesideIcon)
+ menu.addAction(action)
+
+
+def addMenu(icon, folder, modules, parent):
+ toolButton = CustomToolButton(parent)
+ toolButton.setIcon(icon)
+ toolButton.setText(folder)
+ toolButton.setToolTip(folder)
+
+ menu = qt.QMenu(toolButton)
+ for module in modules:
+ addEntry(module, menu, parent)
+ toolButton.setMenu(menu)
+
+ toolButton.setPopupMode(qt.QToolButton.MenuButtonPopup)
+ toolButton.clicked.connect(lambda _: toolButton.showMenu())
+ parent.addWidget(toolButton)
+
+
+# Future use, similar function but different name to avoid conflict
+
+
+def addEntryRaw(module, menu, parent=None):
+ if not parent:
+ parent = menu
+ action = qt.QAction(module.icon, module.title, parent)
+
+ def selectModule():
+ slicer.util.selectModule(module.name)
+
+ action.triggered.connect(selectModule)
+ # menu.setToolButtonStyle(qt.Qt.ToolButtonTextBesideIcon)
+ menu.addAction(action)
+
+
+def addMenuRaw(icon, folder, modules, parent):
+ toolButton = CustomToolButton(parent)
+ toolButton.setIcon(icon)
+ toolButton.setText(folder)
+ toolButton.setToolTip(folder)
+
+ menu = qt.QMenu(toolButton)
+ for module in modules:
+ addEntry(module, menu, parent)
+ toolButton.setMenu(menu)
+
+ toolButton.setPopupMode(qt.QToolButton.MenuButtonPopup)
+ toolButton.clicked.connect(lambda _: toolButton.showMenu())
+ parent.addWidget(toolButton)
+
+
+def addActionWidget(icon, title, callback=None, parent=None):
+ button = CustomToolButton(parent)
+ action = qt.QAction(icon, title, parent)
+ action.setToolTip(title)
+ action.triggered.connect(lambda tb=parent: callback(tb))
+ button.setDefaultAction(action)
+ parent.addWidget(button)
+ return action
diff --git a/src/modules/Charts/Plots/BarPlot/AngleAxisItem.py b/src/ltrace/ltrace/slicer/widget/customized_pyqtgraph/AngleAxisItem.py
similarity index 100%
rename from src/modules/Charts/Plots/BarPlot/AngleAxisItem.py
rename to src/ltrace/ltrace/slicer/widget/customized_pyqtgraph/AngleAxisItem.py
diff --git a/src/modules/Charts/Plots/Crossplot/data_plot_widget.py b/src/ltrace/ltrace/slicer/widget/data_plot_widget.py
similarity index 98%
rename from src/modules/Charts/Plots/Crossplot/data_plot_widget.py
rename to src/ltrace/ltrace/slicer/widget/data_plot_widget.py
index 624ebde..1fa09b2 100644
--- a/src/modules/Charts/Plots/Crossplot/data_plot_widget.py
+++ b/src/ltrace/ltrace/slicer/widget/data_plot_widget.py
@@ -1,21 +1,21 @@
-from Plots.BarPlot.AngleAxisItem import AngleAxisItem
import numpy as np
import pyqtgraph as pg
from pyqtgraph.Qt import QtGui
from ltrace.algorithms.measurements import PORE_SIZE_CATEGORIES
from ltrace.slicer.widget.custom_gradient_legend import CustomGradientLegend
+from ltrace.slicer.widget.customized_pyqtgraph.AngleAxisItem import AngleAxisItem
from ltrace.slicer.widget.customized_pyqtgraph.GraphicsLayoutWidget import GraphicsLayoutWidget
PLOT_MINIMUM_HEIGHT = 480
PLOT_HISTOGRAM_SIZE = 100
-class DataPlotWidget(pg.QtCore.QObject):
+class DataPlotWidget(pg.QtWidgets.QWidget):
toggleLegendSignal = pg.QtCore.Signal()
- def __init__(self):
- super().__init__()
+ def __init__(self, parent=None):
+ super().__init__(parent)
self.__legendItem = None
self.__embeddedLegendVisibility = True
diff --git a/src/ltrace/ltrace/slicer/widget/docked_data.py b/src/ltrace/ltrace/slicer/widget/docked_data.py
index 27d066e..14765c7 100644
--- a/src/ltrace/ltrace/slicer/widget/docked_data.py
+++ b/src/ltrace/ltrace/slicer/widget/docked_data.py
@@ -1,36 +1,98 @@
-import logging
-
import slicer
import qt
from ltrace.constants import ImageLogConst
+from ltrace.slicer.application_observables import ApplicationObservables
+from typing import Union
+
+
+def tryGetWidget(moduleName: str, copyWidget: bool = True) -> Union["LTracePluginWidget", None]:
+ module = getattr(slicer.modules, moduleName, None)
+
+ if not module:
+ return None
+
+ if copyWidget:
+ oldModuleWidget = module.widgetRepresentation()
+ newModuleWidget = module.createNewWidgetRepresentation()
+ setattr(slicer.modules, f"{oldModuleWidget.self().moduleName}Widget", oldModuleWidget.self())
+ setattr(slicer.modules, f"{oldModuleWidget.self().moduleName}DockedWidget", newModuleWidget.self())
+ return newModuleWidget
+
+ return module.widgetRepresentation()
class DockedData(qt.QDockWidget):
def __init__(self):
- super().__init__("Explorer")
+ super().__init__("")
+ self.objectName = "Docked Data"
+ self.tabs = None
- try:
- self.default_data = slicer.modules.customizeddata.createNewWidgetRepresentation()
- self.image_log_data = slicer.modules.imagelogdata.createNewWidgetRepresentation()
- except AttributeError:
- logging.warn("CustomizedData and ImageLogData modules are not available yet.")
- self.default_data = None
- self.image_log_data = None
+ self.setAllowedAreas(qt.Qt.AllDockWidgetAreas)
+ ApplicationObservables().applicationLoadFinished.connect(self.__onApplicationLoadFinished)
- slicer.app.layoutManager().layoutChanged.connect(self.on_layout_changed)
- self.on_layout_changed()
+ def __onApplicationLoadFinished(self):
+ ApplicationObservables().applicationLoadFinished.disconnect(self.__onApplicationLoadFinished)
+ self.setupUI()
- self.setAllowedAreas(qt.Qt.AllDockWidgetAreas)
- main_window = slicer.util.mainWindow()
- main_window.addDockWidget(qt.Qt.RightDockWidgetArea, self)
-
- def on_layout_changed(self):
- current_layout = slicer.app.layoutManager().layout
- if current_layout >= ImageLogConst.DEFAULT_LAYOUT_ID_START_VALUE:
- data_widget = self.image_log_data
- else:
- data_widget = self.default_data
-
- if data_widget is not None:
- self.setWidget(data_widget)
+ def __createImageLogDataWidget(self):
+ self.imageLogData = self._createScrollableWidget("imagelogdata")
+ if self.imageLogData is not None:
+ self.stackedWidget.addWidget(self.imageLogData)
+
+ def setupUI(self):
+ self.defaultData = self._createScrollableWidget("customizeddata")
+ self.jobMonitorWidget = self._createScrollableWidget("jobmonitor", copyWidget=False)
+ self.stackedWidget = qt.QStackedWidget()
+ self.stackedWidget.addWidget(self.defaultData)
+ self.__createImageLogDataWidget()
+ self.stackedWidget.setCurrentWidget(self.defaultData)
+ self.tabs = qt.QTabWidget()
+ self.tabs.addTab(self.stackedWidget, "Explorer")
+ self.tabs.addTab(self.jobMonitorWidget, "Remote Jobs")
+
+ mainWindow = slicer.modules.AppContextInstance.mainWindow
+ mainWindow.addDockWidget(qt.Qt.RightDockWidgetArea, self)
+
+ self.setWidget(self.tabs)
+ slicer.app.layoutManager().layoutChanged.connect(self.onLayoutChanged)
+ self.onLayoutChanged()
+
+ def _createScrollableWidget(self, pluginWidgetName: str, copyWidget: bool = True) -> qt.QScrollArea:
+ """Method to wrap the plugin's widget to a scroll area
+
+ Args:
+ pluginWidget (LTracePluginWidget): the plugin's widget object.
+
+ Returns:
+ qt.QScrollArea: the scroll area widget object.
+ """
+ newWidgetRepresentation = tryGetWidget(pluginWidgetName, copyWidget)
+ if not newWidgetRepresentation:
+ return None
+
+ scroll = qt.QScrollArea()
+ scroll.setVerticalScrollBarPolicy(qt.Qt.ScrollBarAsNeeded)
+ scroll.setHorizontalScrollBarPolicy(qt.Qt.ScrollBarAsNeeded)
+ scroll.setWidgetResizable(True)
+ scroll.setWidget(newWidgetRepresentation)
+
+ return scroll
+
+ def setCurrentWidget(self, index: int):
+ if self.tabs is None:
+ return
+
+ self.tabs.setCurrentIndex(index)
+
+ def onLayoutChanged(self):
+ currentLayout = slicer.app.layoutManager().layout
+ explorerWidget = self.defaultData
+ if currentLayout >= ImageLogConst.DEFAULT_LAYOUT_ID_START_VALUE:
+ if self.imageLogData is None:
+ self.__createImageLogDataWidget()
+
+ explorerWidget = self.imageLogData
+
+ if explorerWidget is not None:
+ self.stackedWidget.setCurrentWidget(explorerWidget)
diff --git a/src/ltrace/ltrace/slicer/widget/filtered_node_combo_box.py b/src/ltrace/ltrace/slicer/widget/filtered_node_combo_box.py
index a1e271a..563497c 100644
--- a/src/ltrace/ltrace/slicer/widget/filtered_node_combo_box.py
+++ b/src/ltrace/ltrace/slicer/widget/filtered_node_combo_box.py
@@ -31,7 +31,7 @@ def __init__(
if node.GetAttribute("ShowInFilteredNodeComboBox") == "False":
continue
else:
- self.addItem(node.GetName(), node.GetID())
+ self.addNode(node)
def changeNode(index):
self.blockSignals(True)
diff --git a/src/ltrace/ltrace/slicer/widget/fuzzysearch.py b/src/ltrace/ltrace/slicer/widget/fuzzysearch.py
new file mode 100644
index 0000000..87a8012
--- /dev/null
+++ b/src/ltrace/ltrace/slicer/widget/fuzzysearch.py
@@ -0,0 +1,133 @@
+import logging
+from collections import defaultdict
+from typing import List, Iterable, Dict, Tuple
+
+import pandas as pd
+import qt, slicer
+
+from ltrace.slicer.module_info import ModuleInfo
+from ltrace.slicer.module_utils import loadModule
+from ltrace.slicer_utils import getResourcePath
+
+
+class FuzzySearchDialog(qt.QDialog):
+ def __init__(self, model=None, parent=None) -> None:
+ super().__init__(parent)
+ self.setWindowIcon(qt.QIcon((getResourcePath("Icons") / "GeoSlicer.ico").as_posix()))
+
+ layout = qt.QVBoxLayout(self)
+
+ self.search_field = qt.QLineEdit()
+ layout.addWidget(self.search_field)
+
+ self.search_field.setPlaceholderText("Search for a module or a keyword")
+ self.search_field.textChanged.connect(self.on_search_changed)
+
+ self.results_layout = qt.QHBoxLayout()
+ self.result_macros_list = qt.QListWidget()
+ self.result_modules_list = qt.QListWidget()
+
+ self.results_layout.addWidget(self.result_macros_list)
+ self.results_layout.addWidget(self.result_modules_list)
+
+ layout.addLayout(self.results_layout)
+
+ self.model = model
+ self.model.setEventHandler(self.eventHandler)
+
+ self.result_macros_list.currentTextChanged.connect(self.on_macro_selected)
+ self.result_modules_list.currentRowChanged.connect(self.on_module_selected)
+
+ self.update_results([], self.model.macros)
+
+ def on_search_changed(self, query: str) -> None:
+ macros, moduleNames = self.model.search("".join(query.split()))
+ self.update_results(moduleNames, macros)
+
+ def update_results(
+ self,
+ moduleNames: List[str],
+ macros: List[str] = None,
+ ) -> None:
+ if macros is not None:
+ self.result_macros_list.clear()
+ self.result_macros_list.addItems(macros)
+
+ self.result_modules_list.clear()
+
+ self.displayData(moduleNames)
+
+ def displayData(self, moduleNames):
+ for name in moduleNames:
+ try:
+ module = getattr(slicer.modules, name.lower())
+ item = qt.QListWidgetItem()
+ item.setIcon(module.icon)
+ item.setText(module.title)
+ item.setData(qt.Qt.UserRole, name)
+ self.result_modules_list.addItem(item)
+
+ except AttributeError:
+ pass
+
+ def on_macro_selected(self, macro: str) -> None:
+ moduleNames = self.model.select_macro(macro)
+ self.update_results(moduleNames)
+
+ def on_module_selected(self, index) -> None:
+ listItem = self.result_modules_list.item(index)
+ if listItem is None:
+ return
+
+ moduleName = listItem.data(qt.Qt.UserRole)
+ slicer.util.selectModule(moduleName)
+
+ def eventHandler(self, event):
+ if event == "new-data":
+ self.update_results([], self.model.macros)
+
+
+class LinearSearchModel:
+ def __init__(self):
+ self.db: pd.DataFrame = None
+ self.modules = None
+ self.grouped = defaultdict(list)
+ self.macros = []
+ self.__eventHandler = None
+
+ def setEventHandler(self, handler):
+ self.__eventHandler = handler
+
+ def setDataSource(self, modules: dict):
+ self.modules = modules
+ self.db, self.grouped = self.compile(modules)
+ self.macros = [k for k in self.grouped]
+ self.__eventHandler("new-data")
+
+ @staticmethod
+ def compile(modules: Dict[str, ModuleInfo]) -> Tuple[pd.DataFrame, Dict[str, List[ModuleInfo]]]:
+ db_raw = []
+ by_category = defaultdict(list)
+ for name in modules:
+ module = modules[name]
+ if module.hidden:
+ continue
+
+ db_raw.append((module.key, module.key))
+ for tag in module.categories:
+ db_raw.append((tag, module.key))
+ by_category[tag].append(module)
+
+ return pd.DataFrame(db_raw, columns=["tag", "module"]), by_category
+
+ def search(self, text):
+ if len(self.db.index) == 0:
+ return self.macros, []
+
+ res = self.db.loc[self.db["tag"].str.contains(text, case=False)]
+ lowtext = text.lower()
+ macros = [v for v in self.macros if lowtext in v.lower()]
+ return macros, res["module"].unique().tolist()
+
+ def select_macro(self, macro: str):
+ return self.db.loc[self.db["tag"].str.lower() == macro.lower(), "module"].unique().tolist()
diff --git a/src/ltrace/ltrace/slicer/widget/global_progress_bar.py b/src/ltrace/ltrace/slicer/widget/global_progress_bar.py
index ddda8e3..1bf99ce 100644
--- a/src/ltrace/ltrace/slicer/widget/global_progress_bar.py
+++ b/src/ltrace/ltrace/slicer/widget/global_progress_bar.py
@@ -1,6 +1,10 @@
-import ctk
+import vtk
import qt
import slicer
+import mrml
+
+from ltrace.slicer.helpers import svgToQIcon
+from ltrace.slicer_utils import getResourcePath
class GlobalProgressBar(qt.QWidget):
@@ -23,55 +27,96 @@ def __init__(self):
layout = qt.QHBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
-
- self.progressBar = slicer.qSlicerCLIProgressBar()
- self.progressBar.findChild(ctk.ctkExpandButton).setVisible(False)
- self.progressBar.setStatusVisibility(False)
- self.progressBar.setNameVisibility(False)
- self.progressBar.setMaximumHeight(25)
- self.progressBar.setMinimumHeight(25)
- self.progressBar.setMaximumWidth(400)
- self.progressBar.setMinimumWidth(400)
- newFont = self.progressBar.font
- newFont.setPixelSize(7)
- self.progressBar.setFont(newFont)
+ layout.setSpacing(2)
+ layout.setAlignment(qt.Qt.AlignRight)
+
+ self.progressBar = qt.QProgressBar()
+ self.progressBar.setFixedHeight(10)
+ self.progressBar.setMinimumWidth(128)
+ self.progressBar.setMaximumWidth(256)
+ self.progressBar.setProperty("style", "thin")
+ # newFont = self.progressBar.font
+ # newFont.setPixelSize(7)
+ # self.progressBar.setFont(newFont)
self.toolButton = qt.QToolButton()
self.toolButton.setToolTip("Return to the last CLI started.")
self.toolButton.setEnabled(False)
- icon = (
- slicer.util.mainWindow().moduleSelector().findChildren(qt.QToolButton)[-2].icon
- ) # Next module tool button
+ icon = svgToQIcon(getResourcePath("Icons") / "IconSet-dark" / "Open.svg")
self.toolButton.setObjectName("GoBackButton")
self.toolButton.setPopupMode(qt.QToolButton.MenuButtonPopup)
self.toolButton.setIcon(icon)
+ self.toolButton.setAutoRaise(True)
self.lastCLILabel = qt.QLabel()
self.lastCLILabel.setObjectName("lastCLILabel")
+ layout.addStretch(1)
layout.addWidget(self.lastCLILabel)
layout.addWidget(self.toolButton)
layout.addWidget(self.progressBar)
self.setLayout(layout)
+ self.visible = False
+
+ self.setSizePolicy(qt.QSizePolicy.Minimum, qt.QSizePolicy.Fixed)
+
GlobalProgressBar._instance = self
- def setCommandLineModuleNode(self, cliNode, localProgressBar):
+ def setCommandLineModuleNode(self, cliNode, localProgressBar, customText=""):
GlobalProgressBar._instance.currentCliNode = cliNode
- GlobalProgressBar._instance.progressBar.setCommandLineModuleNode(cliNode)
+ cliNode.AddObserver("ModifiedEvent", GlobalProgressBar._instance.updateUiFromCommandLineModuleNode)
GlobalProgressBar._instance.toolButton.setEnabled(True)
GlobalProgressBar._instance.toolButton.clicked.disconnect()
GlobalProgressBar._instance.toolButton.clicked.connect(localProgressBar.returnToCLIWidget)
- GlobalProgressBar._instance.lastCLILabel.setText(
- f"Last scheduled job from: {localProgressBar.getCLIWidgetName()}"
- )
+ GlobalProgressBar._instance.lastCLILabel.setText(customText)
+ localProgressBar.refToGlobal = self
+ self.visible = True
def actionCreated(self, action):
GlobalProgressBar._instance.toolButton.addAction(action)
+ def disableWhenJobIsCompleted(self):
+ currentCLiNode = GlobalProgressBar._instance.currentCliNode
+ if currentCLiNode and "Completed" in currentCLiNode.GetStatusString():
+ GlobalProgressBar._instance.toolButton.setEnabled(False)
+ GlobalProgressBar._instance.lastCLILabel.setText("")
+ self.visible = False
+
+ def updateUiFromCommandLineModuleNode(self, cliNode, event):
+ if cliNode is None:
+ return
+
+ status = cliNode.GetStatus()
+ info = cliNode.GetModuleDescriptionAsString()
+
+ if status == mrml.vtkMRMLCommandLineModuleNode.Cancelled:
+ self.progressBar.setRange(0, 0)
+ self.progressBar.setValue(0)
+ self.visible = False
+ elif status == mrml.vtkMRMLCommandLineModuleNode.Scheduled:
+ self.progressBar.setRange(0, 0)
+ self.progressBar.setValue(0)
+ GlobalProgressBar._instance.lastCLILabel.setText("Scheduling...")
+ elif status == mrml.vtkMRMLCommandLineModuleNode.Running:
+ maxRange = 100 if cliNode.GetProgress() != 0 else 0
+ self.progressBar.setRange(0, maxRange)
+ self.progressBar.setValue(cliNode.GetProgress())
+ GlobalProgressBar._instance.lastCLILabel.setText("Running...")
+ elif (
+ status == mrml.vtkMRMLCommandLineModuleNode.Completed
+ or status == mrml.vtkMRMLCommandLineModuleNode.CompletedWithErrors
+ ):
+ self.progressBar.setRange(0, 100)
+ self.progressBar.setValue(100)
+ message = (
+ "Completed with errors" if status == mrml.vtkMRMLCommandLineModuleNode.CompletedWithErrors else "Done"
+ )
+ GlobalProgressBar._instance.lastCLILabel.setText(message)
+
class LocalProgressBar(qt.QWidget):
def __init__(self, parent=None):
@@ -89,11 +134,16 @@ def __init__(self, parent=None):
self.completed = 0
self.running = False
self.goBackAction = None
+ self.refToGlobal = None
def setCommandLineModuleNode(self, cliNode):
- self.module = slicer.util.mainWindow().moduleSelector().selectedModule
- self.moduleWidget = eval(f"slicer.modules.{self.module}Widget")
+ self.module = slicer.modules.AppContextInstance.mainWindow.moduleSelector().selectedModule
+
+ if not self.module:
+ return
+
+ self.moduleWidget = slicer.util.getModuleWidget(self.module)
try:
self.tabIndex = self.moduleWidget.mainTab.currentIndex
@@ -140,7 +190,7 @@ def _onCLIModified(self, cliNode, event):
if cliNode is None:
return
- if cliNode.GetStatusString() == "Completed" and self.running:
+ if "Completed" in cliNode.GetStatusString() and self.running:
self.completed += 1
self.running = False
elif cliNode.GetStatusString() == "Running" and cliNode in self.scheduled:
@@ -154,7 +204,7 @@ def getCLIWidgetName(self):
name = currentModule.parent.title
try:
name += f" → {self.moduleWidget.mainTab.tabText(self.tabIndex)}"
- if self.subtabIndex != None:
+ if self.subtabIndex is not None:
currentTab = self.moduleWidget.mainTab.widget(self.tabIndex)
name += f" → {currentTab.tabText(self.subtabIndex)}"
except AttributeError as e: # no attribute 'mainTab'
@@ -162,7 +212,10 @@ def getCLIWidgetName(self):
return name
def returnToCLIWidget(self):
- moduleSelectorToolBar = slicer.util.mainWindow().findChild(slicer.qSlicerModuleSelectorToolBar)
+ moduleSelectorToolBar = slicer.modules.AppContextInstance.mainWindow.findChild(
+ slicer.qSlicerModuleSelectorToolBar
+ )
moduleSelectorToolBar.selectModule(self.module)
self.completed = 0
+ self.refToGlobal.disableWhenJobIsCompleted()
self.changeAction()
diff --git a/src/ltrace/ltrace/slicer/widget/help_button.py b/src/ltrace/ltrace/slicer/widget/help_button.py
index 18f7108..9a811ff 100644
--- a/src/ltrace/ltrace/slicer/widget/help_button.py
+++ b/src/ltrace/ltrace/slicer/widget/help_button.py
@@ -1,47 +1,91 @@
+from pathlib import Path
import markdown
import qt
+from ltrace.slicer.helpers import get_scripted_modules_path, svgToQIcon
+from ltrace.slicer_utils import getResourcePath
class HelpButton(qt.QToolButton):
- def __init__(self, message: str, *args, **kwargs) -> None:
+ def __init__(self, message: str = None, url: str = None, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
+
self.message = message
- self.html_message = markdown.markdown(message)
+ self.url = url
self.setStyleSheet("border : none;")
- self.setIcon(qt.QApplication.style().standardIcon(qt.QStyle.SP_MessageBoxQuestion))
- self.setIconSize(qt.QSize(20, 20))
- self.setEnabled(True)
- self.clicked.connect(lambda: self.showFloatingMessage(self.html_message))
+ self.setIcon(svgToQIcon(getResourcePath("Icons") / "IconSet-dark" / "CircleHelp.svg"))
+ self.setIconSize(qt.QSize(18, 18))
+ self.clicked.connect(self.handleClick)
+
+ def handleClick(self):
+ self.updateMessage(self.message)
+ if self.url:
+ self.handleLinkClick(self.url)
+ else:
+ self.showFloatingMessage(self.html_message)
def updateMessage(self, message: str) -> None:
self.message = message
- self.html_message = markdown.markdown(message)
-
- def showFloatingMessage(self, message: str = "") -> None:
- pos = qt.QCursor.pos() - qt.QPoint(10, 10)
-
- label = qt.QLabel(self)
- label.setWindowFlags(qt.Qt.ToolTip)
- label.setTextFormat(qt.Qt.RichText)
- label.setTextInteractionFlags(qt.Qt.TextBrowserInteraction)
- label.setOpenExternalLinks(True)
- label.setStyleSheet(
- """
- padding: 20px;
- border: 1px;
- border-style: outset;
- -qt-block-indent: 0;
- font-size: 12px;
- """
- )
- label.setText(message)
- label.setWordWrap(True)
- label.setMaximumWidth(500)
- label.move(pos)
- label.show()
-
- label.installEventFilter(self)
-
- def eventFilter(self, object, event) -> None:
+ if self.message:
+ self.html_message = markdown.markdown(message)
+
+ def updateLink(self, helpURL: str) -> None:
+ self.url = helpURL
+
+ def showFloatingMessage(self, message: str = None) -> None:
+ if message:
+ pos = qt.QCursor.pos() - qt.QPoint(10, 10)
+
+ text_browser = qt.QTextBrowser(self)
+ text_browser.setWindowFlags(qt.Qt.ToolTip)
+ text_browser.setHtml(message)
+ text_browser.setOpenExternalLinks(False) # Disable automatic link handling
+ text_browser.anchorClicked.connect(self.handleLinkClick)
+ text_browser.setStyleSheet(
+ """
+ padding: 20px;
+ border: 1px;
+ border-style: outset;
+ font-size: 12px;
+ """
+ )
+ text_browser.setWordWrapMode(qt.QTextOption.WordWrap)
+ text_browser.setHorizontalScrollBarPolicy(qt.Qt.ScrollBarAlwaysOff)
+ text_browser.setVerticalScrollBarPolicy(qt.Qt.ScrollBarAlwaysOff)
+
+ # Adjust size to fit content
+ document = text_browser.document
+ document.setTextWidth(400) # Set a maximum width
+ size = document.size.toSize()
+ padding = 70
+ text_browser.setFixedSize(qt.QSize(size.width() + padding, size.height() + padding))
+
+ text_browser.move(pos)
+ text_browser.show()
+
+ text_browser.installEventFilter(self)
+
+ text_browser.move(pos)
+ text_browser.show()
+
+ text_browser.installEventFilter(self)
+ self.text_browser = text_browser
+
+ def handleLinkClick(self, url):
+ if isinstance(url, qt.QUrl) and url.isValid():
+ qt.QDesktopServices.openUrl(url)
+ if hasattr(self, "text_browser"):
+ self.text_browser.hide()
+ return
+ if isinstance(url, str) and url.lower().endswith(".html"):
+ fileUrl = qt.QUrl(url)
+ qt.QDesktopServices.openUrl(fileUrl)
+ else:
+ manualPath = (getResourcePath("manual") / "Welcome" / "welcome.html").as_posix()
+ qt.QDesktopServices.openUrl(qt.QUrl(f"file:///{manualPath}"))
+
+ if hasattr(self, "text_browser"):
+ self.text_browser.hide()
+
+ def eventFilter(self, obj, event) -> None:
if event.type() == qt.QEvent.Leave or event.type() == qt.QEvent.MouseButtonRelease:
- object.hide()
+ obj.hide()
diff --git a/src/ltrace/ltrace/slicer/widget/hierarchy_volume_input.py b/src/ltrace/ltrace/slicer/widget/hierarchy_volume_input.py
index 6e366be..03b8b39 100644
--- a/src/ltrace/ltrace/slicer/widget/hierarchy_volume_input.py
+++ b/src/ltrace/ltrace/slicer/widget/hierarchy_volume_input.py
@@ -1,63 +1,150 @@
+import logging
+
import qt
import slicer
+from ltrace.slicer.helpers import BlockSignals
from ltrace.utils.custom_event_filter import CustomEventFilter
-class HierarchyVolumeInput(slicer.qMRMLSubjectHierarchyComboBox):
+class HierarchyVolumeInput(qt.QWidget):
+
+ currentItemChanged = qt.Signal(object)
+
def __init__(
self,
- onChange=None,
hasNone=False,
nodeTypes=["vtkMRMLScalarVolumeNode", "vtkMRMLLabelMapVolumeNode"],
defaultText=None,
+ allowFolders=False,
+ parent=None,
):
- super().__init__()
+ super().__init__(parent)
+
+ self.__itemSelectedHandlerConnected = False
+
+ self.__resetStylesheet = False
+
+ self.allowedNodeTypes = nodeTypes
+ self.foldersAllowed = allowFolders
+
+ self.selectorWidget = slicer.qMRMLSubjectHierarchyComboBox(self)
+
self.subjectHierarchy = slicer.mrmlScene.GetSubjectHierarchyNode()
- self.setNodeTypes(nodeTypes)
- self.setMRMLScene(slicer.mrmlScene)
- self.noneEnabled = hasNone
- if onChange:
- self.currentItemChanged.connect(lambda: onChange(self.currentItem()))
+ self.selectorWidget.setNodeTypes(nodeTypes)
+ self.selectorWidget.setMRMLScene(slicer.mrmlScene)
+ self.selectorWidget.noneEnabled = hasNone
+ self.selectorWidget.setCurrentItem(0)
+
+ layout = qt.QHBoxLayout()
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.addWidget(self.selectorWidget)
+
+ self.__previousItemId = self.selectorWidget.currentItem()
+
self.customDefaultText = defaultText
if self.customDefaultText:
- self.setProperty("defaultText", self.customDefaultText)
+ self.selectorWidget.setProperty("defaultText", self.customDefaultText)
self.node_attribute_filter_list = []
- self.event_filter = CustomEventFilter(self.eventFilter, self)
+ self.event_filter = CustomEventFilter(self.eventFilter, self.selectorWidget)
self.event_filter.install()
self.end_close_scene_observer_handler = slicer.mrmlScene.AddObserver(
slicer.mrmlScene.EndCloseEvent, self.__onEndCloseScene
)
+ self.setLayout(layout)
+ self.destroyed.connect(self.__del__)
+
+ def __del__(self, obj: qt.QObject = None) -> None:
+ slicer.mrmlScene.RemoveObserver(self.end_close_scene_observer_handler)
+
+ def setMRMLScene(self, scene: slicer.mrmlScene) -> None:
+ """Wrapper for qMRMLSubjectHierarchyComboBox setMRMLScene method.
+
+ Args:
+ scene (slicer.mrmlScene): the scene object.
+ """
+ if self.selectorWidget is None:
+ return
+
+ self.selectorWidget.setMRMLScene(scene)
+
def eventFilter(self, object, event):
- if type(event) == qt.QMouseEvent:
- if event.type() == qt.QEvent.MouseButtonPress:
- self.refreshAttributeFilter()
- return True
+ event_type = event.type()
+
+ if event_type == qt.QEvent.MouseButtonPress or event_type == qt.QEvent.KeyPress:
+ self.refreshAttributeFilter() # This is a workaround to properly update the node list
+ elif event_type == qt.QEvent.Show:
+ self._connectItemChangedHandler()
+ elif event_type == qt.QEvent.Hide:
+ self._disconnectItemChangedHandler()
+
return False
+ def _connectItemChangedHandler(self):
+ if not self.__itemSelectedHandlerConnected:
+ self.selectorWidget.currentItemChanged.connect(self.itemChangedHandler)
+ self.__itemSelectedHandlerConnected = True
+
+ def _disconnectItemChangedHandler(self):
+ if self.__itemSelectedHandlerConnected:
+ self.selectorWidget.currentItemChanged.disconnect(self.itemChangedHandler)
+ self.__itemSelectedHandlerConnected = False
+
+ def itemChangedHandler(self, itemId):
+
+ if itemId == self.__previousItemId:
+ return
+
+ if itemId > 0:
+ selectedNode = slicer.mrmlScene.GetSubjectHierarchyNode().GetItemDataNode(itemId)
+
+ if selectedNode is None and not self.foldersAllowed:
+ self.selectorWidget.setCurrentItem(0)
+ return
+
+ if selectedNode and (
+ selectedNode.GetHideFromEditors()
+ or not any(selectedNode.IsA(nodeType) for nodeType in self.allowedNodeTypes)
+ ):
+ self.selectorWidget.setCurrentItem(0)
+ return
+
+ if self.__resetStylesheet:
+ self.resetStyleSheetOnChange()
+
+ self.currentItemChanged.emit(itemId)
+ self.__previousItemId = itemId
+
def refreshAttributeFilter(self):
# This is a workaround to properly update the node list
for attribute_name, attribute_value in self.node_attribute_filter_list:
- self.removeNodeAttributeFilter(attribute_name, attribute_value)
- self.addNodeAttributeFilter(attribute_name, attribute_value)
+ self.selectorWidget.removeNodeAttributeFilter(attribute_name, attribute_value)
+ self.selectorWidget.addNodeAttributeFilter(attribute_name, attribute_value)
def currentNode(self):
- itemId = self.currentItem()
+ itemId = self.selectorWidget.currentItem()
if itemId:
return self.subjectHierarchy.GetItemDataNode(itemId)
return None
+ def currentItem(self):
+ """Deprecated. Use currentNode() instead. Keeping just for back compatibility."""
+ return self.selectorWidget.currentItem()
+
def setCurrentNode(self, node):
self.refreshAttributeFilter()
if node is None or isinstance(node, slicer.vtkMRMLNode):
- self.setCurrentItem(self.subjectHierarchy.GetItemByDataNode(node))
+ self.selectorWidget.setCurrentItem(self.subjectHierarchy.GetItemByDataNode(node))
+
+ def setCurrentItem(self, itemId: int):
+ self.selectorWidget.setCurrentItem(itemId)
def addNodeAttributeIncludeFilter(self, attribute_name, attribute_value):
self.node_attribute_filter_list.append((attribute_name, attribute_value))
- self.addNodeAttributeFilter(attribute_name, attribute_value)
+ self.selectorWidget.addNodeAttributeFilter(attribute_name, attribute_value)
def removeNodeAttributeIncludeFilter(self, attribute_name, attribute_value=None):
"""Remove attribute filter that includes items in the combobox
@@ -77,26 +164,31 @@ def removeNodeAttributeIncludeFilter(self, attribute_name, attribute_value=None)
self.node_attribute_filter_list.remove((attribute_name, attribute_value))
except ValueError:
pass
- self.removeNodeAttributeFilter(attribute_name, attribute_value)
+
+ self.selectorWidget.removeNodeAttributeFilter(attribute_name, attribute_value)
def resetStyleOnValidNode(self):
- def inputChanged():
- if self.currentNode():
- self.setStyleSheet("")
+ self.__resetStylesheet = True
- self.currentItemChanged.connect(inputChanged)
+ def resetStyleSheetOnChange(self):
+ if self.currentNode():
+ self.setStyleSheet("")
def clearSelection(self):
- slicer.qMRMLSubjectHierarchyComboBox.clearSelection(self)
if self.customDefaultText:
- self.setProperty("defaultText", self.customDefaultText)
+ self.selectorWidget.setProperty("defaultText", self.customDefaultText)
+ previousStateConnected = self.__itemSelectedHandlerConnected
+ self._connectItemChangedHandler()
+ if self.selectorWidget.currentItem() != 0:
+ self.selectorWidget.setCurrentItem(0)
+ else:
+ self.itemChangedHandler(0)
+
+ if not previousStateConnected:
+ self._disconnectItemChangedHandler()
def __onEndCloseScene(self, *args):
try:
- self.children()
- except ValueError:
- # Widget was destroyed
- slicer.mrmlScene.RemoveObserver(self.end_close_scene_observer_handler)
- return
- if self.customDefaultText:
- self.setProperty("defaultText", self.customDefaultText)
+ self.clearSelection()
+ except Exception as e:
+ logging.error(e)
diff --git a/src/ltrace/ltrace/slicer/widget/memory_usage.py b/src/ltrace/ltrace/slicer/widget/memory_usage.py
new file mode 100644
index 0000000..ee3b3a8
--- /dev/null
+++ b/src/ltrace/ltrace/slicer/widget/memory_usage.py
@@ -0,0 +1,45 @@
+import qt
+import psutil
+
+from ltrace.slicer_utils import getResourcePath
+
+
+class MemoryUsageWidget(qt.QWidget):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+
+ layout = qt.QHBoxLayout()
+ layout.setContentsMargins(8, 0, 16, 0)
+ layout.setSpacing(3)
+ icon = qt.QIcon((getResourcePath("Icons") / "IconSet-dark" / "Memory.svg").as_posix())
+ self.labelIcon = qt.QLabel()
+ self.labelIcon.setPixmap(icon.pixmap(16, 16))
+ self.memoryCounter = qt.QLabel("")
+ self.memoryUsedTimer = qt.QTimer()
+
+ layout.addWidget(self.labelIcon)
+ layout.addWidget(self.memoryCounter)
+ self.setLayout(layout)
+
+ self.setStyleSheet(
+ "QLabel {\
+ font-size: 12px;\
+ }"
+ )
+ self.toolTip = "Memory Usage"
+
+ def update(self, value, total):
+ percent = int((value / total) * 100)
+ text = f"{value} / {total}GB ({percent}%)"
+ if text != self.memoryCounter.text:
+ self.memoryCounter.setText(text)
+
+ def start(self):
+ self.memoryUsedTimer.setInterval(1000)
+ self.memoryUsedTimer.timeout.connect(self.__poller)
+ self.memoryUsedTimer.start()
+
+ def __poller(self):
+ used_gb = int(round(psutil.virtual_memory().used / (1024**3)))
+ total_gb = int(round(psutil.virtual_memory().total / (1024**3)))
+ self.update(used_gb, total_gb)
diff --git a/src/ltrace/ltrace/slicer/widget/module_header.py b/src/ltrace/ltrace/slicer/widget/module_header.py
new file mode 100644
index 0000000..c83c9c2
--- /dev/null
+++ b/src/ltrace/ltrace/slicer/widget/module_header.py
@@ -0,0 +1,32 @@
+import qt
+
+from ltrace.slicer.helpers import svgToQIcon
+from ltrace.slicer_utils import getResourcePath
+from ltrace.slicer.widget.help_button import HelpButton
+
+
+class ModuleHeader(qt.QWidget):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+
+ # Initialize layout
+ self.headerLayout = qt.QHBoxLayout(self)
+ self.headerLayout.setContentsMargins(6, 6, 6, 6)
+ self.headerLayout.setSpacing(3)
+ self.headerLayout.setAlignment(qt.Qt.AlignLeft)
+
+ self.baseIcon = svgToQIcon(getResourcePath("Icons") / "IconSet-dark" / "ChevronRight.svg")
+ self.baseLabel = qt.QLabel()
+ self.baseLabel.setPixmap(self.baseIcon.pixmap(qt.QSize(16, 16)))
+ self.moduleTitle = qt.QLabel("")
+ self.moduleHelp = HelpButton()
+
+ self.headerLayout.setAlignment(qt.Qt.AlignLeft)
+ self.headerLayout.addWidget(self.baseLabel)
+ self.headerLayout.addWidget(self.moduleTitle)
+ self.headerLayout.addStretch(1)
+ self.headerLayout.addWidget(self.moduleHelp)
+
+ def update(self, title, helpURL):
+ self.moduleTitle.setText(title.upper())
+ self.moduleHelp.updateLink(helpURL)
diff --git a/src/ltrace/ltrace/slicer/widget/module_indicator.py b/src/ltrace/ltrace/slicer/widget/module_indicator.py
new file mode 100644
index 0000000..5c8c9f7
--- /dev/null
+++ b/src/ltrace/ltrace/slicer/widget/module_indicator.py
@@ -0,0 +1,64 @@
+import qt
+import slicer
+
+from ltrace.slicer.module_info import ModuleInfo
+from ltrace.slicer_utils import getResourcePath
+
+
+class ModuleIndicator(qt.QWidget):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+
+ layout = qt.QHBoxLayout(self)
+ layout.setContentsMargins(8, 0, 0, 0)
+ layout.setSpacing(2)
+
+ self.moduleBtn = qt.QToolButton(self)
+ self.moduleBtn.setToolButtonStyle(qt.Qt.ToolButtonTextBesideIcon)
+ self.moduleBtn.setAutoRaise(True)
+
+ # TODO check if it worth it to have a bookmark
+ # self.bookmarkBtn = self.checkableButton(
+ # lambda checked: print("Bookmark checked" if checked else "Bookmark unchecked"),
+ # svgToQIcon(getResourcePath("Icons") / "IconSet-dark" / "BookmarkCheck.svg"),
+ # svgToQIcon(getResourcePath("Icons") / "IconSet-dark" / "Bookmark.svg"),
+ # checked=False
+ # )
+ # self.bookmarkBtn.setToolButtonStyle(qt.Qt.ToolButtonIconOnly)
+ # self.bookmarkBtn.setAutoRaise(True)
+
+ self.docBtn = qt.QToolButton(self)
+ self.docBtn.setIcon(
+ svgToQIcon(getResourcePath("Icons") / "IconSet-dark" / "CircleHelp.svg"),
+ )
+ self.docBtn.setToolButtonStyle(qt.Qt.ToolButtonIconOnly)
+ self.docBtn.setAutoRaise(True)
+
+ layout.addWidget(self.moduleBtn)
+ # layout.addWidget(self.bookmarkBtn)
+ layout.addWidget(self.docBtn)
+
+ def setModule(self, moduleInfo: ModuleInfo):
+ module = getattr(slicer.modules, moduleInfo.key.lower())
+ self.moduleBtn.setIcon(module.icon)
+ self.moduleBtn.setText(module.title)
+
+ @staticmethod
+ def checkableButton(func, checkedIcon, uncheckedIcon, checked=False):
+ button = qt.QToolButton()
+ button.setCheckable(True)
+ button.setChecked(checked)
+ button.setIcon(checkedIcon if checked else uncheckedIcon)
+
+ # Connect the toggled signal to change the icon
+ def update_icon(checked_):
+ if checked_:
+ button.setIcon(checkedIcon)
+ else:
+ button.setIcon(uncheckedIcon)
+
+ func(checked_)
+
+ button.toggled.connect(update_icon)
+
+ return button
diff --git a/src/ltrace/ltrace/slicer/widget/pixel_size_editor.py b/src/ltrace/ltrace/slicer/widget/pixel_size_editor.py
index 1d0c46e..7d7204d 100644
--- a/src/ltrace/ltrace/slicer/widget/pixel_size_editor.py
+++ b/src/ltrace/ltrace/slicer/widget/pixel_size_editor.py
@@ -1,8 +1,9 @@
import slicer
import qt
-from Customizer import Customizer
+
from ltrace.slicer import helpers
from ltrace.slicer.node_observer import NodeObserver
+from ltrace.slicer_utils import getResourcePath
from ltrace.utils.Markup import MarkupLine
@@ -32,7 +33,8 @@ def __init__(self, parent=None):
self.scaleSizePxLineEdit.setToolTip("Scale size in pixels")
self.scaleSizePxLineEdit.textEdited.connect(lambda: self.__on_size_field_edited(self.FIELD_SCALE_SIZE_PX))
self.scaleSizePxRuler = qt.QPushButton()
- self.scaleSizePxRuler.setIcon(qt.QIcon(str(Customizer.ANNOTATION_DISTANCE_ICON_PATH)))
+
+ self.scaleSizePxRuler.setIcon(qt.QIcon(getResourcePath("Icons") / "AnnotationDistance.png"))
self.scaleSizePxRuler.connect("clicked()", self.onScaleSizeRulerButtonClicked)
self.scaleSizePxFrame = qt.QFrame()
scaleSizePxLayout = qt.QHBoxLayout(self.scaleSizePxFrame)
@@ -65,11 +67,14 @@ def __init__(self, parent=None):
self.__set_retain_size_when_hidden(self.loadFormLayout.labelForField(self.imageSpacingLineEdit), True)
def onScaleSizeRulerButtonClicked(self):
- def finish(caller_markup, point_index=None):
+ def finish_callback(caller_markup, point_index=None):
self.scaleSizePxLineEdit.text = round(caller_markup.get_line_length_in_pixels())
self.__on_size_field_edited(self.FIELD_SCALE_SIZE_PX)
- self.markup = MarkupLine(finish)
+ def finish_criterion(caller_markup, point_index=None):
+ return caller_markup.get_number_of_selected_points() >= 2
+
+ self.markup = MarkupLine(finish_callback=finish_callback, finish_criterion=finish_criterion)
self.markup.start_picking()
def reset(self):
diff --git a/src/ltrace/ltrace/slicer/widget/save_netcdf.py b/src/ltrace/ltrace/slicer/widget/save_netcdf.py
index 714cdba..657a973 100644
--- a/src/ltrace/ltrace/slicer/widget/save_netcdf.py
+++ b/src/ltrace/ltrace/slicer/widget/save_netcdf.py
@@ -2,10 +2,11 @@
import ctk
import vtk
import slicer
+
from ltrace.slicer import ui
+from ltrace.slicer import export, netcdf
from pathlib import Path
-import Export
-import NetCDFExport
+
EXPORTABLE_TYPES = (
slicer.vtkMRMLLabelMapVolumeNode,
@@ -19,7 +20,7 @@ def getNodesFromFolder(folderId):
ids = vtk.vtkIdList()
ids.SetNumberOfIds(1)
ids.SetId(0, folderId)
- return Export.ExportLogic().getDataNodes(ids, EXPORTABLE_TYPES)
+ return export.getDataNodes(ids, EXPORTABLE_TYPES)
class SaveNetcdfWidget(qt.QFrame):
@@ -67,13 +68,17 @@ def __init__(self, *args):
detailsLayout.addWidget(detailsLabel)
layout.addRow(detailsGroup)
- self.folderSelector = ui.hierarchyVolumeInput(nodeTypes=EXPORTABLE_TYPES)
- self.folderSelector.setToolTip("All images in this folder that are not yet in the file will be added.")
+ self.folderSelector = ui.hierarchyVolumeInput(
+ nodeTypes=EXPORTABLE_TYPES,
+ tooltip="All images in this folder that are not yet in the file will be added.",
+ allowFolders=True,
+ )
layout.addRow("Folder:", self.folderSelector)
self.fileLabel = qt.QLabel()
self.fileLabel.setToolTip(
- "The selected folder was previously imported from this file. New images will be saved to the file. Existing images and attributes will remain in the file."
+ "The selected folder was previously imported from this file. New images will be saved to the file. "
+ "Existing images and attributes will remain in the file."
)
layout.addRow("Save as:", self.fileLabel)
@@ -104,5 +109,5 @@ def onSave(self):
path = Path(self.fileLabel.text)
folderId = self.folderSelector.currentItem()
nodes = getNodesFromFolder(folderId)
- NetCDFExport.exportNetcdf(path, nodes, single_coords=True, save_in_place=True)
+ netcdf.exportNetcdf(path, nodes, single_coords=True, save_in_place=True)
self.close()
diff --git a/src/ltrace/ltrace/slicer/widget/simulation/__init__.py b/src/ltrace/ltrace/slicer/widget/simulation/__init__.py
new file mode 100644
index 0000000..982183c
--- /dev/null
+++ b/src/ltrace/ltrace/slicer/widget/simulation/__init__.py
@@ -0,0 +1 @@
+from .widgets import *
diff --git a/src/modules/PoreNetworkSimulation/PoreNetworkSimulationLib/widgets.py b/src/ltrace/ltrace/slicer/widget/simulation/widgets.py
similarity index 100%
rename from src/modules/PoreNetworkSimulation/PoreNetworkSimulationLib/widgets.py
rename to src/ltrace/ltrace/slicer/widget/simulation/widgets.py
diff --git a/src/ltrace/ltrace/slicer/widgets.py b/src/ltrace/ltrace/slicer/widgets.py
index 1f347c2..f1d4838 100644
--- a/src/ltrace/ltrace/slicer/widgets.py
+++ b/src/ltrace/ltrace/slicer/widgets.py
@@ -15,6 +15,8 @@
from typing import Union, List, Tuple
+from ltrace.slicer_utils import getResourcePath
+
def findSOISegmentID(segmentation):
n_segments = segmentation.GetNumberOfSegments()
@@ -353,6 +355,12 @@ def getSelectedSegments(self):
return selectedItems
+ def allSegmentsSelected(self):
+ return all(
+ self.segmentListGroup[1].item(nth).checkState() == qt.Qt.Checked
+ for nth in range(self.segmentListGroup[1].count)
+ )
+
def _onAutoPoreCalcToggled(self, state):
mainNode = self.mainInput.currentNode()
if mainNode:
@@ -366,11 +374,8 @@ def _onAutoPoreCalcToggled(self, state):
)
def updateRefNode(self, node):
- self.referenceInput.blockSignals(True)
self.referenceInput.setCurrentNode(node)
- self.referenceInput.blockSignals(False)
self.referenceInput.setStyleSheet("")
- self._onReferenceSelected(node)
def _onMainSelected(self, item):
try:
@@ -562,7 +567,7 @@ def editTargetSegment(self):
self.showEditTargetDialog(self.mainInput.currentNode())
def showEditTargetDialog(self, segmentationNode):
- dialogWidget = qt.QDialog(slicer.util.mainWindow())
+ dialogWidget = qt.QDialog(slicer.modules.AppContextInstance.mainWindow)
dialogWidget.setModal(True)
dialogWidget.setWindowTitle("Edit Target")
@@ -623,7 +628,9 @@ def updateSegmentList(self, segments):
self.segmentListUpdated.emit((mainNode, soiNode, referenceNode), segments)
# TODO check for overlaps
- [s.show() for s in self.segmentListGroup]
+ for s in self.segmentListGroup:
+ s.show()
+
self.dimensionsGroup.show()
@@ -932,12 +939,10 @@ def __init__(self):
self.setChecked(True)
def update(self):
- from Customizer import Customizer
-
if self.checked:
- self.setIcon(qt.QIcon(str(Customizer.OPEN_EYE_ICON_PATH)))
+ self.setIcon(qt.QIcon(getResourcePath("Icons") / "EyeOpen.png"))
else:
- self.setIcon(qt.QIcon(str(Customizer.CLOSED_EYE_ICON_PATH)))
+ self.setIcon(qt.QIcon(getResourcePath("Icons") / "EyeClosed.png"))
def isOpen(self):
return self.checked == 1
diff --git a/src/ltrace/ltrace/slicer_utils.py b/src/ltrace/ltrace/slicer_utils.py
index 951ce4c..40518db 100644
--- a/src/ltrace/ltrace/slicer_utils.py
+++ b/src/ltrace/ltrace/slicer_utils.py
@@ -1,19 +1,17 @@
-import qt
-import slicer
-import vtk
-import json
import markdown2 as markdown
import numpy as np
-import os
import pandas as pd
+from typing import Union
+from pathlib import Path
+from abc import abstractmethod
+
+from SegmentEditorEffects import *
+from slicer import ScriptedLoadableModule
from ltrace.slicer.application_observables import ApplicationObservables
+from ltrace.slicer.helpers import svgToQIcon
from ltrace.slicer.tests.ltrace_plugin_test import LTracePluginTest
from ltrace.slicer.tests.ltrace_tests_widget import LTraceTestsWidget
-from pathlib import Path
-from SegmentEditorEffects import *
-from slicer import ScriptedLoadableModule
-from typing import Union
__all__ = [
@@ -47,6 +45,12 @@ def __init__(self, *args, **kwargs):
if self.SETTING_KEY is None:
raise NotImplementedError
+ moduleDir = Path(self.parent.path).parent
+ iconPath = moduleDir / "Resources" / "Icons" / f"{self.moduleName}.svg"
+
+ if iconPath.is_file():
+ self.parent.icon = svgToQIcon(iconPath)
+
@classmethod
def help(cls):
htmlHelp = ""
@@ -69,7 +73,9 @@ def runTest(self, useGui=True, msec=100, **kwargs):
msec: delay to associate with :func:`ScriptedLoadableModuleTest.delayDisplay()`.
"""
if useGui:
- tests_widget = LTraceTestsWidget(parent=slicer.util.mainWindow(), current_module=self.__class__.__name__)
+ tests_widget = LTraceTestsWidget(
+ parent=slicer.modules.AppContextInstance.mainWindow, currentModule=self.__class__.__name__
+ )
tests_widget.exec()
return
@@ -83,18 +89,19 @@ def get_setting(cls, key, default=None):
def set_setting(cls, key, value):
slicer.app.settings().setValue(f"{cls.SETTING_KEY}/{key}", value)
+ def resource(self, resourceName):
+ return Path(slicer.util.modulePath(self.moduleName)).parent / "Resources" / resourceName
+
+ def title(self):
+ return self.parent.title
+
class LTracePluginWidget(ScriptedLoadableModule.ScriptedLoadableModuleWidget):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
- def isReloading(self) -> bool:
- return slicer.reloadingWidget.get(self.moduleName, False) if hasattr(slicer, "reloadingWidget") else False
-
- def onReload(self) -> None:
- slicer.reloadingWidget[self.moduleName] = True
- ScriptedLoadableModule.ScriptedLoadableModuleWidget.onReload(self)
- slicer.reloadingWidget[self.moduleName] = False
+ def setup(self):
+ super().setup()
def enter(self) -> None:
ApplicationObservables().moduleWidgetEnter.emit(self)
@@ -183,6 +190,44 @@ def is_bool(value):
return tableNode
+class LTraceEnvironmentMixin:
+ @property
+ def modulesToolbar(self):
+ if not self.__modulesToolbar:
+ raise AttributeError("Modules toolbar not set")
+ return self.__modulesToolbar
+
+ @modulesToolbar.setter
+ def modulesToolbar(self, value):
+ self.__modulesToolbar = value
+
+ def segmentEditor(self):
+ return slicer.util.getModuleWidget("CustomizedSegmentEditor")
+
+ def setCategory(self, category):
+ self.category = category
+
+ @abstractmethod
+ def setupEnvironment(self):
+ pass
+
+ def setupSegmentation(self):
+ pass
+
+ def enter(self):
+ pass
+
+ def setupTools(self):
+ self.getModuleManager().addToolsMenu(self.__modulesToolbar)
+
+ def setupLoaders(self):
+ self.getModuleManager().addLoadersMenu(self.__modulesToolbar)
+
+ @staticmethod
+ def getModuleManager():
+ return slicer.modules.AppContextInstance.modules
+
+
def tableNodeToDict(tableNode):
"""
Returns a dictionary of 1D numpy arrays
@@ -228,7 +273,7 @@ def tableNodeToDict(tableNode):
def restartSlicerIn2s():
text = "Slicer has been successfully configured and must be restarted."
- mb = qt.QMessageBox(slicer.util.mainWindow())
+ mb = qt.QMessageBox(slicer.modules.AppContextInstance.mainWindow)
mb.text = text
mb.setWindowTitle("Configuration finished")
qt.QTimer.singleShot(2000, lambda: killSlicer(mb))
@@ -242,18 +287,6 @@ def killSlicer(messagebox=None):
slicer.app.restart()
-def get_json_data():
- folders = [
- *(os.path.dirname(slicer.app.launcherExecutableFilePath).split("/")),
- *(f"lib\\{base_version()}\\qt-scripted-modules\\Resources\\json\\WelcomeGeoSlicer.json".split("\\")),
- ]
- JSON_PATH = os.path.join(folders[0], os.sep, *folders[1:])
- with open(JSON_PATH, "r") as json_file:
- JSON_DATA = json.load(json_file)
-
- return JSON_DATA
-
-
def addNodeToSubjectHierarchy(node, dirPaths: list = None):
"""Add node to the subject hierarchy list regarding the folder hierarchy specified at 'dirPaths' argument.
ex: dirPaths = ["folder_a", "folder_b"] means that node will be displayed inside folder hierarchy: folder_a > folder_b > node.
@@ -453,34 +486,5 @@ def tableWidgetToDataFrame(tableWidget: qt.QTableWidget) -> pd.DataFrame:
return df
-class DebounceSignal:
- """Wrapper for qt.Signal with debouncing, emiting the signal only one time after a given interval."""
-
- def __init__(
- self, parent: Union[qt.QWidget, qt.QObject], signal: qt.Signal, intervalMs: int = 500, qtTimer=qt.QTimer
- ) -> None:
- assert signal is not None, "Invalid signal reference."
- assert parent is not None, "Invalid parent reference."
-
- self.__signal = signal
- self.timer = qtTimer(parent)
- self.timer.setSingleShot(True)
- self.timer.setInterval(intervalMs)
- self.timer.timeout.connect(self.__onTimeout)
- self.timer.stop()
- self.__args = None
- self.__kwargs = None
-
- def emit(self, *args, **kwargs) -> None:
- self.__args = args
- self.__kwargs = kwargs
-
- if self.timer.isActive():
- self.timer.stop()
-
- self.timer.start()
-
- def __onTimeout(self) -> None:
- self.__signal.emit(*self.__args, **self.__kwargs)
- self.__args = None
- self.__kwargs = None
+def getResourcePath(rtype: str) -> Path:
+ return Path(slicer.app.slicerHome) / "LTrace" / "Resources" / rtype
diff --git a/src/ltrace/ltrace/utils/Markup.py b/src/ltrace/ltrace/utils/Markup.py
index d08ae40..807ec30 100644
--- a/src/ltrace/ltrace/utils/Markup.py
+++ b/src/ltrace/ltrace/utils/Markup.py
@@ -43,7 +43,7 @@ def __init__(
self.last_slice_view_name = None
self.markups_node = createTemporaryNode(cls=self.TYPE_TO_SLICER_TYPE[self.type], name="markups_node")
- slicer.util.mainWindow().installEventFilter(self)
+ slicer.modules.AppContextInstance.mainWindow.installEventFilter(self)
def __del__(self):
self.stop_picking()
@@ -82,8 +82,9 @@ def next_pick_or_finish(interaction_node=None, event=None):
else: # pick another point
if self.update_instruction:
self.update_instruction(self, point_index)
- if interaction_node.GetCurrentInteractionMode() != 1: # (i.e., 2)
- interaction_node.SetCurrentInteractionMode(1)
+ if interaction_node.GetCurrentInteractionMode() != 1:
+ self.stop_picking()
+ self.__reset_markups_node()
self.__reset_markups_node()
self.__removeInteractionObserverTags()
@@ -119,7 +120,7 @@ def stop_picking(self):
interactionNode.SetPlaceModePersistence(0)
interactionNode.SetCurrentInteractionMode(2)
interactionNode.SwitchToViewTransformMode()
- slicer.util.mainWindow().removeEventFilter(self)
+ slicer.modules.AppContextInstance.mainWindow.removeEventFilter(self)
def cancel_picking(self):
if self.markups_node is not None:
@@ -182,6 +183,9 @@ def __removeMarkupsObserverTags(self):
def __ras_to_ijk(self, ras, volume_node=None, as_int=True):
if volume_node is None:
volume_node = slicer.mrmlScene.GetNodeByID(self.markups_node.GetNthControlPointAssociatedNodeID(0))
+ if volume_node is None:
+ self.__reset_markups_node()
+ return
ras1Arr = np.c_[ras, np.ones((ras.shape[0], 1))]
ras_to_ijk = vtk.vtkMatrix4x4()
volume_node.GetRASToIJKMatrix(ras_to_ijk)
diff --git a/src/ltrace/ltrace/utils/callback.py b/src/ltrace/ltrace/utils/callback.py
new file mode 100644
index 0000000..d2d9eb4
--- /dev/null
+++ b/src/ltrace/ltrace/utils/callback.py
@@ -0,0 +1,3 @@
+class Callback(object):
+ def __init__(self, on_update=None):
+ self.on_update = on_update or (lambda *args, **kwargs: None)
diff --git a/src/ltrace/ltrace/utils/custom_event_filter.py b/src/ltrace/ltrace/utils/custom_event_filter.py
index 24b7f52..53a57ae 100644
--- a/src/ltrace/ltrace/utils/custom_event_filter.py
+++ b/src/ltrace/ltrace/utils/custom_event_filter.py
@@ -6,7 +6,7 @@ class CustomEventFilter(qt.QObject):
def __init__(self, filter_callback, target=None):
super().__init__()
self.filter_callback = filter_callback
- self.target = target or slicer.util.mainWindow()
+ self.target = target or slicer.modules.AppContextInstance.mainWindow
def install(self):
self.target.installEventFilter(self)
diff --git a/src/ltrace/ltrace/workflow/Workflow.py b/src/ltrace/ltrace/workflow/Workflow.py
index 85336fe..6d2901c 100644
--- a/src/ltrace/ltrace/workflow/Workflow.py
+++ b/src/ltrace/ltrace/workflow/Workflow.py
@@ -1,17 +1,16 @@
import collections
-import ctk
import json
-import numpy as np
-import vtk
-
-from Customizer import Customizer
from dataclasses import dataclass
+from pathlib import Path
+
from ltrace.utils.ProgressBarProc import ProgressBarProc
from ltrace.workflow.workstep import *
from ltrace.workflow.workstep.data import *
from ltrace.workflow.workstep.segmentation import *
from ltrace.workflow.workstep.simulation import *
-from pathlib import Path
+
+from ltrace.slicer_utils import getResourcePath
+
WORKSTEPS = {
module.NAME: module
@@ -326,8 +325,8 @@ def menuBarInterface(self):
optionsMenu = menuBar.addMenu("&Options")
helpMenu = menuBar.addMenu("&Help")
- loadWorkflowAction = qt.QAction(qt.QIcon(str(Customizer.LOAD_ICON_PATH)), "&Load...", fileMenu)
- saveWorkflowAction = qt.QAction(qt.QIcon(str(Customizer.SAVE_ICON_PATH)), "&Save", fileMenu)
+ loadWorkflowAction = qt.QAction(qt.QIcon(getResourcePath("Icons") / "Load.png"), "&Load...", fileMenu)
+ saveWorkflowAction = qt.QAction(qt.QIcon(getResourcePath("Icons") / "Save.png"), "&Save", fileMenu)
saveAsWorkflowAction = qt.QAction("Save as...", fileMenu)
closeWorkflowAction = qt.QAction("&Close", fileMenu)
exitWorkflowAction = qt.QAction("E&xit", fileMenu)
@@ -385,9 +384,9 @@ def toolBarInterface(self):
toolBar.setObjectName("toolBar")
toolBar.setToolButtonStyle(qt.Qt.ToolButtonTextBesideIcon)
toolBar.setStyleSheet("QToolBar {border-top: 1px solid gray; border-bottom: 1px solid gray;}")
- runWorkflowAction = qt.QAction(qt.QIcon(str(Customizer.RUN_ICON_PATH)), "Run workflow", toolBar)
+ runWorkflowAction = qt.QAction(qt.QIcon(getResourcePath("Icons") / "Run.png"), "Run workflow", toolBar)
toolBar.addAction(runWorkflowAction)
- stopWorkflowAction = qt.QAction(qt.QIcon(str(Customizer.STOP_ICON_PATH)), "Stop workflow", toolBar)
+ stopWorkflowAction = qt.QAction(qt.QIcon(getResourcePath("Icons") / "Stop.png"), "Stop workflow", toolBar)
toolBar.addAction(stopWorkflowAction)
runWorkflowAction.triggered.connect(self.run)
@@ -494,10 +493,10 @@ def workflowPanel(self):
addWorkstepButton = qt.QPushButton("Add workstep")
addWorkstepButton.setObjectName("addWorkstepButton")
addWorkstepButton.setAutoDefault(False)
- addWorkstepButton.setIcon(qt.QIcon(str(Customizer.ADD_ICON_PATH)))
+ addWorkstepButton.setIcon(qt.QIcon(getResourcePath("Icons") / "Add.png"))
addCloneDeleteWorkstepsButtonsLayout.addWidget(addWorkstepButton)
self.deleteWorkstepButton = qt.QPushButton("Delete workstep")
- self.deleteWorkstepButton.setIcon(qt.QIcon(str(Customizer.DELETE_ICON_PATH)))
+ self.deleteWorkstepButton.setIcon(qt.QIcon(getResourcePath("Icons") / "Delete.png"))
self.deleteWorkstepButton.setEnabled(False)
self.deleteWorkstepButton.setAutoDefault(False)
addCloneDeleteWorkstepsButtonsLayout.addWidget(self.deleteWorkstepButton)
@@ -632,7 +631,7 @@ def workstepPanel(self):
workstepStateButtonsLayout = qt.QHBoxLayout()
workstepStateButtonsLayout.addWidget(qt.QWidget())
resetWorkstepButton = qt.QPushButton("Reset to default")
- resetWorkstepButton.setIcon(qt.QIcon(str(Customizer.RESET_ICON_PATH)))
+ resetWorkstepButton.setIcon(qt.QIcon(getResourcePath("Icons") / "Reset.png"))
workstepStateButtonsLayout.addWidget(resetWorkstepButton)
workstepStateButtonsLayout.addWidget(qt.QWidget())
workstepLayout.addLayout(workstepStateButtonsLayout)
diff --git a/src/ltrace/ltrace/workflow/workstep/Workstep.py b/src/ltrace/ltrace/workflow/workstep/Workstep.py
index c440a69..5217261 100644
--- a/src/ltrace/ltrace/workflow/workstep/Workstep.py
+++ b/src/ltrace/ltrace/workflow/workstep/Workstep.py
@@ -1,13 +1,14 @@
import qt
import slicer
from ltrace.slicer.helpers import getSourceVolume
-from Customizer import Customizer
+
+from ltrace.slicer_utils import getResourcePath
class Workstep:
NAME = None
- CHECK_ICON_PATH = Customizer.CHECK_CIRCLE_ICON_PATH
- ERROR_ICON_PATH = Customizer.ERROR_CIRCLE_ICON_PATH
+ CHECK_ICON_PATH = getResourcePath("Icons") / "GreenCheckCircle.png"
+ ERROR_ICON_PATH = getResourcePath("Icons") / "RedBangCircle.png"
INPUT_TYPES = None
OUTPUT_TYPE = None
diff --git a/src/ltrace/ltrace/workflow/workstep/data/Export.py b/src/ltrace/ltrace/workflow/workstep/data/Export.py
index 2cc1253..b872441 100644
--- a/src/ltrace/ltrace/workflow/workstep/data/Export.py
+++ b/src/ltrace/ltrace/workflow/workstep/data/Export.py
@@ -302,6 +302,7 @@ def setup(self):
self.layout().addLayout(self.formLayout)
self.exportDirectoryButton = ctk.ctkDirectoryButton()
+ self.exportDirectoryButton.setMaximumWidth(374)
self.exportDirectoryButton.caption = "Export directory"
self.formLayout.addRow("Export directory:", self.exportDirectoryButton)
self.formLayout.addRow(" ", None)
diff --git a/src/ltrace/ltrace/workflow/workstep/data/NetCDFLoader.py b/src/ltrace/ltrace/workflow/workstep/data/NetCDFLoader.py
index e67095d..1aa7056 100644
--- a/src/ltrace/ltrace/workflow/workstep/data/NetCDFLoader.py
+++ b/src/ltrace/ltrace/workflow/workstep/data/NetCDFLoader.py
@@ -107,6 +107,7 @@ def setup(self):
self.layout().addLayout(self.form_layout)
self.input_directory_button = ctk.ctkDirectoryButton()
+ self.input_directory_button.setMaximumWidth(374)
self.input_directory_button.setToolTip("Load all images from all NetCDF files in this directory.")
self.form_layout.addRow("Input directory:", self.input_directory_button)
diff --git a/src/ltrace/ltrace/workflow/workstep/segmentation/BoundaryRemoval.py b/src/ltrace/ltrace/workflow/workstep/segmentation/BoundaryRemoval.py
index ebe14ff..0d60bf5 100644
--- a/src/ltrace/ltrace/workflow/workstep/segmentation/BoundaryRemoval.py
+++ b/src/ltrace/ltrace/workflow/workstep/segmentation/BoundaryRemoval.py
@@ -1,5 +1,4 @@
import SimpleITK as sitk
-import qt
import sitkUtils
from SegmentEditorEffects import *
from ltrace.workflow.workstep import Workstep, WorkstepWidget
diff --git a/src/ltrace/pyproject.toml b/src/ltrace/pyproject.toml
index 4618653..84d60c7 100644
--- a/src/ltrace/pyproject.toml
+++ b/src/ltrace/pyproject.toml
@@ -1,2 +1,2 @@
[build-system]
-requires = ["setuptools==59.8.0", "wheel==0.37.0", "Cython==0.29.28"]
+requires = ["setuptools==60.2.0", "wheel==0.37.0", "Cython==0.29.28"]
diff --git a/src/ltrace/requirements.txt b/src/ltrace/requirements.txt
index d9b8774..58dd0eb 100644
--- a/src/ltrace/requirements.txt
+++ b/src/ltrace/requirements.txt
@@ -7,48 +7,53 @@ dask-image==0.4.0
dask[complete]==2.30.0
distinctipy==1.1.5
dlisio==1.0.1
+drd==0.1.2
h5netcdf==1.1.0
h5py==3.6.0
h5pyd==0.14.1
humanize==3.13.1
-Jinja2==2.11.1
+Jinja2==3.1.4
+ipywidgets==7.6.3
+itkwidgets==0.32.6
joblib==1.1.1
keyring==24.2.0
-keyrings.cryptfile==1.3.9
-lasio @ git+https://github.com/kinverarity1/lasio@b472b1f
+keyrings.cryptfile==1.3.9 ; platform_system != "Windows"
+lasio @ git+https://github.com/kinverarity1/lasio@b472b1f17f545fed03559e0e4fe26761e6769567
loguru==0.6.0
-markdown2==2.4.2
+markdown2==2.5.0
markupsafe==2.0.1
-matplotlib==3.5.1
+matplotlib==3.9.2
mkl==2022.2.1
-mmcv @ https://download.openmmlab.com/mmcv/dist/cu102/torch1.12.0/mmcv-2.0.0-cp39-cp39-manylinux1_x86_64.whl ; platform_system == "Linux"
mmcv @ https://download.openmmlab.com/mmcv/dist/cpu/torch1.12.0/mmcv-2.0.0-cp39-cp39-win_amd64.whl ; platform_system == "Windows"
+mmcv @ https://download.openmmlab.com/mmcv/dist/cu102/torch1.12.0/mmcv-2.0.0-cp39-cp39-manylinux1_x86_64.whl ; platform_system == "Linux"
mmdet==3.1.0
mmengine==0.8.4
monai==1.1.0
natsort==6.2.0
netCDF4==1.5.4
-numba==0.56.2
-numexpr==2.7.2
+numba==0.60.0
+numexpr==2.8.4
numpy==1.23.1
opencv-contrib-python-headless==4.5.5.64
-scikit-fmm==2022.3.26 # Workaround for failed wheels bulding on porespy package install in linux
-pypardiso==0.4.3 # Fix version because they changed the package metadata and defined an invalid string for licence
openpnm==3.4.0
-pandas==1.4.2
+pandas==2.2.2
paramiko==3.4.0
pathvalidate==2.5.0
-pint==0.19.2
pint-pandas==0.2
+pint==0.19.2
+plotly==5.18.0
psutil==5.9.0
pyedt==0.1.4
-pygments==2.11.2
+Pygments==2.18.0
+pynrrd==1.0.0
+pypardiso==0.4.3 # Fix version because they changed the package metadata and defined an invalid string for licence
pyqtgraph @ git+https://github.com/ltracegeo/pyqtgraph.git@pyqtgraph-0.12.4.2
PySide2==5.15.2
pytesseract==0.3.7
pywin32==306 ; platform_system == "Windows"
pyzmq==22.3.0
recordtype==1.3.0
+requests_toolbelt>=1.0.0
sahi==0.11.15
scikit-gstat==1.0.1
scikit-image==0.19.2
@@ -57,21 +62,14 @@ scikit-mps==0.5.0
scipy==1.8.1
shapely==2.0.0
statsmodels==0.14.0
+stopit==1.1.2
+streamlit==1.22.0
sympy==1.10.1
+#tbb>=2021.6.0
tensorflow==2.8.2
toolz==0.11.1
torch==1.12.1
trimesh==3.9.35
+widgetsnbextension==3.5.2
xarray==2022.3.0
zarr==2.5.0
-statsmodels== 0.14.0
-detect_delimiter==0.1.1
-stopit==1.1.2
-streamlit==1.22.0
-pynrrd==1.0.0
-plotly==5.18.0
-itkwidgets==0.32.6
-ipywidgets==7.6.3
-widgetsnbextension==3.5.2
-drd==0.1.2
-requests_toolbelt==1.0.0
diff --git a/src/modules/AppContext/AppContext.py b/src/modules/AppContext/AppContext.py
new file mode 100644
index 0000000..4f93332
--- /dev/null
+++ b/src/modules/AppContext/AppContext.py
@@ -0,0 +1,420 @@
+import itertools
+import logging
+import shutil
+import slicer
+import qt
+import vtk
+
+from functools import partial
+from ltrace.constants import SaveStatus
+from ltrace.slicer.app import updateWindowTitle, getApplicationVersion, parseApplicationVersion, getJsonData
+from ltrace.slicer.app.custom_3dview import customize_3d_view
+from ltrace.slicer.app.custom_colormaps import customize_color_maps
+from ltrace.slicer.app.drawer import ExpandDataDrawer
+from ltrace.slicer.app.onboard import showDataLoaders, loadEnvironmentByName
+from ltrace.slicer.application_observables import ApplicationObservables
+from ltrace.slicer.custom_main_window_event_filter import CustomizerEventFilter
+from ltrace.slicer.helpers import BlockSignals, svgToQIcon
+from ltrace.slicer.lazy import lazy
+from ltrace.slicer.module_info import ModuleInfo
+from ltrace.slicer.module_utils import fetchModulesFrom, mapByCategory
+from ltrace.slicer.project_manager import ProjectManager, handleCopySuffixOnClonedNodes
+from ltrace.slicer.tracking.tracking_manager import TrackingManager
+from ltrace.slicer.widget.custom_toolbar_buttons import addMenuRaw, addAction, addActionWidget
+from ltrace.slicer.widget.docked_data import DockedData
+from ltrace.slicer.widget.fuzzysearch import FuzzySearchDialog, LinearSearchModel
+from ltrace.slicer_utils import LTracePlugin, getResourcePath
+from ltrace.constants import ImageLogConst
+from pathlib import Path
+from typing import List, Any, Tuple, Dict
+
+toBool = slicer.util.toBool
+
+
+class AppContext(LTracePlugin):
+ SETTING_KEY = "AppContext"
+
+ def __init__(self, parent):
+ LTracePlugin.__init__(self, parent)
+ self.parent.title = "App Context"
+ self.parent.categories = ["System"]
+ self.parent.dependencies = []
+ self.parent.hidden = True
+ self.parent.contributors = []
+ self.parent.helpText = ""
+ self.parent.acknowledgementText = ""
+
+ ####################################################################################
+ # Custom properties
+ ####################################################################################
+
+ self.appData = getJsonData()
+
+ self.__mainWindow = None
+ self.appVersionString = parseApplicationVersion(self.appData)
+ self.modulesDir = ""
+ self.__imageLogLayoutId = ImageLogConst.DEFAULT_LAYOUT_ID_START_VALUE
+
+ self.fuzzySearchModel = LinearSearchModel()
+ self.fuzzySearch = FuzzySearchDialog(self.fuzzySearchModel, parent=self.mainWindow)
+
+ try:
+ self.__trackingManager = TrackingManager()
+ except Exception as e:
+ self.__trackingManager = None
+
+ self.___projectManager = ProjectManager(folderIconPath=getResourcePath("Icons") / "ProjectIcon.ico")
+
+ self.projectEventsLogic = ProjectEventsLogic(self.___projectManager)
+
+ self.modules = ModuleManager(self)
+
+ self.rightDrawer = ExpandDataDrawer(DockedData())
+
+ @property
+ def imageLogLayoutId(self):
+ return self.__imageLogLayoutId
+
+ @imageLogLayoutId.setter
+ def imageLogLayoutId(self, lid):
+ self.__imageLogLayoutId = lid
+
+ @property
+ def mainWindow(self):
+ if self.__mainWindow is None:
+ self.__mainWindow = slicer.util.mainWindow()
+
+ return self.__mainWindow
+
+ def setupObservers(self):
+
+ if self.__trackingManager:
+ self.__trackingManager.installTrackers()
+
+ self.projectEventsLogic.register()
+
+ qt.QTimer.singleShot(500, lambda: ApplicationObservables().applicationLoadFinished.emit())
+
+ def getTracker(self):
+ return self.__trackingManager
+
+ def getAboutGeoSlicer(self) -> str:
+ """Returns the HTML string that describes GeoSlicer in the about dialog.
+
+ Returns:
+ str: An HTML-formatted string containing the GeoSlicer's description.
+ """
+ return """
+
+ GeoSlicer is an AI-powered digital rocks platform, developed in collaboration between LTrace, Petrobras, and Equinor. It provides an integrated computational environment for processing digital rocks at all scales, combining machine learning and advanced data processing tools to support geoscientific analysis.
+
+ For more information, visit our website or contact us at contact@ltrace.com.br.
+ """
+
+
+class ModuleManager:
+ def __init__(self, context):
+ self.__ctx = context
+ self.groups = {}
+ self.availableModules = {}
+ self.currentWorkingDataType = None
+
+ def initCache(self, modules):
+ logging.info(f"Found {len(modules)} available LTrace's modules to load.")
+ self.availableModules = modules
+ logging.info("Building reverse index...")
+ self.groups = mapByCategory(modules.values()) # replace this with the movel below
+ self.__ctx.fuzzySearchModel.setDataSource(modules)
+
+ def setEnvironment(self, environment: Tuple[str, Any]):
+ if self.currentWorkingDataType == environment:
+ return
+
+ self.currentWorkingDataType = environment
+ ApplicationObservables().environmentChanged.emit()
+
+ def fetchByCategory(self, query, intersectWith=None) -> Dict[str, ModuleInfo]:
+ if intersectWith:
+ result = set(self.groups.get(intersectWith, []))
+ for category in query:
+ result.intersection_update(self.groups.get(category, []))
+
+ else:
+ result = set(self.groups.get(query[0], []))
+ for category in query[1:]:
+ result.update(self.groups.get(category, []))
+
+ return {m.key: m for m in result}
+
+ def addToolsMenu(self, toolbar):
+ tools = [
+ "VolumeCalculator",
+ "CustomizedTables",
+ "TableFilter",
+ "Charts",
+ ]
+
+ toolModules = [self.availableModules[m] for m in tools]
+
+ addMenuRaw(
+ svgToQIcon(getResourcePath("Icons") / "IconSet-dark" / "Table.svg"),
+ "More tools",
+ toolModules,
+ toolbar,
+ )
+
+ def addLoadersMenu(self, toolbar):
+ loaders = [
+ "BIAEPBrowser",
+ "OpenRockData",
+ "NetCDF",
+ ]
+
+ toolModules = [self.availableModules[m] for m in loaders]
+
+ addMenuRaw(
+ svgToQIcon(getResourcePath("Icons") / "IconSet-dark" / "Database.svg"),
+ "Import",
+ toolModules,
+ toolbar,
+ )
+
+ def showDataLoaders(self, toolbar):
+ showDataLoaders(toolbar)
+
+ def loadEnvironmentByName(self, toolbar, displayName):
+ loadEnvironmentByName(toolbar, displayName)
+
+
+class ProjectEventsLogic:
+ def __init__(self, projectManager):
+ self.__projectManager = projectManager
+ self.__customizerEventFilter = None
+
+ self.startCloseSceneObserverHandler = None
+ self.endCloseSceneObserverHandler = None
+ self.nodeAddedObserverHandler = None
+
+ def __del__(self):
+ super().__del__()
+ slicer.mrmlScene.RemoveObserver(self.startCloseSceneObserverHandler)
+ slicer.mrmlScene.RemoveObserver(self.endCloseSceneObserverHandler)
+ slicer.mrmlScene.RemoveObserver(self.nodeAddedObserverHandler)
+
+ def register(self):
+ self.__projectManager.setup()
+ self.__projectManager.projectChangedSignal.connect(
+ partial(updateWindowTitle, versionString=getApplicationVersion())
+ )
+
+ self.__customizerEventFilter = CustomizerEventFilter(saveSceneCallback=self.saveScene)
+ slicer.modules.AppContextInstance.mainWindow.installEventFilter(self.__customizerEventFilter)
+
+ self.startCloseSceneObserverHandler = slicer.mrmlScene.AddObserver(
+ slicer.mrmlScene.StartCloseEvent, self.__beginSceneClosing
+ )
+ self.endCloseSceneObserverHandler = slicer.mrmlScene.AddObserver(
+ slicer.mrmlScene.EndCloseEvent, self.__endSceneClosing
+ )
+
+ self.nodeAddedObserverHandler = slicer.mrmlScene.AddObserver(
+ slicer.mrmlScene.NodeAddedEvent, self.__onNodeAdded
+ )
+
+ self.endImportSceneObserverHandler = slicer.mrmlScene.AddObserver(
+ slicer.mrmlScene.EndImportEvent, self.__onEndImportEvent
+ )
+
+ lazy.register_eye_event()
+ self.__projectManager.projectChangedSignal.connect(lazy.register_eye_event)
+
+ self.setupRecentlyLoadedMenu()
+
+ def loadScene(self):
+ fileDialog = qt.QFileDialog(
+ slicer.modules.AppContextInstance.mainWindow,
+ "Load a scene",
+ "",
+ "GeoSlicer scene file (*.mrml)",
+ )
+ try:
+ if fileDialog.exec():
+ paths = fileDialog.selectedFiles()
+ projectFilePath = paths[0]
+ status = self.__projectManager.load(projectFilePath)
+ if not status:
+ slicer.util.errorDisplay(
+ "An error occurred while loading the project. Please check the GeoSlicer log file.",
+ "Failed to load project",
+ )
+ self.setupRecentlyLoadedMenu()
+ return True
+ return False
+ finally:
+ fileDialog.deleteLater()
+
+ def saveScene(self):
+ """Save current scene/project
+
+ Returns:
+ bool: True if scene was saved successfully, otherwise returns False
+ """
+ url = slicer.mrmlScene.GetURL()
+ if url == "":
+ return self.saveSceneAs()
+
+ status = self.__projectManager.save(url)
+
+ if status == SaveStatus.FAILED:
+ slicer.util.errorDisplay(
+ "An error occurred while saving the project. Please check the following:\n\n"
+ + "1. Ensure that there is sufficient disk space available.\n"
+ + "2. Verify that you have the necessary file writing permissions.\n\n"
+ + "For further details, please consult the GeoSlicer log file. If the problem persists, consider reaching out to support.",
+ "Failed to save project",
+ )
+
+ return status
+
+ def saveSceneAs(self):
+ """Handles save button clicked on save scene as dialog"""
+ # Save directory
+ path = qt.QFileDialog.getSaveFileName(
+ slicer.modules.AppContextInstance.mainWindow,
+ "Save project",
+ slicer.app.defaultScenePath,
+ "GeoSlicer project folder (*)",
+ "",
+ qt.QFileDialog.DontConfirmOverwrite,
+ )
+
+ if not path:
+ return SaveStatus.CANCELLED # Nothing to do
+
+ status = self.__projectManager.saveAs(path)
+
+ if status == SaveStatus.FAILED:
+ slicer.util.errorDisplay(
+ "An error occurred while saving the project. Please check the following:\n\n"
+ + "1. Ensure that there is sufficient disk space available.\n"
+ + "2. Verify that you have the necessary file writing permissions.\n\n"
+ + "For further details, please consult the GeoSlicer log file. If the problem persists, consider reaching out to support.",
+ "Failed to save project",
+ )
+
+ failedProjectpath = Path(path)
+ if failedProjectpath.exists():
+ shutil.rmtree(failedProjectpath, ignore_errors=True)
+
+ return status
+
+ def onCloseScene(self):
+ """Handle close scene event"""
+
+ def wrapper(save=False):
+ if save:
+ status = self.saveScene()
+ if status != SaveStatus.SUCCEED:
+ # saveScene handles possible errors and warns the user
+ return
+
+ if status == SaveStatus.IN_PROGRESS:
+ logging.debug("Unexpected state from the saving process.")
+ return
+
+ self.__projectManager.close()
+ updateWindowTitle(versionString=getApplicationVersion())
+
+ mainWindow = slicer.modules.AppContextInstance.mainWindow
+ isModified = mainWindow.isWindowModified()
+ if not isModified:
+ wrapper(save=False)
+ return
+
+ messageBox = qt.QMessageBox(mainWindow)
+ messageBox.setWindowTitle("Close scene")
+ messageBox.setIcon(messageBox.Warning)
+ messageBox.setText("Save the changes before closing the scene?")
+ saveButton = messageBox.addButton("&Save and Close", qt.QMessageBox.AcceptRole)
+ dismissButton = messageBox.addButton("Close &without Saving", qt.QMessageBox.RejectRole)
+ cancelButton = messageBox.addButton("&Cancel", qt.QMessageBox.ResetRole)
+ messageBox.exec_()
+
+ if messageBox.clickedButton() == cancelButton:
+ return
+
+ shouldSave = messageBox.clickedButton() == saveButton
+ wrapper(save=shouldSave)
+
+ def __beginSceneClosing(self, *args):
+ """Handle the beginning of the scene closing process"""
+
+ selectedModule = slicer.util.moduleSelector().selectedModule
+
+ # Switch to another module so exit() gets called for the current module
+ # and then switch back to the original module and restore layout
+ layout = slicer.app.layoutManager().layout
+ slicer.util.selectModule(selectedModule)
+ slicer.app.layoutManager().setLayout(layout)
+
+ def __endSceneClosing(self, *args):
+ """Handle the end of the scene closing process"""
+
+ customize_3d_view()
+ customize_color_maps()
+
+ @vtk.calldata_type(vtk.VTK_OBJECT)
+ def __onNodeAdded(self, caller, eventId, callData):
+ if isinstance(callData, slicer.vtkMRMLSegmentationDisplayNode):
+ display_node = callData
+ display_node.SetOpacity(0.50)
+ display_node.SetOpacity2DFill(1.00)
+ display_node.Visibility2DOutlineOff()
+ display_node.SetOpacity2DOutline(0.00)
+ display_node.SetOpacity3D(1.00)
+ self.__noInterpolate()
+
+ if callData and callData.IsA("vtkMRMLVolumeArchetypeStorageNode"):
+ handleCopySuffixOnClonedNodes(callData)
+
+ def __noInterpolate(self, *args):
+ for node in slicer.util.getNodes("*").values():
+ if node.IsA("vtkMRMLScalarVolumeDisplayNode") or node.IsA("vtkMRMLVectorVolumeDisplayNode"):
+ node.SetInterpolate(0)
+
+ def onRecentLoadedActionTriggered(self, sender: qt.QAction, state: bool) -> None:
+ fileParameters = sender.property("fileParameters")
+ fileType = fileParameters.get("fileType")
+ fileName = fileParameters.get("fileName")
+
+ if not fileName:
+ return
+
+ status = self.__projectManager.load(fileName)
+ if not status:
+ slicer.util.errorDisplay(
+ "An error occurred while loading the project. Please check the GeoSlicer log file.",
+ "Failed to load project",
+ )
+ self.setupRecentlyLoadedMenu()
+
+ def setupRecentlyLoadedMenu(self) -> None:
+ """Method to install project manager load method into the recently loaded project actions."""
+ fileMenu = slicer.modules.AppContextInstance.mainWindow.findChild("QMenu", "FileMenu")
+ recentMenu = fileMenu.findChild("QMenu", "RecentlyLoadedMenu")
+
+ for action in recentMenu.actions():
+ if action.text == "Clear History":
+ continue
+
+ try:
+ action.triggered.disconnect()
+ action.triggered.connect(partial(self.onRecentLoadedActionTriggered, action))
+ except Exception as error:
+ logging.error(error)
+
+ def __onEndImportEvent(self, *args, **kwargs) -> None:
+ # Assure method is called after the file history update from the last project load.
+ qt.QTimer.singleShot(10, self.setupRecentlyLoadedMenu)
diff --git a/src/modules/AzimuthShiftTool/AzimuthShiftTool.py b/src/modules/AzimuthShiftTool/AzimuthShiftTool.py
index d576556..ab5db8f 100644
--- a/src/modules/AzimuthShiftTool/AzimuthShiftTool.py
+++ b/src/modules/AzimuthShiftTool/AzimuthShiftTool.py
@@ -23,7 +23,7 @@ class AzimuthShiftTool(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
self.parent.title = "Azimuth Shift Tool"
- self.parent.categories = ["LTrace Tools"]
+ self.parent.categories = ["Tools", "ImageLog", "Multiscale"]
self.parent.contributors = ["LTrace Geophysics Team"]
self.parent.helpText = AzimuthShiftTool.help()
diff --git a/src/modules/AzimuthShiftTool/Resources/Icons/AzimuthShiftTool.svg b/src/modules/AzimuthShiftTool/Resources/Icons/AzimuthShiftTool.svg
new file mode 100644
index 0000000..e371412
--- /dev/null
+++ b/src/modules/AzimuthShiftTool/Resources/Icons/AzimuthShiftTool.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/modules/BoundaryRemovalEffect/BoundaryRemovalEffect.py b/src/modules/BoundaryRemovalEffect/BoundaryRemovalEffect.py
index d72c84d..1780694 100644
--- a/src/modules/BoundaryRemovalEffect/BoundaryRemovalEffect.py
+++ b/src/modules/BoundaryRemovalEffect/BoundaryRemovalEffect.py
@@ -1,6 +1,5 @@
import os
-import slicer
from ltrace.slicer_utils import LTracePlugin
diff --git a/src/modules/BoundaryRemovalEffect/BoundaryRemovalEffectLib/SegmentEditorEffect.py b/src/modules/BoundaryRemovalEffect/BoundaryRemovalEffectLib/SegmentEditorEffect.py
index 546faec..32587dc 100644
--- a/src/modules/BoundaryRemovalEffect/BoundaryRemovalEffectLib/SegmentEditorEffect.py
+++ b/src/modules/BoundaryRemovalEffect/BoundaryRemovalEffectLib/SegmentEditorEffect.py
@@ -301,7 +301,7 @@ def setEnabledSegmentationButtons(self, enabled: bool):
def createCursor(self, widget):
# Turn off effect-specific cursor for this effect
- return slicer.util.mainWindow().cursor
+ return slicer.modules.AppContextInstance.mainWindow.cursor
def sourceVolumeNodeChanged(self):
# Set scalar range of master volume image data to threshold slider
diff --git a/src/modules/CTAutoRegistration/CTAutoRegistration.py b/src/modules/CTAutoRegistration/CTAutoRegistration.py
index 0066f86..bb6df07 100644
--- a/src/modules/CTAutoRegistration/CTAutoRegistration.py
+++ b/src/modules/CTAutoRegistration/CTAutoRegistration.py
@@ -27,8 +27,8 @@ class CTAutoRegistration(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
- self.parent.title = "CT Auto Registration"
- self.parent.categories = ["Registration"]
+ self.parent.title = "MicroCT Auto Registration"
+ self.parent.categories = ["Registration", "MicroCT"]
self.parent.dependencies = []
self.parent.contributors = ["LTrace Geophysical Solutions"]
self.parent.helpText = CTAutoRegistration.help()
diff --git a/src/modules/CTAutoRegistration/Resources/Icons/CTAutoRegistration.svg b/src/modules/CTAutoRegistration/Resources/Icons/CTAutoRegistration.svg
new file mode 100644
index 0000000..98c1389
--- /dev/null
+++ b/src/modules/CTAutoRegistration/Resources/Icons/CTAutoRegistration.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/modules/Charts/Charts.py b/src/modules/Charts/Charts.py
index 04bf20c..1aba6ec 100644
--- a/src/modules/Charts/Charts.py
+++ b/src/modules/Charts/Charts.py
@@ -5,13 +5,14 @@
from ltrace.slicer.node_attributes import NodeEnvironment
from ltrace.slicer_utils import *
from pathlib import Path
+
from Plots.Crossplot.CrossplotWidget import CrossplotWidget
from Plots.BarPlot.BarPlotBuilder import BarPlotBuilder
from Plots.Windrose.WindrosePlotBuilder import WindrosePlotBuilder
from Plots.Crossplot.CrossplotPlotBuilder import CrossplotBuilder
from Plots.HistogramInDepthPlot.HistogramInDepthPlotBuilder import HistogramInDepthPlotBuilder
from Plots.HistogramPlot.HistogramPlotBuilder import HistogramPlotBuilder
-from DLISImportLib.DLISImportLogic import WELL_NAME_TAG
+from ltrace.constants import DLISImportConst
import numpy as np
from ltrace.slicer.helpers import createTemporaryNode, removeTemporaryNodes
from ltrace.slicer_utils import dataframeFromTable, dataFrameToTableNode
@@ -48,7 +49,7 @@ class Charts(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
self.parent.title = "Charts"
- self.parent.categories = ["LTrace Tools"]
+ self.parent.categories = ["Tools", "Charts", "MicroCT"]
self.parent.dependencies = []
self.parent.contributors = ["LTrace Geophysics Team"] # replace with "Firstname Lastname (Organization)"
self.parent.helpText = Charts.help()
@@ -119,7 +120,7 @@ def setup(self):
parametersFormLayout.addRow(self.plotButton)
# connections
- self.plotButton.connect("clicked(bool)", self.onPlotButtonClicked)
+ self.plotButton.clicked.connect(self.onPlotButtonClicked)
self.plotTypeComboBox.currentTextChanged.connect(self.__onPlotTypeComboBoxChanged)
# Add vertical spacer
@@ -163,7 +164,6 @@ def __showNewPlotWidgetDialog(self):
dialog.setWindowFlags(dialog.windowFlags() & ~qt.Qt.WindowContextHelpButtonHint)
dialog.setWindowTitle("New plot")
dialog.setWindowIcon(qt.QIcon(str(self.WINDOWN_ICON)))
- dialog.setFixedSize(200, 60)
# Question Layout
formLayout = qt.QFormLayout()
@@ -194,6 +194,7 @@ def okButtonClicked():
buttonsLayout = qt.QHBoxLayout()
buttonsLayout.addWidget(okButton)
+ buttonsLayout.addSpacing(10)
buttonsLayout.addWidget(cancelButton)
formLayout.addRow(buttonsLayout)
formLayout.setVerticalSpacing(10)
@@ -235,7 +236,9 @@ def __handle_plot_type_creation(self, plotType, plotLabel):
plotWidget.show()
except (RuntimeError, Exception) as error:
logging.warning(error)
- slicer.util.errorDisplay(text=INCOMPATIBLE_MESSAGE, parent=slicer.util.mainWindow())
+ slicer.util.errorDisplay(
+ text=INCOMPATIBLE_MESSAGE, parent=slicer.modules.AppContextInstance.mainWindow
+ )
plotWidget.deleteLater()
plotWidget = None
else:
@@ -289,7 +292,6 @@ def onPlotButtonClicked(self):
self.plot(nodes)
def plot(self, nodes):
-
failures = [node.GetName() for node in nodes if isVarDescriptor(node)]
if failures:
@@ -297,7 +299,7 @@ def plot(self, nodes):
"The nodes below cannot be plotted because its format is not compatible with the chosen chart type.\n"
)
message += "\n".join([f" - {name}" for name in failures])
- slicer.util.errorDisplay(text=message, parent=slicer.util.mainWindow())
+ slicer.util.errorDisplay(text=message, parent=slicer.modules.AppContextInstance.mainWindow)
return
selectedPlotWidgetLabel = self.plotWidgetsComboBox.currentText
@@ -323,12 +325,13 @@ def plot(self, nodes):
except (ValueError, RuntimeError) as error:
if self.__plotWidgets.get(selectedPlotWidgetLabel) is None and selectPlotWidget is not None:
selectPlotWidget.deleteLater()
- slicer.util.errorDisplay(text=error, parent=slicer.util.mainWindow())
+ logging.warning(error)
+ slicer.util.errorDisplay(text=error, parent=slicer.modules.AppContextInstance.mainWindow)
except Exception as error:
- # traceback.print_exc()
if self.__plotWidgets.get(selectedPlotWidgetLabel) is None and selectPlotWidget is not None:
selectPlotWidget.deleteLater()
- slicer.util.errorDisplay(text=INCOMPATIBLE_MESSAGE, parent=slicer.util.mainWindow())
+ logging.warning(error)
+ slicer.util.errorDisplay(text=INCOMPATIBLE_MESSAGE, parent=slicer.modules.AppContextInstance.mainWindow)
else:
self.__plotWidgets[selectedPlotWidgetLabel] = selectPlotWidget
self.__populatePlotWidgetsComboBox()
@@ -352,7 +355,7 @@ def wrapWidget(self):
import PythonQt
import shiboken2
- self.pyqtwidget = PythonQt.Qt.QWidget(slicer.util.mainWindow())
+ self.pyqtwidget = PythonQt.Qt.QWidget(slicer.modules.AppContextInstance.mainWindow)
self.pysidewidget = shiboken2.wrapInstance(hash(self.pyqtwidget), QWidget)
return self.pysidewidget
@@ -374,7 +377,7 @@ def getNodesMergedByWell(nodes):
wellsIndexesNodes = [] # tables from wells need to be merged into a multi-colun table per well
wells = []
for node in nodes:
- wellName = node.GetAttribute(WELL_NAME_TAG)
+ wellName = node.GetAttribute(DLISImportConst.WELL_NAME_TAG)
wellsIndexesNodes.append(
{
"WellName": wellName,
diff --git a/src/modules/Charts/Plots/BarPlot/BarPlotWidget.py b/src/modules/Charts/Plots/BarPlot/BarPlotWidget.py
index 8db7662..337e7b5 100644
--- a/src/modules/Charts/Plots/BarPlot/BarPlotWidget.py
+++ b/src/modules/Charts/Plots/BarPlot/BarPlotWidget.py
@@ -1,7 +1,7 @@
-from .AngleAxisItem import AngleAxisItem
from .BarPlotWidgetModel import BarPlotWidgetModel
from ..BasePlotWidget import BasePlotWidget
from ltrace.slicer.helpers import segmentListAndProportionsFromSegmentation
+from ltrace.slicer.widget.customized_pyqtgraph.AngleAxisItem import AngleAxisItem
from pyqtgraph.Qt import QtGui, QtCore
diff --git a/src/modules/Charts/Plots/Crossplot/CrossplotWidget.py b/src/modules/Charts/Plots/Crossplot/CrossplotWidget.py
index e083f33..295bb53 100644
--- a/src/modules/Charts/Plots/Crossplot/CrossplotWidget.py
+++ b/src/modules/Charts/Plots/Crossplot/CrossplotWidget.py
@@ -12,15 +12,15 @@
from ltrace.slicer import helpers, ui
from ltrace.slicer.equations.equation_base import EquationBase
from ltrace.slicer.equations.fit_data import FitData
+from ltrace.slicer.widget.data_plot_widget import DataPlotWidget
from ltrace.slicer.widget.help_button import HelpButton
from matplotlib import cm as matplotlibcm
from pint import Unit, UndefinedUnitError, DefinitionSyntaxError
from pint_pandas import PintArray
-from Plots.BasePlotWidget import BasePlotWidget
-from Plots.Crossplot.data_table_widget import DataTableWidget
-from Plots.Crossplot.data_plot_widget import DataPlotWidget
-from Plots.Crossplot.equations.line import Line
-from Plots.Crossplot.equations.timur_coates import TimurCoates
+from ..BasePlotWidget import BasePlotWidget
+from .data_table_widget import DataTableWidget
+from .equations.line import Line
+from .equations.timur_coates import TimurCoates
from pyqtgraph.Qt import QtGui, QtCore
RESOURCES_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "Resources")
@@ -77,8 +77,8 @@
class UnitConversionWidget(qt.QWidget):
unitsChanged = qt.Signal()
- def __init__(self):
- super().__init__()
+ def __init__(self, parent=None):
+ super().__init__(parent)
self.__lastUnits = None
self.__currentUnits = None
@@ -199,7 +199,6 @@ def setupUi(self):
parametersLayout = QtGui.QVBoxLayout()
parametersWidget.setLayout(parametersLayout)
plot_layout = QtGui.QVBoxLayout()
-
# Data table widget
self.__tableWidget = DataTableWidget()
self.__tableWidget.signal_style_changed.connect(self.__updatePlot)
@@ -207,14 +206,12 @@ def setupUi(self):
self.__tableWidget.signal_all_style_changed.connect(self.__updateAllDataStyles)
self.__tableWidget.signal_all_visible_changed.connect(self.__updateAllDataVisibility)
parametersLayout.addWidget(self.__tableWidget)
-
# Plot widget
self.dataPlotWidget = DataPlotWidget()
self.dataPlotWidget.toggleLegendSignal.connect(self.__toggleLegend)
plot_layout.addWidget(self.dataPlotWidget.widget)
plot_layout.addStretch()
-
# X axis
# Histogram options
self.__xAxisHistogramEnableCheckBox = QtGui.QCheckBox()
@@ -246,9 +243,12 @@ def setupUi(self):
xAxisParameterLayout = QtGui.QFormLayout()
xAxisParameterLayout.addRow("Parameter", self.__xAxisComboBox)
xAxisParameterLayout.setHorizontalSpacing(8)
-
# Layout
self.__xAxisGridLayout = QtGui.QGridLayout()
+ self.__xAxisGroupBox = QtGui.QGroupBox("X axis")
+ self.__xAxisGroupBox.setLayout(self.__xAxisGridLayout)
+ parametersLayout.addWidget(self.__xAxisGroupBox)
+
self.__xAxisGridLayout.setHorizontalSpacing(5)
self.__xAxisGridLayout.addLayout(xAxisParameterLayout, 0, 0, 1, -1)
self.__xAxisGridLayout.addWidget(xHistogramCheckBoxLabel, 1, 0, 1, 1)
@@ -261,12 +261,7 @@ def setupUi(self):
self.__xAxisGridLayout.addWidget(
shiboken2.wrapInstance(hash(self.__xUnitConversion), QtGui.QWidget), 2, 0, 1, 4
)
-
# Groupbox
- self.__xAxisGroupBox = QtGui.QGroupBox("X axis")
- self.__xAxisGroupBox.setLayout(self.__xAxisGridLayout)
-
- parametersLayout.addWidget(self.__xAxisGroupBox)
# Y axis
# Histogram options
@@ -302,6 +297,10 @@ def setupUi(self):
# Layout
self.__yAxisGridLayout = QtGui.QGridLayout()
+ self.__yAxisGroupBox = QtGui.QGroupBox("Y axis")
+ self.__yAxisGroupBox.setLayout(self.__yAxisGridLayout)
+ parametersLayout.addWidget(self.__yAxisGroupBox)
+
self.__yAxisGridLayout.setHorizontalSpacing(5)
self.__yAxisGridLayout.addLayout(yAxisParameterLayout, 0, 0, 1, -1)
self.__yAxisGridLayout.addWidget(yHistogramCheckBoxLabel, 1, 0, 1, 1)
@@ -314,12 +313,7 @@ def setupUi(self):
self.__yAxisGridLayout.addWidget(
shiboken2.wrapInstance(hash(self.__yUnitConversion), QtGui.QWidget), 2, 0, 1, 4
)
-
# Groupbox
- self.__yAxisGroupBox = QtGui.QGroupBox("Y axis")
- self.__yAxisGroupBox.setLayout(self.__yAxisGridLayout)
-
- parametersLayout.addWidget(self.__yAxisGroupBox)
# Z axis
self.__zAxisComboBox = QtGui.QComboBox()
@@ -334,52 +328,45 @@ def setupUi(self):
self.__colorMapComboBox.setIconSize(QtCore.QSize(80, 20))
self.__populateColorMapComboBox()
- self.__zUnitConversion = UnitConversionWidget()
-
# Manual/Auto range widgets layout
- range_layout = QtGui.QHBoxLayout()
- range_layout.setSpacing(5)
- auto_range_layout = QtGui.QHBoxLayout()
- auto_range_layout.setSpacing(5)
- auto_range_layout.addWidget(QtGui.QLabel("Auto Range"))
- auto_range_layout.addWidget(self.__autoRangeCheckBox)
- range_layout.addLayout(auto_range_layout)
- minimum_layout = QtGui.QHBoxLayout()
- minimum_layout.setSpacing(5)
- minimum_layout.addWidget(QtGui.QLabel("Min"))
- minimum_layout.addWidget(self.__ZMinValueRangeDoubleSpinBox)
- range_layout.addLayout(minimum_layout)
- maximum_layout = QtGui.QHBoxLayout()
- maximum_layout.setSpacing(5)
- maximum_layout.addWidget(QtGui.QLabel("Max"))
- maximum_layout.addWidget(self.__ZMaxValueRangeDoubleSpinBox)
- range_layout.addLayout(maximum_layout)
+ rangeLayout = QtGui.QHBoxLayout()
+ rangeLayout.setSpacing(5)
+ autoRangeLayout = QtGui.QHBoxLayout()
+ autoRangeLayout.setSpacing(5)
+ autoRangeLayout.addWidget(QtGui.QLabel("Auto Range"))
+ autoRangeLayout.addWidget(self.__autoRangeCheckBox)
+ rangeLayout.addLayout(autoRangeLayout)
+ minimumLayout = QtGui.QHBoxLayout()
+ minimumLayout.setSpacing(5)
+ minimumLayout.addWidget(QtGui.QLabel("Min"))
+ minimumLayout.addWidget(self.__ZMinValueRangeDoubleSpinBox)
+ rangeLayout.addLayout(minimumLayout)
+ maximumLayout = QtGui.QHBoxLayout()
+ maximumLayout.setSpacing(5)
+ maximumLayout.addWidget(QtGui.QLabel("Max"))
+ maximumLayout.addWidget(self.__ZMaxValueRangeDoubleSpinBox)
+ rangeLayout.addLayout(maximumLayout)
+ self.__zUnitConversion = UnitConversionWidget()
formLayout = QtGui.QFormLayout()
- formLayout.addRow("Parameter", self.__zAxisComboBox)
- formLayout.addRow(range_layout)
- formLayout.addRow("Color map", self.__colorMapComboBox)
- formLayout.addRow(shiboken2.wrapInstance(hash(self.__zUnitConversion), QtGui.QWidget))
-
zStyleGroupBox = QtGui.QGroupBox("Z axis")
zStyleGroupBox.setLayout(formLayout)
-
parametersLayout.addWidget(zStyleGroupBox)
+ formLayout.addRow("Parameter", self.__zAxisComboBox)
+ formLayout.addRow(rangeLayout)
+ formLayout.addRow("Color map", self.__colorMapComboBox)
+ formLayout.addRow(shiboken2.wrapInstance(hash(self.__zUnitConversion), QtGui.QWidget))
# Settings
self.__settingsGroupBox = QtGui.QGroupBox("Settings")
-
self.__themeComboBox = QtGui.QComboBox()
for themeName in self.dataPlotWidget.themes:
self.__themeComboBox.addItem(themeName)
-
self.__embeddedLegendVisibilityCheckBox = QtGui.QCheckBox()
self.__embeddedLegendVisibilityCheckBox.setChecked(self.dataPlotWidget.embeddedLegendVisibility)
self.__embeddedLegendVisibilityCheckBox.stateChanged.connect(self.__onEmbeddedLegendVisibilityChange)
-
settingsFormLayout = QtGui.QFormLayout()
settingsFormLayout.setHorizontalSpacing(8)
-
settingsFormLayout.addRow("Theme", self.__themeComboBox)
settingsFormLayout.addRow("Show legend", self.__embeddedLegendVisibilityCheckBox)
self.__settingsGroupBox.setLayout(settingsFormLayout)
@@ -444,8 +431,12 @@ def setupUi(self):
def graphDataList(self):
return list(self.__graphDataList)
- def __createFitTab(self):
- ## New fit widget
+ def __createFitTab(self) -> qt.QFrame:
+ """Creates a tab for the curve fitting functionality.
+
+ Returns:
+ qt.QFrame: a QFrame containing the layout of the tab.
+ """
self.__fitDataInputComboBox = qt.QComboBox()
self.__fitEquationComboBox = qt.QComboBox()
@@ -574,6 +565,8 @@ def __onImportClicked(self):
importedVolume.SetAttribute("table_type", "equation")
self.appendData(importedVolume)
+ fileDialog.delete()
+
def __onExportClicked(self, functionCurveName):
fitData = self.__getFitData(functionCurveName)
path = qt.QFileDialog.getSaveFileName(
diff --git a/src/modules/Charts/Resources/Icons/Charts.png b/src/modules/Charts/Resources/Icons/Charts.png
deleted file mode 100644
index 7c3c02b..0000000
Binary files a/src/modules/Charts/Resources/Icons/Charts.png and /dev/null differ
diff --git a/src/modules/Charts/Resources/Icons/Charts.svg b/src/modules/Charts/Resources/Icons/Charts.svg
new file mode 100644
index 0000000..f7738bb
--- /dev/null
+++ b/src/modules/Charts/Resources/Icons/Charts.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/modules/ColorThresholdEffect/ColorThresholdEffectLib/SegmentEditorEffect.py b/src/modules/ColorThresholdEffect/ColorThresholdEffectLib/SegmentEditorEffect.py
index 3832a2d..9ca6b2f 100644
--- a/src/modules/ColorThresholdEffect/ColorThresholdEffectLib/SegmentEditorEffect.py
+++ b/src/modules/ColorThresholdEffect/ColorThresholdEffectLib/SegmentEditorEffect.py
@@ -283,7 +283,7 @@ def setupOptionsFrame(self):
def createCursor(self, widget):
# Turn off effect-specific cursor for this effect
- return slicer.util.mainWindow().cursor
+ return slicer.modules.AppContextInstance.mainWindow.cursor
def sourceVolumeNodeChanged(self):
pass
diff --git a/src/modules/ConnectivityEffect/ConnectivityEffectLib/SegmentEditorEffect.py b/src/modules/ConnectivityEffect/ConnectivityEffectLib/SegmentEditorEffect.py
index fb7113a..2bbb0b9 100644
--- a/src/modules/ConnectivityEffect/ConnectivityEffectLib/SegmentEditorEffect.py
+++ b/src/modules/ConnectivityEffect/ConnectivityEffectLib/SegmentEditorEffect.py
@@ -111,7 +111,7 @@ def deactivate(self):
def createCursor(self, widget):
# Turn off effect-specific cursor for this effect
- return slicer.util.mainWindow().cursor
+ return slicer.modules.AppContextInstance.mainWindow.cursor
def onHopsChanged(self, hops):
if self.scriptedEffect.parameterSetNode() is None:
diff --git a/src/modules/CoreEnv/CoreEnv.py b/src/modules/CoreEnv/CoreEnv.py
index 02789d7..19ef1a0 100644
--- a/src/modules/CoreEnv/CoreEnv.py
+++ b/src/modules/CoreEnv/CoreEnv.py
@@ -1,16 +1,11 @@
import os
from pathlib import Path
-import qt
import slicer
-from ltrace.slicer_utils import LTracePlugin, LTracePluginWidget
-from CTAutoRegistration import CTAutoRegistration
-from CustomizedCropVolume import CustomizedCropVolume
-from CustomizedData import CustomizedData
-from Multicore import Multicore
-from MulticoreTransforms import MulticoreTransforms
-from SegmentationEnv import SegmentationEnv
+from ltrace.slicer.helpers import svgToQIcon
+from ltrace.slicer.widget.custom_toolbar_buttons import addAction, addMenu
+from ltrace.slicer_utils import LTracePlugin, LTracePluginLogic, LTraceEnvironmentMixin, getResourcePath
class CoreEnv(LTracePlugin):
@@ -21,70 +16,63 @@ class CoreEnv(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
self.parent.title = "Core Environment"
- self.parent.categories = ["Environments"]
+ self.parent.categories = ["Environment", "Core"]
self.parent.dependencies = []
+ self.parent.hidden = True
self.parent.contributors = ["LTrace Geophysical Solutions"]
- self.parent.helpText = (
- CoreEnv.help()
- + CustomizedData.help()
- + Multicore.help()
- + MulticoreTransforms.help()
- + CustomizedCropVolume.help()
- + SegmentationEnv.help()
- )
+ self.parent.helpText = ""
+ self.environment = CoreEnvLogic()
@classmethod
def readme_path(cls):
return str(cls.MODULE_DIR / "README.md")
-class CoreEnvWidget(LTracePluginWidget):
- def __init__(self, parent) -> None:
- super().__init__(parent)
- self.lastAccessedWidget = None
-
- def setup(self):
- LTracePluginWidget.setup(self)
- self.mainTab = qt.QTabWidget()
- self.layout.addWidget(self.mainTab)
-
- # Data tab
- dataTab = qt.QTabWidget()
- dataTab.addTab(slicer.modules.customizeddata.createNewWidgetRepresentation(), "Explorer")
- dataTab.addTab(slicer.modules.multicore.createNewWidgetRepresentation(), "CT Import")
- dataTab.addTab(slicer.modules.corephotographloader.createNewWidgetRepresentation(), "Photo Import (beta)")
- dataTab.addTab(slicer.modules.multicoreexport.createNewWidgetRepresentation(), "Export")
-
- self.mainTab.addTab(dataTab, "Data")
- self.mainTab.addTab(slicer.modules.multicoretransforms.createNewWidgetRepresentation(), "Transforms")
- self.mainTab.addTab(slicer.modules.customizedcropvolume.createNewWidgetRepresentation(), "Crop")
- self.segmentationEnv = slicer.modules.segmentationenv.createNewWidgetRepresentation()
- self.mainTab.addTab(self.segmentationEnv, "Segmentation")
- self.mainTab.addTab(slicer.modules.coreinpaint.createNewWidgetRepresentation(), "Inpaint")
-
- self.lastAccessedWidget = dataTab.widget(0)
-
- self.segmentationEnv.self().segmentEditorWidget.self().selectParameterNodeByTag(CoreEnv.SETTING_KEY)
-
- # Start connections
- self.mainTab.tabBarClicked.connect(self.onMainTabClicked)
-
- def onMainTabClicked(self, index):
- self.lastAccessedWidget.exit()
- self.lastAccessedWidget = self.mainTab.widget(index)
- if type(self.lastAccessedWidget) is qt.QTabWidget:
- self.lastAccessedWidget = self.lastAccessedWidget.currentWidget()
- self.lastAccessedWidget.enter()
-
- def enter(self) -> None:
- super().enter()
- if self.lastAccessedWidget is None:
- return
-
- self.lastAccessedWidget.enter()
-
- def exit(self):
- if self.lastAccessedWidget is None:
- return
+class CoreEnvLogic(LTracePluginLogic, LTraceEnvironmentMixin):
+ def __init__(self):
+ super().__init__()
+ self.__modulesToolbar = None
+
+ @property
+ def modulesToolbar(self):
+ if not self.__modulesToolbar:
+ raise AttributeError("Modules toolbar not set")
+ return self.__modulesToolbar
+
+ @modulesToolbar.setter
+ def modulesToolbar(self, value):
+ self.__modulesToolbar = value
+
+ def setupEnvironment(self):
+ relatedModules = self.getModuleManager().fetchByCategory([self.category])
+
+ addAction(relatedModules["CustomizedData"], self.modulesToolbar)
+ addAction(relatedModules["Multicore"], self.modulesToolbar)
+ addAction(relatedModules["CorePhotographLoader"], self.modulesToolbar)
+ addAction(relatedModules["CustomizedCropVolume"], self.modulesToolbar)
+
+ addAction(relatedModules["MulticoreTransforms"], self.modulesToolbar)
+ self.setupSegmentation()
+ addAction(relatedModules["CoreInpaint"], self.modulesToolbar)
+ addAction(relatedModules["MulticoreExport"], self.modulesToolbar)
+
+ self.getModuleManager().setEnvironment(("Core", "CoreEnv"))
+
+ def setupSegmentation(self):
+ modules = self.getModuleManager().fetchByCategory(("Core",), intersectWith="Segmentation")
+
+ addMenu(
+ svgToQIcon(getResourcePath("Icons") / "IconSet-dark" / "Layers.svg"),
+ "Segmentation",
+ [
+ modules["CustomizedSegmentEditor"],
+ modules["Segmenter"],
+ modules["SegmentInspector"],
+ modules["LabelMapEditor"],
+ modules["PoreStats"],
+ ],
+ self.modulesToolbar,
+ )
- self.lastAccessedWidget.exit()
+ segmentEditor = slicer.util.getModuleWidget("CustomizedSegmentEditor")
+ segmentEditor.configureEffects()
diff --git a/src/modules/CoreInpaint/CoreInpaint.py b/src/modules/CoreInpaint/CoreInpaint.py
index 0a2c68e..b192da4 100644
--- a/src/modules/CoreInpaint/CoreInpaint.py
+++ b/src/modules/CoreInpaint/CoreInpaint.py
@@ -22,7 +22,7 @@ class CoreInpaint(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
self.parent.title = "Core Inpaint"
- self.parent.categories = ["Core"]
+ self.parent.categories = ["Core", "ImageLog", "Multiscale"]
self.parent.contributors = ["LTrace Geophysics Team"]
self.parent.helpText = CoreInpaint.help()
@@ -47,6 +47,7 @@ def setup(self):
self.outputNameField = qt.QLineEdit()
self.outputNameField.setToolTip("Name of the output volume")
+ self.outputNameField.objectName = "Output Name Line Edit"
self.progressBar = qt.QProgressBar()
@@ -86,15 +87,16 @@ def onApplyClicked(self):
if not selectedSegments:
highlight_error(self.inputWidget.segmentListGroup[1])
+ self.applyButton.enabled = True
return
labelMapNode = createTemporaryVolumeNode(slicer.vtkMRMLLabelMapVolumeNode, "mask")
labelMapNode.CopyContent(segmentNode)
slicer.modules.segmentations.logic().ExportVisibleSegmentsToLabelmapNode(segmentNode, labelMapNode, imageNode)
- outNode = slicer.vtkSlicerVolumesLogic().CloneVolume(
- slicer.mrmlScene, imageNode, self.outputNameField.text, False
- )
+ outNode = slicer.vtkSlicerVolumesLogic().CloneVolume(slicer.mrmlScene, imageNode, "clonedNode", False)
+ outNode.SetName(slicer.mrmlScene.GenerateUniqueName(self.outputNameField.text))
+ outNode.Modified()
try:
self.logic.apply(imageNode, labelMapNode, selectedSegments, outNode, self.onProgress)
@@ -106,6 +108,8 @@ def onApplyClicked(self):
slicer.util.errorDisplay(str(err))
raise
+ finally:
+ self.applyButton.enabled = True
self.progressBar.setValue(100)
self.onReferenceSelected(self.inputWidget.referenceInput.currentNode(), updateName=False)
diff --git a/src/modules/CoreInpaint/Resources/Icons/CoreInpaint.png b/src/modules/CoreInpaint/Resources/Icons/CoreInpaint.png
deleted file mode 100644
index b89476c..0000000
Binary files a/src/modules/CoreInpaint/Resources/Icons/CoreInpaint.png and /dev/null differ
diff --git a/src/modules/CoreInpaint/Resources/Icons/CoreInpaint.svg b/src/modules/CoreInpaint/Resources/Icons/CoreInpaint.svg
new file mode 100644
index 0000000..19c79d6
--- /dev/null
+++ b/src/modules/CoreInpaint/Resources/Icons/CoreInpaint.svg
@@ -0,0 +1 @@
+
diff --git a/src/modules/CorePhotographLoader/CorePhotographLoader.py b/src/modules/CorePhotographLoader/CorePhotographLoader.py
index adbd443..3f1fca0 100644
--- a/src/modules/CorePhotographLoader/CorePhotographLoader.py
+++ b/src/modules/CorePhotographLoader/CorePhotographLoader.py
@@ -9,6 +9,7 @@
)
from ltrace.slicer.cli_queue import CliQueue
from ltrace.slicer.widget.global_progress_bar import LocalProgressBar
+from ltrace.utils.callback import Callback
from pathlib import Path
import ctk
@@ -21,13 +22,13 @@
class CorePhotographLoader(LTracePlugin):
- SETTING_KEY = "MultipleImageAnalysis"
+ SETTING_KEY = "CorePhotographLoader"
MODULE_DIR = Path(os.path.dirname(os.path.realpath(__file__)))
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
self.parent.title = "Core Photograph Loader" # TODO make this more human readable by adding spaces
- self.parent.categories = ["LTrace Tools"]
+ self.parent.categories = ["Tools", "Core"]
self.parent.dependencies = []
self.parent.contributors = ["LTrace Geophysics Team"] # replace with "Firstname Lastname (Organization)"
self.parent.helpText = CorePhotographLoader.help()
@@ -63,6 +64,7 @@ def setup(self):
# Directory selection widget
self.directory_selector = ctk.ctkDirectoryButton()
+ self.directory_selector.setMaximumWidth(374)
self.directory_selector.caption = "Export directory"
self.directory_selector.directoryChanged.connect(self.on_directory_input_changed)
parameters_form_layout.addRow("Input folder:", self.directory_selector)
@@ -335,7 +337,7 @@ def run(self, data: dict, cli_progress_bar):
)
self.__cli_queue.create_cli_node(slicer.modules.corephotographloadercli, cli_config, modified_callback)
- self.__cli_queue.signal_queue_finished.connect(self._on_process_finished)
+ self.__cli_queue.signal_queue_finished.connect(self._on__process_finished)
self.__cli_queue.run()
self.process_started.emit()
diff --git a/src/modules/CorePhotographLoader/Resources/Icons/CorePhotographLoader.png b/src/modules/CorePhotographLoader/Resources/Icons/CorePhotographLoader.png
deleted file mode 100644
index b89476c..0000000
Binary files a/src/modules/CorePhotographLoader/Resources/Icons/CorePhotographLoader.png and /dev/null differ
diff --git a/src/modules/CorePhotographLoader/Resources/Icons/CorePhotographLoader.svg b/src/modules/CorePhotographLoader/Resources/Icons/CorePhotographLoader.svg
new file mode 100644
index 0000000..3339f5a
--- /dev/null
+++ b/src/modules/CorePhotographLoader/Resources/Icons/CorePhotographLoader.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/modules/CorePluggingExporter/CorePluggingExporter.py b/src/modules/CorePluggingExporter/CorePluggingExporter.py
index 35549eb..7f76b42 100644
--- a/src/modules/CorePluggingExporter/CorePluggingExporter.py
+++ b/src/modules/CorePluggingExporter/CorePluggingExporter.py
@@ -23,7 +23,7 @@ class CorePluggingExporter(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
self.parent.title = "Core Plugging Exporter"
- self.parent.categories = ["LTrace Tools"]
+ self.parent.categories = ["Tools"]
self.parent.dependencies = []
self.parent.contributors = ["LTrace Geophysics Team"] # replace with "Firstname Lastname (Organization)"
self.parent.helpText = ""
diff --git a/src/modules/CustomResampleScalarVolume/CustomResampleScalarVolume.py b/src/modules/CustomResampleScalarVolume/CustomResampleScalarVolume.py
index fee8038..1f2c1ff 100644
--- a/src/modules/CustomResampleScalarVolume/CustomResampleScalarVolume.py
+++ b/src/modules/CustomResampleScalarVolume/CustomResampleScalarVolume.py
@@ -40,8 +40,8 @@ class CustomResampleScalarVolume(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
- self.parent.title = "LTrace Resample Scalar Volume"
- self.parent.categories = ["LTrace Tools"]
+ self.parent.title = "Resample"
+ self.parent.categories = ["Tools", "MicroCT", "Multiscale"]
self.parent.dependencies = []
self.parent.contributors = ["LTrace Geophysics Team"]
self.parent.helpText = CustomResampleScalarVolume.help()
diff --git a/src/modules/CustomResampleScalarVolume/Resources/Icons/CustomResampleScalarVolume.png b/src/modules/CustomResampleScalarVolume/Resources/Icons/CustomResampleScalarVolume.png
deleted file mode 100644
index b89476c..0000000
Binary files a/src/modules/CustomResampleScalarVolume/Resources/Icons/CustomResampleScalarVolume.png and /dev/null differ
diff --git a/src/modules/CustomResampleScalarVolume/Resources/Icons/CustomResampleScalarVolume.svg b/src/modules/CustomResampleScalarVolume/Resources/Icons/CustomResampleScalarVolume.svg
new file mode 100644
index 0000000..e1a5ea0
--- /dev/null
+++ b/src/modules/CustomResampleScalarVolume/Resources/Icons/CustomResampleScalarVolume.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/modules/CustomizedCropVolume/CustomizedCropVolume.py b/src/modules/CustomizedCropVolume/CustomizedCropVolume.py
index be9f8d4..25f0889 100644
--- a/src/modules/CustomizedCropVolume/CustomizedCropVolume.py
+++ b/src/modules/CustomizedCropVolume/CustomizedCropVolume.py
@@ -7,9 +7,11 @@
import slicer
import vtk
+from ltrace.slicer import ui
from ltrace.slicer.helpers import bounds2size, copy_display
from ltrace.slicer_utils import *
-from ltrace.slicer.node_observer import NodeObserver
+from ltrace.slicer_utils import getResourcePath
+from ltrace.utils.callback import Callback
try:
from Test.CustomizedCropVolumeTest import CustomizedCropTest
@@ -25,11 +27,11 @@ class CustomizedCropVolume(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
- self.parent.title = "Customized Crop Volume"
- self.parent.categories = ["LTrace Tools"]
+ self.parent.title = "Volumes Crop"
+ self.parent.categories = ["Tools", "MicroCT", "Thin Section", "Core", "Multiscale"]
self.parent.dependencies = []
self.parent.contributors = ["LTrace Geophysical Solutions"]
- self.parent.helpText = CustomizedCropVolume.help()
+ self.parent.helpText = f"file:///{(getResourcePath('manual') / 'Modules/Thin_section/Crop.html').as_posix()}"
@classmethod
def readme_path(cls):
@@ -107,18 +109,16 @@ def setup(self):
parametersFormLayout.addRow(" ", None)
- self.cropButton = qt.QPushButton("Crop")
- self.cropButton.setFixedHeight(40)
- self.cropButton.clicked.connect(self.onCropButtonClicked)
-
- self.cancelButton = qt.QPushButton("Cancel")
- self.cancelButton.setFixedHeight(40)
- self.cancelButton.clicked.connect(self.onCancelButtonClicked)
-
- buttonsHBoxLayout = qt.QHBoxLayout()
- buttonsHBoxLayout.addWidget(self.cropButton)
- buttonsHBoxLayout.addWidget(self.cancelButton)
- loadFormLayout.addRow(buttonsHBoxLayout)
+ self.applyCancelButtons = ui.ApplyCancelButtons(
+ onApplyClick=self.onCropButtonClicked,
+ onCancelClick=self.onCancelButtonClicked,
+ applyTooltip="Crop",
+ cancelTooltip="Cancel",
+ applyText="Crop",
+ cancelText="Cancel",
+ enabled=True,
+ )
+ loadFormLayout.addWidget(self.applyCancelButtons)
statusLabel = qt.QLabel("Status: ")
self.currentStatusLabel = qt.QLabel("Idle")
@@ -208,12 +208,18 @@ def enter(self) -> None:
self.logic.roi = slicer.mrmlScene.AddNewNodeByClass(slicer.vtkMRMLMarkupsROINode.__name__, "Crop ROI")
self.logic.roi.SetDisplayVisibility(False)
self.logic.roi.GetDisplayNode().SetFillOpacity(0.5)
- self.roiObserver = NodeObserver(node=self.logic.roi, parent=None)
- self.roiObserver.modifiedSignal.connect(self.onRoiModified)
+ self.roiObserver = self.logic.roi.AddObserver(
+ slicer.vtkMRMLDisplayableNode.DisplayModifiedEvent, self.onRoiModified
+ )
+ # self.roiObserver = NodeObserver(node=self.logic.roi, parent=None)
+ # self.roiObserver.modifiedSignal.connect(self.onRoiModified)
def exit(self):
+ if self.roiObserver:
+ self.logic.roi.RemoveObserver(self.roiObserver)
self.roiObserver = None
slicer.mrmlScene.RemoveNode(self.logic.roi)
+ self.logic.roi = None
def updateStatus(self, message, progress=None, processEvents=True):
self.progressBar.show()
@@ -238,11 +244,6 @@ def cleanup(self):
self.exit()
-class Callback(object):
- def __init__(self, on_update=None):
- self.on_update = on_update or (lambda *args, **kwargs: None)
-
-
class CustomizedCropVolumeLogic(LTracePluginLogic):
def __init__(self):
LTracePluginLogic.__init__(self)
@@ -264,7 +265,8 @@ def initializeVolume(self, volume):
def crop(self, volume, ijkSize):
position_ras = [0] * 3
- self.roi.GetXYZ(position_ras)
+ if self.roi is not None:
+ self.roi.GetXYZ(position_ras)
ras_to_ijk = vtk.vtkMatrix4x4()
volume.GetRASToIJKMatrix(ras_to_ijk)
@@ -300,14 +302,16 @@ def crop(self, volume, ijkSize):
slicer.util.setSliceViewerLayers(background=croppedVolume, fit=True)
copy_display(volume, croppedVolume)
- self.roi.SetDisplayVisibility(False)
+ if self.roi is not None:
+ self.roi.SetDisplayVisibility(False)
self.lastCroppedVolume = croppedVolume
def getCroppedSize(self, volume):
position = [0] * 3
radius = [0] * 3
- self.roi.GetRadiusXYZ(radius)
- self.roi.GetXYZ(position)
+ if self.roi is not None:
+ self.roi.GetRadiusXYZ(radius)
+ self.roi.GetXYZ(position)
volumeExtents = [0] * 6
volume.GetRASBounds(volumeExtents)
@@ -327,7 +331,8 @@ def getCroppedSize(self, volume):
def setRoiSizeIjk(self, volume, ijkSize):
spacing = volume.GetSpacing()
rasSize = tuple(ijkDim * spacingDim / 2 for ijkDim, spacingDim in zip(ijkSize, spacing))
- self.roi.SetRadiusXYZ(rasSize)
+ if self.roi is not None:
+ self.roi.SetRadiusXYZ(rasSize)
class CropInfo(RuntimeError):
diff --git a/src/modules/CustomizedCropVolume/Resources/Icons/CustomizedCropVolume.png b/src/modules/CustomizedCropVolume/Resources/Icons/CustomizedCropVolume.png
deleted file mode 100644
index b89476c..0000000
Binary files a/src/modules/CustomizedCropVolume/Resources/Icons/CustomizedCropVolume.png and /dev/null differ
diff --git a/src/modules/CustomizedCropVolume/Resources/Icons/CustomizedCropVolume.svg b/src/modules/CustomizedCropVolume/Resources/Icons/CustomizedCropVolume.svg
new file mode 100644
index 0000000..ee34d04
--- /dev/null
+++ b/src/modules/CustomizedCropVolume/Resources/Icons/CustomizedCropVolume.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/modules/CustomizedCurvatureAnisotropicDiffusion/CustomizedCurvatureAnisotropicDiffusion.py b/src/modules/CustomizedCurvatureAnisotropicDiffusion/CustomizedCurvatureAnisotropicDiffusion.py
index cc3b60e..8bb0663 100644
--- a/src/modules/CustomizedCurvatureAnisotropicDiffusion/CustomizedCurvatureAnisotropicDiffusion.py
+++ b/src/modules/CustomizedCurvatureAnisotropicDiffusion/CustomizedCurvatureAnisotropicDiffusion.py
@@ -21,7 +21,7 @@ class CustomizedCurvatureAnisotropicDiffusion(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
self.parent.title = "Curvature Anisotropic Diffusion"
- self.parent.categories = ["LTrace Tools"]
+ self.parent.categories = ["Tools", "MicroCT", "Multiscale"]
self.parent.dependencies = []
self.parent.contributors = ["LTrace Geophysical Solutions"]
self.parent.helpText = CustomizedCurvatureAnisotropicDiffusion.help()
diff --git a/src/modules/CustomizedData/CustomizedData.py b/src/modules/CustomizedData/CustomizedData.py
index 829a30d..ade42be 100644
--- a/src/modules/CustomizedData/CustomizedData.py
+++ b/src/modules/CustomizedData/CustomizedData.py
@@ -26,8 +26,8 @@ class CustomizedData(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
- self.parent.title = "Customized Data"
- self.parent.categories = ["LTrace Tools"]
+ self.parent.title = "Explorer"
+ self.parent.categories = ["Project", "MicroCT", "Thin Section", "Core", "Multiscale"]
self.parent.dependencies = []
self.parent.contributors = ["LTrace Geophysical Solutions"]
self.parent.helpText = CustomizedData.help()
@@ -64,7 +64,12 @@ def setup(self):
self.subjectHierarchyTreeView = dataWidget.findChild(qt.QObject, "SubjectHierarchyTreeView")
self.subjectHierarchyTreeView.setEditTriggers(qt.QAbstractItemView.NoEditTriggers)
- self.subjectHierarchyTreeView.hideColumn(3)
+
+ def showSearchPopup(current):
+ if current.column() == 0:
+ slicer.modules.AppContextInstance.fuzzySearch.exec_()
+
+ self.subjectHierarchyTreeView.doubleClicked.connect(showSearchPopup)
# Adds confirmation step before delete action
nodeMenu = self.subjectHierarchyTreeView.findChild(qt.QMenu, "nodeMenuTreeView")
@@ -92,7 +97,7 @@ def confirmDeleteSelectedItems():
)
self.subjectHierarchyTreeView.setMinimumHeight(310)
- self.subjectHierarchyTreeView.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Minimum)
+ self.subjectHierarchyTreeView.setSizePolicy(qt.QSizePolicy.Minimum, qt.QSizePolicy.Minimum)
self.formLayout.addRow(self.subjectHierarchyTreeView)
self.scalarVolumeWidget = ScalarVolumeWidget(isLabelMap=False)
diff --git a/src/modules/CustomizedData/Resources/Icons/CustomizedData.png b/src/modules/CustomizedData/Resources/Icons/CustomizedData.png
deleted file mode 100644
index b89476c..0000000
Binary files a/src/modules/CustomizedData/Resources/Icons/CustomizedData.png and /dev/null differ
diff --git a/src/modules/CustomizedData/Resources/Icons/CustomizedData.svg b/src/modules/CustomizedData/Resources/Icons/CustomizedData.svg
new file mode 100644
index 0000000..bc5899b
--- /dev/null
+++ b/src/modules/CustomizedData/Resources/Icons/CustomizedData.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/modules/CustomizedGaussianBlurImageFilter/CustomizedGaussianBlurImageFilter.py b/src/modules/CustomizedGaussianBlurImageFilter/CustomizedGaussianBlurImageFilter.py
index f43f1e4..0a1a87d 100644
--- a/src/modules/CustomizedGaussianBlurImageFilter/CustomizedGaussianBlurImageFilter.py
+++ b/src/modules/CustomizedGaussianBlurImageFilter/CustomizedGaussianBlurImageFilter.py
@@ -24,7 +24,7 @@ class CustomizedGaussianBlurImageFilter(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
self.parent.title = "Gaussian Blur Image Filter"
- self.parent.categories = ["LTrace Tools"]
+ self.parent.categories = ["Tools", "MicroCT", "Multiscale"]
self.parent.dependencies = []
self.parent.contributors = ["LTrace Geophysical Solutions"]
self.parent.helpText = CustomizedGaussianBlurImageFilter.help()
diff --git a/src/modules/CustomizedGradientAnisotropicDiffusion/CustomizedGradientAnisotropicDiffusion.py b/src/modules/CustomizedGradientAnisotropicDiffusion/CustomizedGradientAnisotropicDiffusion.py
index 9920d31..e8e732c 100644
--- a/src/modules/CustomizedGradientAnisotropicDiffusion/CustomizedGradientAnisotropicDiffusion.py
+++ b/src/modules/CustomizedGradientAnisotropicDiffusion/CustomizedGradientAnisotropicDiffusion.py
@@ -53,7 +53,7 @@ class CustomizedGradientAnisotropicDiffusion(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
self.parent.title = "Gradient Anisotropic Diffusion"
- self.parent.categories = ["LTrace Tools"]
+ self.parent.categories = ["Tools", "MicroCT", "Multiscale"]
self.parent.dependencies = []
self.parent.contributors = ["LTrace Geophysical Solutions"]
self.parent.helpText = CustomizedGradientAnisotropicDiffusion.help()
diff --git a/src/modules/CustomizedMedianImageFilter/CustomizedMedianImageFilter.py b/src/modules/CustomizedMedianImageFilter/CustomizedMedianImageFilter.py
index f8d1b67..42456e9 100644
--- a/src/modules/CustomizedMedianImageFilter/CustomizedMedianImageFilter.py
+++ b/src/modules/CustomizedMedianImageFilter/CustomizedMedianImageFilter.py
@@ -22,7 +22,7 @@ class CustomizedMedianImageFilter(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
self.parent.title = "Median Image Filter"
- self.parent.categories = ["LTrace Tools"]
+ self.parent.categories = ["Tools", "MicroCT", "Multiscale"]
self.parent.dependencies = []
self.parent.contributors = ["LTrace Geophysical Solutions"]
self.parent.helpText = CustomizedMedianImageFilter.help()
diff --git a/src/modules/CustomizedSegmentEditor/CustomizedEffects/Margin/SegmentEditorMarginEffect.py b/src/modules/CustomizedSegmentEditor/CustomizedEffects/Margin/SegmentEditorMarginEffect.py
index 2fc86e6..8672694 100644
--- a/src/modules/CustomizedSegmentEditor/CustomizedEffects/Margin/SegmentEditorMarginEffect.py
+++ b/src/modules/CustomizedSegmentEditor/CustomizedEffects/Margin/SegmentEditorMarginEffect.py
@@ -73,7 +73,7 @@ def setupOptionsFrame(self):
def createCursor(self, widget):
# Turn off effect-specific cursor for this effect
- return slicer.util.mainWindow().cursor
+ return slicer.modules.AppContextInstance.mainWindow.cursor
def setMRMLDefaults(self):
self.scriptedEffect.setParameterDefault("MarginSizePixels", 3)
diff --git a/src/modules/CustomizedSegmentEditor/CustomizedSegmentEditor.py b/src/modules/CustomizedSegmentEditor/CustomizedSegmentEditor.py
index 14b609d..82e75e7 100644
--- a/src/modules/CustomizedSegmentEditor/CustomizedSegmentEditor.py
+++ b/src/modules/CustomizedSegmentEditor/CustomizedSegmentEditor.py
@@ -1,18 +1,16 @@
import os
from pathlib import Path
+import qSlicerSegmentationsEditorEffectsPythonQt
+import qSlicerSegmentationsModuleWidgetsPythonQt
import qt
import slicer
+from distinctipy import distinctipy
from slicer.util import VTKObservationMixin
-import qSlicerSegmentationsEditorEffectsPythonQt
-import qSlicerSegmentationsModuleWidgetsPythonQt
-from distinctipy import distinctipy
-from ltrace.slicer.application_observables import ApplicationObservables
-from ltrace.slicer_utils import LTracePlugin, LTracePluginWidget
from CustomizedEffects.Margin import SegmentEditorMarginEffect
from CustomizedEffects.Threshold import SegmentEditorThresholdEffect
-
+from ltrace.slicer_utils import LTracePlugin, LTracePluginWidget, getResourcePath
# Checks if closed source code is available
try:
@@ -30,10 +28,12 @@ class CustomizedSegmentEditor(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
self.parent.title = "Manual Segmentation"
- self.parent.categories = ["LTrace Tools"]
+ self.parent.categories = ["Tools", "Segmentation", "Thin Section", "ImageLog", "Core", "MicroCT", "Multiscale"]
self.parent.dependencies = []
self.parent.contributors = ["LTrace Geophysical Solutions"]
- self.parent.helpText = CustomizedSegmentEditor.help()
+ self.parent.helpText = (
+ f"file:///{(getResourcePath('manual') / 'Modules/Thin_section/SegmentEditor.html').as_posix()}"
+ )
@classmethod
def readme_path(cls):
@@ -114,15 +114,10 @@ def setup(self):
self.configureEffects()
- ApplicationObservables().applicationLoadFinished.connect(self.__onApplicationLoadFinished)
-
- def __onApplicationLoadFinished(self):
- # Connect observers to scene events
self.addObserver(slicer.mrmlScene, slicer.mrmlScene.StartCloseEvent, self.onSceneStartClose)
self.addObserver(slicer.mrmlScene, slicer.mrmlScene.EndCloseEvent, self.onSceneEndClose)
self.addObserver(slicer.mrmlScene, slicer.mrmlScene.EndImportEvent, self.onSceneEndImport)
self.activateEditorRegisteredCallback()
- ApplicationObservables().applicationLoadFinished.disconnect()
def activateEditorRegisteredCallback(self):
self.effectFactorySingleton.effectRegistered.connect(self.editorEffectRegistered)
@@ -148,6 +143,8 @@ def onSourceVolumeNodeChanged(self, node):
self.configureColorSupport(color_support=color_support)
def configureEffectsForThinSectionEnvironment(self):
+ self.selectParameterNodeByTag("ThinSectionEnv")
+
self.editor.setEffectNameOrder(
[
"Paint",
@@ -162,7 +159,9 @@ def configureEffectsForThinSectionEnvironment(self):
"Mask Image",
"Connectivity",
"Level tracing",
+ "Smart foreground",
"Color threshold",
+ "Boundary removal",
]
)
self.editor.unorderedEffectsVisible = False
@@ -188,6 +187,7 @@ def configureEffects(self, color_support=False):
"Boundary removal",
"Expand segments",
"Sample segmentation",
+ "Smart foreground",
]
if color_support:
effects.append("Color threshold")
@@ -282,4 +282,3 @@ def cleanup(self):
super().cleanup()
self.removeObservers()
self.deactivateEditorRegisteredCallback()
- self.__applicationObservables.applicationLoadFinished.disconnect()
diff --git a/src/modules/CustomizedSegmentEditor/Resources/Icons/CustomizedSegmentEditor.svg b/src/modules/CustomizedSegmentEditor/Resources/Icons/CustomizedSegmentEditor.svg
new file mode 100644
index 0000000..7ad384b
--- /dev/null
+++ b/src/modules/CustomizedSegmentEditor/Resources/Icons/CustomizedSegmentEditor.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/modules/CustomizedSmoothingEffect/CustomizedSmoothingEffectLib/SegmentEditorSmoothingEffect.py b/src/modules/CustomizedSmoothingEffect/CustomizedSmoothingEffectLib/SegmentEditorSmoothingEffect.py
index 92939c3..5f9c9ec 100644
--- a/src/modules/CustomizedSmoothingEffect/CustomizedSmoothingEffectLib/SegmentEditorSmoothingEffect.py
+++ b/src/modules/CustomizedSmoothingEffect/CustomizedSmoothingEffectLib/SegmentEditorSmoothingEffect.py
@@ -13,17 +13,41 @@
from ltrace.slicer.helpers import getSourceVolume
from pathlib import Path
-from scipy import ndimage
from SegmentEditorEffects import *
+from scipy import ndimage
+from slicer.i18n import tr as _
from vtk.util import numpy_support
+def _batch_sum_conv_3d(array, kernel_size):
+ assert array.ndim == 4
+ maxValue = kernel_size[0] * kernel_size[1] * kernel_size[2]
+ if maxValue <= 255:
+ array = array.astype(np.uint8)
+ elif maxValue <= 65535:
+ array = array.astype(np.uint16)
+ else:
+ array = array.astype(np.uint32)
+
+ kernel_x = np.ones(kernel_size[2], dtype=array.dtype)
+ kernel_y = np.ones(kernel_size[1], dtype=array.dtype)
+ kernel_z = np.ones(kernel_size[0], dtype=array.dtype) if kernel_size[0] > 1 else None
+
+ array = ndimage.convolve1d(array, kernel_x, axis=1, mode="reflect")
+ array = ndimage.convolve1d(array, kernel_y, axis=2, mode="reflect")
+ if kernel_z is not None:
+ array = ndimage.convolve1d(array, kernel_z, axis=3, mode="reflect")
+
+ return array
+
+
class SegmentEditorSmoothingEffect(AbstractScriptedSegmentEditorPaintEffect):
"""SmoothingEffect is an Effect that smoothes a selected segment"""
def __init__(self, scriptedEffect):
- scriptedEffect.name = "Smoothing"
AbstractScriptedSegmentEditorPaintEffect.__init__(self, scriptedEffect)
+ scriptedEffect.name = "Smoothing"
+ scriptedEffect.title = _("Smoothing")
def clone(self):
import qSlicerSegmentationsEditorEffectsPythonQt as effects
@@ -42,12 +66,10 @@ def helpText(self):
return """Make segment boundaries smoother by removing extrusions and filling small holes. The effect can be either applied locally
(by painting in viewers) or to the whole segment (by clicking Apply button). Available methods:
-
Median: removes small details while keeps smooth contours mostly unchanged. Applied to selected segment only.
+
Median: removes small details while keeps smooth contours mostly unchanged. Applied to one or more segments at once, preserving segment boundaries.
Opening: removes extrusions smaller than the specified kernel size. Applied to selected segment only.
Fill holes: fills contiguous empty spaces smaller than the specified kernel size.
Gaussian: smoothes all contours, tends to shrink the segment. Applied to selected segment only.
-
Joint smoothing: smoothes multiple segments at once, preserving watertight interface between them. Masking settings are bypassed.
-If segments overlap, segment higher in the segments table will have priority. Applied to all visible segments.
+"""
+ )
+ helpLayout.addRow("Data Sources && References:", helpButton)
self.treeView = qt.QTreeView()
self.tree = qt.QTreeWidget()
@@ -82,6 +103,7 @@ def setup(self):
self.onSelectionChanged()
+ self.layout.addLayout(helpLayout)
self.layout.addWidget(self.tree, 5)
self.layout.addWidget(self.downloadButton)
self.layout.addWidget(self.stdoutTextArea, 1)
diff --git a/src/modules/OpenRockData/Resources/Icons/OpenRockData.svg b/src/modules/OpenRockData/Resources/Icons/OpenRockData.svg
new file mode 100644
index 0000000..3bba4b6
--- /dev/null
+++ b/src/modules/OpenRockData/Resources/Icons/OpenRockData.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/modules/PNMReport/PNMReport.py b/src/modules/PNMReport/PNMReport.py
index 8097cfe..919daf0 100644
--- a/src/modules/PNMReport/PNMReport.py
+++ b/src/modules/PNMReport/PNMReport.py
@@ -1,14 +1,7 @@
-import ctk
import os
-import qt
-import slicer
-
-from ltrace.slicer import ui
-from ltrace.slicer_utils import LTracePlugin, LTracePluginWidget, LTracePluginLogic
from pathlib import Path
-from ReportLib.ReportLogic import ReportLogic
-from ReportLib.ReportForm import ReportForm
+from ltrace.slicer_utils import LTracePlugin, LTracePluginWidget, LTracePluginLogic
try:
from ReportLib.StreamlitServer import StreamlitServer
@@ -29,7 +22,7 @@ class PNMReport(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
self.parent.title = "PNMReport"
- self.parent.categories = ["LTrace Tools"]
+ self.parent.categories = ["Tools", "MicroCT"]
self.parent.contributors = ["LTrace Geophysics Team"]
self.parent.helpText = PNMReport.help()
diff --git a/src/modules/PNMReport/ReportLib/ReportLogic.py b/src/modules/PNMReport/ReportLib/ReportLogic.py
index 72739e4..9b267e8 100644
--- a/src/modules/PNMReport/ReportLib/ReportLogic.py
+++ b/src/modules/PNMReport/ReportLib/ReportLogic.py
@@ -14,18 +14,21 @@
from ltrace.pore_networks.functions import geo2spy
from ltrace.slicer import data_utils as du
+from ltrace.slicer.helpers import LazyLoad
from ltrace.slicer.widget.global_progress_bar import LocalProgressBar
from ltrace.utils.ProgressBarProc import ProgressBarProc
from ltrace.slicer_utils import LTracePluginLogic
-from CustomResampleScalarVolume import CustomResampleScalarVolumeLogic
-from PoreNetworkExtractor import PoreNetworkExtractorLogic
-from PoreNetworkProduction import PoreNetworkProductionLogic
-from PoreNetworkSimulationLib.OnePhaseSimulationWidget import OnePhaseSimulationWidget
-from PoreNetworkSimulationLib.TwoPhaseSimulationWidget import TwoPhaseSimulationWidget
-from PoreNetworkSimulationLib.PoreNetworkSimulationLogic import OnePhaseSimulationLogic, TwoPhaseSimulationLogic
-from MercurySimulationLib.MercurySimulationWidget import MercurySimulationWidget, MercurySimulationLogic
-from MercurySimulationLib.SubscaleModelWidget import SubscaleLogicDict
+CustomResampleScalarVolumeLogic = LazyLoad("CustomResampleScalarVolume.CustomResampleScalarVolumeLogic")
+PoreNetworkExtractorLogic = LazyLoad("PoreNetworkExtractor.PoreNetworkExtractorLogic")
+PoreNetworkProductionLogic = LazyLoad("PoreNetworkProduction.PoreNetworkProductionLogic")
+OnePhaseSimulationWidget = LazyLoad("PoreNetworkSimulationLib.OnePhaseSimulationWidget.OnePhaseSimulationWidget")
+TwoPhaseSimulationWidget = LazyLoad("PoreNetworkSimulationLib.TwoPhaseSimulationWidget.TwoPhaseSimulationWidget")
+OnePhaseSimulationLogic = LazyLoad("PoreNetworkSimulationLib.PoreNetworkSimulationLogic.OnePhaseSimulationLogic")
+TwoPhaseSimulationLogic = LazyLoad("PoreNetworkSimulationLib.PoreNetworkSimulationLogic.TwoPhaseSimulationLogic")
+MercurySimulationWidget = LazyLoad("MercurySimulationLib.MercurySimulationWidget.MercurySimulationWidget")
+MercurySimulationLogic = LazyLoad("MercurySimulationLib.MercurySimulationLogic.MercurySimulationLogic")
+SubscaleLogicDict = LazyLoad("MercurySimulationLib.SubscaleModelWidget.SubscaleLogicDict")
class PNMQueue(qt.QObject):
@@ -458,6 +461,10 @@ def sensibility_callback(self, logic):
def onFinishKrel(state):
if state:
try:
+ self.json_entry_node_ids["sensibility_parameters"] = self.params[
+ "sensibility_parameters_node"
+ ].GetID()
+
krelResultsTableNode = slicer.util.getNode(logic.krelResultsTableNodeId)
krelResultsTableNode.SetName("Sensibility")
self.json_entry_node_ids["sensibility"] = krelResultsTableNode.GetID()
@@ -537,8 +544,13 @@ def onFinishMICP(state):
if state:
micp_results_node_id = logic.results_node_id
if micp_results_node_id:
- micp_results = slicer.util.getNode(micp_results_node_id)
- self.json_entry_node_ids["micp"] = micp_results.GetID()
+ self.json_entry_node_ids["micp"] = micp_results_node_id
+
+ flow_props_pore_id = logic.flow_props_pore_id
+ flow_props_throat_id = logic.flow_props_throat_id
+ if flow_props_pore_id and flow_props_throat_id:
+ self.json_entry_node_ids["flow_props_pore_network"] = flow_props_pore_id
+ self.json_entry_node_ids["flow_props_throat_network"] = flow_props_throat_id
self.MICPState = True
@@ -623,6 +635,11 @@ def process_json_entries(self):
else:
continue
+ name = f"{folder}/{self.outputPrefix}/subres_model.json"
+ with open(name, "w") as f:
+ json.dump({self.subres_model_name: self.subres_params}, f)
+ self.json_entry["subres_model"] = os.path.basename(name)
+
self.projects_dict[self.outputPrefix] = self.json_entry
projects_path = folder / "projects.json"
diff --git a/src/modules/ImageLogEnv/ImageLogsLib/PermeabilityModeling.py b/src/modules/PermeabilityModeling/PermeabilityModeling.py
similarity index 59%
rename from src/modules/ImageLogEnv/ImageLogsLib/PermeabilityModeling.py
rename to src/modules/PermeabilityModeling/PermeabilityModeling.py
index 55a9ea2..66269fd 100644
--- a/src/modules/ImageLogEnv/ImageLogsLib/PermeabilityModeling.py
+++ b/src/modules/PermeabilityModeling/PermeabilityModeling.py
@@ -10,26 +10,55 @@
from ltrace.slicer.helpers import reset_style_on_valid_text
from ltrace.slicer.node_attributes import ImageLogDataSelectable
from ltrace.slicer.ui import hierarchyVolumeInput
-from ltrace.slicer_utils import dataframeFromTable, dataFrameToTableNode
+from ltrace.slicer_utils import (
+ dataframeFromTable,
+ dataFrameToTableNode,
+ LTracePlugin,
+ LTracePluginWidget,
+ LTracePluginLogic,
+)
from ltrace.slicer.widget.global_progress_bar import LocalProgressBar
from typing import List
from vtk.util.numpy_support import vtk_to_numpy
from ImageLogsLib.KdsOptimizationTableWidget import KdsOptimizationWidget
+from pathlib import Path
ERROR_CORRECTION_NODE_NAME = "ERROR_CORRECTION_TABLE_NODE"
+try:
+ from Test.PermeabilityModelingTest import PermeabilityModelingTest
+except ImportError:
+ PermeabilityModelingTest = None
-class PermeabilityModelingWidget(qt.QWidget):
+
+class PermeabilityModeling(LTracePlugin):
+ SETTING_KEY = "PermeabilityModeling"
+ MODULE_DIR = Path(os.path.dirname(os.path.realpath(__file__)))
+
+ def __init__(self, parent):
+ LTracePlugin.__init__(self, parent)
+ self.parent.title = "Permeability Modeling"
+ self.parent.categories = ["Tools", "ImageLog"]
+ self.parent.contributors = ["LTrace Geophysics Team"]
+ self.parent.helpText = PermeabilityModeling.help()
+
+ @classmethod
+ def readme_path(cls):
+ return str(cls.MODULE_DIR / "README.md")
+
+
+class PermeabilityModelingWidget(LTracePluginWidget):
def __init__(self, parent=None):
- super().__init__(parent)
+ LTracePluginWidget.__init__(self, parent)
- self.logic = PermeabilityModelingLogic(self)
+ self.__kdsOptimizationTableId = None
+ self.logic = PermeabilityModelingLogic(parent)
self.onOutputNodeReady = lambda volumes: None
- self.__kdsOptimizationTableId = None
+ self.logic.processFinished.connect(self._onProcessFinished)
- layout = qt.QVBoxLayout()
- self.setLayout(layout)
+ def setup(self):
+ LTracePluginWidget.setup(self)
self.progressBar = None
self.ioFileInputLineEdit = None
@@ -54,13 +83,9 @@ def __init__(self, parent=None):
self.progressBar = LocalProgressBar()
- layout.addWidget(self.applyButton)
-
- layout.addWidget(self.progressBar)
-
- layout.addStretch(1)
-
- slicer.mrmlScene.AddObserver(slicer.mrmlScene.EndImportEvent, self.__onLoadProject)
+ self.layout.addWidget(self.applyButton)
+ self.layout.addWidget(self.progressBar)
+ self.layout.addStretch(1)
@classmethod
def help(cls):
@@ -75,31 +100,46 @@ def readme_path(cls):
dir_path = os.path.dirname(os.path.realpath(__file__))
return str(dir_path + "/" + "PermeabilityModeling.md")
+ def cleanup(self):
+ LTracePluginWidget.cleanup(self)
+ # # TO-DO (PL-2580): Fix dangling callback references for hierarchyVolumeInput and numericInput widgets.
+ # # Remove the code below after the fix is merged.
+
+ def _onProcessFinished(self, status: str) -> None:
+ if status == "Completed":
+ self.onComplete()
+ elif status == "Cancelled":
+ self.onCancelled()
+ else:
+ self.onCompletedWithErrors()
+
def enter(self) -> None:
- super().enter()
- self.__updateKdsOptimizationTable()
+ self.__checkUniqueErrorCorrectionNode()
- def __onLoadProject(self, *args, **kwargs):
+ def __checkUniqueErrorCorrectionNode(self):
try:
nodes = slicer.util.getNodes(ERROR_CORRECTION_NODE_NAME, useLists=True)
nodes: List = list(nodes.values()) if nodes else []
except slicer.util.MRMLNodeNotFoundException:
+ # No error correction node detected
self.__updateKdsOptimizationTable()
return
- if len(nodes) <= 0:
+ if not nodes:
+ # Safe check for empty node list
self.__kdsOptimizationTableId = None
self.__updateKdsOptimizationTable()
return
nodes = nodes[0]
if len(nodes) == 1:
+ # Unique node detected
node = nodes[0]
self.__kdsOptimizationTableId = node.GetID()
self.__updateKdsOptimizationTable()
return
- # If there is more than one related node, remove the old one and keep the first one in the list
+ # If there are more than one related node, remove the last one and keep the first one in the list
for node in nodes[:]:
if node.GetID() != self.__kdsOptimizationTableId:
continue
@@ -109,6 +149,7 @@ def __onLoadProject(self, *args, **kwargs):
break
for node in nodes[1:]:
+ # Safe check for multiple nodes with same ID
slicer.mrmlScene.RemoveNode(node)
del node
@@ -116,47 +157,53 @@ def __onLoadProject(self, *args, **kwargs):
self.__updateKdsOptimizationTable()
def __updateKdsOptimizationTable(self):
- if not self.kdsOptimizationWidget:
- return
+ try:
+ if not self.kdsOptimizationWidget:
+ return
- if self.__kdsOptimizationTableId is None:
- self.kdsOptimizationWidget.setTableData(None)
- return
+ if self.__kdsOptimizationTableId is None:
+ self.kdsOptimizationWidget.setTableData(None)
+ return
- node = helpers.tryGetNode(self.__kdsOptimizationTableId)
- if node is None:
- self.kdsOptimizationWidget.setTableData(None)
- return
+ node = helpers.tryGetNode(self.__kdsOptimizationTableId)
+ if node is None:
+ self.kdsOptimizationWidget.setTableData(None)
+ return
- df = dataframeFromTable(node)
- self.kdsOptimizationWidget.setTableData(df)
+ df = dataframeFromTable(node)
+ self.kdsOptimizationWidget.setTableData(df)
+
+ except Exception as e:
+ logging.error(f"Failed to update Kds Optimization Table, cause: {repr(e)}")
def inputSection(self):
parametersCollapsibleButton = ctk.ctkCollapsibleButton()
parametersCollapsibleButton.text = "Input Images"
- self.layout().addWidget(parametersCollapsibleButton)
+ self.layout.addWidget(parametersCollapsibleButton)
# Layout within the dummy collapsible button
parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton)
- self._porosity_log_input = hierarchyVolumeInput(onChange=lambda i: self.__on_porosity_table_changed(i))
- self._porosity_log_input.setNodeTypes(["vtkMRMLTableNode"])
- self._porosity_log_input.objectName = "Well Logs Input"
-
- reset_style_on_valid_node(self._porosity_log_input)
- self._porosity_log_combo_box = qt.QComboBox()
- self._porosity_log_combo_box.objectName = "Porosity Log Combo Box"
- reset_style_on_valid_node(self._porosity_log_combo_box)
- self.segmented_image_input = hierarchyVolumeInput(onChange=self.onSegmentedImageSelected)
- self.segmented_image_input.setNodeTypes(["vtkMRMLSegmentationNode", "vtkMRMLLabelMapVolumeNode"])
- self.segmented_image_input.objectName = "Segmented Image Input"
- reset_style_on_valid_node(self.segmented_image_input)
+ self._porosityLogInput = hierarchyVolumeInput(
+ onChange=self.__onPorosityTableChanged, nodeTypes=["vtkMRMLTableNode"]
+ )
+ self._porosityLogInput.objectName = "Well Logs Input"
+
+ reset_style_on_valid_node(self._porosityLogInput)
+ self._porosityLogComboBox = qt.QComboBox()
+ self._porosityLogComboBox.objectName = "Porosity Log Combo Box"
+ reset_style_on_valid_node(self._porosityLogComboBox)
+ self.segmentedImageInput = hierarchyVolumeInput(
+ onChange=self.onSegmentedImageSelected, nodeTypes=["vtkMRMLSegmentationNode", "vtkMRMLLabelMapVolumeNode"]
+ )
+ self.segmentedImageInput.objectName = "Segmented Image Input"
+ reset_style_on_valid_node(self.segmentedImageInput)
- parametersFormLayout.addRow("Well logs (.las):", self._porosity_log_input)
- parametersFormLayout.addRow("Porosity Log:", self._porosity_log_combo_box)
- parametersFormLayout.addRow("Segmented Image:", self.segmented_image_input)
+ parametersFormLayout.addRow("Well logs (.las):", self._porosityLogInput)
+ parametersFormLayout.addRow("Porosity Log:", self._porosityLogComboBox)
+ parametersFormLayout.addRow("Segmented Image:", self.segmentedImageInput)
- def __on_porosity_table_changed(self, node_id):
+ def __onPorosityTableChanged(self, node_id):
node = self.subjectHierarchyNode.GetItemDataNode(node_id)
if node is None:
@@ -168,18 +215,20 @@ def __on_porosity_table_changed(self, node_id):
"The selected table doesn't have a column related to the 'Depth' data."
"\nPlease select another table or load a log file."
)
- slicer.util.errorDisplay(message, windowTitle="Error", parent=self.__mainWindow)
+ slicer.util.errorDisplay(message, windowTitle="Error", parent=slicer.modules.AppContextInstance.mainWindow)
return
logs.remove("DEPTH")
# populate porosity log combo box
- self._porosity_log_combo_box.clear()
- self._porosity_log_combo_box.addItems(logs)
+ self._porosityLogComboBox.clear()
+ self._porosityLogComboBox.addItems(logs)
+
+ self.__checkUniqueErrorCorrectionNode()
def paramsSection(self):
parametersCollapsibleButton = ctk.ctkCollapsibleButton()
parametersCollapsibleButton.text = "Parameters"
- self.layout().addWidget(parametersCollapsibleButton)
+ self.layout.addWidget(parametersCollapsibleButton)
parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton)
@@ -199,26 +248,26 @@ def paramsSection(self):
def measurementSection(self):
parametersCollapsibleButton = ctk.ctkCollapsibleButton()
parametersCollapsibleButton.text = "Reference Permeability"
- self.layout().addWidget(parametersCollapsibleButton)
+ self.layout.addWidget(parametersCollapsibleButton)
# Layout within the dummy collapsible button
parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton)
- self._plugs_permeability_table_combo_box = hierarchyVolumeInput(
- onChange=lambda i: self.__on_plugs_permeability_table_changed(i)
+ self._plugsPermeabilityTableComboBox = hierarchyVolumeInput(
+ onChange=lambda i: self.__onPlugsPermeabilityTableChanged(i),
+ nodeTypes=["vtkMRMLTableNode"],
)
- reset_style_on_valid_node(self._plugs_permeability_table_combo_box)
- self._plugs_permeability_table_combo_box.setNodeTypes(["vtkMRMLTableNode"])
- self._plugs_permeability_table_combo_box.objectName = "Plugs Measurements Input"
- self._plugs_permeability_log_combo_box = qt.QComboBox()
- self._plugs_permeability_log_combo_box.objectName = "Plugs Permeability Log Combo Box"
- parametersFormLayout.addRow("Plugs measurements:", self._plugs_permeability_table_combo_box)
- parametersFormLayout.addRow("Plugs Permeability Log:", self._plugs_permeability_log_combo_box)
+ reset_style_on_valid_node(self._plugsPermeabilityTableComboBox)
+ self._plugsPermeabilityTableComboBox.objectName = "Plugs Measurements Input"
+ self._plugsPermeabilityLogComboBox = qt.QComboBox()
+ self._plugsPermeabilityLogComboBox.objectName = "Plugs Permeability Log Combo Box"
+ parametersFormLayout.addRow("Plugs measurements:", self._plugsPermeabilityTableComboBox)
+ parametersFormLayout.addRow("Plugs Permeability Log:", self._plugsPermeabilityLogComboBox)
def kdsOptimizationSection(self):
parametersCollapsibleButton = ctk.ctkCollapsibleButton()
parametersCollapsibleButton.text = "Kds Optimization"
- self.layout().addWidget(parametersCollapsibleButton)
+ self.layout.addWidget(parametersCollapsibleButton)
# Layout within the dummy collapsible button
parametersFormLayout = qt.QVBoxLayout(parametersCollapsibleButton)
@@ -243,7 +292,7 @@ def storeKdsOptimizationTable(self, df):
node.Modified()
self.__kdsOptimizationTableId = node.GetID()
- def __on_plugs_permeability_table_changed(self, node_id):
+ def __onPlugsPermeabilityTableChanged(self, node_id):
node = self.subjectHierarchyNode.GetItemDataNode(node_id)
if node is None:
@@ -255,18 +304,18 @@ def __on_plugs_permeability_table_changed(self, node_id):
"The selected table doesn't have a column related to the 'Depth' data."
"\nPlease select another table or load a log file."
)
- slicer.util.errorDisplay(message, windowTitle="Error", parent=slicer.util.mainWindow())
+ slicer.util.errorDisplay(message, windowTitle="Error", parent=slicer.modules.AppContextInstance.mainWindow)
return
logs.remove("DEPTH")
# Populate porosity log combo box
- self._plugs_permeability_log_combo_box.clear()
- self._plugs_permeability_log_combo_box.addItems(logs)
+ self._plugsPermeabilityLogComboBox.clear()
+ self._plugsPermeabilityLogComboBox.addItems(logs)
def outputSection(self):
parametersCollapsibleButton = ctk.ctkCollapsibleButton()
parametersCollapsibleButton.text = "Output"
- self.layout().addWidget(parametersCollapsibleButton)
+ self.layout.addWidget(parametersCollapsibleButton)
self.outputNameLineEdit = qt.QLineEdit()
reset_style_on_valid_text(self.outputNameLineEdit)
@@ -284,20 +333,20 @@ def onPLUGPathChanged(self, p):
self.onTextChanged("file", p)
def onApply(self):
- if self._porosity_log_input.currentNode() is None:
- highlight_error(self._porosity_log_input)
+ if self._porosityLogInput.currentNode() is None:
+ highlight_error(self._porosityLogInput)
return
- if self._porosity_log_combo_box.currentText == "":
- highlight_error(self._porosity_log_combo_box)
+ if self._porosityLogComboBox.currentText == "":
+ highlight_error(self._porosityLogComboBox)
return
- if self.segmented_image_input.currentNode() is None:
- highlight_error(self.segmented_image_input)
+ if self.segmentedImageInput.currentNode() is None:
+ highlight_error(self.segmentedImageInput)
return
- if self._plugs_permeability_table_combo_box.currentNode() is None:
- highlight_error(self._plugs_permeability_table_combo_box)
+ if self._plugsPermeabilityTableComboBox.currentNode() is None:
+ highlight_error(self._plugsPermeabilityTableComboBox)
return
if self.outputNameLineEdit.text.strip() == "":
@@ -310,64 +359,60 @@ def onApply(self):
kdsOptimizationTable = helpers.tryGetNode(ERROR_CORRECTION_NODE_NAME)
- porosity_log_scalar_node = helpers.createTemporaryVolumeNode(
+ porosityLogScalarNode = helpers.createTemporaryVolumeNode(
slicer.vtkMRMLScalarVolumeNode, "POROSITY_LOG_TMP_NODE", hidden=True
)
- porosity_depth_scalar_node = helpers.createTemporaryVolumeNode(
+ porosityDepthScalarNode = helpers.createTemporaryVolumeNode(
slicer.vtkMRMLScalarVolumeNode, "POROSITY_DEPTH_TMP_NODE", hidden=True
)
- permeability_plug_log_scalar_node = helpers.createTemporaryVolumeNode(
+ permeabilityPlugLogScalarNode = helpers.createTemporaryVolumeNode(
slicer.vtkMRMLScalarVolumeNode, "PERMEABILITY_PLUG_TMP_NODE", hidden=True
)
- permeability_plug_depth_scalar_node = helpers.createTemporaryVolumeNode(
+ permeabilityPlugDepthScalarNode = helpers.createTemporaryVolumeNode(
slicer.vtkMRMLScalarVolumeNode, "PERMEABILITY_PLUG_DEPTH_TMP_NODE", hidden=True
)
- permeability_output = slicer.mrmlScene.AddNewNodeByClass(
+ permeabilityOutput = slicer.mrmlScene.AddNewNodeByClass(
slicer.vtkMRMLTableNode.__name__, self.logic.model["outputVolumeName"]
)
- permeability_output.SetAttribute(ImageLogDataSelectable.name(), ImageLogDataSelectable.TRUE.value)
+ permeabilityOutput.SetAttribute(ImageLogDataSelectable.name(), ImageLogDataSelectable.TRUE.value)
- porosity_log_table_node = self.subjectHierarchyNode.GetItemDataNode(self._porosity_log_input.currentItem())
- porosity_log_array = vtk_to_numpy(
- porosity_log_table_node.GetTable().GetColumnByName(self._porosity_log_combo_box.currentText)
+ porosityLogTableNode = self.subjectHierarchyNode.GetItemDataNode(self._porosityLogInput.currentItem())
+ porosityLogArray = vtk_to_numpy(
+ porosityLogTableNode.GetTable().GetColumnByName(self._porosityLogComboBox.currentText)
)
- porosity_depth_array = vtk_to_numpy(porosity_log_table_node.GetTable().GetColumnByName("DEPTH"))
+ porosityDepthArray = vtk_to_numpy(porosityLogTableNode.GetTable().GetColumnByName("DEPTH"))
- permeability_plug_log_table_node = self.subjectHierarchyNode.GetItemDataNode(
- self._plugs_permeability_table_combo_box.currentItem()
- )
- permeability_plug_log_array = vtk_to_numpy(
- permeability_plug_log_table_node.GetTable().GetColumnByName(
- self._plugs_permeability_log_combo_box.currentText
- )
- )
- permeability_plug_depth_array = vtk_to_numpy(
- permeability_plug_log_table_node.GetTable().GetColumnByName("DEPTH")
+ permeabilityPlugLogTableNode = self.subjectHierarchyNode.GetItemDataNode(
+ self._plugsPermeabilityTableComboBox.currentItem()
)
-
- porosity_log_array = porosity_log_array.reshape(porosity_log_array.shape[0], 1, 1)
- porosity_depth_array = porosity_depth_array.reshape(porosity_depth_array.shape[0], 1, 1)
- permeability_plug_log_array = permeability_plug_log_array.reshape(permeability_plug_log_array.shape[0], 1, 1)
- permeability_plug_depth_array = permeability_plug_depth_array.reshape(
- permeability_plug_depth_array.shape[0], 1, 1
+ permeabilityPlugLogArray = vtk_to_numpy(
+ permeabilityPlugLogTableNode.GetTable().GetColumnByName(self._plugsPermeabilityLogComboBox.currentText)
)
-
- slicer.util.updateVolumeFromArray(porosity_log_scalar_node, porosity_log_array)
- slicer.util.updateVolumeFromArray(porosity_depth_scalar_node, porosity_depth_array)
- slicer.util.updateVolumeFromArray(permeability_plug_log_scalar_node, permeability_plug_log_array)
- slicer.util.updateVolumeFromArray(permeability_plug_depth_scalar_node, permeability_plug_depth_array)
-
- self.logic.model["log_por"] = porosity_log_scalar_node.GetID()
- self.logic.model["depth_por"] = porosity_depth_scalar_node.GetID()
- self.logic.model["perm_plugs"] = permeability_plug_log_scalar_node.GetID()
- self.logic.model["depth_plugs"] = permeability_plug_depth_scalar_node.GetID()
- self.logic.model["outputVolume"] = permeability_output.GetID()
+ permeabilityPlugDepthArray = vtk_to_numpy(permeabilityPlugLogTableNode.GetTable().GetColumnByName("DEPTH"))
+
+ porosityLogArray = porosityLogArray.reshape(porosityLogArray.shape[0], 1, 1)
+ porosityDepthArray = porosityDepthArray.reshape(porosityDepthArray.shape[0], 1, 1)
+ permeabilityPlugLogArray = permeabilityPlugLogArray.reshape(permeabilityPlugLogArray.shape[0], 1, 1)
+ permeabilityPlugDepthArray = permeabilityPlugDepthArray.reshape(permeabilityPlugDepthArray.shape[0], 1, 1)
+
+ slicer.util.updateVolumeFromArray(porosityLogScalarNode, porosityLogArray)
+ slicer.util.updateVolumeFromArray(porosityDepthScalarNode, porosityDepthArray)
+ slicer.util.updateVolumeFromArray(permeabilityPlugLogScalarNode, permeabilityPlugLogArray)
+ slicer.util.updateVolumeFromArray(permeabilityPlugDepthScalarNode, permeabilityPlugDepthArray)
+
+ self.logic.model["log_por"] = porosityLogScalarNode.GetID()
+ self.logic.model["depth_por"] = porosityDepthScalarNode.GetID()
+ self.logic.model["perm_plugs"] = permeabilityPlugLogScalarNode.GetID()
+ self.logic.model["depth_plugs"] = permeabilityPlugDepthScalarNode.GetID()
+ self.logic.model["outputVolume"] = permeabilityOutput.GetID()
self.logic.model["kdsOptimizationTable"] = (
kdsOptimizationTable.GetID() if kdsOptimizationTable is not None else None
)
self.logic.model["kdsOptimizationWeight"] = self.kdsOptimizationWidget.weightSpinBox.value
- self.logic.run()
+ task = self.logic.run()
+ if task:
+ self.progressBar.setCommandLineModuleNode(task)
def onCancel(self):
self.logic.cancelCLI()
@@ -383,20 +428,22 @@ def onSegmentedImageSelected(self, itemId):
self.logic.model["inputVolume1"] = None
self.modelSelector.enabled = False
self.missingSelector.enabled = False
+ self.__checkUniqueErrorCorrectionNode()
return
self.modelSelector.clear()
self.missingSelector.clear()
- segments_dict = helpers.extractLabels(volumeNode)
+ segmentsDict = helpers.extractLabels(volumeNode)
for selector in [self.modelSelector, self.missingSelector]:
- selector.addItems(list(segments_dict.values()))
+ selector.addItems(list(segmentsDict.values()))
self.logic.model["inputVolume1"] = volumeNode.GetID()
self.outputNameLineEdit.setText(f"{volumeNode.GetName()}_Permeability_Output")
self.modelSelector.enabled = True
self.missingSelector.enabled = True
+ self.__checkUniqueErrorCorrectionNode()
def onNumericChanged(self, key, value):
self.logic.model[key] = value
@@ -405,21 +452,21 @@ def onTextChanged(self, key, value):
self.logic.model[key] = str(value)
def onComplete(self):
- self.remove_temporary_nodes()
+ self.removeTemporaryNodes()
- output_node_id = self.logic.model["outputVolume"]
+ outputNodeId = self.logic.model["outputVolume"]
self.onOutputNodeReady([self.logic.model["outputVolume"]])
- output_node = helpers.tryGetNode(output_node_id)
- helpers.autoDetectColumnType(output_node)
+ outputNode = helpers.tryGetNode(outputNodeId)
+ helpers.autoDetectColumnType(outputNode)
- def onCompletedWithErrors(self, *args, **kwargs):
- self.remove_temporary_nodes()
+ def onCompletedWithErrors(self):
+ self.removeTemporaryNodes()
def onCancelled(self):
- self.remove_temporary_nodes()
+ self.removeTemporaryNodes()
- def remove_temporary_nodes(self):
+ def removeTemporaryNodes(self):
temp_nodes_name = [
"POROSITY_LOG_TMP_NODE",
"POROSITY_DEPTH_TMP_NODE",
@@ -434,11 +481,13 @@ def remove_temporary_nodes(self):
slicer.mrmlScene.RemoveNode(node)
-class PermeabilityModelingLogic(object):
- def __init__(self, widget):
+class PermeabilityModelingLogic(LTracePluginLogic):
+ processFinished = qt.Signal(object)
+
+ def __init__(self, parent):
+ LTracePluginLogic.__init__(self, parent)
self.model = PermeabilityModelingModel()
self.cliNode = None
- self.widget = widget
def cancelCLI(self):
if self.cliNode:
@@ -452,17 +501,17 @@ def eventHandler(self, caller, event):
status = caller.GetStatusString()
try:
if status == "Completed":
- self.widget.onComplete()
self.cliNode = None
+ self.processFinished.emit(status)
elif "Completed" in status:
- self.widget.onCompletedWithErrors()
self.cliNode = None
+ self.processFinished.emit(status)
elif status == "Cancelled":
- self.widget.onCancelled()
self.cliNode = None
+ self.processFinished.emit(status)
except Exception as e:
logging.info(f'Exception on Event Handler: {repr(e)} with status "{status}"')
- self.widget.onCompletedWithErrors()
+ self.processFinished.emit("Completed with Errors")
def run(self):
self.cliNode = slicer.cli.run(
@@ -473,7 +522,6 @@ def run(self):
)
self.cliNode.AddObserver("ModifiedEvent", lambda c, e: self.eventHandler(c, e))
- self.widget.progressBar.setCommandLineModuleNode(self.cliNode)
return self.cliNode
diff --git a/src/modules/ImageLogEnv/PermeabilityModelingCLI/PermeabilityModelingCLI.py b/src/modules/PermeabilityModeling/PermeabilityModelingCLI/PermeabilityModelingCLI.py
similarity index 99%
rename from src/modules/ImageLogEnv/PermeabilityModelingCLI/PermeabilityModelingCLI.py
rename to src/modules/PermeabilityModeling/PermeabilityModelingCLI/PermeabilityModelingCLI.py
index 1729d56..08b8eae 100644
--- a/src/modules/ImageLogEnv/PermeabilityModelingCLI/PermeabilityModelingCLI.py
+++ b/src/modules/PermeabilityModeling/PermeabilityModelingCLI/PermeabilityModelingCLI.py
@@ -255,7 +255,7 @@ def dataframeFromTable(tableNode):
# Write output data
data = np.column_stack((depth_image_array * 1000, output))
output_df = pd.DataFrame(data, columns=["DEPTH", "PERMEABILITY"])
- writeToTable(output_df, outputNodeID, na_rep="nan")
+ writeToTable(output_df.round(decimals=5), outputNodeID, na_rep="nan")
progressUpdate(value=1)
diff --git a/src/modules/ImageLogEnv/PermeabilityModelingCLI/PermeabilityModelingCLI.xml b/src/modules/PermeabilityModeling/PermeabilityModelingCLI/PermeabilityModelingCLI.xml
similarity index 100%
rename from src/modules/ImageLogEnv/PermeabilityModelingCLI/PermeabilityModelingCLI.xml
rename to src/modules/PermeabilityModeling/PermeabilityModelingCLI/PermeabilityModelingCLI.xml
diff --git a/src/modules/ImageLogEnv/PermeabilityModelingCLI/PermeabilityModelingLib/__init__.py b/src/modules/PermeabilityModeling/PermeabilityModelingCLI/PermeabilityModelingLib/__init__.py
similarity index 100%
rename from src/modules/ImageLogEnv/PermeabilityModelingCLI/PermeabilityModelingLib/__init__.py
rename to src/modules/PermeabilityModeling/PermeabilityModelingCLI/PermeabilityModelingLib/__init__.py
diff --git a/src/modules/ImageLogEnv/PermeabilityModelingCLI/PermeabilityModelingLib/auxiliar_functions.py b/src/modules/PermeabilityModeling/PermeabilityModelingCLI/PermeabilityModelingLib/auxiliar_functions.py
similarity index 100%
rename from src/modules/ImageLogEnv/PermeabilityModelingCLI/PermeabilityModelingLib/auxiliar_functions.py
rename to src/modules/PermeabilityModeling/PermeabilityModelingCLI/PermeabilityModelingLib/auxiliar_functions.py
diff --git a/src/modules/ImageLogEnv/ImageLogsLib/PermeabilityModeling.md b/src/modules/PermeabilityModeling/README.md
similarity index 100%
rename from src/modules/ImageLogEnv/ImageLogsLib/PermeabilityModeling.md
rename to src/modules/PermeabilityModeling/README.md
diff --git a/src/modules/PermeabilityModeling/Resources/Icons/PermeabilityModeling.svg b/src/modules/PermeabilityModeling/Resources/Icons/PermeabilityModeling.svg
new file mode 100644
index 0000000..e462286
--- /dev/null
+++ b/src/modules/PermeabilityModeling/Resources/Icons/PermeabilityModeling.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/modules/PolynomialShadingCorrection/PolynomialShadingCorrection.py b/src/modules/PolynomialShadingCorrection/PolynomialShadingCorrection.py
index e554d9c..6d62370 100644
--- a/src/modules/PolynomialShadingCorrection/PolynomialShadingCorrection.py
+++ b/src/modules/PolynomialShadingCorrection/PolynomialShadingCorrection.py
@@ -41,7 +41,7 @@ class PolynomialShadingCorrection(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
self.parent.title = "Shading correction - Polynomial"
- self.parent.categories = ["LTrace Tools"]
+ self.parent.categories = ["Tools", "MicroCT", "Multiscale"]
self.parent.dependencies = []
self.parent.contributors = ["LTrace Geophysical Solutions"]
self.parent.helpText = PolynomialShadingCorrection.help()
diff --git a/src/modules/PoreNetworkCompare/PoreNetworkCompare.py b/src/modules/PoreNetworkCompare/PoreNetworkCompare.py
index e383614..372762a 100644
--- a/src/modules/PoreNetworkCompare/PoreNetworkCompare.py
+++ b/src/modules/PoreNetworkCompare/PoreNetworkCompare.py
@@ -15,12 +15,12 @@
from scipy.ndimage import zoom
from scipy.spatial import distance
-from Plots.Crossplot.data_plot_widget import DataPlotWidget
from ltrace.pore_networks.visualization_model import PORE_TYPE, TUBE_TYPE
from ltrace.slicer.graph_data import DataFrameGraphData
-from ltrace.slicer.helpers import highlight_error, reset_style_on_valid_text
+from ltrace.slicer.helpers import highlight_error, reset_style_on_valid_text, LazyLoad
from ltrace.slicer.ui import hierarchyVolumeInput
from ltrace.slicer_utils import LTracePlugin, LTracePluginWidget, LTracePluginLogic
+from ltrace.slicer.widget.data_plot_widget import DataPlotWidget
from ltrace.transforms import transformPoints
NUMBER_OF_VIEWS = 2
@@ -41,8 +41,8 @@ class PoreNetworkCompare(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
- self.parent.title = "Pore Network Compare"
- self.parent.categories = ["Micro CT"]
+ self.parent.title = "PNM Compare Models"
+ self.parent.categories = ["MicroCT"]
self.parent.dependencies = []
self.parent.contributors = ["LTrace Geophysical Solutions"]
self.parent.helpText = PoreNetworkCompare.help()
diff --git a/src/modules/PoreNetworkCompare/Resources/Icons/PoreNetworkCompare.png b/src/modules/PoreNetworkCompare/Resources/Icons/PoreNetworkCompare.png
deleted file mode 100644
index b89476c..0000000
Binary files a/src/modules/PoreNetworkCompare/Resources/Icons/PoreNetworkCompare.png and /dev/null differ
diff --git a/src/modules/PoreNetworkCompare/Resources/Icons/PoreNetworkCompare.svg b/src/modules/PoreNetworkCompare/Resources/Icons/PoreNetworkCompare.svg
new file mode 100644
index 0000000..227dc95
--- /dev/null
+++ b/src/modules/PoreNetworkCompare/Resources/Icons/PoreNetworkCompare.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/modules/PoreNetworkExtractor/PoreNetworkExtractor.py b/src/modules/PoreNetworkExtractor/PoreNetworkExtractor.py
index 4219cf4..1b014a6 100644
--- a/src/modules/PoreNetworkExtractor/PoreNetworkExtractor.py
+++ b/src/modules/PoreNetworkExtractor/PoreNetworkExtractor.py
@@ -1,34 +1,28 @@
-import csv
+import json
import logging
import os
-import subprocess
-import time
-import traceback
+import shutil
from pathlib import Path
from typing import Tuple, Union
import ctk
-import numpy as np
import pandas as pd
import qt
import slicer
import slicer.util
import vtk
-import json
-import shutil
import ltrace.pore_networks.functions as pn
-from ltrace.image import optimized_transforms
from ltrace.slicer import ui
+from ltrace.slicer.widget.global_progress_bar import LocalProgressBar
from ltrace.slicer_utils import (
LTracePlugin,
LTracePluginWidget,
LTracePluginLogic,
slicer_is_in_developer_mode,
dataFrameToTableNode,
+ getResourcePath,
)
-from ltrace.slicer.widget.global_progress_bar import LocalProgressBar
-from ltrace.utils.ProgressBarProc import ProgressBarProc
try:
from Test.PoreNetworkExtractorTest import PoreNetworkExtractorTest
@@ -49,11 +43,11 @@ class PoreNetworkExtractor(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
- self.parent.title = "PoreNetworkExtractor"
- self.parent.categories = ["Micro CT"]
+ self.parent.title = "PNM Extraction"
+ self.parent.categories = ["MicroCT"]
self.parent.dependencies = []
self.parent.contributors = ["LTrace Geophysics Team"]
- self.parent.helpText = PoreNetworkExtractor.help()
+ self.parent.helpText = f"file:///{(getResourcePath('manual') / 'Modules/PNM/PNExtraction.html').as_posix()}"
self.parent.acknowledgementText = ""
@classmethod
diff --git a/src/modules/PoreNetworkExtractor/Resources/Icons/PoreNetworkExtractor.png b/src/modules/PoreNetworkExtractor/Resources/Icons/PoreNetworkExtractor.png
deleted file mode 100644
index b89476c..0000000
Binary files a/src/modules/PoreNetworkExtractor/Resources/Icons/PoreNetworkExtractor.png and /dev/null differ
diff --git a/src/modules/PoreNetworkExtractor/Resources/Icons/PoreNetworkExtractor.svg b/src/modules/PoreNetworkExtractor/Resources/Icons/PoreNetworkExtractor.svg
new file mode 100644
index 0000000..227dc95
--- /dev/null
+++ b/src/modules/PoreNetworkExtractor/Resources/Icons/PoreNetworkExtractor.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/modules/PoreNetworkKrelEda/PoreNetworkKrelEda.py b/src/modules/PoreNetworkKrelEda/PoreNetworkKrelEda.py
index 1a2fb5e..e13ed8e 100644
--- a/src/modules/PoreNetworkKrelEda/PoreNetworkKrelEda.py
+++ b/src/modules/PoreNetworkKrelEda/PoreNetworkKrelEda.py
@@ -19,11 +19,13 @@
dataframeFromTable,
dataFrameToTableNode,
slicer_is_in_developer_mode,
+ getResourcePath,
)
from ltrace.slicer.widget.customized_pyqtgraph.GraphicsLayoutWidget import GraphicsLayoutWidget
from PoreNetworkKrelEdaLib.export.PoreNetworkKrelEdaExport import PoreNetworkKrelEdaExportWidget
from PoreNetworkKrelEdaLib.visualization_widgets.crossed_plots import CrossedError, CrossedParameters
from PoreNetworkKrelEdaLib.visualization_widgets.curves_plot import CurvesPlot
+from PoreNetworkKrelEdaLib.visualization_widgets.ca_distribution_plot import CaDistributionPlot
from PoreNetworkKrelEdaLib.visualization_widgets.heatmap_plots import (
ParameterErrorCorrelation,
ParameterResultCorrelation,
@@ -52,10 +54,10 @@ class PoreNetworkKrelEda(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
- self.parent.title = "Krel EDA"
- self.parent.categories = ["LTrace Tools"]
+ self.parent.title = "PNM Krel EDA"
+ self.parent.categories = ["Tools", "MicroCT"]
self.parent.contributors = ["LTrace Geophysics Team"]
- self.parent.helpText = PoreNetworkKrelEda.help()
+ self.parent.helpText = f"file:///{(getResourcePath('manual') / 'Modules/PNM/krelEDA.html').as_posix()}"
@classmethod
def readme_path(cls):
@@ -116,6 +118,7 @@ def setup_eda(self):
self.__visualizationTypeSelector.addWidget(SecondOrderInteraction(data_manager=self.data_manager))
self.__visualizationTypeSelector.addWidget(SecondOrderInteractions(data_manager=self.data_manager))
self.__visualizationTypeSelector.addWidget(ThirdOrderInteractions(data_manager=self.data_manager))
+ self.__visualizationTypeSelector.addWidget(CaDistributionPlot(data_manager=self.data_manager))
self.__visualizationTypeSelector.currentWidgetChanged.connect(self.__update_current_plot)
self.__visualizationTypeSelector.objectName = "Visualization selector"
self.__clear_plots()
@@ -207,7 +210,12 @@ def __on_input_node_changed(self, vtkid=None):
def __update_current_plot(self, vtkid=None):
currentWidget = self.__visualizationTypeSelector.currentWidget()
- if self.data_manager.is_valid():
+
+ curvesWidget = self.__visualizationTypeSelector.widget(0)
+ number_of_simulations = self.data_manager.get_number_of_simulations()
+ invalid_combination = currentWidget != curvesWidget and number_of_simulations == 1
+
+ if self.data_manager.is_valid() and not invalid_combination:
currentWidget.setVisible(True)
currentWidget.update()
else:
diff --git a/src/modules/PoreNetworkKrelEda/PoreNetworkKrelEdaLib/visualization_widgets/ca_distribution_plot.py b/src/modules/PoreNetworkKrelEda/PoreNetworkKrelEdaLib/visualization_widgets/ca_distribution_plot.py
new file mode 100644
index 0000000..96ed96f
--- /dev/null
+++ b/src/modules/PoreNetworkKrelEda/PoreNetworkKrelEdaLib/visualization_widgets/ca_distribution_plot.py
@@ -0,0 +1,119 @@
+import numpy as np
+import pyqtgraph as pg
+import PySide2
+import qt
+import re
+import shiboken2
+import slicer
+
+from ltrace.slicer import helpers
+from ltrace.slicer_utils import dataframeFromTable
+from ltrace.slicer.widget.customized_pyqtgraph.GraphicsLayoutWidget import GraphicsLayoutWidget
+from PoreNetworkKrelEdaLib.visualization_widgets.plot_base import PlotBase
+
+
+class CaDistributionPlot(PlotBase):
+ DISPLAY_NAME = "CA distribution plot"
+ METHOD = "plot9"
+
+ def __init__(self, *args, **kwargs):
+ super().__init__()
+
+ self.data_manager = kwargs["data_manager"]
+
+ self.graphics_layout_widget = GraphicsLayoutWidget()
+ self.graphics_layout_widget.setBackground("w")
+ self.graphics_layout_widget.setFixedHeight(360)
+
+ x_legend_label_item = pg.LabelItem(angle=0)
+ y_legend_label_item = pg.LabelItem(angle=270)
+ x_legend_label_item.setText("Contact angle (degree)", color="k")
+ y_legend_label_item.setText("Bins", color="k")
+ self.graphics_layout_widget.addItem(x_legend_label_item, row=2, col=2, colspan=2)
+ self.graphics_layout_widget.addItem(y_legend_label_item, row=0, col=1, rowspan=2)
+
+ self.plot_item = self.graphics_layout_widget.addPlot()
+ self.plot_item.addLegend()
+
+ self.simulationCombobox = qt.QComboBox()
+ self.simulationCombobox.addItem(0)
+ self.simulationCombobox.currentTextChanged.connect(self.__update_histograms)
+
+ self.phaseCombobox = qt.QComboBox()
+ self.phaseCombobox.addItem("Drainage")
+ self.phaseCombobox.addItem("Imbibition")
+ self.phaseCombobox.currentTextChanged.connect(self.__update_histograms)
+
+ formLayout = qt.QFormLayout()
+ formLayout.addRow("Simulation:", self.simulationCombobox)
+ formLayout.addRow("Phase:", self.phaseCombobox)
+
+ pySideMainLayout = shiboken2.wrapInstance(hash(formLayout), PySide2.QtWidgets.QFormLayout)
+ pySideMainLayout.addRow(self.graphics_layout_widget)
+
+ frameLayout = qt.QVBoxLayout()
+ frameLayout.addLayout(formLayout)
+ frameLayout.addStretch()
+
+ mainFrame = qt.QFrame()
+ mainFrame.setLayout(frameLayout)
+
+ mainLayout = qt.QVBoxLayout()
+ mainLayout.addWidget(mainFrame)
+ mainLayout.addStretch()
+ self.setLayout(mainLayout)
+
+ def clear_saved_plots(self):
+ self.plot_item.clear()
+
+ def update(self):
+ inputNode = self.data_manager.input_node
+ if inputNode is None:
+ return
+
+ self.simulationCombobox.clear()
+
+ regex = re.compile("ca_distribution_(\\d+)_id")
+ for name in inputNode.GetAttributeNames():
+ matches = regex.match(name)
+ if matches:
+ self.simulationCombobox.addItem(matches[1])
+
+ self.__update_histograms()
+
+ def __update_histograms(self):
+ self.clear_saved_plots()
+ self.__update_plots()
+
+ def __update_plots(self):
+ inputNode = self.data_manager.input_node
+ if inputNode is None:
+ return
+
+ tableNode = None
+ regex = re.compile("ca_distribution_(\\d+)_id")
+ for name in inputNode.GetAttributeNames():
+ matches = regex.match(name)
+ if matches and matches[1] == self.simulationCombobox.currentText:
+ tableNode = helpers.tryGetNode(inputNode.GetAttribute(name))
+ break
+
+ if tableNode is None:
+ return
+
+ df = dataframeFromTable(tableNode)
+
+ if self.phaseCombobox.currentText == "Drainage":
+ preffix = "drainage"
+ else:
+ preffix = "imbibition"
+
+ hist, edges = np.histogram(df[f"{preffix}-advancing"], bins=20)
+ self.plot_item.plot(
+ edges, hist, name="advancing", stepMode=True, fillLevel=0, brush=(0, 0, 255, 80), pen=(0, 0, 0)
+ )
+
+ hist, edges = np.histogram(df[f"{preffix}-receding"], bins=20)
+ self.plot_item.plot(
+ edges, hist, name="receding", stepMode=True, fillLevel=0, brush=(255, 0, 0, 80), pen=(0, 0, 0)
+ )
diff --git a/src/modules/PoreNetworkKrelEda/PoreNetworkKrelEdaLib/visualization_widgets/crossed_plots.py b/src/modules/PoreNetworkKrelEda/PoreNetworkKrelEdaLib/visualization_widgets/crossed_plots.py
index f34d8b7..b0c4f5e 100644
--- a/src/modules/PoreNetworkKrelEda/PoreNetworkKrelEdaLib/visualization_widgets/crossed_plots.py
+++ b/src/modules/PoreNetworkKrelEda/PoreNetworkKrelEdaLib/visualization_widgets/crossed_plots.py
@@ -4,7 +4,7 @@
import shiboken2
from ltrace.slicer.graph_data import DataFrameGraphData
-from Plots.Crossplot.data_plot_widget import DataPlotWidget
+from ltrace.slicer.widget.data_plot_widget import DataPlotWidget
from PoreNetworkKrelEdaLib.visualization_widgets.plot_base import PlotBase
@@ -31,8 +31,14 @@ def __init__(self, *args, **kwargs):
self.dataPlotWidget = DataPlotWidget()
self.dataPlotWidget.set_theme("Light")
+
+ self.spacerWidget = pyside.QtWidgets.QSpacerItem(
+ 0, 0, pyside.QtWidgets.QSizePolicy.Expanding, pyside.QtWidgets.QSizePolicy.Expanding
+ )
+
pySideMainLayout = shiboken2.wrapInstance(hash(self.mainLayout), pyside.QtWidgets.QFormLayout)
pySideMainLayout.addRow(self.dataPlotWidget.widget)
+ pySideMainLayout.addItem(self.spacerWidget)
def update(self):
self.xAxisComboBox.blockSignals(True)
@@ -101,8 +107,14 @@ def __init__(self, *args, **kwargs):
self.dataPlotWidget = DataPlotWidget()
self.dataPlotWidget.set_theme("Light")
+
+ self.spacerWidget = pyside.QtWidgets.QSpacerItem(
+ 0, 0, pyside.QtWidgets.QSizePolicy.Expanding, pyside.QtWidgets.QSizePolicy.Expanding
+ )
+
pySideMainLayout = shiboken2.wrapInstance(hash(self.mainLayout), pyside.QtWidgets.QFormLayout)
pySideMainLayout.addRow(self.dataPlotWidget.widget)
+ pySideMainLayout.addItem(self.spacerWidget)
def update(self):
self.xAxisComboBox.blockSignals(True)
diff --git a/src/modules/PoreNetworkKrelEda/PoreNetworkKrelEdaLib/visualization_widgets/curves_plot.py b/src/modules/PoreNetworkKrelEda/PoreNetworkKrelEdaLib/visualization_widgets/curves_plot.py
index 71da9d6..f10d7b5 100644
--- a/src/modules/PoreNetworkKrelEda/PoreNetworkKrelEdaLib/visualization_widgets/curves_plot.py
+++ b/src/modules/PoreNetworkKrelEda/PoreNetworkKrelEdaLib/visualization_widgets/curves_plot.py
@@ -1,33 +1,32 @@
import numbers
-import re
+import PySide2
import ctk
import numpy as np
import pandas as pd
-import PySide2
+import pyqtgraph as pg
import qt
import shiboken2
-import pyqtgraph as pg
import slicer
+import warnings
+from PoreNetworkKrelEdaLib.input_estimation import (
+ closest_estimate,
+ CurveFilter,
+ ErrorToReferences,
+ filter_simulations,
+ regression_estimate,
+)
+from PoreNetworkKrelEdaLib.visualization_widgets.plot_base import PlotBase
+from PoreNetworkKrelEdaLib.visualization_widgets.plot_data import KrelResultCurves
-from Customizer import Customizer
from ltrace.pore_networks.krel_result import KrelParameterParser, RESULT_PREFIX
from ltrace.pore_networks.simulation_parameters_node import (
dataframe_to_parameter_node,
parameters_dict_to_dataframe,
)
from ltrace.slicer import ui, widgets
-from ltrace.slicer_utils import dataframeFromTable
from ltrace.slicer.widget.customized_pyqtgraph.GraphicsLayoutWidget import GraphicsLayoutWidget
-from PoreNetworkKrelEdaLib.visualization_widgets.plot_base import PlotBase
-from PoreNetworkKrelEdaLib.visualization_widgets.plot_data import KrelResultCurves
-from PoreNetworkKrelEdaLib.input_estimation import (
- closest_estimate,
- CurveFilter,
- ErrorToReferences,
- filter_simulations,
- regression_estimate,
-)
+from ltrace.slicer_utils import dataframeFromTable, getResourcePath
class CurvesPlot(PlotBase):
@@ -61,24 +60,22 @@ def __init__(self, *args, **kwargs):
self.mainLayout.addRow(" ", None)
- boxes_layout = qt.QGridLayout()
- boxes_layout.setColumnStretch(1, 1)
- boxes_layout.setColumnStretch(3, 1)
- boxes_layout.setColumnStretch(5, 1)
+ self.boxes_layout = qt.QGridLayout()
+ self.boxes_layout.setColumnStretch(1, 1)
+ self.boxes_layout.setColumnStretch(3, 1)
+ self.boxes_layout.setColumnStretch(5, 1)
self.checkboxes = {}
cycle_labels = ["Drainage", "Imbibition", "Second Drainage"]
for i, label in enumerate(cycle_labels):
for j, phase in enumerate(["Ko", "Kw"]):
name = f"{label} {phase}"
- boxes_layout.addWidget(qt.QLabel(name), j, i * 2)
- self.checkboxes[name] = qt.QCheckBox()
- boxes_layout.addWidget(self.checkboxes[name], j, i * 2 + 1)
- self.checkboxes[name].setChecked(False)
- self.checkboxes[name].objectName = name
- self.checkboxes[name].stateChanged.connect(self.update)
- self.mainLayout.addRow(boxes_layout)
+ self.__add_visibility_checkbox(name, i, j)
+ self.__add_visibility_checkbox("Mean", i + 1, 0)
+
+ self.mainLayout.addRow(self.boxes_layout)
self.checkboxes["Imbibition Ko"].setChecked(True)
self.checkboxes["Imbibition Kw"].setChecked(True)
+ self.checkboxes["Mean"].setChecked(True)
self.mainLayout.addRow(" ", None)
@@ -217,7 +214,8 @@ def generate_refcurve_color(simulation_id, simulation_type):
ref_plot.set_visible(cycle_id, KrelCurvesPlot.KRO, plot_kro)
filtered_simulation_id_list = self.__getSelectedSimulations(parameters_df)
- filtered_simulation_id_list += ["middle"]
+ if self.checkboxes["Mean"].isChecked():
+ filtered_simulation_id_list += ["middle"]
self.__krel_curves_plot.set_all_visible_simulations(filtered_simulation_id_list)
self.filtered_simulation_list = [x for x in filtered_simulation_id_list if isinstance(x, numbers.Number)]
@@ -239,6 +237,14 @@ def generate_refcurve_color(simulation_id, simulation_type):
self.__plot_item.autoRange()
+ def __add_visibility_checkbox(self, name, i, j):
+ self.boxes_layout.addWidget(qt.QLabel(name), j, i * 2)
+ self.checkboxes[name] = qt.QCheckBox()
+ self.boxes_layout.addWidget(self.checkboxes[name], j, i * 2 + 1)
+ self.checkboxes[name].setChecked(False)
+ self.checkboxes[name].objectName = name
+ self.checkboxes[name].stateChanged.connect(self.update)
+
def __update_hidden_ref_curves(self):
hidden_curve_nodes = self.filterListWidget.getHiddenCurveNodes()
for node, krel_curves_plot in self.__ref_curves_plots.items():
@@ -458,13 +464,13 @@ def __init__(self, data_manager, parent=None):
self.estimationMethodCombobox.setCurrentIndex(1)
addRefCurveButton = qt.QPushButton("Add reference curve")
- addRefCurveButton.setIcon(qt.QIcon(str(Customizer.ADD_ICON_PATH)))
+ addRefCurveButton.setIcon(qt.QIcon(getResourcePath("Icons") / "Add.png"))
addRefCurveButton.setIconSize(qt.QSize(16, 16))
addRefCurveButton.clicked.connect(self.__onAddRefCurveClicked)
addFilterButton = qt.QPushButton("Add filter")
addFilterButton.objectName = "Add filter button"
- addFilterButton.setIcon(qt.QIcon(str(Customizer.ADD_ICON_PATH)))
+ addFilterButton.setIcon(qt.QIcon(getResourcePath("Icons") / "Add.png"))
addFilterButton.setIconSize(qt.QSize(16, 16))
addFilterButton.clicked.connect(self.__onAddFilterClicked)
@@ -497,7 +503,7 @@ def getFilters(self):
if filter_name == "-":
continue
new_curve_filter = CurveFilter()
- new_curve_filter.column_name = f"{RESULT_PREFIX}{filter_name}"
+ new_curve_filter.column_name = filter_name
new_curve_filter.min_value = list_item_widget.widget.getMinValue()
new_curve_filter.max_value = list_item_widget.widget.getMaxValue()
filters.append(new_curve_filter)
@@ -535,9 +541,11 @@ def __getFilterList(self):
column_names = list(self.data_manager.parameters_df.columns)
parameter_parser = KrelParameterParser()
for column_name in column_names:
- parameter_name = parameter_parser.get_result_name(column_name)
+ parameter_name = parameter_parser.get_result_name(column_name) or parameter_parser.get_input_name(
+ column_name
+ )
if parameter_name is not None:
- filter_list.append(parameter_name)
+ filter_list.append(column_name)
return filter_list
def __onAddFilterClicked(self):
@@ -581,7 +589,7 @@ def __init__(self, parent=None):
super().__init__(parent)
self.removeFilterButton = qt.QPushButton()
- self.removeFilterButton.setIcon(qt.QIcon(str(Customizer.CANCEL_ICON_PATH)))
+ self.removeFilterButton.setIcon(qt.QIcon(getResourcePath("Icons") / "Cancel.png"))
self.removeFilterButton.setIconSize(qt.QSize(16, 16))
self.removeFilterButton.setFlat(True)
self.removeFilterButton.clicked.connect(self.__onRemoveButtonClicked)
@@ -688,11 +696,15 @@ def recalculate_middle(self, filtered_list):
kro_filtered = [cycle.kro_data[id] for id in filtered_list if id in cycle.kro_data]
if krw_filtered:
- krw_mean = np.nanmean(np.array(krw_filtered), axis=0)
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore", category=RuntimeWarning)
+ krw_mean = np.nanmean(np.array(krw_filtered), axis=0)
cycle.krw_data["middle"] = list(krw_mean)
if kro_filtered:
- kro_mean = np.nanmean(np.array(kro_filtered), axis=0)
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore", category=RuntimeWarning)
+ kro_mean = np.nanmean(np.array(kro_filtered), axis=0)
cycle.kro_data["middle"] = list(kro_mean)
def plot(self, color_callback=None):
@@ -705,7 +717,7 @@ def plot(self, color_callback=None):
number_of_simulations = krel_cycle_curves.get_number_of_simulations()
if number_of_simulations > 0:
- self.transparency = 128 + 128 // number_of_simulations
+ self.transparency = 128 + 64 // number_of_simulations
else:
self.transparency = 128
for simulation_id in range(number_of_simulations):
diff --git a/src/modules/PoreNetworkKrelEda/PoreNetworkKrelEdaLib/visualization_widgets/heatmap_plots.py b/src/modules/PoreNetworkKrelEda/PoreNetworkKrelEdaLib/visualization_widgets/heatmap_plots.py
index 47c64be..b6b3627 100644
--- a/src/modules/PoreNetworkKrelEda/PoreNetworkKrelEdaLib/visualization_widgets/heatmap_plots.py
+++ b/src/modules/PoreNetworkKrelEda/PoreNetworkKrelEdaLib/visualization_widgets/heatmap_plots.py
@@ -119,11 +119,11 @@ def update(self):
for i in anova.index:
if ":" not in i and i != "Intercept" and i != "Residual":
- anova_df.loc[i][i] = anova.loc[i]["PR(>F)"]
+ anova_df.loc[i, i] = anova.loc[i]["PR(>F)"]
elif i != "Intercept" and i != "Residual":
j, k = i.split(":")
- anova_df.loc[j][k] = anova.loc[i]["PR(>F)"]
- anova_df.loc[k][j] = anova.loc[i]["PR(>F)"]
+ anova_df.loc[j, k] = anova.loc[i]["PR(>F)"]
+ anova_df.loc[k, j] = anova.loc[i]["PR(>F)"]
for i in anova_df:
anova_df[i] = anova_df[i].astype(float)
diff --git a/src/modules/PoreNetworkKrelEda/PoreNetworkKrelEdaLib/visualization_widgets/table_plots.py b/src/modules/PoreNetworkKrelEda/PoreNetworkKrelEdaLib/visualization_widgets/table_plots.py
index 2f56762..562f8b5 100644
--- a/src/modules/PoreNetworkKrelEda/PoreNetworkKrelEdaLib/visualization_widgets/table_plots.py
+++ b/src/modules/PoreNetworkKrelEda/PoreNetworkKrelEdaLib/visualization_widgets/table_plots.py
@@ -33,7 +33,7 @@ def _to_html(self):
if i.count(":") < 1:
anova_p = anova_p.drop(i)
- anova_p.sort_values(inplace=True)
+ anova_p = anova_p.sort_values()
alpha = 0.05
gcolor = anova_p.copy()
@@ -85,7 +85,7 @@ def _to_html(self):
if i.count(":") < 2:
anova_p = anova_p.drop(i)
- anova_p.sort_values(inplace=True)
+ anova_p = anova_p.sort_values()
alpha = 0.05
gcolor = anova_p.copy()
diff --git a/src/modules/PoreNetworkKrelEda/Resources/Icons/PoreNetworkKrelEda.png b/src/modules/PoreNetworkKrelEda/Resources/Icons/PoreNetworkKrelEda.png
deleted file mode 100644
index b89476c..0000000
Binary files a/src/modules/PoreNetworkKrelEda/Resources/Icons/PoreNetworkKrelEda.png and /dev/null differ
diff --git a/src/modules/PoreNetworkKrelEda/Resources/Icons/PoreNetworkKrelEda.svg b/src/modules/PoreNetworkKrelEda/Resources/Icons/PoreNetworkKrelEda.svg
new file mode 100644
index 0000000..227dc95
--- /dev/null
+++ b/src/modules/PoreNetworkKrelEda/Resources/Icons/PoreNetworkKrelEda.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/modules/PoreNetworkProduction/PoreNetworkProduction.py b/src/modules/PoreNetworkProduction/PoreNetworkProduction.py
index 76a9dd2..813332f 100644
--- a/src/modules/PoreNetworkProduction/PoreNetworkProduction.py
+++ b/src/modules/PoreNetworkProduction/PoreNetworkProduction.py
@@ -20,7 +20,7 @@
from ltrace.slicer.helpers import highlight_error
from ltrace.slicer.ui import hierarchyVolumeInput
from ltrace.slicer.widget.customized_pyqtgraph.GraphicsLayoutWidget import GraphicsLayoutWidget
-from ltrace.slicer_utils import LTracePlugin, LTracePluginWidget, LTracePluginLogic, dataframeFromTable
+from ltrace.slicer_utils import LTracePlugin, LTracePluginWidget, LTracePluginLogic, dataframeFromTable, getResourcePath
from ltrace.slicer_utils import tableNodeToDict, dataFrameToTableNode
from ltrace.utils.ProgressBarProc import ProgressBarProc
@@ -40,11 +40,11 @@ class PoreNetworkProduction(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
- self.parent.title = "PoreNetworkProduction"
- self.parent.categories = ["Tutorials/Examples"]
+ self.parent.title = "PNM Production Prediction"
+ self.parent.categories = ["Tutorials/Examples", "MicroCT"]
self.parent.dependencies = []
self.parent.contributors = ["LTrace Geophysics Team"]
- self.parent.helpText = PoreNetworkProduction.help()
+ self.parent.helpText = f"file:///{(getResourcePath('manual') / 'Modules/PNM/production.html').as_posix()}"
self.parent.acknowledgementText = ""
@classmethod
@@ -235,6 +235,34 @@ def setup(self):
)
self.sensitivityPlotItem.addLegend()
visualizationPlotForm.addRow(self.SensitivityVisualizationGraphicsLayout)
+
+ self.pessimisticNpD = self.sensitivityPlotItem.plot(
+ name="Pessimistic NpD",
+ pen=pg.mkPen((200, 0, 0, 255), width=4),
+ symbol=None,
+ symbolPen=None,
+ symbolSize=None,
+ symbolBrush=None,
+ )
+
+ self.realisticNpD = self.sensitivityPlotItem.plot(
+ name="Realistic NpD",
+ pen=pg.mkPen((230, 100, 0, 255), width=4),
+ symbol=None,
+ symbolPen=None,
+ symbolSize=None,
+ symbolBrush=None,
+ )
+
+ self.optimisticNpD = self.sensitivityPlotItem.plot(
+ name="Optimistic NpD",
+ pen=pg.mkPen((0, 200, 0, 255), width=4),
+ symbol=None,
+ symbolPen=None,
+ symbolSize=None,
+ symbolBrush=None,
+ )
+
self.SensitivityVisualizationGraphicsLayout.hide()
#
@@ -369,7 +397,6 @@ def onChangeVisualization(self, i):
elif production_node.GetAttribute("table_type") == "production_sensitivity":
self.SensitivityVisualizationGraphicsLayout.show()
self.visualizationGraphicsLayout.hide()
- self.sensitivityPlotItem.clear()
self.sensitivityPlotItem.setXRange(0, 2)
self.sensitivityPlotItem.setYRange(0, 1)
@@ -394,41 +421,20 @@ def onChangeVisualization(self, i):
NpD_arrays_list.append(vtk.util.numpy_support.vtk_to_numpy(npd_points_vtk_array))
series_list[-1].setData(self.td_values, NpD_arrays_list[-1])
- self.pessimisticNpD = self.sensitivityPlotItem.plot(
- name="Pessimistic NpD",
- pen=pg.mkPen((200, 0, 0, 255), width=4),
- symbol=None,
- symbolPen=None,
- symbolSize=None,
- symbolBrush=None,
- )
npd_points_vtk_array = production_node.GetTable().GetColumnByName("pessimistic_NpD")
pessimistic_NpD_numpy = vtk.util.numpy_support.vtk_to_numpy(npd_points_vtk_array)
self.pessimisticNpD.setData(self.td_values, pessimistic_NpD_numpy)
+ self.pessimisticNpD.setZValue(1)
- self.realisticNpD = self.sensitivityPlotItem.plot(
- name="Realistic NpD",
- pen=pg.mkPen((230, 100, 0, 255), width=4),
- symbol=None,
- symbolPen=None,
- symbolSize=None,
- symbolBrush=None,
- )
npd_points_vtk_array = production_node.GetTable().GetColumnByName("realistic_NpD")
realistic_NpD_numpy = vtk.util.numpy_support.vtk_to_numpy(npd_points_vtk_array)
self.realisticNpD.setData(self.td_values, realistic_NpD_numpy)
+ self.realisticNpD.setZValue(1)
- self.optimisticNpD = self.sensitivityPlotItem.plot(
- name="Optimistic NpD",
- pen=pg.mkPen((0, 200, 0, 255), width=4),
- symbol=None,
- symbolPen=None,
- symbolSize=None,
- symbolBrush=None,
- )
npd_points_vtk_array = production_node.GetTable().GetColumnByName("optimistic_NpD")
optimistic_NpD_numpy = vtk.util.numpy_support.vtk_to_numpy(npd_points_vtk_array)
self.optimisticNpD.setData(self.td_values, optimistic_NpD_numpy)
+ self.optimisticNpD.setZValue(1)
#
diff --git a/src/modules/PoreNetworkProduction/Resources/Icons/PoreNetworkProduction.png b/src/modules/PoreNetworkProduction/Resources/Icons/PoreNetworkProduction.png
deleted file mode 100644
index b89476c..0000000
Binary files a/src/modules/PoreNetworkProduction/Resources/Icons/PoreNetworkProduction.png and /dev/null differ
diff --git a/src/modules/PoreNetworkProduction/Resources/Icons/PoreNetworkProduction.svg b/src/modules/PoreNetworkProduction/Resources/Icons/PoreNetworkProduction.svg
new file mode 100644
index 0000000..227dc95
--- /dev/null
+++ b/src/modules/PoreNetworkProduction/Resources/Icons/PoreNetworkProduction.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/modules/PoreNetworkSimulation/MercurySimulationLib/MercurySimulationLogic.py b/src/modules/PoreNetworkSimulation/MercurySimulationLib/MercurySimulationLogic.py
index 7be07d5..22110ed 100644
--- a/src/modules/PoreNetworkSimulation/MercurySimulationLib/MercurySimulationLogic.py
+++ b/src/modules/PoreNetworkSimulation/MercurySimulationLib/MercurySimulationLogic.py
@@ -11,8 +11,12 @@
import shutil
from pathlib import Path
-from ltrace.pore_networks.functions import geo2spy
-from ltrace.slicer_utils import LTracePluginLogic, dataFrameToTableNode, slicer_is_in_developer_mode
+from ltrace.pore_networks.functions import geo2spy, spy2geo
+from ltrace.slicer_utils import (
+ LTracePluginLogic,
+ dataFrameToTableNode,
+ slicer_is_in_developer_mode,
+)
HG_SURFACE_TENSION = 480 # 480N/km 0.48N/m 48e-5N/mm 48dyn/mm 480dyn/cm
HG_CONTACT_ANGLE = 140 # º
@@ -121,6 +125,7 @@ def onFinish(self):
micp_results = pd.DataFrame({"pc": pc.pc, "snwp": pc.snwp, "dsn": delta_saturation, "radii": throat_radii})
folderTree = slicer.mrmlScene.GetSubjectHierarchyNode()
+
micpTableName = slicer.mrmlScene.GenerateUniqueName("MICP")
micpTable = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTableNode", micpTableName)
micpTable.SetAttribute("table_type", "micp")
@@ -128,6 +133,23 @@ def onFinish(self):
_ = dataFrameToTableNode(micp_results, micpTable)
_ = folderTree.CreateItem(self.rootDir, micpTable)
+ with open(str(self.cwd / "net_flow_props.dict"), "rb") as file:
+ net_flow_props = pickle.load(file)
+ spy2geo(net_flow_props)
+
+ flowPropsPoreTableName = slicer.mrmlScene.GenerateUniqueName("Flow properties Pore network")
+ flowPropsPoreTable = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTableNode", flowPropsPoreTableName)
+ flowPropsThroatTableName = slicer.mrmlScene.GenerateUniqueName("Flow properties Throat network")
+ flowPropsThroatTable = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTableNode", flowPropsThroatTableName)
+ self.flow_props_pore_id = flowPropsPoreTable.GetID()
+ self.flow_props_throat_id = flowPropsThroatTable.GetID()
+ net_pore_flow_props = {k: v for k, v in net_flow_props.items() if k.startswith("pore.")}
+ _ = dataFrameToTableNode(pd.DataFrame(net_pore_flow_props), flowPropsPoreTable)
+ _ = folderTree.CreateItem(self.rootDir, flowPropsPoreTable)
+ net_throat_flow_props = {k: v for k, v in net_flow_props.items() if k.startswith("throat.")}
+ _ = dataFrameToTableNode(pd.DataFrame(net_throat_flow_props), flowPropsThroatTable)
+ _ = folderTree.CreateItem(self.rootDir, flowPropsThroatTable)
+
self.setChartNodes(micpTable, self.rootDir)
if self.params["save_radii_distrib_plots"]:
diff --git a/src/modules/PoreNetworkSimulation/MercurySimulationLib/MercurySimulationWidget.py b/src/modules/PoreNetworkSimulation/MercurySimulationLib/MercurySimulationWidget.py
index d649295..02a4365 100644
--- a/src/modules/PoreNetworkSimulation/MercurySimulationLib/MercurySimulationWidget.py
+++ b/src/modules/PoreNetworkSimulation/MercurySimulationLib/MercurySimulationWidget.py
@@ -18,10 +18,8 @@
DirOrFileWidget,
floatParam,
)
-from ltrace.slicer_utils import dataframeFromTable
from ltrace.slicer.widget.customized_pyqtgraph.GraphicsLayoutWidget import GraphicsLayoutWidget
-from MercurySimulationLib.MercurySimulationLogic import MercurySimulationLogic
-from MercurySimulationLib.SubscaleModelWidget import SubscaleModelWidget
+from .SubscaleModelWidget import SubscaleModelWidget
from PoreNetworkSimulationLib.constants import *
diff --git a/src/modules/PoreNetworkSimulation/MercurySimulationLib/SubscaleModelWidget.py b/src/modules/PoreNetworkSimulation/MercurySimulationLib/SubscaleModelWidget.py
index 7f5eaeb..831297b 100644
--- a/src/modules/PoreNetworkSimulation/MercurySimulationLib/SubscaleModelWidget.py
+++ b/src/modules/PoreNetworkSimulation/MercurySimulationLib/SubscaleModelWidget.py
@@ -12,6 +12,7 @@
)
from ltrace.slicer_utils import dataframeFromTable
from ltrace.file_utils import read_csv
+
from MercurySimulationLib import MercurySimulationLogic
diff --git a/src/modules/PoreNetworkSimulation/MercurySimulationLib/__init__.py b/src/modules/PoreNetworkSimulation/MercurySimulationLib/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/modules/PoreNetworkSimulation/PoreNetworkSimulation.py b/src/modules/PoreNetworkSimulation/PoreNetworkSimulation.py
index 7d1b15d..28e139f 100644
--- a/src/modules/PoreNetworkSimulation/PoreNetworkSimulation.py
+++ b/src/modules/PoreNetworkSimulation/PoreNetworkSimulation.py
@@ -1,26 +1,26 @@
-import importlib
-import sys
-import os
-
-from pathlib import Path
import ctk
import pyqtgraph as pg
import qt
import slicer
+import importlib
+import os
+import sys
-from MercurySimulationLib.MercurySimulationWidget import MercurySimulationWidget
from MercurySimulationLib.MercurySimulationLogic import MercurySimulationLogic
+from MercurySimulationLib.MercurySimulationWidget import MercurySimulationWidget
+from PoreNetworkSimulationLib.OnePhaseSimulationWidget import OnePhaseSimulationWidget
from PoreNetworkSimulationLib.PoreNetworkSimulationLogic import OnePhaseSimulationLogic, TwoPhaseSimulationLogic
from PoreNetworkSimulationLib.TwoPhaseSimulationWidget import TwoPhaseSimulationWidget
-from PoreNetworkSimulationLib.OnePhaseSimulationWidget import OnePhaseSimulationWidget
-from PoreNetworkSimulationLib.constants import MICP, ONE_PHASE, TWO_PHASE, ONE_ANGLE, MULTI_ANGLE
+from PoreNetworkSimulationLib.constants import MICP, ONE_PHASE, TWO_PHASE
+
+from pathlib import Path
from ltrace.slicer import ui
from ltrace.slicer.widget.global_progress_bar import LocalProgressBar
from ltrace.slicer_utils import (
LTracePlugin,
LTracePluginWidget,
+ getResourcePath,
)
-from ltrace.utils.ProgressBarProc import ProgressBarProc
try:
from Test.PoreNetworkSimulationTest import PoreNetworkSimulationTest
@@ -37,11 +37,11 @@ class PoreNetworkSimulation(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
- self.parent.title = "PoreNetworkSimulation"
- self.parent.categories = ["Micro CT"]
+ self.parent.title = "PNM Simulation"
+ self.parent.categories = ["MicroCT", "Multiscale"]
self.parent.dependencies = []
self.parent.contributors = ["LTrace Geophysics Team"]
- self.parent.helpText = PoreNetworkSimulation.help()
+ self.parent.helpText = f"file:///{(getResourcePath('manual') / 'Modules/PNM/PNSimulation.html').as_posix()}"
self.parent.acknowledgementText = ""
@classmethod
@@ -145,7 +145,7 @@ def setup(self):
self.layout.addStretch(1)
def onReload(self):
- importlib.reload(sys.modules["PoreNetworkSimulationLib.widgets"])
+ importlib.reload(sys.modules["ltrace.slicer.widget.simulation"])
importlib.reload(sys.modules["PoreNetworkSimulationLib.TwoPhaseSimulationWidget"])
importlib.reload(sys.modules["PoreNetworkSimulationLib.OnePhaseSimulationWidget"])
importlib.reload(sys.modules["MercurySimulationLib.MercurySimulationWidget"])
diff --git a/src/modules/PoreNetworkSimulation/PoreNetworkSimulationLib/OnePhaseSimulationWidget.py b/src/modules/PoreNetworkSimulation/PoreNetworkSimulationLib/OnePhaseSimulationWidget.py
index ee37a7b..4846286 100644
--- a/src/modules/PoreNetworkSimulation/PoreNetworkSimulationLib/OnePhaseSimulationWidget.py
+++ b/src/modules/PoreNetworkSimulation/PoreNetworkSimulationLib/OnePhaseSimulationWidget.py
@@ -1,11 +1,12 @@
import qt
-from MercurySimulationLib.MercurySimulationWidget import MercurySimulationWidget
-from PoreNetworkSimulationLib.constants import *
+from .constants import *
from ltrace.slicer import ui
from ltrace.slicer.widget.help_button import HelpButton
+from MercurySimulationLib.MercurySimulationWidget import MercurySimulationWidget
+
class OnePhaseSimulationWidget(qt.QFrame):
DEFAULT_VALUES = {
diff --git a/src/modules/PoreNetworkSimulation/PoreNetworkSimulationLib/PoreNetworkSimulationLogic.py b/src/modules/PoreNetworkSimulation/PoreNetworkSimulationLib/PoreNetworkSimulationLogic.py
index a1501a0..eed83bf 100644
--- a/src/modules/PoreNetworkSimulation/PoreNetworkSimulationLib/PoreNetworkSimulationLogic.py
+++ b/src/modules/PoreNetworkSimulation/PoreNetworkSimulationLib/PoreNetworkSimulationLogic.py
@@ -4,6 +4,7 @@
import json
import logging
import os
+import re
import shutil
import time
from pathlib import Path
@@ -29,7 +30,7 @@
hide_nodes_of_type,
)
-from PoreNetworkSimulationLib.constants import *
+from .constants import *
NUM_THREADS = 48
@@ -60,6 +61,16 @@ def listFilesInDir(directory):
return files
+def listFilesRegex(directory, regex_pattern=None):
+ matching_files = []
+ regex = re.compile(regex_pattern)
+ for root, dirs, files in os.walk(directory):
+ for file in files:
+ if regex.match(file):
+ matching_files.append(os.path.join(root, file))
+ return matching_files
+
+
def readPolydata(filename):
reader = vtk.vtkPolyDataReader()
reader.SetFileName(filename)
@@ -92,6 +103,7 @@ def __init__(self, progressBar):
self.prefix = None
self.rootDir = None
self.results = {}
+ self.caDistributionTableDir = None
def run_1phase(self, inputTable, params, prefix, callback, wait=False):
self.inputTableID = inputTable.GetID()
@@ -100,6 +112,11 @@ def run_1phase(self, inputTable, params, prefix, callback, wait=False):
self.callback = callback
self.prefix = prefix
+ refNode = inputTable.GetNodeReference("PoresLabelMap")
+ ijktorasDirections = np.zeros([3, 3])
+ refNode.GetIJKToRASDirections(ijktorasDirections)
+ self.params["ijktoras"] = [ijktorasDirections[i, i] for i in range(3)]
+
self.temp_dir = f"{slicer.app.temporaryPath}/porenetworksimulationcli"
shutil.rmtree(self.temp_dir, ignore_errors=True)
os.mkdir(self.temp_dir)
@@ -445,6 +462,16 @@ def simulate_krel(self, pore_node, params, prefix, callback, wait=False):
self.cwd = Path(slicer.util.tempDirectory())
self.callback = callback
+ folderTree = slicer.mrmlScene.GetSubjectHierarchyNode()
+ itemTreeId = folderTree.GetItemByDataNode(self.pore_node)
+ parentItemId = folderTree.GetItemParent(folderTree.GetItemParent(itemTreeId))
+ self.rootDir = folderTree.CreateFolderItem(parentItemId, f"{self.prefix}_Two_Phase_PN_Simulation")
+ folderTree.SetItemExpanded(self.rootDir, False)
+
+ if params["create_ca_distributions"]:
+ self.caDistributionTableDir = folderTree.CreateFolderItem(self.rootDir, "CA Distribution")
+ folderTree.SetItemExpanded(self.caDistributionTableDir, False)
+
self.temp_dir = f"{slicer.app.temporaryPath}/porenetworksimulationcli"
shutil.rmtree(self.temp_dir, ignore_errors=True)
os.mkdir(self.temp_dir)
@@ -465,11 +492,6 @@ def simulate_krel(self, pore_node, params, prefix, callback, wait=False):
self.krelCycleTableNodesId.append(tableNode.GetID())
cliParams[f"krelCycle{cycle}"] = tableNode.GetID()
- folderTree = slicer.mrmlScene.GetSubjectHierarchyNode()
- itemTreeId = folderTree.GetItemByDataNode(self.pore_node)
- parentItemId = folderTree.GetItemParent(folderTree.GetItemParent(itemTreeId))
- self.rootDir = folderTree.CreateFolderItem(parentItemId, f"{self.prefix}_Two_Phase_PN_Simulation")
- folderTree.SetItemExpanded(self.rootDir, False)
tableDir = folderTree.CreateFolderItem(self.rootDir, "Tables")
folderTree.SetItemExpanded(tableDir, False)
@@ -512,6 +534,7 @@ def twoPhaseCLICallback(self, caller, event):
if status == "Completed":
try:
self.updateOutputTables()
+ self.createCaDistributionTables()
self.loadAnimationNodes(caller.GetParameterAsString("saturation_steps"))
except:
self.removeNodes()
@@ -553,6 +576,22 @@ def updateOutputTables(self):
tableNode = helpers.tryGetNode(tableNodeId)
self.updateTableFromDataFrame(tableNode, dataFrameFilename)
+ def createCaDistributionTables(self):
+ krelResultsTableNode = helpers.tryGetNode(self.krelResultsTableNodeId)
+ if krelResultsTableNode and self.params["create_ca_distributions"]:
+ for file in listFilesRegex(str(self.cwd), "ca_distribution_\\d+"):
+ nodeName = Path(file).stem
+ caDistributionNode = self.__createCaDistributionNode(nodeName)
+ krelResultsTableNode.SetAttribute(f"{nodeName}_id", caDistributionNode.GetID())
+
+ def __createCaDistributionNode(self, ca_distribution_file):
+ folderTree = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
+
+ caDistributionTableNode = createTableNode(ca_distribution_file, "ca_distribution")
+ folderTree.CreateItem(self.caDistributionTableDir, caDistributionTableNode)
+ self.updateTableFromDataFrame(caDistributionTableNode, ca_distribution_file)
+ return caDistributionTableNode
+
def loadAnimationNodes(self, staturation_steps_json):
staturation_steps_list = json.loads(staturation_steps_json)
folder_tree = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
diff --git a/src/modules/PoreNetworkSimulation/PoreNetworkSimulationLib/TwoPhaseSimulationWidget.py b/src/modules/PoreNetworkSimulation/PoreNetworkSimulationLib/TwoPhaseSimulationWidget.py
index aed2ec0..62ddf11 100644
--- a/src/modules/PoreNetworkSimulation/PoreNetworkSimulationLib/TwoPhaseSimulationWidget.py
+++ b/src/modules/PoreNetworkSimulation/PoreNetworkSimulationLib/TwoPhaseSimulationWidget.py
@@ -1,14 +1,15 @@
import ctk
import qt
import slicer
-
-from Customizer import Customizer
from MercurySimulationLib.MercurySimulationWidget import MercurySimulationWidget
-from PoreNetworkSimulationLib.widgets import *
-from ltrace.slicer import ui
-from ltrace.slicer.node_attributes import TableType
+
+from ltrace.slicer.widget.help_button import HelpButton
from ltrace.pore_networks.pnflow_parameter_defs import PARAMETERS
from ltrace.pore_networks.simulation_parameters_node import dict_to_parameter_node, parameter_node_to_dict
+from ltrace.slicer import ui, helpers
+from ltrace.slicer.node_attributes import TableType
+from ltrace.slicer_utils import getResourcePath
+from ltrace.slicer.widget.simulation import *
class TwoPhaseParametersEditDialog:
@@ -16,7 +17,7 @@ def __init__(self, node):
self.node = node
def show(self):
- dialog = qt.QDialog(slicer.util.mainWindow())
+ dialog = qt.QDialog(slicer.modules.AppContextInstance.mainWindow)
dialog.setWindowTitle("Sensibility Parameters Edit")
dialog.setWindowFlags(dialog.windowFlags() & ~qt.Qt.WindowContextHelpButtonHint)
@@ -90,6 +91,17 @@ def __init__(self, hide_parameters_io=False):
layout = qt.QFormLayout(self)
self.widgets = {}
+ self.simulator_combo_box = qt.QComboBox()
+ self.simulator_combo_box.objectName = "Simulator Selector"
+ self.simulator_combo_box.addItem("pnflow")
+ self.simulator_combo_box.addItem("py_pore_flow")
+ self.simulator_combo_box.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding)
+ simulator_label = qt.QLabel("Simulator:")
+ simulator_layout = qt.QHBoxLayout()
+ simulator_layout.addWidget(simulator_label)
+ simulator_layout.addWidget(self.simulator_combo_box)
+ layout.addRow(simulator_layout)
+
self.parameterInputLoadCollapsible = ctk.ctkCollapsibleButton()
self.parameterInputLoadCollapsible.text = "Load parameters"
self.parameterInputLoadCollapsible.collapsed = True
@@ -107,7 +119,7 @@ def __init__(self, hide_parameters_io=False):
parameterInputLayout.addRow("Input parameter node:", self.parameterInputWidget)
parameterInputLayout.addRow(parameterInputLoadButton)
parameterInputLoadIcon = qt.QLabel()
- parameterInputLoadIcon.setPixmap(qt.QIcon(str(Customizer.LOAD_ICON_PATH)).pixmap(qt.QSize(13, 13)))
+ parameterInputLoadIcon.setPixmap(qt.QIcon(getResourcePath("Icons") / "Load.png").pixmap(qt.QSize(13, 13)))
if not hide_parameters_io:
layout.addRow(parameterInputLoadIcon, self.parameterInputLoadCollapsible)
@@ -115,7 +127,14 @@ def __init__(self, hide_parameters_io=False):
# Fluid Properties
fluidPropertiesBox = qt.QGroupBox()
- fluidPropertiesBox.setTitle("Fluid properties")
+ fluidPropertiesBox.setTitle("Fluid properties ")
+ help_button = HelpButton(
+ f"### [Fluid properties](file:///{getResourcePath('manual')}/Micro CT/Simulações/pnm.html#fluid-properties) section of GeoSlicer Manual."
+ )
+ help_button.setFixedSize(20, 20)
+ help_button.setParent(fluidPropertiesBox)
+ help_button.move(103, 0)
+
fluidPropertiesLayout = qt.QFormLayout(fluidPropertiesBox)
fluidPropertiesLayout.setContentsMargins(11, 9, 11, 5)
layout.addRow(fluidPropertiesBox)
@@ -146,9 +165,15 @@ def __init__(self, hide_parameters_io=False):
# contact angle
self.contactAngleBox = qt.QGroupBox()
- self.contactAngleBox.setTitle("Contact angle options")
+ self.contactAngleBox.setTitle("Contact angle options ")
self.contactAngleLayout = qt.QFormLayout(self.contactAngleBox)
self.contactAngleLayout.setContentsMargins(11, 9, 11, 5)
+ help_button = HelpButton(
+ f"### [Contact angle options](file:///{getResourcePath('manual')}/Micro CT/Simulações/pnm.html#contact-angle-options) section of GeoSlicer Manual."
+ )
+ help_button.setFixedSize(20, 20)
+ help_button.setParent(self.contactAngleBox)
+ help_button.move(142, 0)
layout.addRow(self.contactAngleBox)
for layout_name, display_string in (
@@ -182,9 +207,15 @@ def __init__(self, hide_parameters_io=False):
# simulation options
self.simulationOptionsBox = qt.QGroupBox()
- self.simulationOptionsBox.setTitle("Simulation options")
+ self.simulationOptionsBox.setTitle("Simulation options ")
self.simulationOptionsLayout = qt.QFormLayout(self.simulationOptionsBox)
self.simulationOptionsLayout.setContentsMargins(11, 9, 11, 5)
+ help_button = HelpButton(
+ f"### [Simulation options](file:///{getResourcePath('manual')}/Micro CT/Simulações/pnm.html#simulation-options) section of GeoSlicer Manual."
+ )
+ help_button.setFixedSize(20, 20)
+ help_button.setParent(self.simulationOptionsBox)
+ help_button.move(122, 0)
layout.addRow(self.simulationOptionsBox)
for layout_name, display_string in (
("cycle_1", "Drainage"),
@@ -245,7 +276,7 @@ def __init__(self, hide_parameters_io=False):
parameterInputLayout.addRow("Output parameter node name:", self.parameterInputLineEdit)
parameterInputLayout.addRow(parameterInputSaveButton)
parameterInputSaveIcon = qt.QLabel()
- parameterInputSaveIcon.setPixmap(qt.QIcon(str(Customizer.SAVE_ICON_PATH)).pixmap(qt.QSize(13, 13)))
+ parameterInputSaveIcon.setPixmap(qt.QIcon(getResourcePath("Icons") / "Save.png").pixmap(qt.QSize(13, 13)))
if not hide_parameters_io:
layout.addRow(parameterInputSaveIcon, parameterInputSaveCollapsible)
@@ -356,6 +387,7 @@ def getParams(self):
subres_params = {
i: subres_params[i].tolist() if subres_params[i] is not None else None for i in subres_params.keys()
}
+ params["simulator"] = self.simulator_combo_box.currentText
for widget in self.widgets.values():
params.update(widget.get_values())
diff --git a/src/modules/PoreNetworkSimulation/PoreNetworkSimulationLib/__init__.py b/src/modules/PoreNetworkSimulation/PoreNetworkSimulationLib/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/modules/PoreNetworkSimulation/PoreNetworkSimulationLib/twophase/__init__.py b/src/modules/PoreNetworkSimulation/PoreNetworkSimulationLib/twophase/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/modules/PoreNetworkSimulation/README.md b/src/modules/PoreNetworkSimulation/README.md
index 465db0b..56214a9 100644
--- a/src/modules/PoreNetworkSimulation/README.md
+++ b/src/modules/PoreNetworkSimulation/README.md
@@ -10,7 +10,7 @@ Performs one-phase and two-phase flow simulations to obtain absolute and relativ
### Parameters
-1. __Fluids simulation__: Chose either "one-phase" for absolute permeability, or "two-phase" for relative permeability.
+1. __Fluids simulation__: Chose either "one-phase" for absolute permeability, "two-phase" for relative permeability or "mercury injection".
#### One-phase
diff --git a/src/modules/PoreNetworkSimulation/Resources/Icons/PoreNetworkSimulation.png b/src/modules/PoreNetworkSimulation/Resources/Icons/PoreNetworkSimulation.png
deleted file mode 100644
index b89476c..0000000
Binary files a/src/modules/PoreNetworkSimulation/Resources/Icons/PoreNetworkSimulation.png and /dev/null differ
diff --git a/src/modules/PoreNetworkSimulation/Resources/Icons/PoreNetworkSimulation.svg b/src/modules/PoreNetworkSimulation/Resources/Icons/PoreNetworkSimulation.svg
new file mode 100644
index 0000000..227dc95
--- /dev/null
+++ b/src/modules/PoreNetworkSimulation/Resources/Icons/PoreNetworkSimulation.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/modules/PoreNetworkSimulationCLI/PoreNetworkSimulationCLI.py b/src/modules/PoreNetworkSimulationCLI/PoreNetworkSimulationCLI.py
index 81c7b0f..f665c3a 100644
--- a/src/modules/PoreNetworkSimulationCLI/PoreNetworkSimulationCLI.py
+++ b/src/modules/PoreNetworkSimulationCLI/PoreNetworkSimulationCLI.py
@@ -24,6 +24,8 @@
)
from ltrace.pore_networks.krel_result import KrelResult, KrelTables
from ltrace.pore_networks.visualization_model import generate_model_variable_scalar
+from PoreNetworkSimulationCLILib.two_phase.two_phase_simulation import PNFLOW, PORE_FLOW, TwoPhaseSimulation
+
from ltrace.pore_networks.functions_simulation import (
get_connected_spy_network,
get_flow_rate,
@@ -32,9 +34,11 @@
set_subresolution_conductance,
single_phase_permeability,
)
+
+from PoreNetworkSimulationCLILib.two_phase.two_phase_simulation import PNFLOW, PORE_FLOW, TwoPhaseSimulation
from PoreNetworkSimulationCLILib.vtk_utils import create_flow_model, create_permeability_sphere
from PoreNetworkSimulationCLILib.subres_models import set_subres_model
-from PoreNetworkSimulationCLILib.pnflow.pnflow_parallel import PnFlow
+
from ltrace.slicer.cli_utils import progressUpdate
import shutil
@@ -71,6 +75,7 @@ def onePhase(args, params):
permeability_array = np.zeros((3, 3), dtype="float")
sizes = params["sizes"]
+ ijktoras = params["ijktoras"]
sizes_product = sizes["x"] * sizes["y"] * sizes["z"]
subres_func = set_subres_model(pore_network, params)
@@ -122,7 +127,7 @@ def onePhase(args, params):
# pore_values = perm["pore.pressure"]
pore_values = pn_pores["pore.pressure"]
- pores_model, throats_model = create_flow_model(perm.project, pore_values, throat_values)
+ pores_model, throats_model = create_flow_model(perm.project, pore_values, throat_values, sizes, ijktoras)
writePolydata(pores_model, f"{args.tempDir}/pore_pressure_{inlet}_{outlet}.vtk")
writePolydata(throats_model, f"{args.tempDir}/throat_flow_rate_{inlet}_{outlet}.vtk")
@@ -131,7 +136,9 @@ def onePhase(args, params):
pore_values = perm.project.network[f"pore.{out_face}"].astype(int) - perm.project.network[
f"pore.{in_face}"
].astype(int)
- border_pores_model_node, null_throats_model_node = create_flow_model(perm.project, pore_values, throat_values)
+ border_pores_model_node, null_throats_model_node = create_flow_model(
+ perm.project, pore_values, throat_values, sizes, ijktoras
+ )
del null_throats_model_node
writePolydata(border_pores_model_node, f"{args.tempDir}/border_pores_{inlet}_{outlet}.vtk")
@@ -255,7 +262,7 @@ def onePhaseMultiAngle(args, params):
max_throat = np.inf
minmax.append({"index": i, "min": min_throat, "max": max_throat})
pore_values = pn_pores["pore.pressure"]
- pores_model, throats_model = create_flow_model(perm.project, pore_values, throat_values)
+ pores_model, throats_model = create_flow_model(perm.project, pore_values, throat_values, None)
writePolydata(pores_model, f"{args.tempDir}/pore_pressure_{i}.vtk")
writePolydata(throats_model, f"{args.tempDir}/throat_flow_rate_{i}.vtk")
@@ -263,7 +270,9 @@ def onePhaseMultiAngle(args, params):
throat_values = perm.network.throats("all")
pore_values = perm.project.network["pore.xmin"].astype(int) - perm.project.network["pore.xmax"].astype(int)
- border_pores_model_node, null_throats_model_node = create_flow_model(perm.project, pore_values, throat_values)
+ border_pores_model_node, null_throats_model_node = create_flow_model(
+ perm.project, pore_values, throat_values, None
+ )
del null_throats_model_node
writePolydata(border_pores_model_node, f"{args.tempDir}/border_pores_{i}.vtk")
@@ -303,14 +312,21 @@ def twoPhaseSensibilityTest(args, params, is_multiscale):
raise RuntimeError("The network is invalid.")
return
- pnflow_parallel = PnFlow(
- cwd=cwd, statoil_dict=statoil_dict, params=params, num_tests=num_tests, timeout_enabled=timeout_enabled
+ parallel = TwoPhaseSimulation(
+ cwd=cwd,
+ statoil_dict=statoil_dict,
+ params=params,
+ num_tests=num_tests,
+ timeout_enabled=timeout_enabled,
+ write_debug_files=keep_temporary,
)
+ parallel.set_simulator(PNFLOW if params["simulator"] == "pnflow" else PORE_FLOW)
+
saturation_steps_list = []
krel_result = KrelResult()
- for i, pnflow_result in enumerate(pnflow_parallel.run_pnflow(args.maxSubprocesses)):
- krel_result.add_single_result(pnflow_result["input_params"], pnflow_result["pnflow_table"])
+ for i, result in enumerate(parallel.run(args.maxSubprocesses)):
+ krel_result.add_single_result(result["input_params"], result["table"])
# Write results only every 10 new results
krel_tables_len = len(krel_result.krel_tables)
@@ -327,13 +343,26 @@ def twoPhaseSensibilityTest(args, params, is_multiscale):
if params["create_sequence"] == "T":
polydata, saturation_steps = generate_model_variable_scalar(
- Path(pnflow_result["cwd"]) / "Output_res", is_multiscale=is_multiscale
+ Path(result["cwd"]) / "Output_res", is_multiscale=is_multiscale
)
writePolydata(polydata, f"{args.tempDir}/cycle_node_{i}.vtk")
saturation_steps_list.append(saturation_steps)
+ if params["create_ca_distributions"]:
+ try:
+ with open(str(Path(result["cwd"]) / "ca_distribution.json"), "r") as fp:
+ ca_distribution_dict = json.load(fp)
+ ca_distribution_df = pd.DataFrame()
+ ca_distribution_df["drainage-advancing"] = ca_distribution_dict["drainage"]["advancing_ca"]
+ ca_distribution_df["drainage-receding"] = ca_distribution_dict["drainage"]["receding_ca"]
+ ca_distribution_df["imbibition-advancing"] = ca_distribution_dict["imbibition"]["advancing_ca"]
+ ca_distribution_df["imbibition-receding"] = ca_distribution_dict["imbibition"]["receding_ca"]
+ writeDataFrame(ca_distribution_df, cwd / f"ca_distribution_{i}")
+ except FileNotFoundError:
+ pass
+
if not keep_temporary:
- shutil.rmtree(pnflow_result["cwd"])
+ shutil.rmtree(result["cwd"])
with open(args.returnparameterfile, "w") as returnFile:
returnFile.write("saturation_steps=" + json.dumps(saturation_steps_list) + "\n")
@@ -358,6 +387,10 @@ def simulate_mercury(args, params):
manual_valvatne_blunt(sub_network)
set_subresolution_conductance(sub_network, subres_func, save_tables=params["save_tables"])
+
+ with open(str(cwd / "net_flow_props.dict"), "wb") as file:
+ pickle.dump(sub_network, file)
+
net = openpnm.io.network_from_porespy(sub_network)
hg = openpnm.phase.Mercury(network=net, name="mercury")
diff --git a/src/modules/PoreNetworkSimulationCLI/PoreNetworkSimulationCLILib/pnflow/pnflow_subprocess.py b/src/modules/PoreNetworkSimulationCLI/PoreNetworkSimulationCLILib/pnflow/pnflow_subprocess.py
index 12ca3e6..866f62c 100644
--- a/src/modules/PoreNetworkSimulationCLI/PoreNetworkSimulationCLILib/pnflow/pnflow_subprocess.py
+++ b/src/modules/PoreNetworkSimulationCLI/PoreNetworkSimulationCLILib/pnflow/pnflow_subprocess.py
@@ -1,15 +1,22 @@
-import ctypes
+import io
+import json
import os
+from pathlib import Path
import queue
+import re
import threading
-from multiprocessing import Process, Manager
from string import Template
-import time
+
+import numpy as np
+import pandas as pd
from pnflow import pnflow
+from PoreNetworkSimulationCLILib.two_phase.two_phase_subprocess import TwoPhaseSubprocess
+
PNFLOW_INPUT = Template(
"""TITLE $output_name; // base name for the output files
+RAND_SEED $seed;
writeStatistics true;
@@ -60,11 +67,12 @@
"""
)
+IGNORE_RI = True
+
-class PnFlowSubprocess:
+class PnflowSubprocess(TwoPhaseSubprocess):
def __init__(
self,
- manager: Manager,
params: dict,
cwd: str,
link1: str,
@@ -74,45 +82,53 @@ def __init__(
id: int,
write_debug_files: bool,
):
- self.manager = manager
- self.params = self._process_parameters(params)
- self.cwd = cwd
- self.link1 = link1
- self.link2 = link2
- self.node1 = node1
- self.node2 = node2
- self.process = None
- self.id = id
- self.write_debug_files = write_debug_files
-
- self.input_string = PNFLOW_INPUT.substitute(
- output_name="Output", enable_vtu_output=self.params["create_sequence"], **self.params
+ super().__init__(
+ params,
+ cwd,
+ link1,
+ link2,
+ node1,
+ node2,
+ id,
+ write_debug_files,
)
- self.start_time = 0
- self.run_count = 0
-
- def start(self):
- self.result = self.manager.Value(ctypes.c_char_p, "")
- self.process = Process(
- target=self.pnflow_caller,
- args=(
- self.result,
- self.cwd,
- self.input_string,
- self.link1,
- self.link2,
- self.node1,
- self.node2,
- self.write_debug_files,
- ),
- )
- self.start_time = time.time()
- self.run_count += 1
- self.process.start()
+
+ def get_cycle_result(self):
+ cycle_results = {"cycle": [], "Sw": [], "Pc": [], "Krw": [], "Kro": [], "RI": []}
+
+ with open(str(Path(self.cwd) / "result.txt"), "r") as file:
+ for line in file:
+ if "cycle" in line:
+ cycle_n = re.search(r"cycle(\d+):", line).group(1)
+ for sub_line in file:
+ if sub_line.strip() == "":
+ break
+ if "//" in sub_line:
+ continue
+ values = [float(i.strip()) for i in sub_line.strip().split("\t")]
+ if len(values) < 5:
+ continue
+ if values[2] < 0 or values[2] > 1 or values[3] < 0 or values[3] > 1:
+ continue
+ cycle_results["cycle"].append(int(cycle_n))
+ cycle_results["Sw"].append(values[0])
+ cycle_results["Pc"].append(values[1])
+ cycle_results["Krw"].append(values[2])
+ cycle_results["Kro"].append(values[3])
+ cycle_results["RI"].append(values[4])
+
+ if IGNORE_RI:
+ del cycle_results["RI"]
+
+ return cycle_results
@staticmethod
- def pnflow_caller(result, cwd, input_string, link1, link2, node1, node2, write_debug_files=False):
+ def caller(cwd, params, link1, link2, node1, node2, write_debug_files=False):
+ input_string = PNFLOW_INPUT.substitute(
+ output_name="Output", enable_vtu_output=params["create_sequence"], **params
+ )
os.chdir(cwd)
+
if write_debug_files:
input_file = open("input.txt", "w")
link1_file = open("Image_link1.dat", "w")
@@ -134,10 +150,16 @@ def pnflow_caller(result, cwd, input_string, link1, link2, node1, node2, write_d
threading.stack_size(0x800000)
result_queue = queue.Queue()
thread = threading.Thread(
- target=PnFlowSubprocess.pnflow_thread, args=(input_string, link1, link2, node1, node2, result_queue)
+ target=PnflowSubprocess.pnflow_thread, args=(input_string, link1, link2, node1, node2, result_queue)
)
thread.start()
- result.value = result_queue.get()
+ result_string = result_queue.get()
+
+ if params["create_ca_distributions"]:
+ PnflowSubprocess.__create_cas_json()
+
+ with open("result.txt", "w") as result_file:
+ result_file.write(result_string)
@staticmethod
def pnflow_thread(input_string, link1, link2, node1, node2, result_queue):
@@ -151,33 +173,21 @@ def _process_parameters(self, params):
params_copy["oil_viscosity"] = params_copy["oil_viscosity"] / 1000
return params_copy
- def terminate(self):
- self.process.terminate()
- self.process.join()
- self.start_time = 0
-
- def get_result(self):
- return self.result.value
-
- def is_finished(self):
- if not self.process.is_alive():
- return True
- return False
-
- def uptime(self) -> float:
- """
- Return duration of the process since started in seconds
- """
- if self.start_time != 0:
- return time.time() - self.start_time
- else:
- return -1
-
- def get_run_count(self) -> int:
- """
- Get how many times this subprocess was started.
- """
- return self.run_count
-
- def get_id(self) -> int:
- return self.id
+ @staticmethod
+ def __create_cas_json():
+ ca_distribution = {
+ "drainage": {
+ "advancing_ca": np.degrees(list(pd.read_csv("initial_adv_con_angles.csv")["Contact angle"])).tolist(),
+ "receding_ca": np.degrees(list(pd.read_csv("initial_rec_con_angles.csv")["Contact angle"])).tolist(),
+ },
+ "imbibition": {
+ "advancing_ca": np.degrees(
+ list(pd.read_csv("equilibrium_adv_con_angles.csv")["Contact angle"])
+ ).tolist(),
+ "receding_ca": np.degrees(
+ list(pd.read_csv("equilibrium_rec_con_angles.csv")["Contact angle"])
+ ).tolist(),
+ },
+ }
+ with open("ca_distribution.json", "w") as fp:
+ json.dump(ca_distribution, fp)
diff --git a/src/modules/PoreNetworkSimulationCLI/PoreNetworkSimulationCLILib/pore_flow/pore_flow_subprocess.py b/src/modules/PoreNetworkSimulationCLI/PoreNetworkSimulationCLILib/pore_flow/pore_flow_subprocess.py
new file mode 100644
index 0000000..00cd89f
--- /dev/null
+++ b/src/modules/PoreNetworkSimulationCLI/PoreNetworkSimulationCLILib/pore_flow/pore_flow_subprocess.py
@@ -0,0 +1,156 @@
+import io
+import json
+import numpy as np
+import os
+from pathlib import Path
+from string import Template
+
+import openpnm
+
+import py_pore_flow as ppf
+from PoreNetworkSimulationCLILib.two_phase.two_phase_subprocess import TwoPhaseSubprocess
+
+
+class PoreFlowSubprocess(TwoPhaseSubprocess):
+ def __init__(
+ self,
+ params: dict,
+ cwd: str,
+ link1: str,
+ link2: str,
+ node1: str,
+ node2: str,
+ id: int,
+ write_debug_files: bool,
+ ):
+ super().__init__(
+ params,
+ cwd,
+ link1,
+ link2,
+ node1,
+ node2,
+ id,
+ write_debug_files,
+ )
+
+ def get_cycle_result(self):
+ try:
+ with open(str(Path(self.cwd) / "result.txt"), "r") as file:
+ cycle_results = json.load(file)
+ except Exception as e:
+ cycle_results = {"cycle": [], "Sw": [], "Pc": [], "Krw": [], "Kro": [], "RI": []}
+ return cycle_results
+
+ @staticmethod
+ def caller(cwd, params_in, link1, link2, node1, node2, write_debug_files=False):
+ params = params_in.copy()
+ os.chdir(cwd)
+
+ py_pore_flow_parameters = {
+ "seed": int(params["seed"]),
+ "water_viscosity": params["water_viscosity"] / 1000,
+ "water_density": params["water_density"],
+ "oil_viscosity": params["oil_viscosity"] / 1000,
+ "oil_density": params["oil_density"],
+ "interfacial_tension": params["interfacial_tension"] / 1000,
+ "initial_ca_center": params["init_contact_angle"],
+ "initial_ca_range": params["init_contact_angle_range"],
+ "initial_ca_model": int(params["init_contact_model"]),
+ "initial_ca_separation": params["init_contact_angle_separation"],
+ "initial_ca_correlation": PoreFlowSubprocess.rctrl_to_correlation(params["init_contact_angle_rctrl"]),
+ "equilibrium_ca_center": params["equil_contact_angle"],
+ "equilibrium_ca_range": params["equil_contact_angle_range"],
+ "equilibrium_ca_model": int(params["equil_contact_model"]),
+ "equilibrium_ca_separation": params["equil_contact_angle_separation"],
+ "equilibrium_ca_correlation": PoreFlowSubprocess.rctrl_to_correlation(params["equil_contact_angle_rctrl"]),
+ "second_ca_center": params["frac_contact_angle"],
+ "second_ca_range": params["frac_contact_angle_range"],
+ "second_ca_fraction": params["frac_contact_angle_fraction"],
+ "second_ca_correlation": PoreFlowSubprocess.rctrl_to_correlation(params["frac_contact_angle_rctrl"]),
+ "drainage_sw_step": params["enforced_steps_1"],
+ "imbibition_sw_step": params["enforced_steps_2"],
+ }
+
+ input_file = open("input.txt", "w")
+ link1_file = open("Image_link1.dat", "w")
+ link2_file = open("Image_link2.dat", "w")
+ node1_file = open("Image_node1.dat", "w")
+ node2_file = open("Image_node2.dat", "w")
+ input_file.write(json.dumps(py_pore_flow_parameters))
+ link1_file.write(link1)
+ link2_file.write(link2)
+ node1_file.write(node1)
+ node2_file.write(node2)
+ input_file.close()
+ link1_file.close()
+ link2_file.close()
+ node1_file.close()
+ node2_file.close()
+
+ generate_vtu = params["create_sequence"] == "T"
+
+ pn = openpnm.io.network_from_statoil(".", "Image")
+
+ if write_debug_files:
+ ppf.log.configure(output=ppf.log.FILE, level=ppf.log.INFO)
+ else:
+ ppf.log.configure(level=ppf.log.WARNING)
+ cycle, Pc, Sw, Krw, Kro = ppf.run_two_phase_simulation(pn, py_pore_flow_parameters, generate_vtu=generate_vtu)
+ result_string = json.dumps(
+ {"cycle": cycle.tolist(), "Pc": Pc.tolist(), "Sw": Sw.tolist(), "Krw": Krw.tolist(), "Kro": Kro.tolist()}
+ )
+
+ if params["create_ca_distributions"]:
+ ca_distribution = {
+ "drainage": {
+ "advancing_ca": np.degrees(
+ np.concatenate(
+ (
+ pn["pore.initial_advancing_ca"],
+ pn["throat.initial_advancing_ca"],
+ )
+ )
+ ).tolist(),
+ "receding_ca": np.degrees(
+ np.concatenate(
+ (
+ pn["pore.initial_receding_ca"],
+ pn["throat.initial_receding_ca"],
+ )
+ )
+ ).tolist(),
+ },
+ "imbibition": {
+ "advancing_ca": np.degrees(
+ np.concatenate(
+ (
+ pn["pore.equilibrium_advancing_ca"],
+ pn["throat.equilibrium_advancing_ca"],
+ )
+ )
+ ).tolist(),
+ "receding_ca": np.degrees(
+ np.concatenate(
+ (
+ pn["pore.equilibrium_receding_ca"],
+ pn["throat.equilibrium_receding_ca"],
+ )
+ )
+ ).tolist(),
+ },
+ }
+ with open("ca_distribution.json", "w") as fp:
+ json.dump(ca_distribution, fp)
+
+ with open("result.txt", "w") as result_file:
+ result_file.write(result_string)
+
+ @staticmethod
+ def rctrl_to_correlation(rctrl):
+ if rctrl == "rand":
+ return ppf.UNCORRELATED
+ elif rctrl == "rMin":
+ return ppf.NEGATIVE_RADIUS
+ elif rctrl == "rMax":
+ return ppf.POSITIVE_RADIUS
diff --git a/src/modules/PoreNetworkSimulationCLI/PoreNetworkSimulationCLILib/pnflow/pnflow_parallel.py b/src/modules/PoreNetworkSimulationCLI/PoreNetworkSimulationCLILib/two_phase/two_phase_simulation.py
similarity index 52%
rename from src/modules/PoreNetworkSimulationCLI/PoreNetworkSimulationCLILib/pnflow/pnflow_parallel.py
rename to src/modules/PoreNetworkSimulationCLI/PoreNetworkSimulationCLILib/two_phase/two_phase_simulation.py
index 055858a..ad40f32 100644
--- a/src/modules/PoreNetworkSimulationCLI/PoreNetworkSimulationCLILib/pnflow/pnflow_parallel.py
+++ b/src/modules/PoreNetworkSimulationCLI/PoreNetworkSimulationCLILib/two_phase/two_phase_simulation.py
@@ -1,70 +1,85 @@
-import io
import itertools
+from pathlib import Path
import random
-import re
import string
import time
-from multiprocessing import Manager
-from pathlib import Path
from ltrace.slicer.cli_utils import progressUpdate
-from .pnflow_subprocess import PnFlowSubprocess
+from PoreNetworkSimulationCLILib.pnflow.pnflow_subprocess import PnflowSubprocess
+from PoreNetworkSimulationCLILib.pore_flow.pore_flow_subprocess import PoreFlowSubprocess
-IGNORE_RI = True
+PNFLOW = 0
+PORE_FLOW = 1
-class PnFlow:
- def __init__(self, cwd: Path, statoil_dict: dict, params: dict, num_tests: int, timeout_enabled: bool):
+
+class TwoPhaseSimulation:
+ def __init__(
+ self,
+ cwd: Path,
+ statoil_dict: dict,
+ params: dict,
+ num_tests: int,
+ timeout_enabled: bool,
+ write_debug_files: bool,
+ ):
self.cwd = cwd
self.statoil_file_strings = self.create_statoil_file_strings(statoil_dict)
self.params_dict = params
self.num_tests = num_tests
self.timeout_enabled = timeout_enabled
- self.manager = Manager()
self.subprocess_id_count = 0
self.subprocess_timeout_s = 0.1 * (len(statoil_dict["node1"]) + len(statoil_dict["link1"]))
+ self.simulator_class = PnflowSubprocess
+ self.simulator = PNFLOW
+ self.write_debug_files = write_debug_files
+
+ def set_simulator(self, simulator):
+ self.simulator = simulator
- def run_pnflow(self, max_subprocesses=8):
+ def run(self, max_subprocesses=8):
LOOP_REFRESH_RATE_S = 0.01
SUBPROCESS_RETRY_LIMIT = 0
params_iterator = self.get_params_iterator(self.params_dict)
- running_pnflow_subprocesses = []
- finished_pnflow_subprocesses = []
+ running_subprocesses = []
+ finished_subprocesses = []
i = 0
while True:
# Listening to allocate slots for new processes to be started
- for j, pnflow_subprocess in enumerate(running_pnflow_subprocesses):
- if pnflow_subprocess.is_finished():
- running_pnflow_subprocesses[j] = None
- finished_pnflow_subprocesses.append(pnflow_subprocess)
- elif self.timeout_enabled and (pnflow_subprocess.uptime() > self.subprocess_timeout_s):
- pnflow_subprocess.terminate()
- if pnflow_subprocess.get_run_count() < SUBPROCESS_RETRY_LIMIT:
- pnflow_subprocess.start()
+ for j, subprocess in enumerate(running_subprocesses):
+ if subprocess.is_finished():
+ running_subprocesses[j] = None
+ finished_subprocesses.append(subprocess)
+ elif (
+ self.timeout_enabled
+ and self.simulator == PNFLOW
+ and (subprocess.uptime() > self.subprocess_timeout_s)
+ ):
+ subprocess.terminate()
+ if subprocess.get_run_count() < SUBPROCESS_RETRY_LIMIT:
+ subprocess.start()
else:
print(
- f"timeout: process {pnflow_subprocess.get_id()} couldn't finish in any of the {SUBPROCESS_RETRY_LIMIT} retries"
+ f"timeout: process {subprocess.get_id()} couldn't finish in any of the {SUBPROCESS_RETRY_LIMIT} retries"
)
- running_pnflow_subprocesses[j] = None
+ running_subprocesses[j] = None
- running_pnflow_subprocesses = [p for p in running_pnflow_subprocesses if p is not None]
+ running_subprocesses = [p for p in running_subprocesses if p is not None]
# Listening to terminate, cleanup
- for pnflow_subprocess in finished_pnflow_subprocesses:
- pnflow_result = self.create_pnflow_result(
- pnflow_subprocess.params, pnflow_subprocess.get_result(), pnflow_subprocess.cwd
- )
- pnflow_subprocess.terminate()
+ for subprocess in finished_subprocesses:
+ subprocess.finish()
+ simulation_result = self.create_result(subprocess.params, subprocess.get_cycle_result(), subprocess.cwd)
i += 1
progressUpdate(value=0.1 + (i / self.num_tests) * 0.85)
- yield pnflow_result
- finished_pnflow_subprocesses = []
+ yield simulation_result
+ finished_subprocesses = []
# Max running process limiter
- if len(running_pnflow_subprocesses) == max_subprocesses:
+ if len(running_subprocesses) == max_subprocesses:
time.sleep(LOOP_REFRESH_RATE_S)
continue
@@ -72,23 +87,26 @@ def run_pnflow(self, max_subprocesses=8):
params = next(params_iterator)
# Listening to finish or wait processes to finish
- if params is None and len(running_pnflow_subprocesses) == 0 and len(finished_pnflow_subprocesses) == 0:
+ if params is None and len(running_subprocesses) == 0 and len(finished_subprocesses) == 0:
break
if params is None:
time.sleep(LOOP_REFRESH_RATE_S)
continue
# Start a new process in the loop if passed all above conditions
- pnflow_subprocess = self.run_pnflow_subprocess(params.copy())
- running_pnflow_subprocesses.append(pnflow_subprocess)
+ subprocess = self.run_subprocess(params.copy())
+ running_subprocesses.append(subprocess)
time.sleep(LOOP_REFRESH_RATE_S)
- def run_pnflow_subprocess(self, params):
+ def run_subprocess(self, params):
directory_name = self.generate_directory_name(22)
directory_path = self.cwd / directory_name
directory_path.mkdir(parents=True, exist_ok=True)
- pnflow_subprocess = PnFlowSubprocess(
- manager=self.manager,
+ if self.simulator == PNFLOW:
+ simulator_class = PnflowSubprocess
+ else:
+ simulator_class = PoreFlowSubprocess
+ subprocess = simulator_class(
params=params,
cwd=str(directory_path),
link1=self.statoil_file_strings["link1"],
@@ -96,45 +114,19 @@ def run_pnflow_subprocess(self, params):
node1=self.statoil_file_strings["node1"],
node2=self.statoil_file_strings["node2"],
id=self.subprocess_id_count,
- write_debug_files=self.num_tests == 1,
+ write_debug_files=self.write_debug_files,
)
self.subprocess_id_count += 1
- pnflow_subprocess.start()
- return pnflow_subprocess
+ subprocess.start()
+ return subprocess
def generate_directory_name(self, length):
characters = string.ascii_letters
directory_name = "".join(random.choices(characters, k=length))
return directory_name
- def create_pnflow_result(self, params, result, cwd):
- cycle_results = {"cycle": [], "Sw": [], "Pc": [], "Krw": [], "Kro": [], "RI": []}
- file = io.StringIO(result)
-
- for line in file:
- if "cycle" in line:
- cycle_n = re.search(r"cycle(\d+):", line).group(1)
- for sub_line in file:
- if sub_line.strip() == "":
- break
- if "//" in sub_line:
- continue
- values = [float(i.strip()) for i in sub_line.strip().split("\t")]
- if len(values) < 5:
- continue
- if values[2] < 0 or values[2] > 1 or values[3] < 0 or values[3] > 1:
- continue
- cycle_results["cycle"].append(int(cycle_n))
- cycle_results["Sw"].append(values[0])
- cycle_results["Pc"].append(values[1])
- cycle_results["Krw"].append(values[2])
- cycle_results["Kro"].append(values[3])
- cycle_results["RI"].append(values[4])
-
- if IGNORE_RI:
- del cycle_results["RI"]
-
- return {"input_params": params, "pnflow_table": cycle_results, "cwd": cwd}
+ def create_result(self, params, cycle_results, cwd):
+ return {"input_params": params, "table": cycle_results, "cwd": cwd}
@staticmethod
def create_statoil_file_strings(statoil_dict):
diff --git a/src/modules/PoreNetworkSimulationCLI/PoreNetworkSimulationCLILib/two_phase/two_phase_subprocess.py b/src/modules/PoreNetworkSimulationCLI/PoreNetworkSimulationCLILib/two_phase/two_phase_subprocess.py
new file mode 100644
index 0000000..4076fdf
--- /dev/null
+++ b/src/modules/PoreNetworkSimulationCLI/PoreNetworkSimulationCLILib/two_phase/two_phase_subprocess.py
@@ -0,0 +1,95 @@
+from abc import abstractmethod
+import ctypes
+from multiprocessing import Process
+import time
+
+
+class TwoPhaseSubprocess:
+ def __init__(
+ self,
+ params: dict,
+ cwd: str,
+ link1: str,
+ link2: str,
+ node1: str,
+ node2: str,
+ id: int,
+ write_debug_files: bool,
+ ):
+ self.params = self._process_parameters(params)
+ self.cwd = cwd
+ self.link1 = link1
+ self.link2 = link2
+ self.node1 = node1
+ self.node2 = node2
+ self.process = None
+ self.id = id
+ self.write_debug_files = write_debug_files
+ self.result = None
+
+ self.start_time = 0
+ self.run_count = 0
+
+ def start(self):
+ self.process = Process(
+ target=self.caller,
+ args=(
+ self.cwd,
+ self.params,
+ self.link1,
+ self.link2,
+ self.node1,
+ self.node2,
+ self.write_debug_files,
+ ),
+ )
+ self.start_time = time.time()
+ self.run_count += 1
+ self.process.start()
+
+ def terminate(self):
+ if self.process.is_alive():
+ self.process.kill()
+ self.process.join(5)
+ self.process.close()
+ self.start_time = 0
+
+ def is_finished(self):
+ if not self.process.is_alive():
+ return True
+ return False
+
+ def finish(self):
+ self.process.join(5)
+ if self.process.is_alive():
+ self.terminate()
+
+ def uptime(self) -> float:
+ """
+ Return duration of the process since started in seconds
+ """
+ if self.start_time != 0:
+ return time.time() - self.start_time
+ else:
+ return -1
+
+ def get_run_count(self) -> int:
+ """
+ Get how many times this subprocess was started.
+ """
+ return self.run_count
+
+ def get_id(self) -> int:
+ return self.id
+
+ def _process_parameters(self, params):
+ return params
+
+ @abstractmethod
+ def get_cycle_result(self):
+ pass
+
+ @staticmethod
+ @abstractmethod
+ def caller(cwd, input_string, link1, link2, node1, node2, write_debug_files=False):
+ pass
diff --git a/src/modules/PoreNetworkSimulationCLI/PoreNetworkSimulationCLILib/vtk_utils.py b/src/modules/PoreNetworkSimulationCLI/PoreNetworkSimulationCLILib/vtk_utils.py
index bc7dd5a..90b0638 100644
--- a/src/modules/PoreNetworkSimulationCLI/PoreNetworkSimulationCLILib/vtk_utils.py
+++ b/src/modules/PoreNetworkSimulationCLI/PoreNetworkSimulationCLILib/vtk_utils.py
@@ -3,8 +3,6 @@
import numpy as np
-IJKTORAS = (1, 1, 1)
-
class GlyphCreator:
def __init__(self, glyph_filter, length_array, radius):
@@ -30,7 +28,11 @@ def __call__(self):
self.glyph_filter.SetSourceConnection(tube.GetOutputPort())
-def create_flow_model(project, pore_values, throat_values):
+def create_flow_model(project, pore_values, throat_values, sizes, IJKTORAS=(-1, -1, 1)):
+ if sizes:
+ offset = [10 * sizes[axis] if IJKTORAS[i] < 0 else 0 for i, axis in enumerate(["x", "y", "z"])]
+ else:
+ offset = [0 for i, axis in enumerate(["x", "y", "z"])]
##### Create pores #####
coordinates = vtk.vtkPoints()
@@ -39,9 +41,9 @@ def create_flow_model(project, pore_values, throat_values):
for pore_index in range(len(project.network["pore.all"])):
coordinates.InsertPoint(
pore_index,
- project.network["pore.coords"][pore_index][0] * IJKTORAS[0],
- project.network["pore.coords"][pore_index][1] * IJKTORAS[1],
- project.network["pore.coords"][pore_index][2] * IJKTORAS[2],
+ project.network["pore.coords"][pore_index][0] * IJKTORAS[0] + offset[0],
+ project.network["pore.coords"][pore_index][1] * IJKTORAS[1] + offset[1],
+ project.network["pore.coords"][pore_index][2] * IJKTORAS[2] + offset[2],
)
pore_value = pore_values[pore_index]
diameters.InsertTuple1(pore_index, pore_value)
@@ -89,13 +91,19 @@ def create_flow_model(project, pore_values, throat_values):
continue
nodes_list.append(
- (throat_index * 2, *[(a[0] / a[1]) for a in zip(project.network["pore.coords"][left_pore_index], IJKTORAS)])
+ (
+ throat_index * 2,
+ *[(a[0] * a[1] + a[2]) for a in zip(project.network["pore.coords"][left_pore_index], IJKTORAS, offset)],
+ )
)
nodes_list.append(
(
throat_index * 2 + 1,
- *[(a[0] / a[1]) for a in zip(project.network["pore.coords"][right_pore_index], IJKTORAS)],
+ *[
+ (a[0] * a[1] + a[2])
+ for a in zip(project.network["pore.coords"][right_pore_index], IJKTORAS, offset)
+ ],
)
)
diff --git a/src/modules/PoreNetworkVisualization/PoreNetworkVisualization.py b/src/modules/PoreNetworkVisualization/PoreNetworkVisualization.py
index b769f7b..ac7c2d5 100644
--- a/src/modules/PoreNetworkVisualization/PoreNetworkVisualization.py
+++ b/src/modules/PoreNetworkVisualization/PoreNetworkVisualization.py
@@ -18,6 +18,7 @@
LTracePlugin,
LTracePluginWidget,
hide_nodes_of_type,
+ getResourcePath,
)
from ltrace.slicer import widgets
@@ -38,11 +39,11 @@ class PoreNetworkVisualization(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
- self.parent.title = "PoreNetworkVisualization"
- self.parent.categories = ["Micro CT"]
+ self.parent.title = "PNM Cycles Visualization"
+ self.parent.categories = ["MicroCT"]
self.parent.dependencies = []
self.parent.contributors = ["LTrace Geophysics Team"]
- self.parent.helpText = PoreNetworkVisualization.help()
+ self.parent.helpText = f"file:///{(getResourcePath('manual') / 'Modules/PNM/cycles.html').as_posix()}"
self.parent.acknowledgementText = ""
@classmethod
diff --git a/src/modules/PoreStats/CleanResinCLI/CleanResinCLI.py b/src/modules/PoreStats/CleanResinCLI/CleanResinCLI.py
new file mode 100644
index 0000000..7ec8493
--- /dev/null
+++ b/src/modules/PoreStats/CleanResinCLI/CleanResinCLI.py
@@ -0,0 +1,244 @@
+#!/usr/bin/env python-real
+# -*- coding: utf-8 -*-
+
+from __future__ import print_function
+import os
+import time
+
+import cv2
+import joblib
+
+import slicer
+import slicer.util
+import mrml
+
+from ltrace.slicer.cli_utils import progressUpdate, readFrom, writeDataInto
+
+import numpy as np
+from skimage.segmentation import watershed
+
+
+def incrementalProgressUpdate(progress, addition):
+ progress += addition
+ progressUpdate(progress)
+ return progress
+
+
+def clean_resin(image, binary_seg, px_image, pp_rock_area, px_rock_area, decide_best_reg):
+ def remove_artifacts(mask, open_kernel_size, close_kernel_size):
+ if open_kernel_size is not None:
+ open_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (open_kernel_size, open_kernel_size))
+ mask = cv2.morphologyEx(mask.astype(np.uint8), cv2.MORPH_OPEN, open_kernel)
+ if close_kernel_size is not None:
+ close_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (close_kernel_size, close_kernel_size))
+ mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, close_kernel).astype(bool)
+
+ return mask
+
+ def get_roi_from_blue_channel(image):
+ nonlocal progress
+
+ blue_channel = image[:, :, 2]
+ blue_channel = cv2.equalizeHist(blue_channel)
+
+ # with open(os.path.join(__file__, '..', '..', "PoreStatsCLI", "Libs", "pore_stats", 'models', 'pore_residues', 'blue_channel.pkl'), 'rb') as pkl:
+ # kmeans = pickle.load(pkl)
+
+ # Usando joblib em vez de pickle para aproveitar a compressão do modelo (K-Means pickle fica muito grande)
+ # O modelo joblib foi salvo usando a mesma versão do Python usado para executar este script (PythonSlicer). Divergência de versão causa erro.
+ kmeans = joblib.load(
+ os.path.join(
+ __file__,
+ "..",
+ "..",
+ "PoreStatsCLI",
+ "Libs",
+ "pore_stats",
+ "models",
+ "pore_residues",
+ "blue_channel.pkl",
+ )
+ )
+
+ clusters = kmeans.predict(blue_channel.flatten().reshape(-1, 1))
+ progress = incrementalProgressUpdate(progress, 0.25)
+ blue_mask = clusters.reshape(blue_channel.shape) == kmeans.cluster_centers_.argmax()
+ return remove_artifacts(blue_mask, open_kernel_size=20, close_kernel_size=None)
+
+ def get_roi_from_hue_channel(image):
+ nonlocal progress
+
+ progress = incrementalProgressUpdate(progress, 0.03)
+ hue_channel = cv2.cvtColor(image, cv2.COLOR_RGB2HSV)[:, :, 0]
+
+ hue_mask = (hue_channel >= 75) & (
+ hue_channel <= 135
+ ) # blue hue, which catches both the resin and the dark bubbles/residues
+ for open_kernel_size, close_kernel_size in [(5, None), (None, 13), (13, None)]:
+ hue_mask = remove_artifacts(
+ hue_mask, open_kernel_size=open_kernel_size, close_kernel_size=close_kernel_size
+ )
+ progress = incrementalProgressUpdate(progress, 0.01)
+
+ return hue_mask
+
+ def get_roi_from_px_hsv(pp, px, pores_mask, decide_best_reg):
+ def crop_rock_area(image, rock_area):
+ non_zero_coords = cv2.findNonZero(rock_area.astype(np.uint8))
+ x, y, w, h = cv2.boundingRect(non_zero_coords)
+ crop = image[y : y + h, x : x + w]
+ return crop
+
+ def register_px_to_pp(pp, px, pp_rock_area=None, px_rock_area=None):
+ reg_px = np.zeros((max(pp.shape[0], px.shape[0]), max(pp.shape[1], px.shape[1]), 3)).astype(np.uint8)
+ orig_pp_shape = pp.shape
+
+ if pp_rock_area is not None:
+ pp = crop_rock_area(pp, pp_rock_area)
+ if px_rock_area is not None:
+ px = crop_rock_area(px, px_rock_area)
+
+ px_y0 = reg_px.shape[0] // 2 - px.shape[0] // 2
+ px_y1 = reg_px.shape[0] // 2 + px.shape[0] // 2 + px.shape[0] % 2
+ px_x0 = reg_px.shape[1] // 2 - px.shape[1] // 2
+ px_x1 = reg_px.shape[1] // 2 + px.shape[1] // 2 + px.shape[1] % 2
+
+ reg_px[px_y0:px_y1, px_x0:px_x1] = px.copy()
+
+ pp_y0 = reg_px.shape[0] // 2 - orig_pp_shape[0] // 2
+ pp_y1 = reg_px.shape[0] // 2 + orig_pp_shape[0] // 2 + orig_pp_shape[0] % 2
+ pp_x0 = reg_px.shape[1] // 2 - orig_pp_shape[1] // 2
+ pp_x1 = reg_px.shape[1] // 2 + orig_pp_shape[1] // 2 + orig_pp_shape[1] % 2
+
+ return reg_px[pp_y0:pp_y1, pp_x0:pp_x1]
+
+ nonlocal progress
+
+ reg_px = {
+ "Centralized": register_px_to_pp(pp, px),
+ }
+ progress = incrementalProgressUpdate(progress, 0.02)
+ if decide_best_reg:
+ reg_px.update(
+ {
+ "Cropped and centralized": register_px_to_pp(
+ pp, px, pp_rock_area=pp_rock_area, px_rock_area=px_rock_area
+ )
+ }
+ )
+ progress = incrementalProgressUpdate(progress, 0.07)
+ px_pores_mask = None
+ best_reg_quality = -1
+ best_method = None
+ for method, px in reg_px.items():
+ px_hsv = cv2.cvtColor(cv2.GaussianBlur(px, (99, 99), 9), cv2.COLOR_RGB2HSV)
+
+ kmeans = joblib.load(
+ os.path.join(
+ __file__, "..", "..", "PoreStatsCLI", "Libs", "pore_stats", "models", "pore_residues", "px_hsv.pkl"
+ )
+ )
+ clusters = kmeans.predict(px_hsv.flatten().reshape(-1, 3))
+ test_px_pores_mask = clusters.reshape(px_hsv.shape[:2]) == 3
+ test_px_pores_mask = remove_artifacts(test_px_pores_mask, open_kernel_size=13, close_kernel_size=13)
+
+ reg_area = np.count_nonzero(test_px_pores_mask & pores_mask)
+ pore_area = np.count_nonzero(pores_mask)
+ test_reg_quality = reg_area / pore_area if pore_area > 0 else 0
+ print(method, "registration quality:", "{:.2f} %".format(100 * test_reg_quality))
+ if test_reg_quality > best_reg_quality:
+ best_method = method
+ best_reg_quality = test_reg_quality
+ px_pores_mask = test_px_pores_mask
+ progress = incrementalProgressUpdate(progress, 0.25)
+
+ print(best_method, "registration method chosen.")
+ return px_pores_mask
+
+ def grow_pores_through_mask(mask, pores):
+ markers = pores & mask
+ return watershed(~mask, markers=markers, mask=mask)
+
+ use_px = px_image is not None
+
+ if pp_rock_area is not None:
+ image *= pp_rock_area.astype(np.uint8)[..., np.newaxis]
+
+ print("Detecting air bubbles and residues in pore resin... Using PX:", {False: "No", True: "Yes"}[use_px])
+ start_time = time.time()
+ progress = incrementalProgressUpdate(0, 0)
+
+ # O canal azul da imagem funde as bolhas brancas à resina azul
+ blue_mask = get_roi_from_blue_channel(image)
+ # O canal Hue da imagem funde as bolhas negras e resíduos à resina azul
+ hue_mask = get_roi_from_hue_channel(image)
+
+ if use_px:
+ # A região escura do PX funde as bolhas e resíduos à região porosa
+ px_pores_mask = get_roi_from_px_hsv(image, px_image, binary_seg, decide_best_reg)
+
+ # As regiões não-escuras do PX são descartadas das regiões úteis dos canais azul e Hue
+ blue_mask &= px_pores_mask
+ hue_mask &= px_pores_mask
+
+ # Os poros detectados crescem sobre a região azul, cobrindo as bolhas brancas
+ bubbled_blue_mask = grow_pores_through_mask(blue_mask, binary_seg)
+ # Os poros detectados crescem sobre a região Hue, cobrindo as bolhas negras e resíduos
+ bubbled_hue_mask = grow_pores_through_mask(hue_mask, binary_seg)
+
+ print(f"Done ({time.time() - start_time}s).")
+ progressUpdate(1)
+ # As regiões crescidas são unidas. Os poros originais são reinclusos para o caso de terem sido
+ # perdidos por um mal alinhamento entre PP e PX
+ return bubbled_blue_mask | bubbled_hue_mask | binary_seg
+
+
+def get_array_if_exists(input_path, is_segmentation):
+ if input_path:
+ builder = mrml.vtkMRMLLabelMapVolumeNode if is_segmentation else mrml.vtkMRMLVectorVolumeNode
+ node = readFrom(input_path, builder)
+ return slicer.util.arrayFromVolume(node)[0]
+ return None
+
+
+def runcli(args):
+ sourceVolumeNode = readFrom(args.ppImage, mrml.vtkMRMLVectorVolumeNode)
+ pp_image = slicer.util.arrayFromVolume(sourceVolumeNode)[0]
+
+ poreSegmentationNode = readFrom(args.poreSegmentation, mrml.vtkMRMLLabelMapVolumeNode)
+ pore_seg = slicer.util.arrayFromVolume(poreSegmentationNode)[0].astype(bool)
+
+ px_image = get_array_if_exists(args.pxImage, is_segmentation=False)
+ pp_rock_area = get_array_if_exists(args.ppRockArea, is_segmentation=True)
+ px_rock_area = get_array_if_exists(args.pxRockArea, is_segmentation=True)
+
+ pore_seg = clean_resin(pp_image, pore_seg, px_image, pp_rock_area, px_rock_area, args.smartReg)
+
+ writeDataInto(args.output, pore_seg, mrml.vtkMRMLLabelMapVolumeNode, reference=sourceVolumeNode)
+
+
+if __name__ == "__main__":
+ import argparse
+
+ parser = argparse.ArgumentParser(description="LTrace Clean Resin Wrapper for Slicer.")
+ parser.add_argument("--ppimage", type=str, dest="ppImage", default=None, help="Input PP Image")
+ parser.add_argument(
+ "--poreseg",
+ type=str,
+ dest="poreSegmentation",
+ default=None,
+ help="Prior Pore Segmentation For Resin Cleaning",
+ )
+ parser.add_argument("--output", type=str, dest="output", default=None, help="Output Segmentation")
+ parser.add_argument("--pximage", type=str, dest="pxImage", default=None, help="Input PX Image")
+ parser.add_argument("--pprockarea", type=str, dest="ppRockArea", default=None, help="PP Rock Area Segmentation")
+ parser.add_argument("--pxrockarea", type=str, dest="pxRockArea", default=None, help="PX Rock Area Segmentation")
+ parser.add_argument(
+ "--smartreg",
+ action="store_true",
+ dest="smartReg",
+ help="Decide Best Auto-Registration Heuristic",
+ )
+
+ args = parser.parse_args()
+ runcli(args)
diff --git a/src/modules/PoreStats/CleanResinCLI/CleanResinCLI.xml b/src/modules/PoreStats/CleanResinCLI/CleanResinCLI.xml
new file mode 100644
index 0000000..dff133c
--- /dev/null
+++ b/src/modules/PoreStats/CleanResinCLI/CleanResinCLI.xml
@@ -0,0 +1,79 @@
+
+
+ LTrace Tools
+ 3
+ CleanResin CLI
+
+ 0.2.0.
+ https://github.com/lassoan/SlicerPythonCLIExample
+
+ LTrace Team
+
+
+
+
+ ppImage
+ ppimage
+
+
+ input
+
+
+
+
+ poreSegmentation
+ poreseg
+
+
+ input
+
+
+
+
+ output
+ output
+
+
+ output
+
+
+
+
+
+
+ pxImage
+ pximage
+
+
+ input
+
+
+
+
+ ppRockArea
+ pprockarea
+
+
+ input
+
+
+
+
+ pxRockArea
+ pxrockarea
+
+
+ input
+
+
+
+
+ smartReg
+ smartreg
+
+
+
+
+
+
+
diff --git a/src/modules/PoreStats/PoreStats.py b/src/modules/PoreStats/PoreStats.py
index 86fb82f..0698bfb 100644
--- a/src/modules/PoreStats/PoreStats.py
+++ b/src/modules/PoreStats/PoreStats.py
@@ -20,14 +20,11 @@
from slicer.ScriptedLoadableModule import *
-# $TODO: Tests PoreStats
-"""
# Checks if closed source code is available
try:
- from Test.PoreStatsTest import PoreStatsTest
+ from Test.PoreStatsTest import PoreStatsTest
except ImportError:
PoreStatsTest = None # tests not deployed to final version or closed source
-"""
class PoreStats(LTracePlugin):
@@ -38,11 +35,10 @@ class PoreStats(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
self.parent.title = "PoreStats" # TODO make this more human readable by adding spaces
- self.parent.categories = ["Segmentation"]
+ self.parent.categories = ["Segmentation", "Thin Section", "ImageLog", "Core"]
self.parent.dependencies = []
self.parent.contributors = ["LTrace Geophysics Team"] # replace with "Firstname Lastname (Organization)"
- self.parent.helpText = PoreStats.help()
- self.parent.helpText += self.getDefaultModuleDocumentationLink()
+ self.parent.helpText = f"file:///{Path(helpers.get_scripted_modules_path() + '/Resources/manual/Modules/Thin_section/PoreStats.html').as_posix()}"
self.parent.acknowledgementText = "" # replace with organization, grant and thanks.
@classmethod
@@ -55,6 +51,7 @@ def __init__(self, parent):
LTracePluginWidget.__init__(self, parent)
self.cliNode = None
+ self.__cliNodeObserver = None
self.refNodeId = None
self.filterUpdateThread = None
self.inputsSelector = None
@@ -290,14 +287,11 @@ def enter(self) -> None:
self._onChangedClassifier()
def _addPretrainedModelsIfAvailable(self):
- def _getNetNameFromModelName(model_name):
- return re.search(r"\(([^()]*)\)", model_name).group(1)
+ def _getNetNameFromModelName(modelName):
+ return re.search(r"\(([^()]*)\)", modelName).group(1)
- env = slicer.util.selectedModule()
- envs = tuple(map(lambda x: x.value, NodeEnvironment))
-
- if env not in envs:
- return
+ env = helpers.getCurrentEnvironment().value
+ assert env is not None, "Missing environment definition"
model_dirs = get_trained_models_with_metadata(env)
for model_dir in model_dirs:
@@ -405,6 +399,10 @@ def _checkRequirementsForApply(self):
def _onScriptFinished(self, caller, event):
if caller is None:
+ if self.cliNode is not None:
+ self.cliNode = None
+ self.cliNode.RemoveObserver(self.__cliNodeObserver)
+ self.__cliNodeObserver = None
return
if caller.GetStatusString() == "Completed":
@@ -414,19 +412,24 @@ def _onScriptFinished(self, caller, event):
f"Batch porosity analysis failed:\n\n{caller.GetErrorText().strip().splitlines()[-1]}"
)
- self._checkRequirementsForApply()
-
if not caller.IsBusy():
print("ExecCmd CLI %s" % caller.GetStatusString())
+ self.cliNode.RemoveObserver(self.__cliNodeObserver)
+ self.__cliNodeObserver = None
+ del self.cliNode
+ self.cliNode = None
+
+ self._checkRequirementsForApply()
def _validateDirs(self, inputDir, outputDir):
if not os.path.exists(inputDir):
slicer.util.errorDisplay("Input directory does not exist.")
return False
- checkpointPath = os.path.join(outputDir, "checkpoint.txt")
- if os.path.exists(checkpointPath):
- resumeMessageBox = qt.QMessageBox()
+ checkpointPath = Path(outputDir) / "checkpoint.txt"
+ if checkpointPath.exists():
+ resumeMessageBox = qt.QMessageBox(slicer.util.mainWindow())
+ resumeMessageBox.setIcon(qt.QMessageBox.Warning)
resumeMessageBox.setWindowTitle("Resume execution")
resumeMessageBox.setText(
"A previous unfinished execution was detected on the selected output directory. What do you want to do?"
@@ -440,7 +443,7 @@ def _validateDirs(self, inputDir, outputDir):
if resumeMessageBox.clickedButton() == resumeButton:
return True
elif resumeMessageBox.clickedButton() == restartButton:
- os.remove(checkpointPath)
+ checkpointPath.unlink()
return True
elif resumeMessageBox.clickedButton() == cancelButton:
return False
@@ -451,15 +454,15 @@ def _validateDirs(self, inputDir, outputDir):
return True
def _onApplyClicked(self):
- def _getSegCLIPath(model_name):
- if "bayes" in model_name.lower():
+ def _getSegCLIPath(modelName):
+ if "bayes" in modelName.lower():
cliModule = slicer.modules.bayesianinferencecli
else:
cliModule = slicer.modules.monaimodelscli
return cliModule.path
- def _getModelFilePath(model_path):
- return str(model_path if os.path.isfile(model_path) else get_pth(model_path))
+ def _getModelFilePath(modelPath):
+ return str(modelPath if os.path.isfile(modelPath) else get_pth(modelPath))
inputDir = self.inputDirectoryLineEdit.currentPath
outputDir = self.outputDirectoryLineEdit.currentPath
@@ -477,9 +480,11 @@ def _getModelFilePath(model_path):
minSize=str(self.methodSelector.currentWidget().sizeFilterThreshold.value),
usePx="all" if self.usePXCheckbox.checked else "none",
regMethod="auto" if self.regMethodCheckbox.checked else "centralized",
- maxFrags="all"
- if not self.limitFragsCheckbox.checked
- else int(self.limitFragsHBoxLayout.itemAt(0).widget().value),
+ maxFrags=(
+ "all"
+ if not self.limitFragsCheckbox.checked
+ else int(self.limitFragsHBoxLayout.itemAt(0).widget().value)
+ ),
)
),
flags=json.dumps(
@@ -494,6 +499,9 @@ def _getModelFilePath(model_path):
poreModel=_getModelFilePath(self.classifierInput.currentData),
segCLI=_getSegCLIPath(self.classifierInput.currentText),
inspectorCLI=slicer.modules.segmentinspectorcli.path,
+ foregroundCLI=slicer.modules.smartforegroundcli.path,
+ removeSpuriousCLI=slicer.modules.removespuriouscli.path,
+ cleanResinCLI=slicer.modules.cleanresincli.path,
)
if hasattr(self.methodSelector.currentWidget(), "smoothFactor"):
cliConf.update(
@@ -504,8 +512,9 @@ def _getModelFilePath(model_path):
)
self.cliNode = slicer.cli.run(slicer.modules.porestatscli, None, cliConf, wait_for_completion=False)
- self.cliNode.AddObserver("ModifiedEvent", self._onScriptFinished)
+ self.__cliNodeObserver = self.cliNode.AddObserver("ModifiedEvent", self._onScriptFinished)
self.progressBar.setCommandLineModuleNode(self.cliNode)
+ self._checkRequirementsForApply()
except Exception as e:
slicer.util.errorDisplay(f"Failed to complete execution: {e}")
self._checkRequirementsForApply()
diff --git a/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/README.md b/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/README.md
index fefdadf..7f3ac7b 100644
--- a/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/README.md
+++ b/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/README.md
@@ -3,6 +3,7 @@
Este repositório disponibiliza o *script* de cálculo de estatísticas e propriedades de poros e partículas (mais especificamente oóides) em imagens de seção delgada de rocha. Foi desenvolvido de modo a aproveitar alguns recursos do GeoSlicer e combinar com soluções próprias para a segmentação de poros e oóides e a geração de imagens e tabelas dos dados computados.
+**Nota: o módulo de segmentação de oóides ainda não foi atualizado para acompanhar as atualizações dos demais módulos, então pode não funcionar corretamente no momento.**
# Funcionamento
@@ -36,7 +37,7 @@ A princípio, o *script* foi desenvolvido para funcionar sobre as imagens dos po
### Formato esperado
-Espera-se que as imagens de um mesmo poço estejam contidas em um mesmo diretório com seu nome. Ambas as versões de iluminação em polarização direta (PP ou c1) e polarização cruzada (PX ou c2) são necessárias, exceto para as imagens expecificadas no arquivo `not_use_px.csv`. As imagens devem ser nomeadas no padrão `_(-índice-opcional)_<…>_c<1/2>.`. Exemplo:
+Espera-se que as imagens de um mesmo poço estejam contidas em um mesmo diretório com seu nome. A versão de iluminação em polarização direta (PP ou c1) é necessária, enquanto a de polarização cruzada (PX ou c2) é desejável para uma melhor qualidade da segmentação final. As imagens devem ser nomeadas no padrão `_(-índice-opcional)_<…>_c<1/2>.`. Exemplo:
```
RJS-661
@@ -97,7 +98,7 @@ Opcionalmente, também se podem gerar imagens netCDF dos resultados. Elas estar
```
-/bin/PythonSlicer pore_stats.py [--algorithm {watershed,islands}] [--pixel-size TAMANHO_DO_PIXEL] [--min-size TAMANHO_MÍNIMO] [--sigma SIGMA] [--min-distance DISTÂNCIA_MÍNIMA] [--pore-model {unet,sbayes,bbayes}] [--reg-method {centralized,auto}] [--netcdf]
+/bin/PythonSlicer pore_stats.py [--algorithm {watershed,islands}] [--pixel-size TAMANHO_DO_PIXEL] [--min-size TAMANHO_MÍNIMO] [--sigma SIGMA] [--min-distance DISTÂNCIA_MÍNIMA] [--pore-model {unet,sbayes,bbayes,CAMINHO_MODELO}] [--max-frags {custom,all,NÚMERO_FRAGMENTOS}] [--netcdf] [--keep-spurious] [--keep-residues] [--use-px {none,custom,all}] [--reg-method {centralized,auto}] [--no-images] [--no-sheets] [--no-las] [--seg-cli CAMINHO_CLI_SEGMENTAÇÃO_DE_POROS] [--foreground-cli CAMINHO_CLI_PRIMEIRO_PLANO] [--remove-spurious-cli CAMINHO_CLI_REMOÇÃO_DE_ESPÚRIOS] [--clean-resin-cli CAMINHO_CLI_LIMPEZA_DE_RESINA] [--inspector-cli CAMINHO_CLI_INSPETOR_DE_INSTÂNCIAS]
* : caminho do diretório de entrada;
* : caminho do diretório de saída;
@@ -108,6 +109,7 @@ Opcionalmente, também se podem gerar imagens netCDF dos resultados. Elas estar
* --min-distance: distância mínima (em pixeis) que separa picos nos segmentos a serem separados. Ignorado para o algoritmo "islands". Padrão: 5;
* --pore-model: modelo de segmentação binária de poros. Escolha entre "unet" (U-Net), "sbayes" (small-bayesian: modelo bayesiano de kernel pequeno small) ou "bbayes" (big-bayesian: modelo bayesiano de kernel grande), ou forneça o caminho do modelo diretamente (recomendado para versões de desenvolvimento (não-release) do GeoSlicer). Padrão: "unet";
* --max-frags: limita a quantidade máxima de fragmentos de rocha a serem analisados, do maior para o menor. Pode ser um número inteiro descrevendo a quantidade diretamente, "all" para considerar todos os fragmentos e "custom" para uma análise individual de cada imagem listada no arquivo "filter_images.csv" (use 0 para ignorar a imagem). Padrão: "custom";
+* --netcdf: se especificado, salva os resultados das segmentações em um arquivo netCDF para cada imagem;
* --keep-spurious: se especificado, detecções espúrias de poros não são removidas;
* --keep-residues: se especificado, bolhas e resíduos na resina de poro não são limpas;
* --use-px: opção de uso de imagem PX para auxiliar na limpeza da resina de poro. Se "none", apenas a imagem PP é usada. Se "all", ambas PP e PX são usadas. Se "custom", imagens que constem no arquivo "not_use_px.csv" não terão a imagem PX usada. Ignorado se --keep-residues for especificado. Padrão: "custom";
@@ -116,8 +118,10 @@ Opcionalmente, também se podem gerar imagens netCDF dos resultados. Elas estar
* --no-sheets: se especificado, as planilhas de propriedades e estatísticas não são geradas;
* --no-las: se especificado, os arquivos LAS de saída não são gerados;
* --seg-cli: caminho opcional do CLI de segmentação de poros a ser utilizado. Caso não seja especificado, é inferido automaticamente, o que é recomendado para versões release do GeoSlicer. Para versões de desenvolvimento, deve ser especificado. Padrão: inferir;
-* --inspector-cli: caminho opcional do CLI de inspeção de segmento (separação e cálculo de propriedades/estatísticas das instâncias detectadas) de poros a ser utilizado. Caso não seja especificado, é inferido automaticamente, o que é recomendado para versões release do GeoSlicer. Para versões de desenvolvimento, deve ser especificado. Padrão: inferir;
-* --netcdf: se especificado, salva os resultados das segmentações em um arquivo netCDF para cada imagem.
+* --foreground-cli: caminho opcional do CLI de segmentação de primeiro plano (área de rocha; fragmentos) a ser utilizado. Caso não seja especificado, é inferido automaticamente, o que é recomendado para versões release do GeoSlicer. Para versões de desenvolvimento, deve ser especificado. Padrão: inferir;
+--remove-spurious-cli: caminho opcional do CLI de remoção de detecções espúrias a ser utilizado. Caso não seja especificado, é inferido automaticamente, o que é recomendado para versões release do GeoSlicer. Para versões de desenvolvimento, deve ser especificado. Padrão: inferir;
+--clean-resin-cli: caminho opcional do CLI de limpeza de resina a ser utilizado. Caso não seja especificado, é inferido automaticamente, o que é recomendado para versões release do GeoSlicer. Para versões de desenvolvimento, deve ser especificado. Padrão: inferir;
+* --inspector-cli: caminho opcional do CLI de inspeção de segmento (separação e cálculo de propriedades/estatísticas das instâncias detectadas) de poros a ser utilizado. Caso não seja especificado, é inferido automaticamente, o que é recomendado para versões release do GeoSlicer. Para versões de desenvolvimento, deve ser especificado. Padrão: inferir.
```
Durante a execução, um arquivo `checkpoint.txt` no diretório de saída é atualizado com a identificação da imagem atualmente em processamento. Caso a execução seja interrompida, a próxima execução retomará o processamento a partir desta.
diff --git a/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/pore_stats.py b/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/pore_stats.py
index 05f3600..4860581 100644
--- a/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/pore_stats.py
+++ b/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/pore_stats.py
@@ -3,35 +3,38 @@
import re
import shutil
import argparse
-import warnings
import pandas as pd
+import tempfile
+from pathlib import Path
from workflow.ThinSectionLoader import ThinSectionLoader
from workflow.PoreSegmenter import PoreSegmenter
-from workflow.FragmentsSplitter import FragmentsSplitter
+from workflow.ForegroundSegmenter import ForegroundSegmenter
from workflow.PoreCleaner import PoreCleaner
from workflow.InspectorInstanceSegmenter import InspectorInstanceSegmenter
from workflow.commons import delete_tmp_nrrds, get_model_type
def main(args):
- tmp_dir = os.path.join(__file__, "..", "tmp")
+ tmp_dir = Path(tempfile.TemporaryDirectory().name)
try:
- os.makedirs(tmp_dir, exist_ok=True)
- os.makedirs(args.output_dir, exist_ok=True)
+ output_dir_path = Path(args.output_dir)
+ output_dir_path.mkdir(parents=True, exist_ok=True)
+ tmp_dir.mkdir(parents=True, exist_ok=True)
image_filter = None
no_px_filter = None
if args.max_frags == "custom":
- image_filter = pd.read_csv(os.path.join(__file__, "..", "filter_images.csv"), delimiter=";")
+ image_filter_file = Path(__file__).parent / "filter_images.csv"
+ image_filter = pd.read_csv(image_filter_file, delimiter=";")
if args.use_px == "custom":
- no_px_filter = pd.read_csv(os.path.join(__file__, "..", "not_use_px.csv"), header=None)[0]
+ no_px_filter = pd.read_csv(Path(__file__).parent / "not_use_px.csv", header=None)[0]
pores_output_dir = None
if args.export_images or args.export_sheets or args.export_las:
- pores_output_dir = os.path.join(args.output_dir, "pores")
- os.makedirs(pores_output_dir, exist_ok=True)
+ pores_output_dir = output_dir_path / "pores"
+ pores_output_dir.mkdir(parents=True, exist_ok=True)
pore_model_type = get_model_type(args.pore_model)
@@ -39,9 +42,14 @@ def main(args):
args.pixel_size, using_bayesian="bayes" in pore_model_type, do_resize=args.resize
)
pore_segmenter = PoreSegmenter(args.pore_model, args.seg_cli)
- fragments_splitter = FragmentsSplitter()
+ foreground_segmenter = ForegroundSegmenter(args.foreground_cli)
pore_cleaner = PoreCleaner(
- pore_model_type, args.keep_spurious, args.keep_residues, save_unclean_resin=args.save_unclean_resin
+ pore_model_type,
+ args.keep_spurious,
+ args.keep_residues,
+ args.remove_spurious_cli,
+ args.clean_resin_cli,
+ save_unclean_resin=args.save_unclean_resin,
)
inspector_instance_segmenter = InspectorInstanceSegmenter(
args.algorithm,
@@ -58,8 +66,8 @@ def main(args):
OoidSegmenter,
) # importando só aqui para não requerer as dependências específicas (stardist e csbdeep) desnecessariamente
- ooids_output_dir = os.path.join(args.output_dir, "ooids")
- os.makedirs(ooids_output_dir, exist_ok=True)
+ ooids_output_dir = output_dir_path / "ooids"
+ ooids_output_dir.mkdir(parents=True, exist_ok=True)
ooid_segmenter = OoidSegmenter(resized_input=args.resize)
ooids_size_min_scales_log2 = [float("-inf")] + list(range(-8, 3)) + [6, 8, float("inf")]
@@ -93,8 +101,8 @@ def main(args):
if args.netcdf:
from workflow.NetCDFExporter import NetCDFExporter
- netcdfs_output_dir = os.path.join(args.output_dir, "netCDFs")
- os.makedirs(netcdfs_output_dir, exist_ok=True)
+ netcdfs_output_dir = output_dir_path / "netCDFs"
+ netcdfs_output_dir.mkdir(parents=True, exist_ok=True)
netcdf_exporter = NetCDFExporter(netcdfs_output_dir, args.pixel_size)
generate_sheets = args.export_sheets or args.export_las
@@ -120,7 +128,7 @@ def main(args):
for filename in os.listdir(args.input_dir):
if re.search(image_file_pattern, filename):
well_names.add(filename.split("_")[0])
- image_paths.append(os.path.join(args.input_dir, filename))
+ image_paths.append((Path(args.input_dir) / filename).as_posix())
if len(image_paths) == 0:
raise FileNotFoundError("No valid images were found in the input directory.")
@@ -132,10 +140,11 @@ def main(args):
image_paths = sorted(image_paths)
n_images = len(image_paths)
image_idx = 0
- checkpoint_path = os.path.join(args.output_dir, "checkpoint.txt")
- if os.path.exists(checkpoint_path):
+ checkpoint_path = output_dir_path / "checkpoint.txt"
+ if checkpoint_path.exists():
with open(checkpoint_path, "r") as checkpoint:
resume_image_path = checkpoint.readline()
+ resume_image_path = Path(resume_image_path).as_posix()
image_idx = image_paths.index(resume_image_path)
print("\n * Resuming from", resume_image_path, end=" *\n")
image_paths = image_paths[image_idx:]
@@ -161,25 +170,25 @@ def main(args):
if n_largest_islands == 0:
continue
- px_image = None
- px_rock_area = None
+ load_px_image_path = None
+ px_rock_area_path = None
if not args.keep_residues:
if args.use_px == "all" or (no_px_filter is not None and not no_px_filter.isin([image_filename]).any()):
- px_image = thin_section_loader.run(image_path.replace("_c1", "_c2"))
+ load_px_image_path = thin_section_loader.run(image_path.replace("_c1", "_c2"), tmp_nrrd_dir=tmp_dir)
if args.reg_method == "auto":
- px_rock_area = fragments_splitter.get_rock_area(px_image)
+ px_rock_area_path = foreground_segmenter.run(load_px_image_path)
loaded_image_file_path = thin_section_loader.run(image_path, tmp_nrrd_dir=tmp_dir)
pore_binary_seg_file_path = pore_segmenter.run(loaded_image_file_path)
- frags_file_path, rock_area = fragments_splitter.run(
+ frags_file_path, rock_area_path = foreground_segmenter.run(
loaded_image_file_path, pore_binary_seg_file_path, n_largest_islands
)
pore_binary_seg_file_path = pore_cleaner.run(
frags_file_path,
pore_binary_seg_file_path,
- px_image=px_image,
- pp_rock_area=rock_area,
- px_rock_area=px_rock_area,
+ px_image_path=load_px_image_path,
+ pp_rock_area_path=rock_area_path,
+ px_rock_area_path=px_rock_area_path,
decide_best_reg=args.reg_method == "auto",
)
pore_instance_seg_file_path, pore_report_file_path = inspector_instance_segmenter.run(
@@ -187,9 +196,9 @@ def main(args):
)
if args.export_images:
- image_exporter.run(loaded_image_file_path, pore_instance_seg_file_path, pores_output_dir)
+ image_exporter.run(loaded_image_file_path, pore_instance_seg_file_path, pores_output_dir.as_posix())
if generate_sheets:
- sheet_exporter.run(loaded_image_file_path, pore_report_file_path, pores_output_dir)
+ sheet_exporter.run(loaded_image_file_path, pore_report_file_path, pores_output_dir.as_posix())
if not args.exclude_ooids:
ooid_seg_file_path = ooid_segmenter.run(frags_file_path, pore_instance_seg_file_path)
@@ -198,12 +207,12 @@ def main(args):
)
if args.export_images:
- image_exporter.run(loaded_image_file_path, ooid_seg_file_path, ooids_output_dir)
+ image_exporter.run(loaded_image_file_path, ooid_seg_file_path, ooids_output_dir.as_posix())
if generate_sheets:
sheet_exporter.run(
loaded_image_file_path,
ooid_report_file_path,
- ooids_output_dir,
+ ooids_output_dir.as_posix(),
instance_type="ooids",
groups={
"property": "max_feret (mm)",
@@ -236,10 +245,10 @@ def main(args):
if os.path.basename(d) != "LAS"
]
- las_exporter.run(image_names, sheet_exporter.stats_sheet_prefix, pores_output_dir)
+ las_exporter.run(image_names, sheet_exporter.stats_sheet_prefix, pores_output_dir.as_posix())
if not args.exclude_ooids:
las_exporter.run(
- image_names, sheet_exporter.stats_sheet_prefix, ooids_output_dir, instance_type="ooids"
+ image_names, sheet_exporter.stats_sheet_prefix, ooids_output_dir.as_posix(), instance_type="ooids"
)
if sheet_exporter.temporary:
@@ -256,12 +265,13 @@ def main(args):
for image_dir in image_dirs:
shutil.rmtree(image_dir)
- os.remove(checkpoint_path)
+ checkpoint_path.unlink(missing_ok=True)
except Exception as e:
raise e
finally:
if not args.keep_temp and os.path.exists(tmp_dir):
shutil.rmtree(tmp_dir)
+ print("Done.")
if __name__ == "__main__":
@@ -405,6 +415,30 @@ def max_frags_validator(value):
help="Path to the pore segmentation CLI to use. Must be provided for deployed versions of GeoSlicer. For release versions, it is recommended \
not to be provided, so it will be inferred automatically.",
)
+ parser.add_argument(
+ "-fc",
+ "--foreground-cli",
+ type=str,
+ default=None,
+ help="Path to the foreground segmenter CLI to use. Must be provided for deployed versions of GeoSlicer. For release versions, it is recommended \
+ not to be provided, so it will be inferred automatically.",
+ )
+ parser.add_argument(
+ "-rc",
+ "--remove-spurious-cli",
+ type=str,
+ default=None,
+ help="Path to the spurious pores remover CLI to use. Must be provided for deployed versions of GeoSlicer. For release versions, it is recommended \
+ not to be provided, so it will be inferred automatically.",
+ )
+ parser.add_argument(
+ "-cc",
+ "--clean-resin-cli",
+ type=str,
+ default=None,
+ help="Path to the resin cleaner CLI to use. Must be provided for deployed versions of GeoSlicer. For release versions, it is recommended \
+ not to be provided, so it will be inferred automatically.",
+ )
parser.add_argument(
"-ic",
"--inspector-cli",
diff --git a/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/workflow/ForegroundSegmenter.py b/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/workflow/ForegroundSegmenter.py
new file mode 100644
index 0000000..9ff3bd5
--- /dev/null
+++ b/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/workflow/ForegroundSegmenter.py
@@ -0,0 +1,80 @@
+import os
+import sys
+import nrrd
+import numpy as np
+from workflow.commons import get_check_cli_path, no_extra_dim_read, run_subprocess, write
+
+
+class ForegroundSegmenter:
+
+ ## ForegroundSegmenter: Isolamento da área de interesse da rocha
+
+ # Normalmente, as imagens de seção delgada incluem grandes áreas de borda não-úteis para a análise
+ # da rocha. Além disso, muitas imagens possuem grandes regiões "vazias", preenchidas por resina de poro,
+ # que são detectadas pelo PoreSegmenter mas que não correspondem de fato à porosidade da rocha, mas
+ # apenas à região em volta de seu(s) fragmento(s). Em alguns casos específicos, não todos mas apenas os N
+ # maiores fragmentos da seção de rocha interessam.
+ #
+ # Este módulo permite isolar a área útil (não-borda) da rocha e, opcionalmente, separar seus fragmentos.
+ # Apenas para o segundo caso a segmentação prévia da resina de poro é necessária. O processo completo
+ # inclui a seguinte sequência de operações:
+
+ # * Primeiramente, o maior fragmento, correspondente à toda área da seção, é isolado das bordas da
+ # imagem;
+ # * Então, no caso de separação dos fragmentos, toda porosidade detectada que toque a borda da imagem
+ # é também descartada, pois é interpretada como resina de poro visível ao redor da área útil da rocha;
+ # * Por fim, no caso opcional de se considerar apenas os *N* maiores fragmentos, o tamanho (em pixeis)
+ # de cada fragmento da área útil restante é medido e apenas os N maiores são mantidos.
+
+ # O NRRD dos poros detectados é atualizado descartando toda detecção não-contida na área útil da
+ # rocha.
+
+ def __init__(self, cli_path):
+ self.cli_path = get_check_cli_path(cli_file_prefix="SmartForeground", cli_path=cli_path)
+
+ def run(self, image_path, seg_path=None, n_largest_islands=None):
+ rock_seg_path = image_path.replace(".nrrd", "_seg_rock.nrrd")
+ extra_args = []
+
+ if seg_path:
+ binary_data = nrrd.read(seg_path, index_order="C")
+ binary_seg = binary_data[0][0].astype(np.uint8)
+ extra_args += ["--outputfrags", seg_path, "--poreseg", seg_path]
+
+ image_name = os.path.splitext(os.path.basename(image_path))[0]
+ print(f"Getting {'PX' if image_name.endswith('c2') else ''} rock area", end="")
+ if seg_path:
+ if n_largest_islands is None:
+ print(" and splitting fragments", end="")
+ else:
+ print(f", splitting fragments and filtering the {n_largest_islands} largest one(s)", end="")
+ print("...")
+
+ run_subprocess(
+ [
+ sys.executable,
+ self.cli_path,
+ "--input",
+ image_path,
+ "--outputrock",
+ rock_seg_path,
+ "--max_frags",
+ str(n_largest_islands) if n_largest_islands is not None else "-1",
+ ]
+ + extra_args
+ )
+
+ if seg_path:
+ frags_file_path = image_path.replace(".nrrd", "_frags.nrrd")
+
+ frags_mask = nrrd.read(seg_path, index_order="C")[0][0].astype(np.uint8)
+
+ image, image_header = no_extra_dim_read(image_path, return_header=True)
+ frags_image = image * np.stack(3 * [frags_mask], axis=2)
+
+ write(frags_file_path, frags_image, header=image_header, extra_dim=image_header["dimension"] > 3)
+ write(seg_path, binary_seg * frags_mask, header=binary_data[1])
+
+ return frags_file_path, rock_seg_path
+
+ return rock_seg_path
diff --git a/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/workflow/FragmentsSplitter.py b/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/workflow/FragmentsSplitter.py
deleted file mode 100644
index 617f8fc..0000000
--- a/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/workflow/FragmentsSplitter.py
+++ /dev/null
@@ -1,94 +0,0 @@
-import os
-import cv2
-import nrrd
-import numpy as np
-from scipy import ndimage as ndi
-from skimage import color, morphology
-from workflow.commons import no_extra_dim_read, write
-
-
-class FragmentsSplitter:
-
- ## FragmentsSplitter: Isolamento dos fragmentos de interesse da rocha
-
- # Muitas imagens possuem grandes regiões "vazias", preenchidas por resina de poro, que são
- # detectadas pelo PoreSegmenter mas que não correspondem de fato à porosidade da rocha, mas apenas à
- # região em volta de seu(s) fragmento(s). Em alguns casos específicos, não todos mas apenas os N
- # maiores fragmentos da seção de rocha interessam. Os nomes das imagens que se encaixam nessa
- # situação devem constar no arquivo `filter_images.csv`, juntamente ao valor de N. Para isolar os
- # fragmentos úteis da rocha, a seguinte sequência de operações é aplicada:
-
- # * Primeiramente, o maior fragmento, correspondente à toda área da seção, é isolado das bordas da
- # imagem;
- # * Então, toda porosidade detectada que toque a borda da imagem é também descartada, pois é
- # interpretada como resina de poro visível ao redor da área útil da rocha;
- # * Por fim, caso a imagem conste entre as precisem considerar apenas os *N* maiores fragmentos, o
- # tamanho (em pixeis) de cada fragmento da área útil restante é medido e apenas os N maiores são
- # mantidos.
-
- # O NRRD dos poros detectados é atualizado descartando toda detecção não-contida na área útil da
- # rocha.
-
- def _filter_largest_islands(self, seg, n_largest_islands):
-
- # Filtra os N maiores fragmentos.
-
- print("** Filtering the", n_largest_islands, "largest island(s)... ***")
-
- seg = cv2.erode(seg.astype(np.uint8), np.ones((23, 23), np.uint8)) # para limpar pequenos artefatos
- islands = ndi.label(seg)[0]
- islands_sizes = np.bincount(islands.ravel())
- sorted_labels = np.argsort(islands_sizes)[::-1]
- sorted_labels = sorted_labels[sorted_labels != 0]
- seg[~np.isin(islands, sorted_labels[:n_largest_islands])] = 0
-
- print("*** Filtered. ***")
- return seg.astype(bool)
-
- def get_rock_area(self, image):
-
- # Isola a área da rocha da borda da imagem
-
- def equalize_each_channel(image):
- return np.stack([cv2.equalizeHist(image[:, :, i]) for i in range(3)], axis=2)
-
- eq = equalize_each_channel(image)
- blur = cv2.GaussianBlur(eq, (199, 199), 255)
-
- lum = color.rgb2gray(blur)
- mask = morphology.remove_small_holes(morphology.remove_small_objects((lum > 0.3) & (lum < 0.7), 500), 500)
-
- mask = morphology.opening(mask, morphology.disk(3))
- mask = self._filter_largest_islands(
- mask, 1
- ) # para pegar a área central da rocha e eliminar artefatos deixados nas bordas
- return ndi.binary_fill_holes(mask)
-
- def run(self, image_path, seg_path, n_largest_islands=None):
- frags_file_path = image_path.replace(".nrrd", "_frags.nrrd")
- image, image_header = no_extra_dim_read(image_path, return_header=True)
-
- binary_data = nrrd.read(seg_path, index_order="C")
- binary_seg = binary_data[0][0]
-
- # Separa a área da rocha das bordas as imagem
- binary_seg_with_border = binary_seg.copy()
- rock_area = self.get_rock_area(image)
-
- if not os.path.exists(frags_file_path):
- # Funde as bordas da imagem aos poros detectados que as tocam
- binary_seg_with_border[np.where(rock_area == 0)] = 1
-
- # Descarta o conjunto borda + poros periféricos
- binary_frags = np.logical_not(binary_seg_with_border)
- frags_mask = ndi.binary_fill_holes(binary_frags)
-
- # Filtra os N maiores fragmentos se necessário
- if n_largest_islands:
- frags_mask = self._filter_largest_islands(frags_mask, n_largest_islands)
-
- frags_image = image * np.stack(3 * [frags_mask], axis=2)
-
- write(frags_file_path, frags_image, header=image_header)
- write(seg_path, binary_seg * frags_mask, header=binary_data[1])
- return frags_file_path, rock_area
diff --git a/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/workflow/InspectorInstanceSegmenter.py b/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/workflow/InspectorInstanceSegmenter.py
index 8595809..dcc5897 100644
--- a/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/workflow/InspectorInstanceSegmenter.py
+++ b/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/workflow/InspectorInstanceSegmenter.py
@@ -2,7 +2,7 @@
import sys
import nrrd
from scipy import ndimage as ndi
-from workflow.commons import run_subprocess, write, get_cli_modules_dir, dict_to_arg
+from workflow.commons import get_check_cli_path, run_subprocess, write, dict_to_arg
class InspectorInstanceSegmenter:
@@ -24,16 +24,9 @@ def __init__(self, algorithm, min_size, sigma, min_distance, pixel_size, inspect
self.sigma = sigma
self.min_distance = min_distance
self.pixel_size = pixel_size
- self.inspector_cli_path = self._get_cli_path(inspector_cli_path)
+ self.inspector_cli_path = get_check_cli_path(cli_file_prefix="SegmentInspector", cli_path=inspector_cli_path)
self.no_cli = no_cli
- def _get_cli_path(self, cli_path):
- if cli_path is None:
- cli_path = os.path.join(get_cli_modules_dir(), "SegmentInspectorCLI", "SegmentInspectorCLI.py")
-
- assert os.path.exists(cli_path), f"CLI {cli_path} not found."
- return cli_path
-
def _get_params(self):
if self.algorithm == "islands":
params = {
@@ -70,6 +63,7 @@ def run(self, nrrd_seg_file_path, generate_partitions):
else:
params = self._get_params()
+ print("Getting properties and statistics...")
run_subprocess(
[
sys.executable,
@@ -86,7 +80,7 @@ def run(self, nrrd_seg_file_path, generate_partitions):
report_file_path,
"--returnparameterfile",
params_file_path,
- ]
+ ],
)
os.remove(params_file_path)
diff --git a/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/workflow/PoreCleaner.py b/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/workflow/PoreCleaner.py
index 542f111..55ffd84 100644
--- a/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/workflow/PoreCleaner.py
+++ b/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/workflow/PoreCleaner.py
@@ -1,16 +1,8 @@
-import os
-import pickle
-import time
-import warnings
-import cv2
-import joblib
+import sys
import nrrd
import numpy as np
-from scipy import ndimage as ndi
-from skimage import measure
-from tqdm import tqdm
-from skimage.segmentation import watershed
-from workflow.commons import no_extra_dim_read, write
+
+from workflow.commons import run_subprocess, write
class PoreCleaner:
@@ -45,274 +37,81 @@ class PoreCleaner:
# em que o artefato tome 100% da área do poro;
# 3. **Ser pouco visível na imagem PX/c2:** alguns elementos da rocha podem ser parecidos com os
# artefatos e também ter contato com a resina. Porém, no geral, os artefatos são pouco ou nada
- # visíveis nas imagens PX/c2, enquanto os demais elementos são geralmente notáveis. Algumas imagens
- # do poço RJS-702, porém, não têm um bom alinhamento natual ou facilmente corrigível entre as imagens
- # PP e PX, dificultando essa correspondência de regiões. Esta etapa é então ignorada para essas
- # imagens, listadas no arquivo `not_use_px.csv`, o que pode resultar em sobressegmentação dos poros
- # por capturar falsos artefatos.
-
- # O NRRD de poros é novamente atualizado, desta vez com os poros "limpos".
-
- def __init__(self, pore_model, keep_spurious, keep_residues, save_unclean_resin=False):
+ # visíveis nas imagens PX/c2, enquanto os demais elementos são geralmente notáveis. Note que essa
+ # análise exige um bom registro (alinhamento espacial) entre as imagens PP/c1 e PX/c2. O algoritmo
+ # tenta corrigir possíveis desalinhamentos centralizando uma imagem sobre a outra, opcionalmente
+ # considerando apenas a área útil (obtida através do recurso ForegroundSegmenter) de cada uma.
+
+ # O NRRD de poros é novamente atualizado, desta vez com a porosidade "limpa" com base no(s) recurso(s)
+ # aplicado(s).
+
+ def __init__(
+ self,
+ pore_model,
+ keep_spurious,
+ keep_residues,
+ remove_spurious_cli_path,
+ clean_resin_cli_path,
+ save_unclean_resin=False,
+ ):
self.pore_model = pore_model
self.keep_spurious = keep_spurious
self.keep_residues = keep_residues
+ self.remove_spurious_cli_path = remove_spurious_cli_path
+ self.clean_resin_cli_path = clean_resin_cli_path
self.save_unclean_resin = save_unclean_resin
- def _remove_spurious(self, image, binary_seg):
- def get_roi_from_centroid(cy, cx, image, seg, i_seg, roi_size):
- def get_ref_point(cy, cx, seg, i_seg):
- def try_getting_ref_point(y, x, seg):
- if any(np.isnan(coord) for coord in [y, x]):
- return False, y, x
- y, x = int(y), int(x)
- return seg[y, x], y, x
-
- cy, cx = np.clip(int(cy), 0, seg.shape[0] - 1), np.clip(int(cx), 0, seg.shape[1] - 1)
-
- if not seg[cy, cx]:
- seg_y, seg_x = np.where(seg == i_seg)
-
- y, x = cy, cx
- success, y, x = try_getting_ref_point(cy, np.median(seg_x[(seg_x < cx) & (seg_y == cy)]), seg)
- if not success:
- success, y, x = try_getting_ref_point(cy, np.median(seg_x[(seg_x > cx) & (seg_y == cy)]), seg)
- if not success:
- success, y, x = try_getting_ref_point(np.median(seg_y[(seg_y < cy) & (seg_x == cx)]), cx, seg)
- if not success:
- success, y, x = try_getting_ref_point(np.median(seg_y[(seg_y > cy) & (seg_x == cx)]), cx, seg)
-
- cy = y
- cx = x
-
- return cy, cx
-
- offset = roi_size // 2
-
- # Em alguns casos, o centróide do segmento reside fora dele. Então, tenta-se obter o pixel mediano do segmento à esquerda
- # do centróide. Se não houver, tenta-se à direita. Então, acima. Em último caso, abaixo.
- cy, cx = get_ref_point(cy, cx, seg, i_seg)
- y0, x0 = max(0, cy - offset), max(0, cx - offset)
- y1, x1 = y0 + roi_size, x0 + roi_size
- if y1 > image.shape[0]:
- d = y1 - image.shape[0]
- y0, y1 = y0 - d, y1 - d
- if x1 > image.shape[1]:
- d = x1 - image.shape[1]
- x0, x1 = x0 - d, x1 - d
- assert y1 - y0 == roi_size
- assert x1 - x0 == roi_size
-
- return image[y0:y1, x0:x1].flatten()
-
- split_pores = ndi.label(binary_seg)[0]
-
- if split_pores.max() > 0:
- # Há um modelo RandomForest de remoção de poros espúrios para cada modelo de segmentação de poros
- with open(
- os.path.join(__file__, "..", "..", "models", "spurious_removal", f"spurious_{self.pore_model}.pkl"),
- "rb",
- ) as pkl:
- scaler_and_model = pickle.load(pkl)
- scaler = scaler_and_model["scaler"]
- model = scaler_and_model["model"]
-
- # Para cada segmento de poro, é obtida uma pequena região de interesse (ROI) em volta do centróide
- with warnings.catch_warnings():
- warnings.filterwarnings("ignore")
-
- print("Removing spurious pore detections...")
- start_time = time.time()
- regions_props = measure.regionprops(split_pores, intensity_image=image)
-
- rois = []
- pred_indexes = []
- for i in tqdm(range(1, split_pores.max() + 1)):
- region_props = regions_props[i - 1]
- if (
- region_props.area < 3
- ): # porque o Segment Inspector não inclui segmentos com menos de 3 pixeis no relatório
- split_pores[split_pores == i] = 0
- else:
- cy, cx = region_props.centroid
- roi = get_roi_from_centroid(cy, cx, image, split_pores, i, roi_size=10)
- rois.append(roi)
- pred_indexes.append(i)
-
- if len(rois) > 0:
- # O modelo detecta os ROIs espúrios e os descarta
- predictions = model.predict(scaler.transform(np.array(rois)))
- valid_pred_indexes = np.array(pred_indexes)[np.nonzero(predictions)[0]]
-
- split_pores = np.where(np.isin(split_pores, valid_pred_indexes), split_pores, 0)
- n_valid = len(valid_pred_indexes)
- else:
- n_valid = 0
-
- n_discarded = i - n_valid
-
- print(f"Done: {n_valid} detections kept, {n_discarded} discarded ({time.time() - start_time}s).")
- else:
- print("No pores found.")
-
- return split_pores.astype(bool)
-
- def _clean_resin(self, image, binary_seg, px_image, pp_rock_area, px_rock_area, decide_best_reg):
- def remove_artifacts(mask, open_kernel_size, close_kernel_size):
- if open_kernel_size is not None:
- open_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (open_kernel_size, open_kernel_size))
- mask = cv2.morphologyEx(mask.astype(np.uint8), cv2.MORPH_OPEN, open_kernel)
- if close_kernel_size is not None:
- close_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (close_kernel_size, close_kernel_size))
- mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, close_kernel).astype(bool)
-
- return mask
-
- def get_roi_from_blue_channel(image):
- blue_channel = image[:, :, 2]
- blue_channel = cv2.equalizeHist(blue_channel)
-
- # with open(os.path.join(__file__, '..', '..', 'models', 'pore_residues', 'blue_channel.pkl'), 'rb') as pkl:
- # kmeans = pickle.load(pkl)
-
- # Usando joblib em vez de pickle para aproveitar a compressão do modelo (K-Means pickle fica muito grande)
- # O modelo joblib foi salvo usando a mesma versão do Python usado para executar este script (PythonSlicer). Divergência de versão causa erro.
- kmeans = joblib.load(os.path.join(__file__, "..", "..", "models", "pore_residues", "blue_channel.pkl"))
-
- clusters = kmeans.predict(blue_channel.flatten().reshape(-1, 1))
- blue_mask = clusters.reshape(blue_channel.shape) == kmeans.cluster_centers_.argmax()
- return remove_artifacts(blue_mask, open_kernel_size=20, close_kernel_size=None)
-
- def get_roi_from_hue_channel(image):
- hue_channel = cv2.cvtColor(image, cv2.COLOR_RGB2HSV)[:, :, 0]
-
- hue_mask = (hue_channel >= 75) & (
- hue_channel <= 135
- ) # blue hue, which catches both the resin and the dark bubbles/residues
- for open_kernel_size, close_kernel_size in [(5, None), (None, 13), (13, None)]:
- hue_mask = remove_artifacts(
- hue_mask, open_kernel_size=open_kernel_size, close_kernel_size=close_kernel_size
- )
-
- return hue_mask
-
- def get_roi_from_px_hsv(pp, px, pores_mask, decide_best_reg):
- def crop_rock_area(image, rock_area):
- non_zero_coords = cv2.findNonZero(rock_area.astype(np.uint8))
- x, y, w, h = cv2.boundingRect(non_zero_coords)
- crop = image[y : y + h, x : x + w]
- return crop
-
- def register_px_to_pp(pp, px, pp_rock_area=None, px_rock_area=None):
- reg_px = np.zeros((max(pp.shape[0], px.shape[0]), max(pp.shape[1], px.shape[1]), 3)).astype(np.uint8)
- orig_pp_shape = pp.shape
-
- if pp_rock_area is not None:
- pp = crop_rock_area(pp, pp_rock_area)
- if px_rock_area is not None:
- px = crop_rock_area(px, px_rock_area)
-
- px_y0 = reg_px.shape[0] // 2 - px.shape[0] // 2
- px_y1 = reg_px.shape[0] // 2 + px.shape[0] // 2 + px.shape[0] % 2
- px_x0 = reg_px.shape[1] // 2 - px.shape[1] // 2
- px_x1 = reg_px.shape[1] // 2 + px.shape[1] // 2 + px.shape[1] % 2
-
- reg_px[px_y0:px_y1, px_x0:px_x1] = px.copy()
-
- pp_y0 = reg_px.shape[0] // 2 - orig_pp_shape[0] // 2
- pp_y1 = reg_px.shape[0] // 2 + orig_pp_shape[0] // 2 + orig_pp_shape[0] % 2
- pp_x0 = reg_px.shape[1] // 2 - orig_pp_shape[1] // 2
- pp_x1 = reg_px.shape[1] // 2 + orig_pp_shape[1] // 2 + orig_pp_shape[1] % 2
-
- return reg_px[pp_y0:pp_y1, pp_x0:pp_x1]
-
- reg_px = {
- "Centralized": register_px_to_pp(pp, px),
- }
- if decide_best_reg:
- reg_px.update(
- {
- "Cropped and centralized": register_px_to_pp(
- pp, px, pp_rock_area=pp_rock_area, px_rock_area=px_rock_area
- )
- }
- )
- px_pores_mask = None
- best_reg_quality = -1
- best_method = None
- for method, px in reg_px.items():
- px_hsv = cv2.cvtColor(cv2.GaussianBlur(px, (99, 99), 9), cv2.COLOR_RGB2HSV)
-
- kmeans = joblib.load(os.path.join(__file__, "..", "..", "models", "pore_residues", "px_hsv.pkl"))
- clusters = kmeans.predict(px_hsv.flatten().reshape(-1, 3))
- test_px_pores_mask = clusters.reshape(px_hsv.shape[:2]) == 3
- test_px_pores_mask = remove_artifacts(test_px_pores_mask, open_kernel_size=13, close_kernel_size=13)
-
- reg_area = np.count_nonzero(test_px_pores_mask & pores_mask)
- pore_area = np.count_nonzero(pores_mask)
- test_reg_quality = reg_area / pore_area if pore_area > 0 else 0
- print(method, "registration quality:", "{:.2f} %".format(100 * test_reg_quality))
- if test_reg_quality > best_reg_quality:
- best_method = method
- best_reg_quality = test_reg_quality
- px_pores_mask = test_px_pores_mask
-
- print(best_method, "registration method chosen.")
- return px_pores_mask
-
- def grow_pores_through_mask(mask, pores):
- markers = pores & mask
- return watershed(~mask, markers=markers, mask=mask)
-
- use_px = px_image is not None
-
- print("Detecting air bubbles and residues in pore resin... Using PX:", {False: "No", True: "Yes"}[use_px])
- start_time = time.time()
-
- # O canal azul da imagem funde as bolhas brancas à resina azul
- blue_mask = get_roi_from_blue_channel(image)
- # O canal Hue da imagem funde as bolhas negras e resíduos à resina azul
- hue_mask = get_roi_from_hue_channel(image)
-
- if use_px:
- # A região escura do PX funde as bolhas e resíduos à região porosa
- px_pores_mask = get_roi_from_px_hsv(image, px_image, binary_seg, decide_best_reg)
-
- # As regiões não-escuras do PX são descartadas das regiões úteis dos canais azul e Hue
- blue_mask &= px_pores_mask
- hue_mask &= px_pores_mask
-
- # Os poros detectados crescem sobre a região azul, cobrindo as bolhas brancas
- bubbled_blue_mask = grow_pores_through_mask(blue_mask, binary_seg)
- # Os poros detectados crescem sobre a região Hue, cobrindo as bolhas negras e resíduos
- bubbled_hue_mask = grow_pores_through_mask(hue_mask, binary_seg)
-
- print(f"Done ({time.time() - start_time}s).")
- # As regiões crescidas são unidas. Os poros originais são reinclusos para o caso de terem sido
- # perdidos por um mal alinhamento entre PP e PX
- return bubbled_blue_mask | bubbled_hue_mask | binary_seg
-
- def run(self, frags_file_path, seg_path, px_image, pp_rock_area, px_rock_area, decide_best_reg):
+ def run(self, frags_file_path, seg_path, px_image_path, pp_rock_area_path, px_rock_area_path, decide_best_reg):
if self.keep_spurious and self.keep_residues:
return seg_path
- image = no_extra_dim_read(frags_file_path)
- binary_data = nrrd.read(seg_path, index_order="C")
- binary_seg = binary_data[0][0].astype(bool)
-
unclean_resin_path = seg_path.replace(".nrrd", "_noclean.nrrd")
if not self.keep_spurious:
# Remoção de poros espúrios
- binary_seg = (
- self._remove_spurious(image, binary_seg)
- if not os.path.exists(unclean_resin_path)
- else nrrd.read(unclean_resin_path, index_order="C")[0][0]
+ run_subprocess(
+ [
+ sys.executable,
+ self.remove_spurious_cli_path,
+ "--input",
+ frags_file_path,
+ "--output",
+ seg_path,
+ "--poreseg",
+ seg_path,
+ "--poresegmodel",
+ self.pore_model,
+ ]
)
+
if not self.keep_residues:
if self.save_unclean_resin:
+ binary_data = nrrd.read(seg_path, index_order="C")
+ binary_seg = binary_data[0][0].astype(bool)
+
write(unclean_resin_path, binary_seg.astype(np.uint8), header=binary_data[1])
+
# Incorporação de bolhas e resíduos na resina de poro
- binary_seg = self._clean_resin(image, binary_seg, px_image, pp_rock_area, px_rock_area, decide_best_reg)
+ extra_args = []
+ for arg, array_path in zip(
+ ["--pximage", "--pprockarea", "--pxrockarea"], [px_image_path, pp_rock_area_path, px_rock_area_path]
+ ):
+ if array_path is not None:
+ extra_args += [arg, array_path]
+ if decide_best_reg:
+ extra_args.append("--smartreg")
+
+ run_subprocess(
+ [
+ sys.executable,
+ self.clean_resin_cli_path,
+ "--ppimage",
+ frags_file_path,
+ "--poreseg",
+ seg_path,
+ "--output",
+ seg_path,
+ ]
+ + extra_args
+ )
- write(seg_path, binary_seg.astype(np.uint8), header=binary_data[1])
return seg_path
diff --git a/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/workflow/PoreSegmenter.py b/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/workflow/PoreSegmenter.py
index ff62096..0091a09 100644
--- a/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/workflow/PoreSegmenter.py
+++ b/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/workflow/PoreSegmenter.py
@@ -2,7 +2,7 @@
import sys
from workflow.commons import (
dict_to_arg,
- get_cli_modules_dir,
+ get_check_cli_path,
get_model_type,
get_models_dir,
get_models_info,
@@ -55,10 +55,7 @@ def get_cli_info():
return cli_file_prefix, xargs, extra_args, model_type
cli_file_prefix, xargs, extra_args, model_type = get_cli_info()
- if cli_path is None:
- cli_path = os.path.join(get_cli_modules_dir(), f"{cli_file_prefix}CLI", f"{cli_file_prefix}CLI.py")
-
- assert os.path.exists(cli_path), f"CLI {cli_path} not found."
+ cli_path = get_check_cli_path(cli_file_prefix, cli_path)
assert ("bayes" in model_type) == (
cli_file_prefix == "BayesianInference"
), f"{cli_file_prefix}CLI is not compatible with model of type {model_type}"
@@ -68,6 +65,7 @@ def get_cli_info():
def run(self, nrrd_image_file_path):
output_file_path = nrrd_image_file_path.replace(".nrrd", "_seg.nrrd")
if not os.path.exists(output_file_path):
+ print("* Segmenting pores...")
run_subprocess(
[
sys.executable,
diff --git a/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/workflow/README.md b/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/workflow/README.md
index 67eec17..b99fe70 100644
--- a/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/workflow/README.md
+++ b/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/workflow/README.md
@@ -2,17 +2,13 @@
Uma vez iniciada a execução do *script* principal, o programa itera sobre as imagens do diretório de entrada e executa uma série de operações. No geral, as operações salvam arquivos temporários em formato NRRD e retornam o caminho onde foram armazenados, de modo que possam ser utilizados pelas aplicações CLI do GeoSlicer em etapas posteriores.
-
-Caso o nome da imagem conste no arquivo `filter_images.csv` e o número de ilhas úteis conste como 0, a imagem é ignorada.
-
-
![](overview/overview.png)
## [ThinSectionLoader](ThinSectionLoader.py): Carregamento das imagens
-A imagem PP/c1 é carregada e salva em formato NRRD compatível com as aplicações em CLI executadas nas etapas posteriores. O cabeçalho do arquivo é diferente de acordo com o modelo de segmentação de poro escolhido (Bayesiano ou neural), visto que são administrados por diferentes CLI's. A menos que o nome da imagem conste no arquivo `not_use_px.csv`, a versão PX/c2 também é carregada para uso posterior, porém é mantida apenas em memória em vez de salva em disco.
+A imagem PP/c1 é carregada e salva em formato NRRD compatível com as aplicações em CLI executadas nas etapas posteriores. O cabeçalho do arquivo é diferente de acordo com o modelo de segmentação de poro escolhido (Bayesiano ou neural), visto que são administrados por diferentes CLI's. Opcionalmente, a versão PX/c2 também é carregada para uso posterior.
## [PoreSegmenter](PoreSegmenter.py): Segmentação binária de poros
@@ -21,18 +17,20 @@ A imagem PP/c1 é carregada e salva em formato NRRD compatível com as aplicaç
O CLI de segmentação binária de poro é invocado para operar a partir do caminho para o NRRD da imagem original, salvo pelo [ThinSectionLoader](ThinSectionLoader.py). O resultado é salvo em um novo NRRD.
-## [FragmentsSplitter](FragmentsSplitter.py): Isolamento dos fragmentos de interesse da rocha
+## [ForegroundSegmenter](ForegroundSegmenter.py): Isolamento dos fragmentos de interesse da rocha
+
+Normalmente, as imagens de seção delgada incluem grandes áreas de borda não-úteis para a análise da rocha. Além disso, muitas imagens possuem grandes regiões "vazias", preenchidas por resina de poro, que são detectadas pelo [PoreSegmenter](PoreSegmenter.py) mas que não correspondem de fato à porosidade da rocha, mas apenas à região em volta de seu(s) fragmento(s). Em alguns casos específicos, não todos mas apenas os *N* maiores fragmentos da seção de rocha interessam.
-Muitas imagens possuem grandes regiões "vazias", preenchidas por resina de poro, que são detectadas pelo [PoreSegmenter](PoreSegmenter.py) mas que não correspondem de fato à porosidade da rocha, mas apenas à região em volta de seu(s) fragmento(s). Em alguns casos específicos, não todos mas apenas os *N* maiores fragmentos da seção de rocha interessam. Os nomes das imagens que se encaixam nessa situação devem constar no arquivo `filter_images.csv`, juntamente ao valor de *N*. Para isolar os fragmentos úteis da rocha, a seguinte sequência de operações é aplicada:
+Este módulo permite isolar a área útil (não-borda) da rocha e, opcionalmente, separar seus fragmentos. Apenas para o segundo caso a segmentação prévia da resina de poro é necessária. O processo completo inclui a seguinte sequência de operações:
* Primeiramente, o maior fragmento, correspondente à toda área da seção, é isolado das bordas da imagem;
-* Então, toda porosidade detectada que toque a borda da imagem é também descartada, pois é interpretada como resina de poro visível ao redor da área útil da rocha;
-* Por fim, caso a imagem conste entre as precisem considerar apenas os *N* maiores fragmentos, o tamanho (em pixeis) de cada fragmento da área útil restante é medido e apenas os *N* maiores são mantidos.
+* Então, no caso de separação dos fragmentos, toda porosidade detectada que toque a borda da imagem é também descartada, pois é interpretada como resina de poro visível ao redor da área útil da rocha;
+* Por fim, no caso opcional de se considerar apenas os *N* maiores fragmentos, o tamanho (em pixeis) de cada fragmento da área útil restante é medido e apenas os *N* maiores são mantidos.
-O NRRD dos poros detectados é atualizado descartando toda detecção não-contida na área útil da rocha.
+No caso de separação dos fragmentos, o NRRD dos poros detectados é atualizado descartando toda detecção não-contida na área útil da rocha.
## [PoreCleaner](PoreCleaner.py): Remoção de poros espúrios e artefatos na resina
@@ -54,10 +52,10 @@ Os segmentadores de poro atuais do GeoSlicer tendem a gerar detecções espúria
1. **Ter cor branca ou ter cor azul com pouca intensidade e saturação:** em geral, as bolhas são brancas ou, quando cobertas de material, têm um tom de azul quase negro. Os resíduos que eventualmente circundam as bolhas também tem um nível de azul pouco intenso;
2. **Tocar na resina de poro:** a transição entre a resina e os artefatos é normalmente direta e suave. Como o modelo de segmentação de poro detecta bem a região de resina, o artefato precisa tocar nessa região. Consequentemente, o algoritmo atual não consegue detectar casos menos comuns em que o artefato tome 100% da área do poro;
-3. **Ser pouco visível na imagem PX/c2:** alguns elementos da rocha podem ser parecidos com os artefatos e também ter contato com a resina. Porém, no geral, os artefatos são pouco ou nada visíveis nas imagens PX/c2, enquanto os demais elementos são geralmente notáveis. Algumas imagens do poço RJS-702, porém, não têm um bom alinhamento natual ou facilmente corrigível entre as imagens PP e PX, dificultando essa correspondência de regiões. Esta etapa é então ignorada para essas imagens, listadas no arquivo `not_use_px.csv`, o que pode resultar em sobressegmentação dos poros por capturar falsos artefatos.
+3. **Ser pouco visível na imagem PX/c2:** alguns elementos da rocha podem ser parecidos com os artefatos e também ter contato com a resina. Porém, no geral, os artefatos são pouco ou nada visíveis nas imagens PX/c2, enquanto os demais elementos são geralmente notáveis. Note que essa análise exige um bom registro (alinhamento espacial) entre as imagens PP/c1 e PX/c2. O algoritmo tenta corrigir possíveis desalinhamentos centralizando uma imagem sobre a outra, opcionalmente considerando apenas a área útil (obtida através do recurso [ForegroundSegmenter](ForegroundSegmenter.py)) de cada uma.
-O NRRD de poros é novamente atualizado, desta vez com os poros "limpos".
+O NRRD de poros é novamente atualizado, desta vez com porosidade "limpa" com base no(s) recurso(s) aplicado(s).
## [OoidSegmenter](OoidSegmenter.py): Segmentação de oóides
diff --git a/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/workflow/ThinSectionLoader.py b/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/workflow/ThinSectionLoader.py
index 258a1c3..4ac8420 100644
--- a/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/workflow/ThinSectionLoader.py
+++ b/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/workflow/ThinSectionLoader.py
@@ -15,9 +15,9 @@ class ThinSectionLoader:
# A imagem PP/c1 é carregada e salva em formato NRRD compatível com as aplicações em CLI executadas nas etapas
# posteriores. O cabeçalho do arquivo é diferente de acordo com o modelo de segmentação de poro escolhido
- # (Bayesiano ou neural), visto que são administrados por diferentes CLI's. A menos que o nome da imagem
- # conste no arquivo `not_use_px.csv`, a versão PX/c2 também é carregada para uso posterior, porém é mantida
- # apenas em memória em vez de salva em disco.
+ # (Bayesiano ou neural), visto que são administrados por diferentes CLI's. Opcionalmente, a versão PP/c2 também
+ # pode ser carregada para uso posterior, mais especificamente para incorporação de bolhas e resíduos na resina
+ # de poro através do módulo PoreCleaner.
THIN_SECTION_LOADER_FILE_EXTENSIONS = [".tif", ".tiff", ".png", ".jpg", ".jpeg"]
@@ -46,7 +46,7 @@ def run(self, image_path, tmp_nrrd_dir=None):
if self.do_resize:
image = cv2.resize(image, (0, 0), fx=0.1, fy=0.1)
- # Apenas a imagem PP tem o NRRD salvo. A PX é carregada apenas em memória.
+ # Fornecer o caminho do diretório temporário faz com que o NRRD seja salvo. Caso contrário, a imagem é carregada apenas em memória.
if tmp_nrrd_dir is not None:
image_name = os.path.splitext(os.path.basename(image_path))[0]
output_file_path = os.path.join(tmp_nrrd_dir, f"{image_name}.nrrd")
diff --git a/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/workflow/commons.py b/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/workflow/commons.py
index cfec6da..f67821d 100644
--- a/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/workflow/commons.py
+++ b/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/workflow/commons.py
@@ -59,6 +59,14 @@ def get_cli_modules_dir():
raise FileNotFoundError(f"CLI modules directory {cli_modules_dir} not found.")
+def get_check_cli_path(cli_file_prefix, cli_path=None):
+ if cli_path is None:
+ cli_path = os.path.join(get_cli_modules_dir(), f"{cli_file_prefix}CLI", f"{cli_file_prefix}CLI.py")
+
+ assert os.path.exists(cli_path), f"CLI {cli_path} not found."
+ return cli_path
+
+
def get_models_dir():
candidate_dirs = []
geoslicer_path = sys.executable.split(os.path.join(os.sep, "bin"))[0]
@@ -121,10 +129,20 @@ def get_model_type(model_type_or_path):
def run_subprocess(args):
- process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
+ process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1)
+ for line in process.stdout:
+ if line.startswith(r"No module named 'logic'"):
+ continue
+ if not line.startswith(""):
+ print(f"\n\n{line}", end="")
+ else:
+ print(
+ f"\r{line[:-1]}".ljust(70), end="", flush=True
+ ) # to update the progress line in terminal instead of printing in subsequent lines
_, error = process.communicate()
if process.returncode != 0:
raise RuntimeError(error)
+ print()
# from SegmentInspectorCLI.py
diff --git a/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/workflow/overview/overview.png b/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/workflow/overview/overview.png
new file mode 100644
index 0000000..c1e1fad
Binary files /dev/null and b/src/modules/PoreStats/PoreStatsCLI/Libs/pore_stats/workflow/overview/overview.png differ
diff --git a/src/modules/PoreStats/PoreStatsCLI/PoreStatsCLI.py b/src/modules/PoreStats/PoreStatsCLI/PoreStatsCLI.py
index a2b1079..f1bf6e3 100644
--- a/src/modules/PoreStats/PoreStatsCLI/PoreStatsCLI.py
+++ b/src/modules/PoreStats/PoreStatsCLI/PoreStatsCLI.py
@@ -4,13 +4,14 @@
from __future__ import print_function
import json
+import numpy as np
import os
import re
import subprocess
import sys
+from pathlib import Path
from ltrace.slicer.cli_utils import progressUpdate
-import numpy as np
def getProgress(bufferedLine):
@@ -33,9 +34,22 @@ def capitalizedToHyphenated(arg):
def runcli(args):
python_executable = sys.executable
- script_path = os.path.join(__file__, "..", "Libs", "pore_stats", "pore_stats.py")
+ script_path = (Path(__file__).parent / "Libs" / "pore_stats" / "pore_stats.py").as_posix()
required_args = [args.inputDir, args.outputDir]
- optional_paths = ["--pore-model", args.poreModel, "--seg-cli", args.segCLI, "--inspector-cli", args.inspectorCLI]
+ optional_paths = [
+ "--pore-model",
+ args.poreModel,
+ "--seg-cli",
+ args.segCLI,
+ "--inspector-cli",
+ args.inspectorCLI,
+ "--foreground-cli",
+ args.foregroundCLI,
+ "--remove-spurious-cli",
+ args.removeSpuriousCLI,
+ "--clean-resin-cli",
+ args.cleanResinCLI,
+ ]
optional_params = (
np.array([[capitalizedToHyphenated(param), value] for param, value in json.loads(args.params).items()])
.flatten()
@@ -57,7 +71,7 @@ def runcli(args):
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
- shell=True,
+ shell=True if sys.platform.startswith("win32") else False,
)
progress = 0
@@ -93,6 +107,19 @@ def runcli(args):
parser.add_argument(
"--inspectorcli", dest="inspectorCLI", type=str, default=None, help="Path to the segment inspector CLI to use"
)
+ parser.add_argument(
+ "--foregroundcli", dest="foregroundCLI", type=str, default=None, help="Path to the smart foreground CLI to use"
+ )
+ parser.add_argument(
+ "--removespuriouscli",
+ dest="removeSpuriousCLI",
+ type=str,
+ default=None,
+ help="Path to the spurious removal CLI to use",
+ )
+ parser.add_argument(
+ "--cleanresincli", dest="cleanResinCLI", type=str, default=None, help="Path to the resin cleaning CLI to use"
+ )
args = parser.parse_args()
diff --git a/src/modules/PoreStats/PoreStatsCLI/PoreStatsCLI.xml b/src/modules/PoreStats/PoreStatsCLI/PoreStatsCLI.xml
index 2231e14..3f02d65 100644
--- a/src/modules/PoreStats/PoreStatsCLI/PoreStatsCLI.xml
+++ b/src/modules/PoreStats/PoreStatsCLI/PoreStatsCLI.xml
@@ -25,7 +25,7 @@
outputDiroutputdir
-
+ output
@@ -36,7 +36,7 @@
paramsparams
-
+
@@ -44,7 +44,7 @@
flagsflags
-
+
@@ -62,7 +62,7 @@
segCLIsegcli
-
+ input
@@ -71,7 +71,34 @@
inspectorCLIinspectorcli
-
+
+ input
+
+
+
+
+ foregroundCLI
+
+ foregroundcli
+
+ input
+
+
+
+
+ removeSpuriousCLI
+
+ removespuriouscli
+
+ input
+
+
+
+
+ cleanResinCLI
+
+ cleanresincli
+ input
diff --git a/src/modules/PoreStats/RemoveSpuriousCLI/RemoveSpuriousCLI.py b/src/modules/PoreStats/RemoveSpuriousCLI/RemoveSpuriousCLI.py
new file mode 100644
index 0000000..b77783a
--- /dev/null
+++ b/src/modules/PoreStats/RemoveSpuriousCLI/RemoveSpuriousCLI.py
@@ -0,0 +1,167 @@
+#!/usr/bin/env python-real
+# -*- coding: utf-8 -*-
+
+from __future__ import print_function
+import os
+import pickle
+import time
+import warnings
+
+import slicer
+import slicer.util
+import mrml
+
+from ltrace.slicer.cli_utils import progressUpdate, readFrom, writeDataInto
+
+import numpy as np
+import scipy.ndimage as ndi
+from skimage import measure
+
+
+def remove_spurious(image, binary_seg, pore_model):
+ def get_roi_from_centroid(cy, cx, image, seg, i_seg, roi_size):
+ def get_ref_point(cy, cx, seg, i_seg):
+ def try_getting_ref_point(y, x, seg):
+ if any(np.isnan(coord) for coord in [y, x]):
+ return False, y, x
+ y, x = int(y), int(x)
+ return seg[y, x], y, x
+
+ cy, cx = np.clip(int(cy), 0, seg.shape[0] - 1), np.clip(int(cx), 0, seg.shape[1] - 1)
+
+ if not seg[cy, cx]:
+ seg_y, seg_x = np.where(seg == i_seg)
+
+ y, x = cy, cx
+ success, y, x = try_getting_ref_point(cy, np.median(seg_x[(seg_x < cx) & (seg_y == cy)]), seg)
+ if not success:
+ success, y, x = try_getting_ref_point(cy, np.median(seg_x[(seg_x > cx) & (seg_y == cy)]), seg)
+ if not success:
+ success, y, x = try_getting_ref_point(np.median(seg_y[(seg_y < cy) & (seg_x == cx)]), cx, seg)
+ if not success:
+ success, y, x = try_getting_ref_point(np.median(seg_y[(seg_y > cy) & (seg_x == cx)]), cx, seg)
+
+ cy = y
+ cx = x
+
+ return cy, cx
+
+ offset = roi_size // 2
+
+ # Em alguns casos, o centróide do segmento reside fora dele. Então, tenta-se obter o pixel mediano do segmento à esquerda
+ # do centróide. Se não houver, tenta-se à direita. Então, acima. Em último caso, abaixo.
+ cy, cx = get_ref_point(cy, cx, seg, i_seg)
+ y0, x0 = max(0, cy - offset), max(0, cx - offset)
+ y1, x1 = y0 + roi_size, x0 + roi_size
+ if y1 > image.shape[0]:
+ d = y1 - image.shape[0]
+ y0, y1 = y0 - d, y1 - d
+ if x1 > image.shape[1]:
+ d = x1 - image.shape[1]
+ x0, x1 = x0 - d, x1 - d
+ assert y1 - y0 == roi_size
+ assert x1 - x0 == roi_size
+
+ return image[y0:y1, x0:x1].flatten()
+
+ split_pores = ndi.label(binary_seg)[0]
+
+ if split_pores.max() > 0:
+ # Há um modelo RandomForest de remoção de poros espúrios para cada modelo de segmentação de poros
+ with open(
+ os.path.join(
+ __file__,
+ "..",
+ "..",
+ "PoreStatsCLI",
+ "Libs",
+ "pore_stats",
+ "models",
+ "spurious_removal",
+ f"spurious_{pore_model}.pkl",
+ ),
+ "rb",
+ ) as pkl:
+ scaler_and_model = pickle.load(pkl)
+ scaler = scaler_and_model["scaler"]
+ model = scaler_and_model["model"]
+
+ # Para cada segmento de poro, é obtida uma pequena região de interesse (ROI) em volta do centróide
+ with warnings.catch_warnings():
+ warnings.filterwarnings("ignore")
+
+ print("Removing spurious pore detections...")
+ start_time = time.time()
+ regions_props = measure.regionprops(split_pores, intensity_image=image)
+
+ rois = []
+ candidate_pred_indexes = []
+ pred_indexes = range(1, split_pores.max() + 1)
+ for i in pred_indexes:
+ progressUpdate(i / len(pred_indexes))
+ region_props = regions_props[i - 1]
+ if (
+ region_props.area < 3
+ ): # porque o Segment Inspector não inclui segmentos com menos de 3 pixeis no relatório
+ split_pores[split_pores == i] = 0
+ else:
+ cy, cx = region_props.centroid
+ roi = get_roi_from_centroid(cy, cx, image, split_pores, i, roi_size=10)
+ rois.append(roi)
+ candidate_pred_indexes.append(i)
+
+ if len(rois) > 0:
+ # O modelo detecta os ROIs espúrios e os descarta
+ predictions = model.predict(scaler.transform(np.array(rois)))
+ valid_pred_indexes = np.array(candidate_pred_indexes)[np.nonzero(predictions)[0]]
+
+ split_pores = np.where(np.isin(split_pores, valid_pred_indexes), split_pores, 0)
+ n_valid = len(valid_pred_indexes)
+ else:
+ n_valid = 0
+
+ n_discarded = i - n_valid
+
+ print(f"Done: {n_valid} detections kept, {n_discarded} discarded ({time.time() - start_time}s).")
+ else:
+ print("No pores found.")
+
+ return split_pores.astype(bool)
+
+
+def runcli(args):
+ sourceVolumeNode = readFrom(args.input, mrml.vtkMRMLVectorVolumeNode)
+ image = slicer.util.arrayFromVolume(sourceVolumeNode)[0]
+
+ poreSegmentationNode = readFrom(args.poreSegmentation, mrml.vtkMRMLLabelMapVolumeNode)
+ pore_seg = slicer.util.arrayFromVolume(poreSegmentationNode)[0].astype(bool)
+
+ pore_seg = remove_spurious(image, pore_seg, args.poreSegModel)
+
+ progressUpdate(1)
+ writeDataInto(args.output, pore_seg, mrml.vtkMRMLLabelMapVolumeNode, reference=sourceVolumeNode)
+
+
+if __name__ == "__main__":
+ import argparse
+
+ parser = argparse.ArgumentParser(description="LTrace Remove Spurious Wrapper for Slicer.")
+ parser.add_argument("--input", type=str, dest="input", default=None, help="Intensity Input Values")
+ parser.add_argument("--output", type=str, dest="output", default=None, help="Output Segmentation")
+ parser.add_argument(
+ "--poreseg",
+ type=str,
+ dest="poreSegmentation",
+ default=None,
+ help="Prior Pore Segmentation For Spurious Removal",
+ )
+ parser.add_argument(
+ "--poresegmodel",
+ type=str,
+ dest="poreSegModel",
+ default=None,
+ help="Pore Segmentation Model File",
+ )
+
+ args = parser.parse_args()
+ runcli(args)
diff --git a/src/modules/PoreStats/RemoveSpuriousCLI/RemoveSpuriousCLI.xml b/src/modules/PoreStats/RemoveSpuriousCLI/RemoveSpuriousCLI.xml
new file mode 100644
index 0000000..ec8c59a
--- /dev/null
+++ b/src/modules/PoreStats/RemoveSpuriousCLI/RemoveSpuriousCLI.xml
@@ -0,0 +1,51 @@
+
+
+ LTrace Tools
+ 3
+ RemoveSpurious CLI
+
+ 0.2.0.
+ https://github.com/lassoan/SlicerPythonCLIExample
+
+ LTrace Team
+
+
+
+
+ input
+ input
+
+
+ input
+
+
+
+
+ output
+ output
+
+
+ output
+
+
+
+
+ poreSegmentation
+ poreseg
+
+
+ input
+
+
+
+
+ poreSegModel
+ poresegmodel
+
+
+ input
+
+
+
+
+
diff --git a/src/modules/PoreStats/Resources/Icons/PoreStats.svg b/src/modules/PoreStats/Resources/Icons/PoreStats.svg
new file mode 100644
index 0000000..f7738bb
--- /dev/null
+++ b/src/modules/PoreStats/Resources/Icons/PoreStats.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/modules/PoreStats/Resources/Icons/Segmenter.png b/src/modules/PoreStats/Resources/Icons/Segmenter.png
deleted file mode 100644
index b89476c..0000000
Binary files a/src/modules/PoreStats/Resources/Icons/Segmenter.png and /dev/null differ
diff --git a/src/modules/PoreStats/SmartForegroundCLI/SmartForegroundCLI.py b/src/modules/PoreStats/SmartForegroundCLI/SmartForegroundCLI.py
new file mode 100644
index 0000000..097c46f
--- /dev/null
+++ b/src/modules/PoreStats/SmartForegroundCLI/SmartForegroundCLI.py
@@ -0,0 +1,109 @@
+#!/usr/bin/env python-real
+# -*- coding: utf-8 -*-
+
+from __future__ import print_function
+
+import slicer
+import slicer.util
+import mrml
+
+from ltrace.slicer.cli_utils import progressUpdate, readFrom, writeDataInto
+
+import numpy as np
+import cv2
+from skimage import morphology, color
+import scipy.ndimage as ndi
+
+
+def filter_largest_islands(seg, n_largest_islands):
+
+ # Filtra os N maiores fragmentos.
+
+ seg = cv2.erode(seg.astype(np.uint8), np.ones((23, 23), np.uint8)) # para limpar pequenos artefatos
+ islands = ndi.label(seg)[0]
+ islands_sizes = np.bincount(islands.ravel())
+ sorted_labels = np.argsort(islands_sizes)[::-1]
+ sorted_labels = sorted_labels[sorted_labels != 0]
+ seg[~np.isin(islands, sorted_labels[:n_largest_islands])] = 0
+
+ return seg.astype(bool)
+
+
+def get_rock_area(image):
+
+ # Isola a área da rocha da borda da imagem
+ def equalize_each_channel(image):
+ return np.stack([cv2.equalizeHist(image[:, :, i]) for i in range(3)], axis=2)
+
+ eq = equalize_each_channel(image)
+ blur = cv2.GaussianBlur(eq, (199, 199), 255)
+ progressUpdate(0.2)
+
+ lum = color.rgb2gray(blur)
+ mask = morphology.remove_small_holes(morphology.remove_small_objects((lum > 0.3) & (lum < 0.7), 500), 500)
+ progressUpdate(0.4)
+
+ mask = morphology.opening(mask, morphology.disk(3))
+ progressUpdate(0.7)
+
+ mask = filter_largest_islands(
+ mask, 1
+ ) # para pegar a área central da rocha e eliminar artefatos deixados nas bordas
+ progressUpdate(0.75)
+
+ return ndi.binary_fill_holes(mask)
+
+
+def runcli(args):
+ sourceVolumeNode = readFrom(args.input, mrml.vtkMRMLVectorVolumeNode)
+ image = slicer.util.arrayFromVolume(sourceVolumeNode)[0]
+
+ rock_area = get_rock_area(image)
+ progressUpdate(0.85)
+
+ writeDataInto(args.outputRock, rock_area, mrml.vtkMRMLLabelMapVolumeNode, reference=sourceVolumeNode)
+
+ if args.poreSegmentation is not None:
+ poreSegmentationNode = readFrom(args.poreSegmentation, mrml.vtkMRMLLabelMapVolumeNode)
+ pore_seg = slicer.util.arrayFromVolume(poreSegmentationNode)[0].astype(bool)
+
+ frags_mask = (~pore_seg) & rock_area
+ progressUpdate(0.9)
+
+ frags_mask = ndi.binary_fill_holes(frags_mask)
+ progressUpdate(0.95)
+
+ if args.nLargestFrags >= 0:
+ frags_mask = filter_largest_islands(frags_mask, args.nLargestFrags)
+
+ writeDataInto(args.outputFrags, frags_mask, mrml.vtkMRMLLabelMapVolumeNode, reference=sourceVolumeNode)
+
+ progressUpdate(1)
+
+
+if __name__ == "__main__":
+ import argparse
+
+ parser = argparse.ArgumentParser(description="LTrace Smart Foreground Wrapper for Slicer.")
+ parser.add_argument("--input", type=str, dest="input", default=None, help="Intensity Input Values")
+ parser.add_argument("--outputrock", type=str, dest="outputRock", default=None, help="Output Rock Area Segmentation")
+ parser.add_argument(
+ "--outputfrags", type=str, dest="outputFrags", default=None, help="Output Fragments Segmentation"
+ )
+ parser.add_argument(
+ "--poreseg",
+ type=str,
+ dest="poreSegmentation",
+ default=None,
+ help="Prior Pore Segmentation For Fragment Splitting",
+ )
+ parser.add_argument(
+ "--max_frags",
+ type=int,
+ dest="nLargestFrags",
+ default=-1,
+ help="Number of Fragments to Filter (From Largest to Smallest)",
+ )
+
+ args = parser.parse_args()
+ runcli(args)
diff --git a/src/modules/PoreStats/SmartForegroundCLI/SmartForegroundCLI.xml b/src/modules/PoreStats/SmartForegroundCLI/SmartForegroundCLI.xml
new file mode 100644
index 0000000..c35816b
--- /dev/null
+++ b/src/modules/PoreStats/SmartForegroundCLI/SmartForegroundCLI.xml
@@ -0,0 +1,61 @@
+
+
+ LTrace Tools
+ 3
+ SmartForeground CLI
+
+ 0.2.0.
+ https://github.com/lassoan/SlicerPythonCLIExample
+
+ LTrace Team
+
+
+
+
+ input
+ input
+
+
+ input
+
+
+
+
+ outputRock
+ outputrock
+
+
+ output
+
+
+
+
+
+
+ outputFrags
+ outputfrags
+
+
+ output
+
+
+
+
+ poreSegmentation
+ poreseg
+
+
+ input
+
+
+
+
+ nLargestFrags
+ max_frags
+
+
+ -1
+
+
+
+
diff --git a/src/modules/PpFlow/PpFlow.py b/src/modules/PpFlow/PpFlow.py
index 949084c..f03218d 100644
--- a/src/modules/PpFlow/PpFlow.py
+++ b/src/modules/PpFlow/PpFlow.py
@@ -1,7 +1,8 @@
-from ltrace.flow.thin_section import ppFlowWidget
-from ltrace.slicer_utils import LTracePlugin, LTracePluginWidget
-from pathlib import Path
import os
+from pathlib import Path
+
+from ltrace.flow.thin_section import ppFlowWidget
+from ltrace.slicer_utils import LTracePlugin, LTracePluginWidget, getResourcePath
class PpFlow(LTracePlugin):
@@ -11,9 +12,11 @@ class PpFlow(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
self.parent.title = "PP Flow"
- self.parent.categories = ["LTrace Tools"]
+ self.parent.categories = ["Tools", "Thin Section"]
self.parent.contributors = ["LTrace Geophysics Team"]
- self.parent.helpText = PpFlow.help()
+ self.parent.helpText = (
+ f"file:///{(getResourcePath('manual') / 'Modules/Thin_section/Fluxo%20PP.html').as_posix()}"
+ )
@classmethod
def readme_path(cls):
diff --git a/src/modules/PpFlow/Resources/Icons/PpFlow.png b/src/modules/PpFlow/Resources/Icons/PpFlow.png
deleted file mode 100644
index f575fe2..0000000
Binary files a/src/modules/PpFlow/Resources/Icons/PpFlow.png and /dev/null differ
diff --git a/src/modules/PpFlow/Resources/Icons/PpFlow.svg b/src/modules/PpFlow/Resources/Icons/PpFlow.svg
new file mode 100644
index 0000000..ef79afb
--- /dev/null
+++ b/src/modules/PpFlow/Resources/Icons/PpFlow.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/modules/PpPxFlow/PpPxFlow.py b/src/modules/PpPxFlow/PpPxFlow.py
index af022ae..29c14c6 100644
--- a/src/modules/PpPxFlow/PpPxFlow.py
+++ b/src/modules/PpPxFlow/PpPxFlow.py
@@ -1,5 +1,6 @@
from ltrace.flow.thin_section import ppPxFlowWidget
from ltrace.slicer_utils import LTracePlugin, LTracePluginWidget
+from ltrace.slicer_utils import getResourcePath
from pathlib import Path
import os
@@ -11,9 +12,11 @@ class PpPxFlow(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
self.parent.title = "PP/PX Flow"
- self.parent.categories = ["LTrace Tools"]
+ self.parent.categories = ["Tools", "Thin Section"]
self.parent.contributors = ["LTrace Geophysics Team"]
- self.parent.helpText = PpPxFlow.help()
+ self.parent.helpText = (
+ f"file:///{(getResourcePath('manual') / 'Modules/Thin_section/Fluxo%20PP%20PX.html').as_posix()}"
+ )
@classmethod
def readme_path(cls):
diff --git a/src/modules/PpPxFlow/Resources/Icons/PpPxFlow.png b/src/modules/PpPxFlow/Resources/Icons/PpPxFlow.png
deleted file mode 100644
index 9a88f5b..0000000
Binary files a/src/modules/PpPxFlow/Resources/Icons/PpPxFlow.png and /dev/null differ
diff --git a/src/modules/PpPxFlow/Resources/Icons/PpPxFlow.svg b/src/modules/PpPxFlow/Resources/Icons/PpPxFlow.svg
new file mode 100644
index 0000000..71b10df
--- /dev/null
+++ b/src/modules/PpPxFlow/Resources/Icons/PpPxFlow.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/modules/QEMSCANLoader/QEMSCANLoader.py b/src/modules/QEMSCANLoader/QEMSCANLoader.py
index 9d3f352..693eb08 100644
--- a/src/modules/QEMSCANLoader/QEMSCANLoader.py
+++ b/src/modules/QEMSCANLoader/QEMSCANLoader.py
@@ -4,22 +4,24 @@
from pathlib import Path
from threading import Lock
+import ctk
import numpy as np
import pandas as pd
import qt
import slicer
import vtk
-import ctk
-from Customizer import Customizer
-from ltrace.file_utils import read_csv
-from ltrace.slicer_utils import *
-from ltrace.transforms import getRoundedInteger
-from ltrace.units import global_unit_registry as ureg, SLICER_LENGTH_UNIT
-from ltrace.slicer import helpers
+from ltrace.slicer import ui
from slicer.util import MRMLNodeNotFoundException
from slicer.util import dataframeFromTable
+from ltrace.file_utils import read_csv
+from ltrace.slicer import helpers
+from ltrace.slicer_utils import *
+from ltrace.slicer_utils import getResourcePath
+from ltrace.units import global_unit_registry as ureg, SLICER_LENGTH_UNIT
+from ltrace.utils.callback import Callback
+
class QEMSCANLoader(LTracePlugin):
SETTING_KEY = "QEMSCANLoader"
@@ -33,7 +35,7 @@ def __init__(self, parent):
self.parent.categories = ["Thin Section"]
self.parent.dependencies = []
self.parent.contributors = ["LTrace Geophysical Solutions"]
- self.parent.helpText = QEMSCANLoader.help()
+ self.parent.helpText = f"file:///{Path(helpers.get_scripted_modules_path() + '/Resources/manual/Thin%20Section/Modulos/QemscanLoader.html').as_posix()}"
@classmethod
def readme_path(cls):
@@ -140,10 +142,18 @@ def setup(self):
loadFormLayout.addRow(" ", None)
- self.loadButton = qt.QPushButton("Load QEMSCANs")
- self.loadButton.setFixedHeight(40)
- self.loadButton.clicked.connect(self.onLoadButtonClicked)
- loadFormLayout.addRow(None, self.loadButton)
+ self.applyCancelButtons = ui.ApplyCancelButtons(
+ onApplyClick=self.onLoadButtonClicked,
+ onCancelClick=self.onCancelButtonClicked,
+ applyTooltip="Load QEMSCANs",
+ cancelTooltip="Cancel",
+ applyText="Load QEMSCANs",
+ cancelText="Cancel",
+ enabled=True,
+ applyObjectName=None,
+ cancelObjectName=None,
+ )
+ self.layout.addWidget(self.applyCancelButtons)
statusLabel = qt.QLabel("Status: ")
self.currentStatusLabel = qt.QLabel("Idle")
@@ -207,6 +217,9 @@ def onDeleteLookupColorTableButtonClicked(self):
self.logic.deleteQEMSCANLookupColorTable(tableNode)
self.addLookupColorTableButton.setFocus(True) # To avoid focusing the pixel size input after deleting an item
+ def onCancelButtonClicked(self):
+ self.logic.onCancel()
+
def onLoadButtonClicked(self):
if not self.validateInput():
return
@@ -268,13 +281,8 @@ def updateStatus(self, message, progress=None, processEvents=True):
slicer.app.processEvents()
-class Callback(object):
- def __init__(self, on_update=None):
- self.on_update = on_update or (lambda *args, **kwargs: None)
-
-
class QEMSCANLoaderLogic(LTracePluginLogic):
- QEMSCAN_LOOKUP_COLOR_TABLES_PATH = Customizer.RESOURCES_PATH / "QEMSCAN/LookupColorTables"
+ QEMSCAN_LOOKUP_COLOR_TABLES_PATH = getResourcePath("QEMSCAN") / "LookupColorTables"
ROOT_DATASET_DIRECTORY_NAME = "Thin Section"
QEMSCAN_LOADER_FILE_EXTENSIONS = [".tif", ".tiff"]
@@ -308,6 +316,7 @@ def configureInitialNodeMetadata(self, node, baseName, p):
)
def loadImage(self, file, p, baseName):
+ self.cancel = False
outputVolumeNode = None
segmentationNode = None
try:
@@ -380,7 +389,13 @@ def loadImage(self, file, p, baseName):
slicer.modules.segmentations.logic().ExportAllSegmentsToLabelmapNode(segmentationNode, labelMapVolumeNode)
self.setImageSpacing(labelMapVolumeNode, p.imageSpacing)
self.configureInitialNodeMetadata(labelMapVolumeNode, baseName, p)
+ if self.cancel:
+ slicer.mrmlScene.RemoveNode(labelMapVolumeNode)
+ slicer.util.resetSliceViews()
+ p.callback.on_update("Cancelled", 100)
+ return
return labelMapVolumeNode
+
except Exception as e:
raise e
slicer.mrmlScene.RemoveNode(labelMapVolumeNode)
@@ -390,6 +405,9 @@ def loadImage(self, file, p, baseName):
slicer.mrmlScene.RemoveNode(outputVolumeNode)
slicer.util.resetSliceViews()
+ def onCancel(self):
+ self.cancel = True
+
def setImageSpacing(self, node, imageSpacing):
node.SetSpacing(
imageSpacing.m_as(SLICER_LENGTH_UNIT),
diff --git a/src/modules/QEMSCANLoader/Resources/Icons/QEMSCANLoader.png b/src/modules/QEMSCANLoader/Resources/Icons/QEMSCANLoader.png
deleted file mode 100644
index b89476c..0000000
Binary files a/src/modules/QEMSCANLoader/Resources/Icons/QEMSCANLoader.png and /dev/null differ
diff --git a/src/modules/QEMSCANLoader/Resources/Icons/QEMSCANLoader.svg b/src/modules/QEMSCANLoader/Resources/Icons/QEMSCANLoader.svg
new file mode 100644
index 0000000..9dce262
--- /dev/null
+++ b/src/modules/QEMSCANLoader/Resources/Icons/QEMSCANLoader.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/modules/QemscanFlow/QemscanFlow.py b/src/modules/QemscanFlow/QemscanFlow.py
index 7a43de4..3115874 100644
--- a/src/modules/QemscanFlow/QemscanFlow.py
+++ b/src/modules/QemscanFlow/QemscanFlow.py
@@ -1,5 +1,6 @@
from ltrace.flow.thin_section import qemscanFlowWidget
from ltrace.slicer_utils import LTracePlugin, LTracePluginWidget
+from ltrace.slicer_utils import getResourcePath
from pathlib import Path
import os
@@ -11,9 +12,11 @@ class QemscanFlow(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
self.parent.title = "QEMSCAN Flow"
- self.parent.categories = ["LTrace Tools"]
+ self.parent.categories = ["Tools", "Thin Section"]
self.parent.contributors = ["LTrace Geophysics Team"]
- self.parent.helpText = QemscanFlow.help()
+ self.parent.helpText = (
+ f"file:///{(getResourcePath('manual') / 'Modules/Thin_section/Fluxo%20QEMSCAN.html').as_posix()}"
+ )
@classmethod
def readme_path(cls):
diff --git a/src/modules/QemscanFlow/Resources/Icons/QemscanFlow.png b/src/modules/QemscanFlow/Resources/Icons/QemscanFlow.png
deleted file mode 100644
index 962de99..0000000
Binary files a/src/modules/QemscanFlow/Resources/Icons/QemscanFlow.png and /dev/null differ
diff --git a/src/modules/QemscanFlow/Resources/Icons/QemscanFlow.svg b/src/modules/QemscanFlow/Resources/Icons/QemscanFlow.svg
new file mode 100644
index 0000000..091077f
--- /dev/null
+++ b/src/modules/QemscanFlow/Resources/Icons/QemscanFlow.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/modules/QualityIndicator/QualityIndicator.py b/src/modules/QualityIndicator/QualityIndicator.py
index a66e36b..9f7f572 100644
--- a/src/modules/QualityIndicator/QualityIndicator.py
+++ b/src/modules/QualityIndicator/QualityIndicator.py
@@ -32,7 +32,7 @@ class QualityIndicator(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
self.parent.title = "Quality Indicator"
- self.parent.categories = ["LTrace Tools"]
+ self.parent.categories = ["Tools", "ImageLog"]
self.parent.dependencies = []
self.parent.contributors = ["LTrace Geophysical Solutions"]
self.parent.helpText = QualityIndicator.help()
diff --git a/src/modules/QualityIndicator/Resources/Icons/QualityIndicator.png b/src/modules/QualityIndicator/Resources/Icons/QualityIndicator.png
deleted file mode 100644
index b89476c..0000000
Binary files a/src/modules/QualityIndicator/Resources/Icons/QualityIndicator.png and /dev/null differ
diff --git a/src/modules/QualityIndicator/Resources/Icons/QualityIndicator.svg b/src/modules/QualityIndicator/Resources/Icons/QualityIndicator.svg
new file mode 100644
index 0000000..e5fb2e4
--- /dev/null
+++ b/src/modules/QualityIndicator/Resources/Icons/QualityIndicator.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/modules/RawLoader/README.md b/src/modules/RawLoader/README.md
deleted file mode 100644
index cefb3e0..0000000
--- a/src/modules/RawLoader/README.md
+++ /dev/null
@@ -1,40 +0,0 @@
-# Raw Loader
-
-_GeoSlicer_ module to load images stored in an unknown file format by allowing quickly trying various voxel types and image sizes, as described in the steps bellow:
-
-1. Select the _Input file_.
-
-2. Try to guess image parameters based on any information available about the image.
-
-3. Click _Load_ to see preview of the image that can be loaded.
-
-4. Experiment with image parameters (click the checkbox on _Load_ button to automatically update output volume when any parameter is changed).
-
-5. Move _X dimension_ slider until straight image columns appear (if image columns are slightly skewed then it means the value is close to the correct value), try with different endianness and pixel type values if no _X dimension_ value seems to make sense.
-
-6. Move _Header size_ until the first row of the image appears on top.
-
-7. If loading a 3D volume: set _Z dimension_ slider to a few ten slices to make it easier to see when _Y dimension_ value is correct.
-
-8. Move _Y dimension_ slider until last row of the image appears at the bottom.
-
-9. If loading a 3D volume: Move _Z dimension_ slider until all the slices of the image are included.
-
-10. When the correct combination of parameters is found then either save the current output volume or click _Generate NRRD header_ to create a header file that can be loaded directly into Slicer.
-
-## Further information about export formats
-
-**RAW** - to load this format exported by the *Export* module, these necessary parameters need to be set:
-
- - *Endianness*: Little endian
- - *X dimension*, *Y dimension*, *Z dimension*: the data dimensions
-
-
- - For scalar volumes and images:
-
- - *Pixel type*: 16 bit unsigned
-
-
- - For label maps and segmentations:
-
- - *Pixel type*: 8 bit unsigned
diff --git a/src/modules/RawLoader/RawLoader.py b/src/modules/RawLoader/RawLoader.py
deleted file mode 100644
index 8bf9618..0000000
--- a/src/modules/RawLoader/RawLoader.py
+++ /dev/null
@@ -1,38 +0,0 @@
-import os
-from pathlib import Path
-import qt
-from ltrace.slicer_utils import *
-
-
-class RawLoader(LTracePlugin):
- SETTING_KEY = "RawLoader"
-
- MODULE_DIR = Path(os.path.dirname(os.path.realpath(__file__)))
- RES_DIR = MODULE_DIR / "Resources"
-
- def __init__(self, parent):
- LTracePlugin.__init__(self, parent)
- self.parent.title = "Raw Loader (deprecated)"
- self.parent.categories = ["LTrace Tools"]
- self.parent.dependencies = []
- self.parent.contributors = ["LTrace Geophysical Solutions"]
- self.parent.helpText = RawLoader.help()
-
- @classmethod
- def readme_path(cls):
- return str(cls.MODULE_DIR / "README.md")
-
-
-class RawLoaderWidget(LTracePluginWidget):
- def __init__(self, parent):
- super().__init__(parent)
-
- def setup(self):
- LTracePluginWidget.setup(self)
- self.layout.addWidget(qt.QLabel("RAW Import has been moved to Import."))
- self.layout.addWidget(qt.QLabel(""))
- self.layout.addWidget(qt.QLabel("To import a RAW file:"))
- self.layout.addWidget(qt.QLabel("1. Click on the Import tab (left of this tab)."))
- self.layout.addWidget(qt.QLabel("2. Click Choose file..."))
- self.layout.addWidget(qt.QLabel("3. Select a RAW file."))
- self.layout.addStretch(1)
diff --git a/src/modules/RawLoader/Resources/Icons/RawLoader.png b/src/modules/RawLoader/Resources/Icons/RawLoader.png
deleted file mode 100644
index b89476c..0000000
Binary files a/src/modules/RawLoader/Resources/Icons/RawLoader.png and /dev/null differ
diff --git a/src/modules/ResampleVectorVolume/ResampleVectorVolume.py b/src/modules/ResampleVectorVolume/ResampleVectorVolume.py
index f388f3e..a04de44 100644
--- a/src/modules/ResampleVectorVolume/ResampleVectorVolume.py
+++ b/src/modules/ResampleVectorVolume/ResampleVectorVolume.py
@@ -27,7 +27,7 @@ class ResampleVectorVolume(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
self.parent.title = "Resample Vector Volume"
- self.parent.categories = ["LTrace Tools"]
+ self.parent.categories = ["Tools"]
self.parent.dependencies = []
self.parent.contributors = ["LTrace Geophysical Solutions"]
self.parent.helpText = ResampleVectorVolume.help()
diff --git a/src/modules/SampleSegmentationEffect/SampleSegmentationEffectLib/SegmentEditorEffect.py b/src/modules/SampleSegmentationEffect/SampleSegmentationEffectLib/SegmentEditorEffect.py
index f561932..563e847 100644
--- a/src/modules/SampleSegmentationEffect/SampleSegmentationEffectLib/SegmentEditorEffect.py
+++ b/src/modules/SampleSegmentationEffect/SampleSegmentationEffectLib/SegmentEditorEffect.py
@@ -256,7 +256,7 @@ def deactivate(self):
def createCursor(self, widget):
# Turn off effect-specific cursor for this effect
- return slicer.util.mainWindow().cursor
+ return slicer.modules.AppContextInstance.mainWindow.cursor
def onThresholdValuesChanged(self, *args):
min = self.thresholdSlider.minimumValue
diff --git a/src/modules/SegmentInspector/Resources/Icons/SegmentInspector.png b/src/modules/SegmentInspector/Resources/Icons/SegmentInspector.png
deleted file mode 100644
index b89476c..0000000
Binary files a/src/modules/SegmentInspector/Resources/Icons/SegmentInspector.png and /dev/null differ
diff --git a/src/modules/SegmentInspector/Resources/Icons/SegmentInspector.svg b/src/modules/SegmentInspector/Resources/Icons/SegmentInspector.svg
new file mode 100644
index 0000000..a6dae89
--- /dev/null
+++ b/src/modules/SegmentInspector/Resources/Icons/SegmentInspector.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/modules/SegmentInspector/SegmentInspector.py b/src/modules/SegmentInspector/SegmentInspector.py
index 9116251..8ce76c4 100644
--- a/src/modules/SegmentInspector/SegmentInspector.py
+++ b/src/modules/SegmentInspector/SegmentInspector.py
@@ -1,29 +1,24 @@
-from functools import partial
import importlib
import json
import logging
import os
-import uuid
-
import time
+import uuid
from pathlib import Path
-import logging
+import ctk
import matplotlib.colors as mcolors
-
import numpy as np
import pandas as pd
-
-
import qt
import slicer
-import vtk
-import ctk
-
+from recordtype import recordtype # mutable
from slicer.util import VTKObservationMixin
-from ltrace.algorithms.partition import InvalidSegmentError
-from ltrace.slicer.node_attributes import Tag
+from Output import SegmentInspectorVariablesOutput as SegmentInspectorVariablesOutputClass
+from Output.BasicPetrophysicsOutput import generate_basic_petrophysics_output
+from Output.SegmentInspectorVariablesOutput import SegmentInspectorVariablesOutput
+from ltrace.algorithms.partition import InvalidSegmentError, runPartitioning, ResultInfo
from ltrace.slicer import ui, helpers, widgets
from ltrace.slicer.helpers import (
tryGetNode,
@@ -36,7 +31,8 @@
themeIsDark,
isNodeImage2D,
)
-from ltrace.slicer.ui import numberParam, fixedRangeNumberParam
+from ltrace.slicer.node_attributes import Tag
+from ltrace.slicer.throat_analysis.throat_analysis_generator import ThroatAnalysisGenerator
from ltrace.slicer.widget.global_progress_bar import LocalProgressBar
from ltrace.slicer.widgets import BaseSettingsWidget
from ltrace.slicer_utils import (
@@ -44,14 +40,8 @@
LTracePluginWidget,
LTracePluginLogic,
dataFrameToTableNode,
+ getResourcePath,
)
-from ltrace.algorithms.partition import runPartitioning, ResultInfo
-from Output import SegmentInspectorVariablesOutput as SegmentInspectorVariablesOutputClass
-from Output.SegmentInspectorVariablesOutput import SegmentInspectorVariablesOutput
-from Output.BasicPetrophysicsOutput import generate_basic_petrophysics_output
-from recordtype import recordtype # mutable
-
-from ltrace.slicer.throat_analysis.throat_analysis_generator import ThroatAnalysisGenerator
# Checks if closed source code is available
try:
@@ -73,11 +63,12 @@ class SegmentInspector(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
self.parent.title = "Segment Inspector"
- self.parent.categories = ["Segmentation"]
+ self.parent.categories = ["Segmentation", "Thin Section", "MicroCT", "ImageLog", "Core", "Multiscale"]
self.parent.dependencies = []
self.parent.contributors = ["LTrace Geophysics Team"] # replace with "Firstname Lastname (Organization)"
- self.parent.helpText = SegmentInspector.help()
- self.parent.helpText += self.getDefaultModuleDocumentationLink()
+ self.parent.helpText = (
+ f"file:///{(getResourcePath('manual') / 'Modules/Thin_section/SegmentInspector.html').as_posix()}"
+ )
self.parent.acknowledgementText = "" # replace with organization, grant and thanks.
@classmethod
@@ -95,7 +86,7 @@ def __init__(self, parent):
self.modeSelectors = {}
self.modeWidgets = {}
- self.selectedProducts = set(["all"])
+ self.selectedProducts = {"all"}
self.currentMode = widgets.SingleShotInputWidget.MODE_NAME
@@ -112,15 +103,6 @@ def onReload(self) -> None:
importlib.reload(widgets)
importlib.reload(ui)
- def onSceneEndClose(self, caller, event):
- try:
- self.modeWidgets[widgets.SingleShotInputWidget.MODE_NAME].fullResetUI()
- except Exception as e:
- import traceback
-
- traceback.print_exc()
- pass
-
def setup(self):
LTracePluginWidget.setup(self)
@@ -137,7 +119,7 @@ def setup(self):
self.layout.addStretch(1)
# Setup handlers
- self.addObserver(slicer.mrmlScene, slicer.mrmlScene.EndCloseEvent, self.onSceneEndClose)
+ # self.addObserver(slicer.mrmlScene, slicer.mrmlScene.EndCloseEvent, self.onSceneEndClose)
def _setupInputsSection(self):
widget = ctk.ctkCollapsibleButton()
@@ -266,20 +248,34 @@ def _setupApplySection(self):
widget = qt.QWidget()
vlayout = qt.QVBoxLayout(widget)
- self.applyButton = ui.ApplyButton(
- onClick=self._onApplyClicked, tooltip="Run pore analysis on input data limited by ROI", enabled=False
+ self.applyCancelButtons = ui.ApplyCancelButtons(
+ onApplyClick=self._onApplyClicked,
+ onCancelClick=self._onCancelClicked,
+ applyTooltip="Run pore analysis on input data limited by ROI",
+ cancelTooltip="Cancel",
+ applyText="Apply",
+ cancelText="Cancel",
+ enabled=False,
+ applyObjectName="SegmentInspector.ApplyButton",
+ cancelObjectName="SegmentInspector.CancelButton",
)
- self.applyButton.objectName = "Apply Button"
+ def onProcessStart():
+ self.applyCancelButtons.applyBtn.setEnabled(False)
+ self.applyCancelButtons.cancelBtn.setEnabled(True)
+
+ def onProcessFinish():
+ self.applyCancelButtons.applyBtn.setEnabled(True)
+ self.applyCancelButtons.cancelBtn.setEnabled(False)
- self.logic.inspectorProcessStarted.connect(lambda: self.applyButton.setEnabled(False))
- self.logic.inspector_process_finished.connect(lambda: self.applyButton.setEnabled(True))
+ self.logic.inspectorProcessStarted.connect(onProcessStart)
+ self.logic.inspector_process_finished.connect(onProcessFinish)
self.progressBar = LocalProgressBar()
self.logic.progressBar = self.progressBar
hlayout = qt.QHBoxLayout()
- hlayout.addWidget(self.applyButton)
+ hlayout.addWidget(self.applyCancelButtons)
hlayout.setContentsMargins(0, 8, 0, 8)
vlayout.addLayout(hlayout)
@@ -287,6 +283,10 @@ def _setupApplySection(self):
return widget
+ def _onCancelClicked(self):
+ if self.logic.cliNode is not None:
+ self.logic.cliNode.Cancel()
+
def _onModeClicked(self):
for index, mode in enumerate(self.MODES):
try:
@@ -306,7 +306,7 @@ def enter(self) -> None:
super().enter()
def exit(self):
- pass
+ self.modeWidgets[self.currentMode].autoPorosityCalcCb.setChecked(False)
def cleanup(self):
super().cleanup()
@@ -336,19 +336,18 @@ def _callSingleShot(self):
prefix = self.outputPrefix.text + "_{type}"
products = list(self.selectedProducts)
+ try:
+ referenceVolumeNode = modeWidget.referenceInput.currentNode() ## Can be null
- referenceVolumeNode = modeWidget.referenceInput.currentNode() ## Can be null
+ segmentationNode = modeWidget.mainInput.currentNode()
+ # doing this with an if because when setting the visibility it messes with the imagelog viewer
+ if not self.blockVisibilityChanges:
+ segmentationNode.GetDisplayNode().SetVisibility(False)
- segmentationNode = modeWidget.mainInput.currentNode()
- # doing this with an if because when setting the visibility it messes with the imagelog viewer
- if not self.blockVisibilityChanges:
- segmentationNode.GetDisplayNode().SetVisibility(False)
+ roiSegNode = modeWidget.soiInput.currentNode()
+ if roiSegNode and not self.blockVisibilityChanges:
+ roiSegNode.GetDisplayNode().SetVisibility(False)
- roiSegNode = modeWidget.soiInput.currentNode()
- if roiSegNode and not self.blockVisibilityChanges:
- roiSegNode.GetDisplayNode().SetVisibility(False)
-
- try:
params = self.methodSelector.currentWidget().toJson()
cli = self.logic.runSelectedMethod(
@@ -422,7 +421,7 @@ def __update_apply_button_state(self):
elif self.currentMode == widgets.BatchInputWidget.MODE_NAME:
valid_input_config = input_mode_widget.ioFileInputLineEdit.currentPath != ""
- self.applyButton.enabled = valid_output_prefix and valid_input_config
+ self.applyCancelButtons.applyBtn.setEnabled(valid_output_prefix and valid_input_config)
def __on_output_prefix_changed(self, text):
modeWidget: widgets.SingleShotInputWidget = self.modeWidgets[self.currentMode]
@@ -514,7 +513,7 @@ def setup(self):
sizeFilterBox = qt.QHBoxLayout()
step = 1 if self.showPxOnly else 0.001
- self.sizeFilterThreshold = numberParam((0.0, 99999.0), value=0, step=step, decimals=4)
+ self.sizeFilterThreshold = ui.numberParam((0.0, 99999.0), value=0, step=step, decimals=4)
self.sizeFilterThreshold.setToolTip(
"Filter spurious partitions with major axis (feret_max) smaller than Size Filter value."
)
@@ -555,7 +554,7 @@ def setup(self):
smoothFactorBox = qt.QHBoxLayout()
step = 1 if self.showPxOnly else 0.001
- self.smoothFactor = numberParam((0.0, 99999.0), value=0, step=step, decimals=4)
+ self.smoothFactor = ui.numberParam((0.0, 99999.0), value=0, step=step, decimals=4)
self.smoothFactor.setToolTip(
"Smooth Factor being the standard deviation of the Gaussian filter applied to distance transform. "
"As Smooth Factor increases less partitions will be created. Use small values for more reliable results."
@@ -569,7 +568,7 @@ def setup(self):
smoothFactorBox.addWidget(self.smoothFactorPixelLabel)
minDistBox = qt.QHBoxLayout()
- self.minimumDistance = fixedRangeNumberParam(2, 30, value=5)
+ self.minimumDistance = ui.fixedRangeNumberParam(2, 30, value=5)
self.minimumDistance.setToolTip(
"Minimum distance separating peaks in a region of 2 * min_distance + 1 "
"(i.e. peaks are separated by at least min_distance). To found the maximum number of partitions, "
@@ -709,7 +708,7 @@ def setup(self):
formLayout = qt.QFormLayout(self)
sizeFilterBox = qt.QHBoxLayout()
- self.sizeFilterThreshold = numberParam((0.0, 99999.0), value=0, step=0.001, decimals=4)
+ self.sizeFilterThreshold = ui.numberParam((0.0, 99999.0), value=0, step=0.001, decimals=4)
self.sizeFilterThreshold.setToolTip(
"Filter spurious partitions with major axis (feret_max) smaller than Size Filter value."
)
@@ -774,7 +773,7 @@ def setup(self):
formLayout = qt.QFormLayout(self)
smoothFilterSigmaBox = qt.QHBoxLayout()
- self.smoothFilterSigma = numberParam((0.0, 99999.0), value=0, step=0.001, decimals=4)
+ self.smoothFilterSigma = ui.numberParam((0.0, 99999.0), value=0, step=0.001, decimals=4)
self.smoothFilterSigma.setToolTip("Smooth label interfaces.")
self.smoothFilterSigma.objectName = "Smooth Filter Sigma SpinBox"
self.smoothFilterSigmaPixelLabel = qt.QLabel(" 0 px")
@@ -782,7 +781,7 @@ def setup(self):
smoothFilterSigmaBox.addWidget(self.smoothFilterSigmaPixelLabel)
numProcessesBox = qt.QHBoxLayout()
- self.numProcesses = numberParam((1, 64), value=8, step=1, decimals=0)
+ self.numProcesses = ui.numberParam((1, 64), value=8, step=1, decimals=0)
self.numProcesses.setToolTip("Number of processes to use during some parts of the execution.")
self.numProcesses.objectName = "Number Processes SpinBox"
numProcessesBox.addWidget(self.numProcesses)
@@ -832,7 +831,7 @@ def setup(self):
ThresholdSplitBox = qt.QHBoxLayout()
step = 1 if self.showPxOnly else 0.001
- self.ThresholdSplit = numberParam((0.0, 1.0), value=0.95, step=step, decimals=4)
+ self.ThresholdSplit = ui.numberParam((0.0, 1.0), value=0.95, step=step, decimals=4)
SplitThresholdTooltip = "Threshold used to split regions"
self.ThresholdSplit.setToolTip(SplitThresholdTooltip)
self.ThresholdSplit.objectName = "Deep Watershed Split Threshold SpinBox"
@@ -869,7 +868,7 @@ def setup(self):
baseVolumeBox = qt.QHBoxLayout()
step = 1
- self.baseVolume = numberParam((0.0, 99999.0), value=150, step=step, decimals=0)
+ self.baseVolume = ui.numberParam((0.0, 99999.0), value=150, step=step, decimals=0)
baseVolumeTooltip = (
"Initial value that will be used to split the input volume into smaller patches for inferece"
)
@@ -880,7 +879,7 @@ def setup(self):
intersectionBox = qt.QHBoxLayout()
step = 1
- self.intersection = numberParam((0.0, 99999.0), value=60, step=step, decimals=0)
+ self.intersection = ui.numberParam((0.0, 99999.0), value=60, step=step, decimals=0)
intersectionTooltip = "Intersection between inferences"
self.intersection.setToolTip(intersectionTooltip)
@@ -889,7 +888,7 @@ def setup(self):
borderBox = qt.QHBoxLayout()
step = 1
- self.border = numberParam((0.0, 99999.0), value=40, step=step, decimals=0)
+ self.border = ui.numberParam((0.0, 99999.0), value=40, step=step, decimals=0)
borderTooltip = "Border to be cut from inferences"
self.border.setToolTip(borderTooltip)
@@ -898,7 +897,7 @@ def setup(self):
ThresholdBackgroundBox = qt.QHBoxLayout()
step = 1 if self.showPxOnly else 0.001
- self.ThresholdBackground = numberParam((0.0, 1.0), value=0.05, step=step, decimals=4)
+ self.ThresholdBackground = ui.numberParam((0.0, 1.0), value=0.05, step=step, decimals=4)
ThresholdBackgroundTooltip = "Threshold used to remove the background (pore/non-pore segmentation)"
self.ThresholdBackground.setToolTip(ThresholdBackgroundTooltip)
self.ThresholdBackground.objectName = "Deep Watershed Background Threshold SpinBox"
@@ -1147,13 +1146,28 @@ def __callSelectedMethod(self, segmentationNode, segments, outputPrefix, **kwarg
topSegments=[s + 1 for s in segments],
)
+ bins = np.bincount(slicer.util.arrayFromVolume(labelMapNode).ravel())
+ count_nonbg = sum([bins[i] for i in targetLabels])
+
+ if count_nonbg == np.prod(labelMapNode.GetImageData().GetDimensions()):
+ msg = "The combination of the selected segments covers the entire image. This can lead to unexpected results. Do you want to proceed?"
+ ret = qt.QMessageBox.warning(
+ slicer.modules.AppContextInstance.mainWindow,
+ "Input Validation",
+ msg,
+ qt.QMessageBox.Yes,
+ qt.QMessageBox.No,
+ )
+
+ if ret == qt.QMessageBox.No:
+ raise AttributeError(msg)
+
if params["method"] == "medial surface":
array = slicer.util.arrayFromVolume(labelMapNode)
ndim = np.squeeze(array).ndim
if ndim != 3:
msg = "Medial surface segmentation is only available for 3D images."
slicer.util.errorDisplay(msg)
- helpers.removeTemporaryNodes(environment=self.tag)
raise AttributeError(msg)
if params.get("generate_throat_analysis", False) == True:
@@ -1339,13 +1353,6 @@ def createResultArtifacts(self, info, caller=None):
dpath.unlink(missing_ok=True)
segmentMap = getCountForLabels(info.sourceLabelMapNode, info.roiNode)
- self.__createBasicPetrophysicsOutputNode(
- info.allLabels, # force all targets here
- info.allLabels,
- segmentMap=dict(segmentMap),
- prefix=info.outputPrefix,
- where=outputDir,
- )
if method == "basic_petrophysics":
segmentMap = getCountForLabels(info.sourceLabelMapNode, info.roiNode)
@@ -1376,7 +1383,8 @@ def createResultArtifacts(self, info, caller=None):
helpers.moveNodeTo(outputDir, resultNode, dirTree=folderTree)
makeTemporaryNodePermanent(resultNode, show=True)
if resultNode.IsA("vtkMRMLLabelMapVolumeNode"):
- nsegments = int(caller.GetParameterAsString("number_of_partitions"))
+ numPartitions = caller.GetParameterAsString("number_of_partitions")
+ nsegments = int(numPartitions) if numPartitions else 0
colors = rand_cmap(nsegments)
colorTableNode = helpers.create_color_table(
node_name=f"{resultNode.GetName()}_ColorMap",
diff --git a/src/modules/SegmentInspector/SegmentInspectorCLI/SegmentInspectorCLI.py b/src/modules/SegmentInspector/SegmentInspectorCLI/SegmentInspectorCLI.py
index 8e59f9e..ebade7c 100644
--- a/src/modules/SegmentInspector/SegmentInspectorCLI/SegmentInspectorCLI.py
+++ b/src/modules/SegmentInspector/SegmentInspectorCLI/SegmentInspectorCLI.py
@@ -260,9 +260,9 @@ def main(args):
shape = np.array(im.shape)
spacing = getIJKSpacing(labelVolumeNode)[np.where(shape != 1)]
+
"""This case does not handle non-orthogonal volumes.
"""
- voxel_size = np.prod(spacing) # TODO fix get_voxel_volume(labelVolumeNode) to handle this case (dim=1)
if ("all" in products or "partitions" in products) and params.get("method") is not None:
if np.any(shape == 1):
@@ -342,20 +342,20 @@ def main(args):
progressUpdate(0.3)
if df.shape[1] > 1 and df.shape[0] > 0:
- df.set_axis(operator.ATTRIBUTES, axis=1, inplace=True)
+ df = df.set_axis(operator.ATTRIBUTES, axis=1)
- df.dropna(axis=1, how="all", inplace=True) # Remove unused columns
- df.dropna(axis=0, how="any", inplace=True) # Remove unused columns
+ df = df.dropna(axis=1, how="all") # Remove unused columns
+ df = df.dropna(axis=0, how="any") # Remove unused columns
filtered_indices = df[df.max_feret < size_min_threshold].index
# Remove filtered segments by user's diameter choice
- df.drop(filtered_indices, axis=0, inplace=True)
+ df = df.drop(filtered_indices, axis=0)
# Round all float columns
for col in df.select_dtypes(include=["float"]).columns:
df[col] = df[col].round(5)
- df.sort_values(by=["voxelCount", "max_feret", "label"], ascending=False, inplace=True)
+ df = df.sort_values(by=["voxelCount", "max_feret", "label"], ascending=False)
# After sorting, old labels serve as a reverse map
# Added one more position because regions has the label '0'
@@ -374,7 +374,7 @@ def main(args):
if args.outputReport:
df = addUnitsToDataFrameParameters(df)
categ = {i: v for i, v in enumerate(PORE_SIZE_CATEGORIES)}
- df.pore_size_class = df.pore_size_class.replace(categ, inplace=False)
+ df.pore_size_class = df.pore_size_class.replace(categ)
print(f"writing df to tableNode, {args.outputReport}")
writeToTable(df, args.outputReport)
diff --git a/src/modules/SegmentationEnv/MicroCTSegmentationEnv.py b/src/modules/SegmentationEnv/MicroCTSegmentationEnv.py
deleted file mode 100644
index 336a5e4..0000000
--- a/src/modules/SegmentationEnv/MicroCTSegmentationEnv.py
+++ /dev/null
@@ -1,15 +0,0 @@
-from SegmentationEnv import SegmentationEnv, SegmentationEnvWidget
-
-
-class MicroCTSegmentationEnv(SegmentationEnv):
- SETTING_KEY = "Micro CT Segmentation Environment"
-
- def __init__(self, parent):
- SegmentationEnv.__init__(self, parent)
- self.parent.title = "Micro CT Segmentation Tools"
-
-
-class MicroCTSegmentationEnvWidget(SegmentationEnvWidget):
- def __init__(self, parent) -> None:
- super().__init__(parent)
- self.hasModelling = True
diff --git a/src/modules/SegmentationEnv/SegmentationEnv.py b/src/modules/SegmentationEnv/SegmentationEnv.py
index 1c80d2f..edd32bc 100644
--- a/src/modules/SegmentationEnv/SegmentationEnv.py
+++ b/src/modules/SegmentationEnv/SegmentationEnv.py
@@ -1,15 +1,11 @@
import os
from pathlib import Path
-import qt, slicer
-
+from ltrace.slicer.helpers import svgToQIcon
+from ltrace.slicer.module_utils import loadModules
+from ltrace.slicer.widget.custom_toolbar_buttons import addMenu
from ltrace.slicer_utils import *
-
-from LabelMapEditor import LabelMapEditor
-from ltrace.slicer.node_attributes import NodeEnvironment
-from Segmenter import Segmenter
-from ThinSectionInstanceSegmenter import ThinSectionInstanceSegmenter
-from SegmentInspector import SegmentInspector
+from ltrace.slicer_utils import getResourcePath
#
@@ -25,128 +21,167 @@ def __init__(self, parent):
self.parent.title = "Segmentation Tools"
self.parent.categories = ["Segmentation"]
self.parent.dependencies = []
+ self.parent.hidden = True
self.parent.contributors = ["LTrace Geophysics Team"] # replace with "Firstname Lastname (Organization)"
- self.parent.helpText = SegmentationEnv.help()
+ self.parent.helpText = ""
self.parent.acknowledgementText = ""
+ self.environment = SegmentationEnvLogic()
+
@classmethod
def readme_path(cls):
return str(cls.MODULE_DIR / "README.md")
- @classmethod
- def help(cls):
- import markdown
-
- htmlHelp = ""
- with open(cls.readme_path(), "r", encoding="utf-8") as docfile:
- md = markdown.Markdown(extras=["fenced-code-blocks"])
- htmlHelp = md.convert(docfile.read())
-
- htmlHelp += "\n".join([Segmenter.help(), SegmentInspector.help(), LabelMapEditor.help()])
-
- return htmlHelp
-
-
-def ManualSegmentationTab():
- return slicer.modules.customizedsegmenteditor.createNewWidgetRepresentation()
-
-
-def SmartSegmenterTab():
- return slicer.modules.segmenter.createNewWidgetRepresentation()
-
-
-def PoreStatsTab():
- return slicer.modules.porestats.createNewWidgetRepresentation()
-
-
-def ThinSectionInstanceSegmenterTab():
- return slicer.modules.thinsectioninstancesegmenter.createNewWidgetRepresentation()
-
-
-def ThinSectionInstanceEditorTab():
- return slicer.modules.thinsectioninstanceeditor.createNewWidgetRepresentation()
-
-
-def SegmentInspectorTab():
- return slicer.modules.segmentinspector.createNewWidgetRepresentation()
-
-
-def ThinSectionSegmentInspectorTab():
- return slicer.modules.thinsectionsegmentinspector.createNewWidgetRepresentation()
-
-
-def LabelMapEditorTab():
- return slicer.modules.labelmapeditor.createNewWidgetRepresentation()
-
-
-def SegmentationModellingTab():
- return slicer.modules.segmentationmodelling.createNewWidgetRepresentation()
-
-
-def HistogramTab():
- return slicer.modules.histogramsegmenter.createNewWidgetRepresentation()
-
-
-class SegmentationEnvWidget(LTracePluginWidget):
- def __init__(self, parent):
- LTracePluginWidget.__init__(self, parent)
- self.hasModelling = False
- self.hasPetrography = False
- self.isThinSection = False
-
- def setup(self):
- LTracePluginWidget.setup(self)
-
- self.currentTabIndex = 0
-
- # Instantiate and connect widgets ...
-
- self.tabsWidget = qt.QTabWidget()
- self.segmentEditorWidget = ManualSegmentationTab()
- self.smartSegWidget = SmartSegmenterTab()
- self.poreStatsWdiget = PoreStatsTab()
- self.tabsWidget.addTab(self.segmentEditorWidget, "Manual")
- self.tabsWidget.addTab(self.smartSegWidget, "Smart-seg")
- if self.hasPetrography:
- self.petrographyWidget = ThinSectionInstanceSegmenterTab()
- self.tabsWidget.addTab(self.petrographyWidget, "Instance")
- self.tabsWidget.addTab(ThinSectionInstanceEditorTab(), "Instance Editor")
- if self.isThinSection:
- self.tabsWidget.addTab(ThinSectionSegmentInspectorTab(), "Inspector")
- self.tabsWidget.addTab(self.poreStatsWdiget, "Pore Stats")
- else:
- self.tabsWidget.addTab(SegmentInspectorTab(), "Inspector")
- self.tabsWidget.addTab(LabelMapEditorTab(), "Label Editor")
- if self.hasModelling:
- self.tabsWidget.addTab(SegmentationModellingTab(), "Modelling")
-
- tabsWidgetLayout = qt.QVBoxLayout(self.tabsWidget)
- tabsWidgetLayout.addStretch(1)
-
- self.layout.addWidget(self.tabsWidget)
- self.tabsWidget.tabBarClicked.connect(self.onTabBarClicked)
-
- def enter(self) -> None:
- super().enter()
- self.tabsWidget.widget(self.currentTabIndex).enter()
- self.smartSegWidget.self().enter()
- if self.hasPetrography:
- self.petrographyWidget.self().enter()
-
- def exit(self):
- self.tabsWidget.widget(self.currentTabIndex).exit()
-
- def onTabBarClicked(self, index):
- self.exit()
- self.currentTabIndex = index
- self.enter()
-
- def removeTab(self, index):
- self.tabsWidget.removeTab(index)
+#
+#
+# def ManualSegmentationTab():
+# return slicer.modules.customizedsegmenteditor.createNewWidgetRepresentation()
+#
+#
+# def SmartSegmenterTab():
+# return slicer.modules.segmenter.createNewWidgetRepresentation()
+#
+#
+# def PoreStatsTab():
+# return slicer.modules.porestats.createNewWidgetRepresentation()
+#
+#
+# def ThinSectionInstanceSegmenterTab():
+# return slicer.modules.thinsectioninstancesegmenter.createNewWidgetRepresentation()
+#
+#
+# def ThinSectionInstanceEditorTab():
+# return slicer.modules.thinsectioninstanceeditor.createNewWidgetRepresentation()
+#
+#
+# def SegmentInspectorTab():
+# return slicer.modules.segmentinspector.createNewWidgetRepresentation()
+#
+#
+# def ThinSectionSegmentInspectorTab():
+# return slicer.modules.thinsectionsegmentinspector.createNewWidgetRepresentation()
+#
+#
+# def LabelMapEditorTab():
+# return slicer.modules.labelmapeditor.createNewWidgetRepresentation()
+#
+#
+# def SegmentationModellingTab():
+# return slicer.modules.segmentationmodelling.createNewWidgetRepresentation()
+#
+#
+# def HistogramTab():
+# return slicer.modules.histogramsegmenter.createNewWidgetRepresentation()
+#
+#
+# class SegmentationEnvWidget(LTracePluginWidget):
+# def __init__(self, parent):
+# LTracePluginWidget.__init__(self, parent)
+# self.hasModelling = False
+# self.hasPetrography = False
+# self.isThinSection = False
+#
+# def setup(self):
+# LTracePluginWidget.setup(self)
+#
+# self.currentTabIndex = 0
+#
+# # Instantiate and connect widgets ...
+#
+# self.tabsWidget = qt.QTabWidget()
+# self.segmentEditorWidget = ManualSegmentationTab()
+# self.smartSegWidget = SmartSegmenterTab()
+# self.poreStatsWdiget = PoreStatsTab()
+# self.tabsWidget.addTab(self.segmentEditorWidget, "Manual")
+# self.tabsWidget.addTab(self.smartSegWidget, "Smart-seg")
+# if self.hasPetrography:
+# self.petrographyWidget = ThinSectionInstanceSegmenterTab()
+# self.tabsWidget.addTab(self.petrographyWidget, "Instance")
+# self.tabsWidget.addTab(ThinSectionInstanceEditorTab(), "Instance Editor")
+# if self.isThinSection:
+# self.tabsWidget.addTab(ThinSectionSegmentInspectorTab(), "Inspector")
+# self.tabsWidget.addTab(self.poreStatsWdiget, "Pore Stats")
+# else:
+# self.tabsWidget.addTab(SegmentInspectorTab(), "Inspector")
+# self.tabsWidget.addTab(LabelMapEditorTab(), "Label Editor")
+# if self.hasModelling:
+# self.tabsWidget.addTab(SegmentationModellingTab(), "Modelling")
+#
+# tabsWidgetLayout = qt.QVBoxLayout(self.tabsWidget)
+# tabsWidgetLayout.addStretch(1)
+#
+# self.layout.addWidget(self.tabsWidget)
+# self.tabsWidget.tabBarClicked.connect(self.onTabBarClicked)
+#
+# def enter(self) -> None:
+# super().enter()
+# self.tabsWidget.widget(self.currentTabIndex).enter()
+# self.smartSegWidget.self().enter()
+# if self.hasPetrography:
+# self.petrographyWidget.self().enter()
+#
+# def exit(self):
+# self.tabsWidget.widget(self.currentTabIndex).exit()
+#
+# def onTabBarClicked(self, index):
+# self.exit()
+# self.currentTabIndex = index
+# self.enter()
+#
+# def removeTab(self, index):
+# self.tabsWidget.removeTab(index)
+#
#
# SegmentationEnvLogic
#
class SegmentationEnvLogic(LTracePluginLogic):
- pass
+ def __init__(self):
+ super().__init__()
+ self.__modulesToolbar = None
+ self.__modulesInfo = None
+
+ @property
+ def modulesToolbar(self):
+ if not self.__modulesToolbar:
+ raise AttributeError("Modules toolbar not set")
+ return self.__modulesToolbar
+
+ @modulesToolbar.setter
+ def modulesToolbar(self, value):
+ self.__modulesToolbar = value
+
+ def setModules(self, modules):
+ self.__modulesInfo = {module.key: module for module in modules}
+
+ def onStartupCompleted(self):
+ pass
+
+ def setupEnviron(self):
+ pass
+
+ def setupEnv(self, moduleManager):
+ modulesForThinSection = set(moduleManager["Thin Section"])
+ modulesForSegmentation = set(moduleManager["Segmentation"])
+ modulesForSegmentationTools = set(moduleManager["Tools"])
+ modules = modulesForSegmentation.intersection(modulesForThinSection)
+ modules.update(modulesForSegmentation.intersection(modulesForSegmentationTools))
+
+ loadModules(modules, permanent=False, favorite=False)
+ self.setModules(modules)
+
+ addMenu(
+ svgToQIcon(getResourcePath("Icons") / "IconSet-dark" / "Layers.svg"),
+ "Segmentation",
+ [
+ self.__modulesInfo["CustomizedSegmentEditor"],
+ self.__modulesInfo["Segmenter"],
+ self.__modulesInfo["SegmentInspector"],
+ self.__modulesInfo["ThinSectionInstanceSegmenter"],
+ self.__modulesInfo["ThinSectionInstanceEditor"],
+ self.__modulesInfo["LabelMapEditor"],
+ self.__modulesInfo["PoreStats"],
+ ],
+ self.modulesToolbar,
+ )
diff --git a/src/modules/SegmentationEnv/ThinSectionSegmentationEnv.py b/src/modules/SegmentationEnv/ThinSectionSegmentationEnv.py
deleted file mode 100644
index cc2135f..0000000
--- a/src/modules/SegmentationEnv/ThinSectionSegmentationEnv.py
+++ /dev/null
@@ -1,16 +0,0 @@
-from SegmentationEnv import SegmentationEnv, SegmentationEnvWidget
-
-
-class ThinSectionSegmentationEnv(SegmentationEnv):
- SETTING_KEY = "Thin Section Segmentation Environment"
-
- def __init__(self, parent):
- SegmentationEnv.__init__(self, parent)
- self.parent.title = "Thin Section Segmentation Tools"
-
-
-class ThinSectionSegmentationEnvWidget(SegmentationEnvWidget):
- def __init__(self, parent) -> None:
- super().__init__(parent)
- self.hasPetrography = True
- self.isThinSection = True
diff --git a/src/modules/SegmentationKmeans/SegmentationKmeans.py b/src/modules/SegmentationKmeans/SegmentationKmeans.py
index 1cdd64f..78bd873 100644
--- a/src/modules/SegmentationKmeans/SegmentationKmeans.py
+++ b/src/modules/SegmentationKmeans/SegmentationKmeans.py
@@ -28,7 +28,7 @@ class SegmentationKmeans(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
self.parent.title = "Segmentation Kmeans"
- self.parent.categories = ["LTrace Tools"]
+ self.parent.categories = ["Tools"]
self.parent.contributors = ["LTrace Geophysics Team"]
self.parent.helpText = SegmentationKmeans.help()
diff --git a/src/modules/SegmentationModelling/Resources/Icons/SegmentationModelling.png b/src/modules/SegmentationModelling/Resources/Icons/SegmentationModelling.png
deleted file mode 100644
index b89476c..0000000
Binary files a/src/modules/SegmentationModelling/Resources/Icons/SegmentationModelling.png and /dev/null differ
diff --git a/src/modules/SegmentationModelling/Resources/Icons/SegmentationModelling.svg b/src/modules/SegmentationModelling/Resources/Icons/SegmentationModelling.svg
new file mode 100644
index 0000000..d55c70f
--- /dev/null
+++ b/src/modules/SegmentationModelling/Resources/Icons/SegmentationModelling.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/modules/SegmentationModelling/SegmentationModelling.py b/src/modules/SegmentationModelling/SegmentationModelling.py
index fe25393..322d2d8 100644
--- a/src/modules/SegmentationModelling/SegmentationModelling.py
+++ b/src/modules/SegmentationModelling/SegmentationModelling.py
@@ -24,8 +24,8 @@ class SegmentationModelling(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
- self.parent.title = "SegmentationModelling"
- self.parent.categories = ["LTrace Tools"]
+ self.parent.title = "Modelling"
+ self.parent.categories = ["Segmentation", "MicroCT"]
self.parent.contributors = ["LTrace Geophysics Team"]
self.parent.helpText = SegmentationModelling.help()
diff --git a/src/modules/Segmenter/BayesianInferenceCLI/BayesianInferenceCLI.xml b/src/modules/Segmenter/BayesianInferenceCLI/BayesianInferenceCLI.xml
index cd4b98d..8469f3f 100644
--- a/src/modules/Segmenter/BayesianInferenceCLI/BayesianInferenceCLI.xml
+++ b/src/modules/Segmenter/BayesianInferenceCLI/BayesianInferenceCLI.xml
@@ -11,6 +11,14 @@
+
+ inputVolume
+ master
+
+
+ input
+
+ inputVolume1
diff --git a/src/modules/Segmenter/MonaiModelsCLI/MonaiModelsCLI.py b/src/modules/Segmenter/MonaiModelsCLI/MonaiModelsCLI.py
index 0f46a38..de5ad36 100644
--- a/src/modules/Segmenter/MonaiModelsCLI/MonaiModelsCLI.py
+++ b/src/modules/Segmenter/MonaiModelsCLI/MonaiModelsCLI.py
@@ -34,9 +34,9 @@
from copy import deepcopy
-from MonaiModelsCLILib.models.unet import UNetAct, UNetActWithBoundarySupervision
+from MonaiModelsLib.models.unet import UNetAct, UNetActWithBoundarySupervision
-from MonaiModelsCLILib.transforms import (
+from MonaiModelsLib.transforms import (
ComposedTransform,
IdentityTransform,
ReadNetCDFTransform,
diff --git a/src/modules/Segmenter/MonaiModelsCLI/MonaiModelsLib/__init__.py b/src/modules/Segmenter/MonaiModelsCLI/MonaiModelsLib/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/modules/Segmenter/MonaiModelsCLI/MonaiModelsLib/models/__init__.py b/src/modules/Segmenter/MonaiModelsCLI/MonaiModelsLib/models/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/modules/Segmenter/MonaiModelsCLI/MonaiModelsCLILib/models/unet.py b/src/modules/Segmenter/MonaiModelsCLI/MonaiModelsLib/models/unet.py
similarity index 100%
rename from src/modules/Segmenter/MonaiModelsCLI/MonaiModelsCLILib/models/unet.py
rename to src/modules/Segmenter/MonaiModelsCLI/MonaiModelsLib/models/unet.py
diff --git a/src/modules/Segmenter/MonaiModelsCLI/MonaiModelsCLILib/transforms.py b/src/modules/Segmenter/MonaiModelsCLI/MonaiModelsLib/transforms.py
similarity index 100%
rename from src/modules/Segmenter/MonaiModelsCLI/MonaiModelsCLILib/transforms.py
rename to src/modules/Segmenter/MonaiModelsCLI/MonaiModelsLib/transforms.py
diff --git a/src/modules/Segmenter/MonaiModelsCLI/models/__init__.py b/src/modules/Segmenter/MonaiModelsCLI/models/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/modules/Segmenter/Resources/Icons/Segmenter.png b/src/modules/Segmenter/Resources/Icons/Segmenter.png
deleted file mode 100644
index b89476c..0000000
Binary files a/src/modules/Segmenter/Resources/Icons/Segmenter.png and /dev/null differ
diff --git a/src/modules/Segmenter/Resources/Icons/Segmenter.svg b/src/modules/Segmenter/Resources/Icons/Segmenter.svg
new file mode 100644
index 0000000..fb991e9
--- /dev/null
+++ b/src/modules/Segmenter/Resources/Icons/Segmenter.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/modules/Segmenter/Segmenter.py b/src/modules/Segmenter/Segmenter.py
index f86dc04..d5a271a 100644
--- a/src/modules/Segmenter/Segmenter.py
+++ b/src/modules/Segmenter/Segmenter.py
@@ -1,22 +1,25 @@
+import copy
import glob
import importlib
import json
+import logging
import math
import os
import pickle
-from pathlib import Path
import shutil
-import cv2
-import markdown
-import logging
+from pathlib import Path
import ctk
-import copy
+import cv2
+import markdown
import matplotlib.colors as mcolors
import numpy as np
import qt
import slicer
import vtk
+from recordtype import recordtype # mutable
+
+from SegmenterMethods.correlation_distance import CorrelationDistance
from ltrace.algorithms.gabor import get_gabor_kernels
from ltrace.assets_utils import get_trained_models_with_metadata, get_metadata, get_pth
from ltrace.slicer import ui, helpers, widgets
@@ -29,17 +32,14 @@
maskInputWithROI,
highlight_error,
hex2Rgb,
+ getCurrentEnvironment,
)
from ltrace.slicer.node_attributes import NodeEnvironment
-from ltrace.slicer.widgets import BaseSettingsWidget, PixelLabel
from ltrace.slicer.widget.global_progress_bar import LocalProgressBar
from ltrace.slicer.widget.help_button import HelpButton
+from ltrace.slicer.widgets import BaseSettingsWidget, PixelLabel
from ltrace.slicer_utils import LTracePlugin, LTracePluginWidget, LTracePluginLogic
-from Customizer import Customizer
-from SegmenterMethods.correlation_distance import CorrelationDistance
-from recordtype import recordtype # mutable
-from slicer.ScriptedLoadableModule import *
-
+from ltrace.slicer.cli_queue import CliQueue
# Checks if closed source code is available
try:
@@ -156,12 +156,11 @@ class Segmenter(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
- self.parent.title = "Segmenter" # TODO make this more human readable by adding spaces
- self.parent.categories = ["Segmentation"]
+ self.parent.title = "AI Segmenter"
+ self.parent.categories = ["Segmentation", "Thin Section", "MicroCT", "ImageLog", "Core", "Multiscale"]
self.parent.dependencies = []
self.parent.contributors = ["LTrace Geophysics Team"] # replace with "Firstname Lastname (Organization)"
- self.parent.helpText = Segmenter.help()
- self.parent.helpText += self.getDefaultModuleDocumentationLink()
+ self.parent.helpText = f"file:///{Path(helpers.get_scripted_modules_path() + '/Resources/manual/Segmenter/Automatic/automatic_thinSection.html').as_posix()}"
self.parent.acknowledgementText = "" # replace with organization, grant and thanks.
@classmethod
@@ -173,13 +172,14 @@ class SegmenterWidget(LTracePluginWidget):
def __init__(self, parent):
LTracePluginWidget.__init__(self, parent)
- self.cliNode = None
+ self.cliQueue = None
self.refNodeId = None
self.filterUpdateThread = None
self.inputsSelector = None
self.inputSelectorMode = None
self.imageLogMode = False
self.deterministicPreTrainedModels = False
+ self.poreCleaningOptionsWidget = None
self.hideWhenCreatingClassifier = []
self.hideWhenLoadingClassifier = []
@@ -195,6 +195,7 @@ def setup(self):
self.layout.addWidget(self._setupClassifierSection())
self.layout.addWidget(self._setupInputsSection())
+ self.layout.addWidget(self._setupCleaningSection())
self.layout.addWidget(self._setupSettingsSection())
self.layout.addWidget(self._setupOutputSection())
self.layout.addWidget(self._setupApplySection())
@@ -226,7 +227,8 @@ def _setupClassifierSection(self):
self.classifierInput.objectName = "Classifier Input ComboBox"
self.userClassifierInput.objectName = "User Classifier Input ComboBox"
- self.classifierInput.activated.connect(self._onChangedClassifier)
+ # self.classifierInput.activated.connect(self._onChangedClassifier)
+ self.classifierInput.currentTextChanged.connect(self._onChangedClassifier)
self.classifierInput.setToolTip("Select pre-trained model for segmentation")
self.classifierInput.currentIndexChanged.connect(lambda _: self.classifierInput.setStyleSheet(""))
@@ -283,6 +285,55 @@ def _setupClassifierSection(self):
return widget
+ def _setupCleaningSection(self):
+ widget = ctk.ctkCollapsibleButton()
+ widget.text = "Cleaning"
+ widget.objectName = "Cleaning Collapsible Button"
+ layout = qt.QFormLayout(widget)
+
+ self.removeSpuriousCheckbox = qt.QCheckBox("Remove spurious")
+ self.removeSpuriousCheckbox.toolTip = "Detect and remove spurious predictions."
+ self.removeSpuriousCheckbox.checked = True
+ self.removeSpuriousCheckbox.objectName = "Remove Spurious CheckBox"
+
+ self.cleanResinCheckbox = qt.QCheckBox("Clean resin")
+ self.cleanResinCheckbox.toolTip = "Detect and clean bubbles and residues in pore resin."
+ self.cleanResinCheckbox.checked = True
+ self.cleanResinCheckbox.objectName = "Clean Resin CheckBox"
+ self.cleanResinCheckbox.connect("toggled(bool)", self._onCleanResinClicked)
+
+ self.pxForCleaningInput = ui.hierarchyVolumeInput(
+ hasNone=True,
+ nodeTypes=[
+ "vtkMRMLScalarVolumeNode",
+ "vtkMRMLVectorVolumeNode",
+ ],
+ tooltip="Combine PP and PX images for more accurate resin cleaning. If None, only the PP image is used. \
+ If a PX image is required as a model input, this selector will remain blocked and set to use the same image.",
+ onChange=self._onPxForCleaningSelected,
+ onActivation=self._onPxForCleaningSelected,
+ )
+ self.pxForCleaningInput.objectName = "PX For Cleaning ComboBox"
+
+ self.pxForCleaningInputLabel = qt.QLabel(" PX:")
+
+ self.smartRegCheckbox = qt.QCheckBox("Smart registration")
+ self.smartRegCheckbox.toolTip = "Method for registrating PP and PX images for pore resin cleaning. If unchecked, the images will be overlapped so that \
+ each one's center will share the same location: recommended when the images seem to be naturally registered already. \
+ If checked, the algorithm will decide between just centralizing the images (as in the unchecked case) or cropping their \
+ rock region before: recommended when PP and PX have different dimensions or do not seem to overlap naturally."
+ self.smartRegCheckbox.objectName = "Smart Registration CheckBox"
+ self.smartRegCheckbox.checked = False
+
+ layout.addRow(self.removeSpuriousCheckbox)
+ layout.addRow(self.cleanResinCheckbox)
+ layout.addRow(self.pxForCleaningInputLabel, self.pxForCleaningInput)
+ layout.addRow("", self.smartRegCheckbox)
+
+ self.poreCleaningOptionsWidget = widget
+
+ return widget
+
def _setupInputsSection(self):
widget = ctk.ctkCollapsibleButton()
widget.text = "Inputs"
@@ -319,6 +370,8 @@ def _setupInputsSection(self):
self.inputComboboxes = [self.inputsSelector.referenceInput, *extraInputComboboxes]
self.inputLabels = [self.inputsSelector.referenceLabel, *extraInputLabels]
+ self.pxInputCombobox = self.inputComboboxes[1]
+
for i in range(maxInputChannels):
combobox = self.inputComboboxes[i]
label = self.inputLabels[i]
@@ -390,28 +443,38 @@ def _setupApplySection(self):
widget = qt.QWidget()
vlayout = qt.QVBoxLayout(widget)
- self.applyButton = ui.ButtonWidget(
- text="Apply", tooltip="Run segmenter on input data limited by ROI", onClick=self._onApplyClicked
+ self.applyCancelButtons = ui.ApplyCancelButtons(
+ onApplyClick=self._onApplyClicked,
+ onCancelClick=self._onCancel,
+ applyTooltip="Run segmenter on input data limited by ROI",
+ cancelTooltip="Cancel",
+ applyText="Apply",
+ cancelText="Cancel",
+ enabled=False,
+ applyObjectName="Apply Button",
+ cancelObjectName=None,
)
- self.applyButton.objectName = "Apply Button"
-
- self.applyButton.setStyleSheet("QPushButton {font-size: 11px; font-weight: bold; padding: 8px; margin: 0px}")
- self.applyButton.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding)
-
- self.applyButton.enabled = False
+ self.stepLabel = qt.QLabel("")
self.progressBar = LocalProgressBar()
hlayout = qt.QHBoxLayout()
- hlayout.addWidget(self.applyButton)
+ hlayout.addWidget(self.applyCancelButtons)
hlayout.setContentsMargins(0, 8, 0, 8)
vlayout.addLayout(hlayout)
+ vlayout.addWidget(self.stepLabel)
vlayout.addWidget(self.progressBar)
return widget
+ def _onCleanResinClicked(self):
+ self.pxForCleaningInput.visible = self.cleanResinCheckbox.checked
+ self.pxForCleaningInputLabel.visible = self.cleanResinCheckbox.checked
+ self._onPxForCleaningSelected()
+
def _onLoadClassifierRadioToggled(self):
+ self._onCleanResinClicked()
self._onChangedClassifier()
self._updateWidgetsVisibility()
@@ -449,12 +512,12 @@ def _validateSourceVolume(self, annotationNode, soiNode, imageNode):
def enter(self) -> None:
super().enter()
# Update URL from Automatic classifier help's message
- targetEnvUrlRelation = {"MicroCTEnv": "microCT", "ThinSectionEnv": "thinSection"}
-
- env = slicer.util.selectedModule()
- envLabel = targetEnvUrlRelation.get(env, "microCT")
- message = self.loadClassifierHelpButton.message.replace("[ENV]", envLabel)
- self.loadClassifierHelpButton.updateMessage(message)
+ # targetEnvUrlRelation = {"MicroCTEnv": "microCT", "ThinSectionEnv": "thinSection"}
+ #
+ # env = slicer.util.selectedModule()
+ # envLabel = targetEnvUrlRelation.get(env, "microCT")
+ # message = self.loadClassifierHelpButton.message.replace("[ENV]", envLabel)
+ # self.loadClassifierHelpButton.updateMessage(message)
# Add pretrained models
if self.classifierInput.count == 0:
@@ -462,13 +525,14 @@ def enter(self) -> None:
self._updateWidgetsVisibility()
def _addPretrainedModelsIfAvailable(self):
- env = slicer.util.selectedModule()
+ env = getCurrentEnvironment().value
envs = tuple(map(lambda x: x.value, NodeEnvironment))
if env not in envs:
return
self.classifierInput.clear()
+
model_dirs = get_trained_models_with_metadata(env)
for model_dir in model_dirs:
try:
@@ -568,6 +632,10 @@ def _onChangedClassifier(self, selected=None):
combobox.setCurrentNode(None)
self.inputsSelector.mainInput.setCurrentNode(None)
+ self.poreCleaningOptionsWidget.visible = model_classes == ["Pore"]
+ self.pxForCleaningInput.enabled = len(model_inputs) == 1
+ if not self.pxForCleaningInput.enabled:
+ self._onPxSelected()
def _onChangedUserClassifier(self, selected):
isNodeTypeValid = selected and selected.IsA("vtkMRMLTextNode")
@@ -622,9 +690,9 @@ def _onInputSelected(self, node):
def _onReferenceSelected(self, node):
self.refNodeId = node.GetID() if node is not None else None
- self._checkRequirementsForApply()
if node is None:
return
+ self._checkRequirementsForApply()
spacing = min([x for x in node.GetSpacing()])
minSide = min(filter(lambda i: i != 1, node.GetImageData().GetDimensions())) * spacing
@@ -637,6 +705,14 @@ def _onReferenceSelected(self, node):
self.bayes_widget.setImageInput(node)
+ def _onPxSelected(self):
+ self.pxForCleaningInput.setCurrentNode(self.pxInputCombobox.currentNode())
+
+ def _onPxForCleaningSelected(self):
+ self.smartRegCheckbox.visible = self.pxForCleaningInput.visible and (
+ self.pxForCleaningInput.currentNode() is not None
+ )
+
def _checkHaveFilters(self):
if self.createClassifierRadio.isChecked() and self.methodSelector.currentWidget().METHOD == "random_forest":
valid = len(self.methodSelector.currentWidget().customFilters) and (self.refNodeId != None)
@@ -647,22 +723,10 @@ def _checkRequirementsForApply(self):
if self.methodSelector.currentWidget() == None:
return
- if self.cliNode == None or not self.cliNode.IsBusy():
- self.applyButton.enabled = self.refNodeId is not None
+ if self.cliQueue == None or not self.cliQueue.is_running():
+ self.applyCancelButtons.setEnabled(self.refNodeId is not None)
else:
- self.applyButton.enabled = False
-
- def _onSegmenterCLIModified(self, cliNode, event):
- if cliNode is None:
- return
-
- if cliNode.GetStatusString() == "Completed":
- warning = cliNode.GetParameterAsString("report")
- if warning != "":
- slicer.util.warningDisplay(warning)
-
- if not cliNode.IsBusy():
- print("ExecCmd CLI %s" % cliNode.GetStatusString())
+ self.applyCancelButtons.setEnabled(False)
def _currentMethod(self):
widget = self.methodSelector.currentWidget()
@@ -686,8 +750,8 @@ def _onApplyClicked(self):
if not self._validateSourceVolume(segmentationNode, roiSegNode, referenceVolumeNode):
return
- self.applyButton.enabled = False
-
+ self.applyCancelButtons.applyBtn.setEnabled(False)
+ self.applyCancelButtons.cancelBtn.setEnabled(True)
prefix = self.outputPrefix.text + "_{type}"
if not self.imageLogMode:
@@ -709,11 +773,13 @@ def _onApplyClicked(self):
return
try:
+ self.cliQueue = CliQueue(update_display=False, progress_bar=self.progressBar, progress_label=self.stepLabel)
+
if self._currentMethod() == "bayesian-inference":
inputModelDir = None
params = self.methodSelector.currentWidget().getValuesAsDict()
logic = BayesianInferenceLogic(self.imageLogMode, onFinish=self.resetUI, parent=self.parent)
- self.cliNode = logic.run(
+ logic.run(
inputModelDir,
segmentationNode,
referenceVolumeNode,
@@ -721,12 +787,13 @@ def _onApplyClicked(self):
roiSegNode,
prefix,
params,
+ self.cliQueue,
)
elif self.createClassifierRadio.checked:
inputModelDir = None
params = self.methodSelector.currentWidget().getValuesAsDict()
logic = SegmenterLogic(self.imageLogMode, onFinish=self.resetUI, parent=self.parent)
- self.cliNode = logic.run(
+ logic.run(
inputModelDir,
segmentationNode,
referenceVolumeNode,
@@ -735,6 +802,7 @@ def _onApplyClicked(self):
prefix,
params,
self.keepFeaturesCheckbox.checked,
+ self.cliQueue,
)
elif self.userClassifierRadio.checked:
inputModelDir = self.userClassifierInput.currentNode()
@@ -743,7 +811,7 @@ def _onApplyClicked(self):
raise ValueError("Please select a valid model.")
params = None
logic = SegmenterLogic(self.imageLogMode, onFinish=self.resetUI, parent=self.parent)
- self.cliNode = logic.run(
+ logic.run(
inputModelDir,
segmentationNode,
referenceVolumeNode,
@@ -752,25 +820,29 @@ def _onApplyClicked(self):
prefix,
params,
self.keepFeaturesCheckbox.checked,
+ self.cliQueue,
)
else:
inputModelComboBox = self.classifierInput
modelKind = get_metadata(inputModelComboBox.currentData)["kind"]
+ kernelSize = None
if modelKind == "torch":
logic = MonaiModelsLogic(self.imageLogMode, onFinish=self.resetUI, parent=self.parent)
- self.cliNode = logic.run(
+ tmpReferenceNode, tmpOutNode = logic.run(
inputModelComboBox,
referenceVolumeNode,
extraVolumeNodes,
roiSegNode,
prefix,
self.deterministicPreTrainedModels,
+ self.cliQueue,
)
elif modelKind == "bayesian":
+ kernelSize = int(inputModelComboBox.currentData.split("_")[-1][0])
params = None
logic = BayesianInferenceLogic(self.imageLogMode, onFinish=self.resetUI, parent=self.parent)
- self.cliNode = logic.run(
+ tmpReferenceNode, tmpOutNode = logic.run(
inputModelComboBox.currentData,
segmentationNode,
referenceVolumeNode,
@@ -778,21 +850,38 @@ def _onApplyClicked(self):
roiSegNode,
prefix,
params,
+ self.cliQueue,
)
+
+ if self.cliQueue and self.poreCleaningOptionsWidget.visible:
+ logic = PoreCleaningLogic(
+ removeSpurious=self.removeSpuriousCheckbox.isChecked(),
+ cleanResin=self.cleanResinCheckbox.isChecked(),
+ selectedPxNode=self.pxForCleaningInput.currentNode(),
+ smartReg=self.smartRegCheckbox.isChecked(),
+ )
+ logic.run(tmpReferenceNode, tmpOutNode, roiSegNode, modelKind, kernelSize, self.cliQueue)
+
+ self.cliQueue.run()
except Exception as e:
slicer.util.errorDisplay(f"Failed to complete execution. {e}")
tmpPrefix = prefix.replace("_{type}", "_TMP_*")
clearPattern(tmpPrefix)
- self.applyButton.enabled = True
+ clearPattern("TMP_P*_ROCK_AREA*")
+ self.applyCancelButtons.applyBtn.setEnabled(True)
+ self.applyCancelButtons.cancelBtn.setEnabled(False)
raise
- self.progressBar.setCommandLineModuleNode(self.cliNode)
+ def _onCancel(self):
+ if self.cliQueue is None:
+ return
+ self.cliQueue.stop(cancelled=True)
def resetUI(self):
self._checkRequirementsForApply()
- if self.cliNode:
- del self.cliNode
- self.cliNode = None
+ if self.cliQueue:
+ del self.cliQueue
+ self.cliQueue = None
def _updateWidgetsVisibility(self):
self._checkRequirementsForApply()
@@ -813,6 +902,12 @@ def _updateWidgetsVisibility(self):
self.classifierInput.visible = self.loadClassifierRadio.isChecked()
self.userClassifierInput.visible = self.userClassifierRadio.isChecked()
+ if self.classifierInput.visible:
+ self.pxInputCombobox.currentItemChanged.connect(self._onPxSelected)
+ else:
+ self.poreCleaningOptionsWidget.visible = False
+ self.pxInputCombobox.currentItemChanged.disconnect()
+
method = self._currentMethod()
if method and method != "random_forest":
self.keepFeaturesCheckbox.visible = False
@@ -880,6 +975,11 @@ def setupResultInScene(segmentationNode, referenceNode, imageLogMode, soiNode=No
slicer.util.setSliceViewerLayers(background=referenceNode, fit=True)
+def hideTmpOutput(caller, event, params):
+ if caller.GetStatus() == slicer.vtkMRMLCommandLineModuleNode.Completed:
+ slicer.util.setSliceViewerLayers(label=None)
+
+
class ClassifierProps:
"""Properties that determine whether specified inputs are compatible
with a pre-trained classifier.
@@ -903,6 +1003,81 @@ def prettify(dict_):
return "\n".join(f"{key}: {val}" for key, val in sorted(dict_.items()))
+class PoreCleaningLogic(LTracePluginLogic):
+ def __init__(self, removeSpurious, cleanResin, selectedPxNode, smartReg):
+ self.removeSpurious = removeSpurious
+ self.cleanResin = cleanResin
+ self.selectedPxNode = selectedPxNode
+ self.smartReg = smartReg
+
+ def run(self, referenceNode, outNode, soiNode, modelKind, bayesianKernelSize, cliQueue):
+ if self.removeSpurious:
+ cliConf = {
+ "input": referenceNode.GetID(),
+ "output": outNode.GetID(),
+ "poreSegmentation": outNode.GetID(),
+ "poreSegModel": "unet" if modelKind == "torch" else {3: "sbayes", 7: "bbayes"}[bayesianKernelSize],
+ }
+ cliQueue.create_cli_node(
+ slicer.modules.removespuriouscli,
+ cliConf,
+ progress_text="Removing spurious detections",
+ modified_callback=hideTmpOutput,
+ )
+
+ if self.cleanResin:
+ tmpPpRockAreaNode = helpers.createTemporaryVolumeNode(slicer.vtkMRMLLabelMapVolumeNode, "TMP_PP_ROCK_AREA")
+ cliQueue.create_cli_node(
+ slicer.modules.smartforegroundcli,
+ parameters={"input": referenceNode.GetID(), "outputRock": tmpPpRockAreaNode.GetID()},
+ progress_text="Getting PP rock area",
+ modified_callback=hideTmpOutput,
+ )
+
+ cliConf = {
+ "ppImage": referenceNode.GetID(),
+ "poreSegmentation": outNode.GetID(),
+ "output": outNode.GetID(),
+ "ppRockArea": tmpPpRockAreaNode.GetID(),
+ }
+
+ tmpPxForCleaningNode = self.selectedPxNode
+
+ if self.selectedPxNode is not None:
+ if soiNode is not None:
+ tmpPxForCleaningNode = prepareTemporaryInputs(
+ [tmpPxForCleaningNode],
+ tmpPxForCleaningNode.GetName(),
+ soiNode=soiNode,
+ referenceNode=self.selectedPxNode,
+ )[0][0]
+
+ cliConf.update({"pxImage": tmpPxForCleaningNode.GetID()})
+
+ if self.smartReg:
+ tmpPxRockAreaNode = helpers.createTemporaryVolumeNode(
+ slicer.vtkMRMLLabelMapVolumeNode, "TMP_PX_ROCK_AREA"
+ )
+ cliQueue.create_cli_node(
+ slicer.modules.smartforegroundcli,
+ parameters={
+ "input": tmpPxForCleaningNode.GetID(),
+ "outputRock": tmpPxRockAreaNode.GetID(),
+ },
+ progress_text="Getting PX rock area",
+ modified_callback=hideTmpOutput,
+ )
+
+ cliConf.update({"pxRockArea": tmpPxRockAreaNode.GetID(), "smartReg": True})
+
+ cliQueue.create_cli_node(
+ slicer.modules.cleanresincli,
+ cliConf,
+ progress_text="Cleaning pore resin",
+ modified_callback=hideTmpOutput,
+ )
+
+
class SegmenterLogic(LTracePluginLogic):
def __init__(self, imageLogMode, parent=None, onFinish=None):
super().__init__(parent)
@@ -993,6 +1168,7 @@ def run(
outputPrefix,
params,
enableKeepFeatures,
+ cliQueue,
):
if not inputClassifierNode and not segmentationNode:
slicer.util.errorDisplay("Please select a valid Segmentation Node as Annotation input.")
@@ -1055,9 +1231,9 @@ def run(
cliConf["xargs"] = json.dumps(params)
# End Setup Outputs -----------------------------------------------------------------------------
- cliNode = slicer.cli.run(slicer.modules.segmentercli, None, cliConf, wait_for_completion=False)
- def onSucess(caller):
+ def onSucess():
+ caller = cliQueue.get_current_node()
try:
outNode = helpers.createNode(
slicer.vtkMRMLSegmentationNode, outputPrefix.replace("{type}", "Segmentation")
@@ -1138,20 +1314,27 @@ def onSucess(caller):
self.progressUpdate(0)
raise
- def onFinish(caller):
+ def onFinish():
+ caller = cliQueue.get_current_node()
print("ExecCmd CLI %s" % caller.GetStatusString())
tmpPrefix = outputPrefix.replace("_{type}", "_TMP_*")
clearPattern(tmpPrefix)
self.progressUpdate(1.0)
self.onFinish()
- ehandler = CLIEventHandler()
- ehandler.onSucessEvent = onSucess
- ehandler.onFinish = onFinish
+ def onCancel():
+ slicer.mrmlScene.RemoveNode(tmpOutNode)
- cliNode.AddObserver("ModifiedEvent", ehandler)
+ def onFailure():
+ slicer.util.errorDisplay(f"Operation failed on {cliQueue.get_error_message()}")
- return cliNode
+ cliQueue.signal_queue_successful.connect(onSucess)
+ cliQueue.signal_queue_finished.connect(onFinish)
+ cliQueue.signal_queue_cancelled.connect(onCancel)
+ cliQueue.signal_queue_failed.connect(onFailure)
+ cliQueue.create_cli_node(slicer.modules.segmentercli, cliConf)
+
+ return tmpReferenceNode, tmpOutNode
def create_node(self, name, reference, data):
subjectHierarchyNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
@@ -1207,6 +1390,7 @@ def run(
soiNode,
outputPrefix,
deterministic,
+ cliQueue,
):
tmpOutNode = helpers.createNode(slicer.vtkMRMLLabelMapVolumeNode, outputPrefix.replace("{type}", "TMP_OUTNODE"))
slicer.mrmlScene.AddNode(tmpOutNode)
@@ -1257,9 +1441,9 @@ def run(
raise RuntimeError(message)
# End Setup Outputs -----------------------------------------------------------------------------
- cliNode = slicer.cli.run(slicer.modules.monaimodelscli, None, cliConf, wait_for_completion=False)
- def onSucess(caller):
+ def onSucess():
+ caller = cliQueue.get_current_node()
try:
outNode = helpers.createNode(
slicer.vtkMRMLSegmentationNode, outputPrefix.replace("{type}", "Segmentation")
@@ -1284,29 +1468,38 @@ def onSucess(caller):
else:
slicer.util.setSliceViewerLayers(background=referenceNode, fit=True)
- self.outNodeId = outNode.GetID()
-
except Exception as e:
print("Handle errors on state: %s" % caller.GetStatusString())
tmpPrefix = outputPrefix.replace("_{type}", "_TMP_*")
clearPattern(tmpPrefix)
+ clearPattern("TMP_P*_ROCK_AREA*")
self.progressUpdate(0)
raise
- def onFinish(caller):
+ def onFinish():
+ caller = cliQueue.get_current_node()
print("ExecCmd CLI %s" % caller.GetStatusString())
tmpPrefix = outputPrefix.replace("_{type}", "_TMP_*")
clearPattern(tmpPrefix)
+ clearPattern("TMP_P*_ROCK_AREA*")
self.progressUpdate(1.0)
self.onFinish()
- ehandler = CLIEventHandler()
- ehandler.onSucessEvent = onSucess
- ehandler.onFinish = onFinish
+ def onCancel():
+ slicer.mrmlScene.RemoveNode(tmpOutNode)
- cliNode.AddObserver("ModifiedEvent", ehandler)
+ def onFailure():
+ slicer.util.errorDisplay(f"Operation failed on {cliQueue.get_error_message()}")
- return cliNode
+ cliQueue.signal_queue_successful.connect(onSucess)
+ cliQueue.signal_queue_finished.connect(onFinish)
+ cliQueue.signal_queue_cancelled.connect(onCancel)
+ cliQueue.signal_queue_failed.connect(onFailure)
+ cliQueue.create_cli_node(
+ slicer.modules.monaimodelscli, cliConf, progress_text="Segmenting pores", modified_callback=hideTmpOutput
+ )
+
+ return tmpReferenceNode, tmpOutNode
class BayesianInferenceLogic(LTracePluginLogic):
@@ -1413,6 +1606,7 @@ def run(
soiNode,
outputPrefix,
params,
+ cliQueue,
):
if not inputModelDir and not segmentationNode:
slicer.util.errorDisplay("Please select a valid Segmentation Node as Annotation input.")
@@ -1477,9 +1671,9 @@ def run(
cliConf["xargs"] = json.dumps(params)
# End Setup Outputs -----------------------------------------------------------------------------
- cliNode = slicer.cli.run(slicer.modules.bayesianinferencecli, None, cliConf, wait_for_completion=False)
- def onSucess(caller):
+ def onSucess():
+ caller = cliQueue.get_current_node()
try:
outNode = helpers.createNode(
slicer.vtkMRMLSegmentationNode, outputPrefix.replace("{type}", "Segmentation")
@@ -1505,65 +1699,41 @@ def onSucess(caller):
else:
slicer.util.setSliceViewerLayers(background=referenceNode, fit=True)
- self.outNodeId = outNode.GetID()
-
except Exception as e:
print("Handle errors on state: %s" % caller.GetStatusString())
tmpPrefix = outputPrefix.replace("_{type}", "_TMP_*")
clearPattern(tmpPrefix)
+ clearPattern("TMP_P*_ROCK_AREA*")
self.progressUpdate(0)
raise
- def onFinish(caller):
+ def onFinish():
+ caller = cliQueue.get_current_node()
print("ExecCmd CLI %s" % caller.GetStatusString())
tmpPrefix = outputPrefix.replace("_{type}", "_TMP_*")
clearPattern(tmpPrefix)
+ clearPattern("TMP_P*_ROCK_AREA*")
self.progressUpdate(1.0)
self.onFinish()
- ehandler = CLIEventHandler()
- ehandler.onSucessEvent = onSucess
- ehandler.onFinish = onFinish
-
- cliNode.AddObserver("ModifiedEvent", ehandler)
-
- return cliNode
-
-
-class CLIEventHandler:
- COMPlETED = "completed"
- CANCELLED = "cancelled"
-
- def __init__(self):
- self.onSucessEvent = lambda cliNode: print("Completed")
- self.onErrorEvent = lambda cliNode: print("Completed with Errors")
- self.onCancelEvent = lambda cliNode: print("Cancelled")
- self.onFinish = lambda cliNode: None
-
- self.shouldProcess = True
-
- def getStatus(self, caller):
- return caller.GetStatusString().lower()
-
- def __call__(self, cliNode, event):
- if cliNode is None or not self.shouldProcess:
- return
-
- status = self.getStatus(cliNode)
-
- if status == self.COMPlETED:
- self.onSucessEvent(cliNode)
-
- elif "error" in status:
- self.onErrorEvent(cliNode)
-
- elif status == self.CANCELLED:
- self.onCancelEvent(cliNode)
+ def onCancel():
+ slicer.mrmlScene.RemoveNode(tmpOutNode)
+
+ def onFailure():
+ slicer.util.errorDisplay(f"Operation failed on {cliQueue.get_error_message()}")
+
+ cliQueue.signal_queue_successful.connect(onSucess)
+ cliQueue.signal_queue_finished.connect(onFinish)
+ cliQueue.signal_queue_cancelled.connect(onCancel)
+ cliQueue.signal_queue_failed.connect(onFailure)
+ cliQueue.create_cli_node(
+ slicer.modules.bayesianinferencecli,
+ cliConf,
+ progress_text="Segmenting pores",
+ modified_callback=hideTmpOutput,
+ )
- if not cliNode.IsBusy():
- self.onFinish(cliNode)
- self.shouldProcess = False
- cliNode.RemoveObservers("ModifiedEvent")
+ return tmpReferenceNode, tmpOutNode
class RandomForestSettingsWidget(BaseSettingsWidget):
@@ -1660,7 +1830,7 @@ def addFilter(self):
self.tableFilters.horizontalHeader().setStretchLastSection(qt.QHeaderView.Stretch)
self.tableFilters.verticalHeader().hide()
else:
- dialog = qt.QDialog(slicer.util.mainWindow())
+ dialog = qt.QDialog(slicer.modules.AppContextInstance.mainWindow)
dialog.setWindowFlags(dialog.windowFlags() & ~qt.Qt.WindowContextHelpButtonHint)
dialog.setWindowTitle("Feature already been added")
@@ -1696,7 +1866,7 @@ def editFilter(self, item):
self.customFilters[filter_func] = True
status = True
else:
- dialog = qt.QDialog(slicer.util.mainWindow())
+ dialog = qt.QDialog(slicer.modules.AppContextInstance.mainWindow)
dialog.setWindowFlags(dialog.windowFlags() & ~qt.Qt.WindowContextHelpButtonHint)
dialog.setWindowTitle("Customize applied filters")
diff --git a/src/modules/ShadingCorrection/ShadingCorrection.py b/src/modules/ShadingCorrection/ShadingCorrection.py
index aea5fe5..8c93bf8 100644
--- a/src/modules/ShadingCorrection/ShadingCorrection.py
+++ b/src/modules/ShadingCorrection/ShadingCorrection.py
@@ -32,7 +32,7 @@ class ShadingCorrection(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
self.parent.title = "Shading correction - Gaussian"
- self.parent.categories = ["LTrace Tools"]
+ self.parent.categories = ["Tools", "MicroCT", "Multiscale"]
self.parent.dependencies = []
self.parent.contributors = ["LTrace Geophysical Solutions"]
self.parent.helpText = ShadingCorrection.help()
diff --git a/tools/deploy/Resources/SideBySideImageIcon.png b/src/modules/SideBySideLayoutView/Resources/SideBySideImage.png
similarity index 100%
rename from tools/deploy/Resources/SideBySideImageIcon.png
rename to src/modules/SideBySideLayoutView/Resources/SideBySideImage.png
diff --git a/tools/deploy/Resources/SideBySideSegmentationIcon.png b/src/modules/SideBySideLayoutView/Resources/SideBySideSegmentation.png
similarity index 100%
rename from tools/deploy/Resources/SideBySideSegmentationIcon.png
rename to src/modules/SideBySideLayoutView/Resources/SideBySideSegmentation.png
diff --git a/src/modules/SideBySideLayoutView/SideBySideLayoutView.py b/src/modules/SideBySideLayoutView/SideBySideLayoutView.py
new file mode 100644
index 0000000..1484218
--- /dev/null
+++ b/src/modules/SideBySideLayoutView/SideBySideLayoutView.py
@@ -0,0 +1,236 @@
+from string import Template
+
+import slicer
+import vtk
+
+from ltrace.slicer.app.layouts import customLayout
+from ltrace.slicer.side_by_side_image_layout import setupViews, SideBySideImageManager
+from ltrace.slicer_utils import LTracePlugin
+
+
+class SideBySideLayoutView(LTracePlugin):
+ SETTING_KEY = "SideBySideLayoutView"
+
+ SIDE_BY_SIDE_IMAGE_LAYOUT_ID = 200
+ SIDE_BY_SIDE_SEGMENTATION_LAYOUT_ID = 201
+ SIDE_BY_SIDE_IMAGE_GROUP = 70
+ SIDE_BY_SIDE_SEGMENTATION_GROUP = 71
+
+ def __init__(self, parent):
+ LTracePlugin.__init__(self, parent)
+ self.parent.title = "Side by Side"
+ self.parent.categories = ["System"]
+ self.parent.dependencies = []
+ self.parent.hidden = True
+ self.parent.contributors = []
+ self.parent.helpText = ""
+ self.parent.acknowledgementText = ""
+
+ ####################################################################################
+ # Custom properties
+ ####################################################################################
+
+ self.sideBySideImageManager = None
+ self.sideBySideSegmentationSetupComplete = False
+
+ @staticmethod
+ def view_2D_with_group_template():
+ return Template(
+ """
+
+
+ $orientation
+ $label
+ $color
+ $group
+
+ """
+ )
+
+ @staticmethod
+ def side_by_side_layout_template():
+ return Template(
+ """
+
+ $view1
+ $view2
+ """
+ )
+
+ def sideBySideImageLayout(self):
+ layout_template = self.side_by_side_layout_template()
+ view_2d_template_with_group = self.view_2D_with_group_template()
+
+ layoutXML = layout_template.substitute(
+ view1=view_2d_template_with_group.substitute(
+ size="500",
+ tag="SideBySideSlice1",
+ orientation="XY",
+ label="1",
+ color="#EEEEEE",
+ group=self.SIDE_BY_SIDE_IMAGE_GROUP,
+ ),
+ view2=view_2d_template_with_group.substitute(
+ size="500",
+ tag="SideBySideSlice2",
+ orientation="XY",
+ label="2",
+ color="#EEEEEE",
+ group=self.SIDE_BY_SIDE_IMAGE_GROUP,
+ ),
+ )
+ customLayout(self.SIDE_BY_SIDE_IMAGE_LAYOUT_ID, layoutXML, "Side by side", self.resource("SideBySideImage.png"))
+
+ def sideBySideSegmentationLayout(self):
+ layout_template = self.side_by_side_layout_template()
+ view_2d_template_with_group = self.view_2D_with_group_template()
+
+ layoutXML = layout_template.substitute(
+ view1=view_2d_template_with_group.substitute(
+ size="500",
+ tag="SideBySideImageSlice",
+ orientation="XY",
+ label="I",
+ color="#EEEEEE",
+ group=self.SIDE_BY_SIDE_SEGMENTATION_GROUP,
+ ),
+ view2=view_2d_template_with_group.substitute(
+ size="500",
+ tag="SideBySideSegmentationSlice",
+ orientation="XY",
+ label="S",
+ color="#CCCCCC",
+ group=self.SIDE_BY_SIDE_SEGMENTATION_GROUP,
+ ),
+ )
+ customLayout(
+ self.SIDE_BY_SIDE_SEGMENTATION_LAYOUT_ID,
+ layoutXML,
+ "Side by side segmentation",
+ self.resource("SideBySideSegmentation.png"),
+ )
+
+ layout = slicer.app.layoutManager()
+
+ def onLayoutChanged(id_):
+ if id_ == self.SIDE_BY_SIDE_SEGMENTATION_LAYOUT_ID:
+ self.updateSideBySideSegmentation()
+ self._linkViews(("SideBySideSegmentationSlice", "SideBySideImageSlice"))
+ self._useSameBackgroundAs("Red", "SideBySideImageSlice")
+ self._useSameForegroundAs("Red", "SideBySideImageSlice")
+ self._useSameBackgroundAs("Red", "SideBySideSegmentationSlice", opacity=0)
+ self._useSameForegroundAs("Red", "SideBySideSegmentationSlice", opacity=0)
+
+ if not self.sideBySideSegmentationSetupComplete:
+ setupViews("SideBySideImageSlice", "SideBySideSegmentationSlice")
+ self.sideBySideSegmentationSetupComplete = True
+
+ # These are necessary despite also being called inside _useSameBackgroundAs and _useSameForegroundAs
+ slicer.app.processEvents(1000)
+ layout.sliceWidget("SideBySideImageSlice").sliceLogic().FitSliceToAll()
+ layout.sliceWidget("SideBySideSegmentationSlice").sliceLogic().FitSliceToAll()
+ else:
+ self.exitSideBySideSegmentation()
+
+ if id_ == self.SIDE_BY_SIDE_IMAGE_LAYOUT_ID:
+ self._linkViews(("SideBySideSlice1", "SideBySideSlice2"))
+ self._useSameBackgroundAs("Red", "SideBySideSlice1")
+ self._useSameBackgroundAs("Red", "SideBySideSlice2")
+
+ if not self.sideBySideImageManager:
+ self.sideBySideImageManager = SideBySideImageManager()
+ setupViews("SideBySideSlice1", "SideBySideSlice2")
+ self.sideBySideImageManager.enterLayout()
+ elif self.sideBySideImageManager:
+ self.sideBySideImageManager.exitLayout()
+
+ @vtk.calldata_type(vtk.VTK_OBJECT)
+ def onNodeAdded(caller, event, callData):
+ if (
+ isinstance(callData, slicer.vtkMRMLSegmentationNode)
+ and layout.layout == self.SIDE_BY_SIDE_SEGMENTATION_LAYOUT_ID
+ ):
+ self.updateSideBySideSegmentation()
+
+ layout.layoutChanged.connect(onLayoutChanged)
+
+ slicer.mrmlScene.AddObserver(slicer.mrmlScene.NodeAddedEvent, onNodeAdded)
+
+ def updateSideBySideSegmentation(self):
+ sliceWidget = slicer.app.layoutManager().sliceWidget("SideBySideSegmentationSlice")
+ if not sliceWidget or slicer.mrmlScene.IsImporting():
+ # Project is loading, will update later when layout is changed
+ return
+ segSliceLogic = sliceWidget.sliceLogic()
+ segCompositeNode = segSliceLogic.GetSliceCompositeNode()
+
+ # Hide image but still keep it as background for segmentation logic to work
+ segCompositeNode.SetBackgroundOpacity(0)
+
+ segSliceId = segSliceLogic.GetSliceNode().GetID()
+ segNodes = slicer.util.getNodesByClass("vtkMRMLSegmentationNode")
+ for segNode in segNodes:
+ # Image log has its own handling of segmentation visibility
+ if not segNode.GetAttribute("ImageLogSegmentation"):
+ segNode.CreateDefaultDisplayNodes()
+ displayNode = segNode.GetDisplayNode()
+ displayNode.SetOpacity(1)
+
+ # Show segmentation on 'S' slice view only
+ displayNode.AddViewNodeID(segSliceId)
+
+ def exitSideBySideSegmentation(self):
+ segNodes = slicer.util.getNodesByClass("vtkMRMLSegmentationNode")
+ for segNode in segNodes:
+ # Image log has its own handling of segmentation visibility
+ if not segNode.GetAttribute("ImageLogSegmentation"):
+ displayNode = segNode.GetDisplayNode()
+ if displayNode is None:
+ continue
+
+ displayNode.SetOpacity(0.5)
+ # Show segmentation on any view
+ displayNode.RemoveAllViewNodeIDs()
+
+ @staticmethod
+ def _linkViews(viewNames):
+ for viewName in viewNames:
+ slicer.app.layoutManager().sliceWidget(viewName).sliceLogic().GetSliceCompositeNode().SetLinkedControl(1)
+
+ @staticmethod
+ def _useSameBackgroundAs(fromSlice, toSlice, opacity=-1):
+ layout = slicer.app.layoutManager()
+ toLogic = layout.sliceWidget(toSlice).sliceLogic()
+ toComposite = toLogic.GetSliceCompositeNode()
+ if toComposite.GetBackgroundVolumeID() != None:
+ # This slice already has a background, don't change it
+ return
+ fromLogic = layout.sliceWidget(fromSlice).sliceLogic()
+ fromComposite = fromLogic.GetSliceCompositeNode()
+
+ toComposite.SetBackgroundVolumeID(fromComposite.GetBackgroundVolumeID())
+ if opacity < 0:
+ opacity = fromComposite.GetBackgroundOpacity()
+ toComposite.SetBackgroundOpacity(opacity)
+
+ fromLogic.FitSliceToAll()
+ toLogic.FitSliceToAll()
+
+ @staticmethod
+ def _useSameForegroundAs(fromSlice, toSlice, opacity=-1):
+ layout = slicer.app.layoutManager()
+ toLogic = layout.sliceWidget(toSlice).sliceLogic()
+ toComposite = toLogic.GetSliceCompositeNode()
+ if toComposite.GetForegroundVolumeID() != None:
+ # This slice already has a foreground, don't change it
+ return
+ fromLogic = layout.sliceWidget(fromSlice).sliceLogic()
+ fromComposite = fromLogic.GetSliceCompositeNode()
+
+ toComposite.SetForegroundVolumeID(fromComposite.GetForegroundVolumeID())
+ if opacity < 0:
+ opacity = fromComposite.GetForegroundOpacity()
+ toComposite.SetForegroundOpacity(opacity)
+
+ fromLogic.FitSliceToAll()
+ toLogic.FitSliceToAll()
diff --git a/src/modules/SmartForegroundEffect/README.md b/src/modules/SmartForegroundEffect/README.md
new file mode 100644
index 0000000..83bbcb4
--- /dev/null
+++ b/src/modules/SmartForegroundEffect/README.md
@@ -0,0 +1,11 @@
+# Smart foreground effect
+
+Essa extensão permite aplicar um efeito de segmentação da área útil da lâmina, isto é, da região da imagem que não inclui as bordas e, opcionalmente, a área de resina inter-fragmentos.
+
+## Descrição
+
+Essa aplicação não é um módulo, mas um efeito que está disponível no módulo Segment Editor.
+
+## Casos de Uso
+
+* Thin Section
diff --git a/src/modules/CTAutoRegistration/Resources/Icons/CTAutoRegistration.png b/src/modules/SmartForegroundEffect/Resources/Icons/SmartForegroundEffect.png
similarity index 100%
rename from src/modules/CTAutoRegistration/Resources/Icons/CTAutoRegistration.png
rename to src/modules/SmartForegroundEffect/Resources/Icons/SmartForegroundEffect.png
diff --git a/src/modules/SmartForegroundEffect/SmartForegroundEffect.py b/src/modules/SmartForegroundEffect/SmartForegroundEffect.py
new file mode 100644
index 0000000..f331df8
--- /dev/null
+++ b/src/modules/SmartForegroundEffect/SmartForegroundEffect.py
@@ -0,0 +1,27 @@
+import os
+
+from ltrace.slicer_utils import LTracePlugin
+
+
+class SmartForegroundEffect(LTracePlugin):
+
+ SETTING_KEY = "SmartForegroundEffect"
+
+ def __init__(self, parent):
+ LTracePlugin.__init__(self, parent)
+ self.parent.title = "Smart foreground effect"
+ self.parent.categories = ["Segmentation"]
+ self.parent.dependencies = ["Segmentations"]
+ self.parent.contributors = ["LTrace Geophysics Team"]
+ self.parent.hidden = True
+ self.parent.helpText = "This hidden module registers the segment editor effect"
+ self.parent.helpText += self.getDefaultModuleDocumentationLink()
+ self.parent.acknowledgementText = ""
+
+ def registerEditorEffect(self):
+ import qSlicerSegmentationsEditorEffectsPythonQt as qSlicerSegmentationsEditorEffects
+
+ instance = qSlicerSegmentationsEditorEffects.qSlicerSegmentEditorScriptedEffect(None)
+ effectFilename = os.path.join(os.path.dirname(__file__), self.__class__.__name__ + "Lib/SegmentEditorEffect.py")
+ instance.setPythonSource(effectFilename.replace("\\", "/"))
+ instance.self().register()
diff --git a/src/modules/SmartForegroundEffect/SmartForegroundEffectLib/SegmentEditorEffect.png b/src/modules/SmartForegroundEffect/SmartForegroundEffectLib/SegmentEditorEffect.png
new file mode 100644
index 0000000..d2d4793
Binary files /dev/null and b/src/modules/SmartForegroundEffect/SmartForegroundEffectLib/SegmentEditorEffect.png differ
diff --git a/src/modules/SmartForegroundEffect/SmartForegroundEffectLib/SegmentEditorEffect.py b/src/modules/SmartForegroundEffect/SmartForegroundEffectLib/SegmentEditorEffect.py
new file mode 100644
index 0000000..7a3dfd0
--- /dev/null
+++ b/src/modules/SmartForegroundEffect/SmartForegroundEffectLib/SegmentEditorEffect.py
@@ -0,0 +1,280 @@
+import logging
+import os
+
+import vtk.util.numpy_support as vn
+
+import qt
+import slicer
+import vtk
+import qSlicerSegmentationsEditorEffectsPythonQt as effects
+import traceback
+
+from ltrace.slicer import helpers
+from ltrace.slicer_utils import LTraceSegmentEditorEffectMixin
+from ltrace.slicer.ui import numberParamInt
+from ltrace.assets_utils import get_asset, get_pth
+from ltrace.slicer.helpers import LazyLoad
+from ltrace.slicer.widget.global_progress_bar import LocalProgressBar
+from ltrace.slicer.cli_queue import CliQueue
+from SegmentEditorEffects import *
+
+
+FILTER_GRADIENT_MAGNITUDE = "GRADIENT_MAGNITUDE"
+
+
+class SegmentEditorEffect(AbstractScriptedSegmentEditorEffect, LTraceSegmentEditorEffectMixin):
+ def __init__(self, scriptedEffect):
+ AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect)
+ scriptedEffect.name = "Smart foreground"
+ scriptedEffect.perSegment = False
+ scriptedEffect.requireSegments = True
+
+ def clone(self):
+ clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None)
+ clonedEffect.setPythonSource(__file__.replace("\\", "/"))
+ return clonedEffect
+
+ def icon(self):
+ iconPath = os.path.join(os.path.dirname(__file__), "SegmentEditorEffect.png")
+ if os.path.exists(iconPath):
+ return qt.QIcon(iconPath)
+ return qt.QIcon()
+
+ def helpText(self):
+ return """
+
Segment only the useful area of a thin section image, discarding borders and inter-fragments areas.
+
+ Operation:
+
+
Fill inside: fill the selected segment along the detected useful area;
+
Erase outside: erase the region from the selected segment which lies outside the useful area.
+
+
+
+ Fragments (recommended for plane-polarized (PP) images only):
+
+
Split: if not checked, only the image's borders will be considered non-useful area. Otherwise, the area between fragments will be too.
+
+
+
Keep all: every fragment will be considered useful area;
+
Filter the largest N: only the N fragments with the largest area will be considered useful.
+
+
+
+
+
Click Apply to start. It may take a while.
+ """
+
+ def setupOptionsFrame(self):
+ # Operation buttons
+ self.fillInsideButton = qt.QRadioButton("Fill inside")
+ self.fillInsideButton.objectName = "Smart Foreground Fill Inside Button"
+
+ self.eraseOutsideButton = qt.QRadioButton("Erase outside")
+ self.eraseOutsideButton.objectName = "Smart Foreground Erase Outside Button"
+
+ # Grouping
+ self.operationRadioGroup = qt.QButtonGroup()
+ self.operationRadioGroup.setExclusive(True)
+ self.operationRadioGroup.addButton(self.fillInsideButton)
+ self.operationRadioGroup.addButton(self.eraseOutsideButton)
+
+ self.fillInsideButton.setChecked(True)
+
+ # Operation buttons layout
+ operationLayout = qt.QGridLayout()
+ operationLayout.addWidget(self.fillInsideButton, 0, 0)
+ operationLayout.addWidget(self.eraseOutsideButton, 0, 1)
+ self.scriptedEffect.addLabeledOptionsWidget("Operation:", operationLayout)
+
+ # Fragment splitting options
+ self.fragSplitCheckbox = qt.QCheckBox("Split")
+ self.fragSplitCheckbox.objectName = "Smart Foreground Split Checkbox"
+ self.fragSplitAllButton = qt.QRadioButton("Keep all")
+ self.fragSplitAllButton.objectName = "Smart Foreground Split All Button"
+ self.fragFilterButton = qt.QRadioButton("Filter the largest")
+ self.fragFilterButton.objectName = "Smart Foreground Fragments Filter Button"
+ self.fragFilterInput = numberParamInt((1, 20), value=1, step=1)
+
+ # Grouping
+ self.fragRadioGroup = qt.QButtonGroup()
+ self.fragRadioGroup.setExclusive(True)
+ self.fragRadioGroup.addButton(self.fragSplitAllButton)
+ self.fragRadioGroup.addButton(self.fragFilterButton)
+
+ self.fragSplitCheckbox.clicked.connect(self.setVisibleFragSplitting)
+ self.fragSplitAllButton.setChecked(True)
+ self.fragSplitAllButton.setVisible(False)
+ self.fragSplitAllButton.clicked.connect(self.setVisibleFragLimit)
+ self.fragFilterButton.setVisible(False)
+ self.fragFilterButton.clicked.connect(self.setVisibleFragLimit)
+ self.fragFilterInput.setEnabled(False)
+ self.fragFilterInput.setVisible(False)
+
+ # Fragment splitting options layout
+ fragSplitLayout = qt.QGridLayout()
+ fragSplitLayout.addWidget(self.fragSplitCheckbox, 0, 0)
+ fragSplitLayout.addWidget(self.fragSplitAllButton, 1, 0)
+ fragSplitLayout.addWidget(self.fragFilterButton, 1, 1)
+ fragSplitLayout.addWidget(self.fragFilterInput, 1, 2)
+ self.scriptedEffect.addLabeledOptionsWidget("Fragments:", fragSplitLayout)
+
+ # Apply button
+ self.applyButton = qt.QPushButton("Apply")
+ self.applyButton.setMinimumHeight(25)
+ self.scriptedEffect.addOptionsWidget(self.applyButton)
+ self.applyButton.setVisible(True)
+ self.applyButton.objectName = "Smart Foreground Apply Button"
+ self.applyButton.connect("clicked()", self.onApply)
+
+ # Step label (multistep CLI)
+ self.stepLabel = qt.QLabel("")
+ self.scriptedEffect.addOptionsWidget(self.stepLabel)
+
+ # Progress bar
+ self.progressBar = LocalProgressBar()
+ self.scriptedEffect.addOptionsWidget(self.progressBar)
+
+ def createCursor(self, widget):
+ # Turn off effect-specific cursor for this effect
+ return slicer.util.mainWindow().cursor
+
+ def setVisibleFragSplitting(self):
+ self.fragSplitAllButton.setVisible(self.fragSplitCheckbox.isChecked())
+ self.fragFilterButton.setVisible(self.fragSplitCheckbox.isChecked())
+ self.fragFilterInput.setVisible(self.fragSplitCheckbox.isChecked())
+
+ def setVisibleFragLimit(self):
+ self.fragFilterInput.setEnabled(self.fragFilterButton.isChecked())
+
+ def onApply(self):
+ def onFinish():
+ self.applyButton.setEnabled(True)
+ for node in [tmpForegroundNode, tmpPoreSegNode, tmpSlicedReferenceNode]:
+ if node is not None:
+ slicer.mrmlScene.RemoveNode(node)
+
+ del self.cliQueue
+ self.cliQueue = None
+
+ def onFailure():
+ slicer.util.errorDisplay(f"Operation failed on {self.cliQueue.get_error_message()}")
+
+ def onSuccess():
+ mask = slicer.util.arrayFromVolume(tmpForegroundNode)[0].astype(bool)
+
+ if eraseOutside:
+ segmentArray = slicer.util.arrayFromSegmentBinaryLabelmap(
+ segmentationNode, segmentID, sourceVolumeNode
+ )[0].astype(bool)
+ mask &= segmentArray
+
+ maskImage = vtk.vtkImageData()
+ maskImage.SetDimensions(cols, rows, 1)
+ maskImage.SetSpacing(sourceVolumeNode.GetSpacing())
+ maskImage.SetOrigin(sourceVolumeNode.GetOrigin())
+
+ maskData = vn.numpy_to_vtk(num_array=mask.ravel(), deep=True, array_type=vtk.VTK_UNSIGNED_CHAR)
+ maskData.SetNumberOfComponents(1)
+ maskImage.GetPointData().SetScalars(maskData)
+
+ modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
+ originalImageToWorldMatrix = vtk.vtkMatrix4x4()
+ modifierLabelmap.GetImageToWorldMatrix(originalImageToWorldMatrix)
+ modifierLabelmap.DeepCopy(maskImage)
+
+ # Apply changes
+ self.scriptedEffect.modifySelectedSegmentByLabelmap(
+ modifierLabelmap,
+ slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet,
+ )
+ slicer.util.setSliceViewerLayers(background=sourceVolumeNode, foreground=None, fit=True)
+
+ self.scriptedEffect.saveStateForUndo()
+
+ # De-select effect
+ self.scriptedEffect.selectEffect("")
+ self.progressBar.visible = False
+
+ def hideTmpOutput(caller, event, params):
+ if caller.GetStatus() == slicer.vtkMRMLCommandLineModuleNode.Completed:
+ slicer.util.setSliceViewerLayers(label=None)
+
+ tmpForegroundNode = None
+ tmpPoreSegNode = None
+ tmpSlicedReferenceNode = None
+ try:
+ self.applyButton.setEnabled(False)
+
+ self.progressBar.visible = True
+ self.cliQueue = CliQueue(update_display=False, progress_bar=self.progressBar, progress_label=self.stepLabel)
+ self.cliQueue.signal_queue_successful.connect(onSuccess)
+ self.cliQueue.signal_queue_failed.connect(onFailure)
+ self.cliQueue.signal_queue_finished.connect(onFinish)
+
+ sourceVolumeNode = self.scriptedEffect.parameterSetNode().GetSourceVolumeNode()
+ sourceImageData = sourceVolumeNode.GetImageData()
+ cols, rows, _ = sourceImageData.GetDimensions() # x, y, z
+
+ segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
+ segmentID = self.scriptedEffect.parameterSetNode().GetSelectedSegmentID()
+
+ tmpForegroundNode = helpers.createTemporaryVolumeNode(slicer.vtkMRMLLabelMapVolumeNode, "TMP_ROCK_AREA")
+
+ eraseOutside = self.eraseOutsideButton.isChecked()
+ splitFrags = self.fragSplitCheckbox.isChecked()
+
+ smartForegroundParams = {
+ "input": sourceVolumeNode.GetID(),
+ "outputRock": tmpForegroundNode.GetID(),
+ }
+
+ if splitFrags:
+ limitFrags = self.fragFilterButton.isChecked()
+ numberFrags = self.fragFilterInput.value
+
+ Segmenter = LazyLoad("Segmenter")
+
+ tmpPoreSegNode = helpers.createTemporaryVolumeNode(slicer.vtkMRMLLabelMapVolumeNode, "TMP_PORE_SEG")
+
+ tmpSlicedReferenceNode = Segmenter.prepareTemporaryInputs(
+ [sourceVolumeNode],
+ outputPrefix="TMP_INPUT_NODE",
+ soiNode=None,
+ referenceNode=sourceVolumeNode,
+ colorsToSlices=True,
+ )[0][0]
+ poreSegParams = {
+ "inputVolume": tmpSlicedReferenceNode.GetID(),
+ "inputModel": get_pth(os.path.join(get_asset("ThinSectionEnv"), "bayes_3px")).as_posix(),
+ "xargs": "null",
+ "ctypes": "rgb",
+ "outputVolume": tmpPoreSegNode.GetID(),
+ }
+ self.cliQueue.create_cli_node(
+ slicer.modules.bayesianinferencecli,
+ poreSegParams,
+ progress_text="Detecting resin region",
+ modified_callback=hideTmpOutput,
+ )
+
+ smartForegroundParams.update(
+ {
+ "outputFrags": tmpForegroundNode.GetID(), # here specifically, there's no need to outputRock != outputFrags
+ "poreSegmentation": tmpPoreSegNode.GetID(),
+ "nLargestFrags": numberFrags if limitFrags else -1,
+ }
+ )
+
+ self.cliQueue.create_cli_node(
+ slicer.modules.smartforegroundcli,
+ smartForegroundParams,
+ progress_text="Removing borders",
+ modified_callback=hideTmpOutput,
+ )
+ self.cliQueue.run()
+ except Exception as error:
+ print(traceback.format_exc())
+ logging.debug(f"Error: {error}.\n{traceback.format_exc()}")
+ slicer.util.errorDisplay(f"Failed to apply the effect.\nError: {error}")
+ onFinish()
diff --git a/src/modules/SpiralFilter/Resources/Icons/SpiralFilter.png b/src/modules/SpiralFilter/Resources/Icons/SpiralFilter.png
deleted file mode 100644
index b89476c..0000000
Binary files a/src/modules/SpiralFilter/Resources/Icons/SpiralFilter.png and /dev/null differ
diff --git a/src/modules/SpiralFilter/Resources/Icons/SpiralFilter.svg b/src/modules/SpiralFilter/Resources/Icons/SpiralFilter.svg
new file mode 100644
index 0000000..b12587a
--- /dev/null
+++ b/src/modules/SpiralFilter/Resources/Icons/SpiralFilter.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/modules/SpiralFilter/SpiralFilter.py b/src/modules/SpiralFilter/SpiralFilter.py
index 512839d..0af5cdc 100644
--- a/src/modules/SpiralFilter/SpiralFilter.py
+++ b/src/modules/SpiralFilter/SpiralFilter.py
@@ -2,6 +2,7 @@
import os
from collections import namedtuple
from pathlib import Path
+from typing import Union
import ctk
import qt
@@ -29,7 +30,7 @@ class SpiralFilter(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
self.parent.title = "Spiral Filter"
- self.parent.categories = ["LTrace Tools"]
+ self.parent.categories = ["Tools", "ImageLog", "Multiscale"]
self.parent.dependencies = []
self.parent.contributors = ["LTrace Geophysical Solutions"]
self.parent.helpText = SpiralFilter.help()
@@ -78,6 +79,7 @@ def setup(self):
self.logic = SpiralFilterLogic(self.progressBar)
self.logic.setParent(self.parent)
self.logic.filterFinished.connect(lambda: self.updateApplyCancelButtonsEnablement(True))
+ self.logic.filterFinished.connect(self.__updateFilteredDifferenceLabel)
frame = qt.QFrame()
self.layout.addWidget(frame)
@@ -104,6 +106,8 @@ def setup(self):
self.inputVolume.setToolTip("Select the image input to the algorithm.")
self.inputVolume.currentNodeChanged.connect(self.onInputVolumeCurrentNodeChanged)
self.inputVolume.objectName = "Image Input"
+ self.inputVolume.setSizePolicy(qt.QSizePolicy.Minimum, qt.QSizePolicy.Minimum)
+ self.inputVolume.setMaximumHeight(24)
inputFormLayout.addRow("Image:", self.inputVolume)
inputFormLayout.addRow(" ", None)
@@ -115,18 +119,18 @@ def setup(self):
parametersFormLayout.setLabelAlignment(qt.Qt.AlignRight)
self.minimumWavelengthDoubleSpinBox = qt.QDoubleSpinBox()
- self.minimumWavelengthDoubleSpinBox.setRange(1, 100)
- self.minimumWavelengthDoubleSpinBox.setDecimals(0)
- self.minimumWavelengthDoubleSpinBox.setSingleStep(1)
+ self.minimumWavelengthDoubleSpinBox.setRange(0, 100)
+ self.minimumWavelengthDoubleSpinBox.setDecimals(2)
+ self.minimumWavelengthDoubleSpinBox.setSingleStep(0.1)
self.minimumWavelengthDoubleSpinBox.setValue(float(self.getMinimumWavelength()))
self.minimumWavelengthDoubleSpinBox.setToolTip("Minimum vertical wavelength of the spiraling effect in meters.")
self.minimumWavelengthDoubleSpinBox.objectName = "Minimum Wave Length Input"
parametersFormLayout.addRow("Minimum wavelength (m):", self.minimumWavelengthDoubleSpinBox)
self.maximumWavelengthDoubleSpinBox = qt.QDoubleSpinBox()
- self.maximumWavelengthDoubleSpinBox.setRange(1, 500)
- self.maximumWavelengthDoubleSpinBox.setDecimals(0)
- self.maximumWavelengthDoubleSpinBox.setSingleStep(1)
+ self.maximumWavelengthDoubleSpinBox.setRange(0, 500)
+ self.maximumWavelengthDoubleSpinBox.setDecimals(2)
+ self.maximumWavelengthDoubleSpinBox.setSingleStep(0.1)
self.maximumWavelengthDoubleSpinBox.setValue(float(self.getMaximumWavelength()))
self.maximumWavelengthDoubleSpinBox.setToolTip("Maximum vertical wavelength of the spiraling effect in meters.")
self.maximumWavelengthDoubleSpinBox.objectName = "Maximum Wave Length Input"
@@ -198,6 +202,19 @@ def setup(self):
self.layout.addWidget(self.progressBar)
+ # Add filter difference label indicator
+ self.indicatorsWidget = qt.QWidget()
+ self.indicatorsWidget.objectName = "Indicators Widget"
+ self.indicatorsWidget.visible = False
+ self.indicatorsWidget.setStyleSheet("QLabel { color : green; }")
+
+ self.filteredDifferenceValueLabel = qt.QLabel("")
+ self.filteredDifferenceLayout = qt.QFormLayout()
+ self.filteredDifferenceLayout.addRow("Filter difference:", self.filteredDifferenceValueLabel)
+
+ self.indicatorsWidget.setLayout(self.filteredDifferenceLayout)
+ self.layout.addWidget(self.indicatorsWidget)
+
self.layout.addStretch()
def onOutputPrefixTextChanged(self, text: str) -> None:
@@ -219,6 +236,9 @@ def onInputVolumeCurrentNodeChanged(self, node) -> None:
self.applyButton.enabled = node is not None
def onApplyButtonClicked(self):
+ self.filteredDifferenceValueLabel.text = ""
+ self.indicatorsWidget.visible = False
+
try:
if self.inputVolume.currentNode() is None:
raise FilterInfo("Input image is required.")
@@ -254,9 +274,21 @@ def updateApplyCancelButtonsEnablement(self, applyEnabled):
self.applyButton.enabled = applyEnabled and self.inputVolume.currentNode() is not None
self.cancelButton.enabled = not applyEnabled
+ def __updateFilteredDifferenceLabel(self, value: Union[None, str]) -> None:
+ if value is None:
+ return
+
+ try:
+ valueFloat = float(value)
+ except ValueError:
+ return
+
+ self.filteredDifferenceValueLabel.text = f"{valueFloat*100:.2f}%"
+ self.indicatorsWidget.visible = True
+
class SpiralFilterLogic(LTracePluginLogic):
- filterFinished = qt.Signal()
+ filterFinished = qt.Signal(object)
def __init__(self, progressBar):
LTracePluginLogic.__init__(self)
@@ -306,6 +338,7 @@ def filterCallback(self, caller, event):
status = caller.GetStatusString()
outputVolumeNode = helpers.tryGetNode(self.outputVolumeNodeId)
+ filteredDiff = None
if "Completed" in status or status == "Cancelled":
logging.debug(status)
@@ -315,6 +348,8 @@ def filterCallback(self, caller, event):
elif status != "Completed":
slicer.mrmlScene.RemoveNode(outputVolumeNode)
slicer.util.errorDisplay("Filtering failed.")
+ else: # Completed
+ filteredDiff = self.cliNode.GetParameterAsString(f"filtered_diff")
outputVolumeNode.SetAttribute(ImageLogDataSelectable.name(), ImageLogDataSelectable.TRUE.value)
@@ -322,7 +357,7 @@ def filterCallback(self, caller, event):
self.outputVolumeNodeId = None
del self.cliNode
self.cliNode = None
- self.filterFinished.emit()
+ self.filterFinished.emit(filteredDiff)
def cancel(self):
if self.cliNode is None:
diff --git a/src/modules/StreamlinedModelling/Resources/Icons/StreamlinedModelling.png b/src/modules/StreamlinedModelling/Resources/Icons/StreamlinedModelling.png
deleted file mode 100644
index b89476c..0000000
Binary files a/src/modules/StreamlinedModelling/Resources/Icons/StreamlinedModelling.png and /dev/null differ
diff --git a/src/modules/StreamlinedModelling/Resources/Icons/StreamlinedModelling.svg b/src/modules/StreamlinedModelling/Resources/Icons/StreamlinedModelling.svg
new file mode 100644
index 0000000..001e90d
--- /dev/null
+++ b/src/modules/StreamlinedModelling/Resources/Icons/StreamlinedModelling.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/modules/StreamlinedModelling/StreamlinedModelling.py b/src/modules/StreamlinedModelling/StreamlinedModelling.py
index a831c58..360214d 100644
--- a/src/modules/StreamlinedModelling/StreamlinedModelling.py
+++ b/src/modules/StreamlinedModelling/StreamlinedModelling.py
@@ -246,7 +246,7 @@ class StreamlinedModelling(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
self.parent.title = "Modelling Flow"
- self.parent.categories = ["LTrace Tools"]
+ self.parent.categories = ["Tools", "MicroCT"]
self.parent.dependencies = []
self.parent.contributors = ["LTrace Geophysical Solutions"]
self.parent.helpText = StreamlinedModelling.help()
@@ -302,8 +302,7 @@ def setupCrop(self):
cropModule = slicer.modules.customizedcropvolume.createNewWidgetRepresentation()
self.cropWidget = cropModule.self()
self.cropWidget.inputCollapsibleButton.visible = False
- self.cropWidget.cropButton.visible = False
- self.cropWidget.cancelButton.visible = False
+ self.cropWidget.applyCancelButtons.visible = False
return cropModule
def enterCrop(self):
@@ -576,9 +575,12 @@ def enterModelling(self):
self.nextButton.setToolTip("Run modelling")
inputWidget = self.microporosityWidget.inputWidget
- inputWidget.soiInput.setCurrentNode(self.flowState.soi)
inputWidget.mainInput.setCurrentNode(self.flowState.segmentation)
+ inputWidget.mainInput.itemChangedHandler(inputWidget.mainInput.currentItem())
+ inputWidget.soiInput.setCurrentNode(self.flowState.soi)
+ inputWidget.soiInput.itemChangedHandler(inputWidget.soiInput.currentItem())
inputWidget.referenceInput.setCurrentNode(self.flowState.scalarVolume)
+ inputWidget.referenceInput.itemChangedHandler(inputWidget.referenceInput.currentItem())
self.microporosityWidget.poreDistSelector[0].setCurrentText("Macroporosity")
self.microporosityWidget.poreDistSelector[1].setCurrentText("Microporosity")
diff --git a/src/modules/StreamlinedSegmentation/StreamlinedSegmentation.py b/src/modules/StreamlinedSegmentation/StreamlinedSegmentation.py
index a2a1426..7901140 100644
--- a/src/modules/StreamlinedSegmentation/StreamlinedSegmentation.py
+++ b/src/modules/StreamlinedSegmentation/StreamlinedSegmentation.py
@@ -8,12 +8,13 @@
from slicer.util import VTKObservationMixin
import qSlicerSegmentationsEditorEffectsPythonQt
import qSlicerSegmentationsModuleWidgetsPythonQt
-from ltrace.utils.ProgressBarProc import ProgressBarProc
+from ltrace.slicer.app import getApplicationVersion
+from ltrace.utils.ProgressBarProc import ProgressBarProc
from distinctipy import distinctipy
from ltrace.slicer.ui import hierarchyVolumeInput
from ltrace.slicer.application_observables import ApplicationObservables
-from ltrace.slicer_utils import LTracePlugin, LTracePluginWidget, LTracePluginLogic
+from ltrace.slicer_utils import LTracePlugin, LTracePluginWidget, LTracePluginLogic, getResourcePath
from ltrace.slicer.lazy import lazy
from ltrace.slicer.widget.global_progress_bar import LocalProgressBar
from ltrace.slicer import helpers
@@ -34,10 +35,12 @@ class StreamlinedSegmentation(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
self.parent.title = "Virtual Segmentation Flow"
- self.parent.categories = ["LTrace Tools"]
+ self.parent.categories = ["Tools"]
self.parent.dependencies = []
self.parent.contributors = ["LTrace Geophysical Solutions"]
- self.parent.helpText = StreamlinedSegmentation.help()
+ self.parent.helpText = (
+ f"file:///{(getResourcePath('manual') / 'Modules/Volumes/StreamlinedSegmentation.html').as_posix()}"
+ )
@classmethod
def readme_path(cls):
@@ -149,7 +152,7 @@ def setup(self):
nodeTypes=["vtkMRMLTextNode"],
tooltip="Select the image within the NetCDF dataset to preview.",
)
- self.volumeSelector.addNodeAttributeFilter("LazyNode", "1")
+ self.volumeSelector.selectorWidget.addNodeAttributeFilter("LazyNode", "1")
self.exportPathEdit = ctk.ctkPathLineEdit()
self.exportPathEdit.filters = ctk.ctkPathLineEdit.Files | ctk.ctkPathLineEdit.Writable
@@ -391,7 +394,7 @@ def apply(self, lazyData, outputPath, progress_bar=None):
"boundary_thresholds": self.boundaryThresholds,
"colors": self.segmentColors,
"names": self.segmentNames,
- "geoslicer_version": slicer.app.applicationVersion,
+ "geoslicer_version": getApplicationVersion(),
}
cli_config = {
"params": json.dumps(params),
diff --git a/src/modules/SubjectHierarchyPlugins/CenterSubjectHierarchyPlugin.py b/src/modules/SubjectHierarchyPlugins/CenterSubjectHierarchyPlugin.py
index 0e95e16..4af70a3 100644
--- a/src/modules/SubjectHierarchyPlugins/CenterSubjectHierarchyPlugin.py
+++ b/src/modules/SubjectHierarchyPlugins/CenterSubjectHierarchyPlugin.py
@@ -2,6 +2,8 @@
import logging
from AbstractScriptedSubjectHierarchyPlugin import *
import numpy as np
+from ltrace.slicer import helpers
+from ltrace.slicer.node_attributes import NodeEnvironment
from ltrace.utils.ProgressBarProc import ProgressBarProc
from ltrace.vtk_utils.well_model.well_model import create_well_model_from_node
@@ -95,7 +97,7 @@ def itemContextMenuActions(self):
self.folderToSequenceAction,
self.sequenceToFolderAction,
]
- if slicer.util.selectedModule() != "ImageLogEnv":
+ if helpers.getCurrentEnvironment() != NodeEnvironment.IMAGE_LOG:
actions.append(self.centerVolumeAction)
actions.append(self.createWellModelAction)
return actions
@@ -141,7 +143,7 @@ def centerToThisSegment(self):
markupsLogic = slicer.modules.markups.logic()
segmentCenterRAS = segmentationNode.GetSegmentCenterRAS(segmentID)
- if slicer.util.selectedModule() != "ImageLogEnv":
+ if helpers.getCurrentEnvironment() != NodeEnvironment.IMAGE_LOG:
markupsLogic.JumpSlicesToLocation(*segmentCenterRAS, True)
else:
markupsLogic.JumpSlicesToLocation(0, 0, segmentCenterRAS[2], True)
diff --git a/src/modules/SuperBuild.cmake b/src/modules/SuperBuild.cmake
deleted file mode 100644
index 8610cb0..0000000
--- a/src/modules/SuperBuild.cmake
+++ /dev/null
@@ -1,60 +0,0 @@
-
-#-----------------------------------------------------------------------------
-# External project common settings
-#-----------------------------------------------------------------------------
-
-set(ep_common_c_flags "${CMAKE_C_FLAGS_INIT} ${ADDITIONAL_C_FLAGS}")
-set(ep_common_cxx_flags "${CMAKE_CXX_FLAGS_INIT} ${ADDITIONAL_CXX_FLAGS}")
-
-#-----------------------------------------------------------------------------
-# Top-level "external" project
-#-----------------------------------------------------------------------------
-
-# Extension dependencies
-foreach(dep ${EXTENSION_DEPENDS})
- mark_as_superbuild(${dep}_DIR)
-endforeach()
-
-set(proj ${SUPERBUILD_TOPLEVEL_PROJECT})
-
-# Project dependencies
-set(${proj}_DEPENDS
- Foo
- python-requirements
- )
-
-ExternalProject_Include_Dependencies(${proj}
- PROJECT_VAR proj
- SUPERBUILD_VAR ${EXTENSION_NAME}_SUPERBUILD
- )
-
-ExternalProject_Add(${proj}
- ${${proj}_EP_ARGS}
- DOWNLOAD_COMMAND ""
- INSTALL_COMMAND ""
- SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}
- BINARY_DIR ${EXTENSION_BUILD_SUBDIRECTORY}
- CMAKE_CACHE_ARGS
- # Compiler settings
- -DCMAKE_C_COMPILER:FILEPATH=${CMAKE_C_COMPILER}
- -DCMAKE_C_FLAGS:STRING=${ep_common_c_flags}
- -DCMAKE_CXX_COMPILER:FILEPATH=${CMAKE_CXX_COMPILER}
- -DCMAKE_CXX_FLAGS:STRING=${ep_common_cxx_flags}
- -DCMAKE_CXX_STANDARD:STRING=${CMAKE_CXX_STANDARD}
- -DCMAKE_CXX_STANDARD_REQUIRED:BOOL=${CMAKE_CXX_STANDARD_REQUIRED}
- -DCMAKE_CXX_EXTENSIONS:BOOL=${CMAKE_CXX_EXTENSIONS}
- # Output directories
- -DCMAKE_RUNTIME_OUTPUT_DIRECTORY:PATH=${CMAKE_RUNTIME_OUTPUT_DIRECTORY}
- -DCMAKE_LIBRARY_OUTPUT_DIRECTORY:PATH=${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
- -DCMAKE_ARCHIVE_OUTPUT_DIRECTORY:PATH=${CMAKE_ARCHIVE_OUTPUT_DIRECTORY}
- # Packaging
- -DMIDAS_PACKAGE_EMAIL:STRING=${MIDAS_PACKAGE_EMAIL}
- -DMIDAS_PACKAGE_API_KEY:STRING=${MIDAS_PACKAGE_API_KEY}
- # Superbuild
- -D${EXTENSION_NAME}_SUPERBUILD:BOOL=OFF
- -DEXTENSION_SUPERBUILD_BINARY_DIR:PATH=${${EXTENSION_NAME}_BINARY_DIR}
- DEPENDS
- ${${proj}_DEPENDS}
- )
-
-ExternalProject_AlwaysConfigure(${proj})
diff --git a/src/modules/SuperBuild/External_Foo.cmake b/src/modules/SuperBuild/External_Foo.cmake
deleted file mode 100644
index d6037fe..0000000
--- a/src/modules/SuperBuild/External_Foo.cmake
+++ /dev/null
@@ -1,88 +0,0 @@
-
-set(proj Foo)
-
-# Set dependency list
-set(${proj}_DEPENDS
- ""
- )
-
-# Include dependent projects if any
-ExternalProject_Include_Dependencies(${proj} PROJECT_VAR proj)
-
-if(${SUPERBUILD_TOPLEVEL_PROJECT}_USE_SYSTEM_${proj})
- message(FATAL_ERROR "Enabling ${SUPERBUILD_TOPLEVEL_PROJECT}_USE_SYSTEM_${proj} is not supported !")
-endif()
-
-# Sanity checks
-if(DEFINED Foo_DIR AND NOT EXISTS ${Foo_DIR})
- message(FATAL_ERROR "Foo_DIR [${Foo_DIR}] variable is defined but corresponds to nonexistent directory")
-endif()
-
-if(NOT DEFINED ${proj}_DIR AND NOT ${SUPERBUILD_TOPLEVEL_PROJECT}_USE_SYSTEM_${proj})
-
- #ExternalProject_SetIfNotDefined(
- # ${SUPERBUILD_TOPLEVEL_PROJECT}_${proj}_GIT_REPOSITORY
- # "${EP_GIT_PROTOCOL}://github.com/Foo/Foo.git"
- # QUIET
- # )
-
- #ExternalProject_SetIfNotDefined(
- # ${SUPERBUILD_TOPLEVEL_PROJECT}_${proj}_GIT_TAG
- # "1e823001cb41c92667299635643f1007876d09f6"
- # QUIET
- # )
-
- set(EP_SOURCE_DIR ${CMAKE_BINARY_DIR}/${proj})
- set(EP_BINARY_DIR ${CMAKE_BINARY_DIR}/${proj}-build)
-
- ExternalProject_Add(${proj}
- ${${proj}_EP_ARGS}
- #GIT_REPOSITORY "${${SUPERBUILD_TOPLEVEL_PROJECT}_${proj}_GIT_REPOSITORY}"
- #GIT_TAG "${${SUPERBUILD_TOPLEVEL_PROJECT}_${proj}_GIT_TAG}"
- DOWNLOAD_COMMAND ${CMAKE_COMMAND} -E echo "Remove this line and uncomment GIT_REPOSITORY and GIT_TAG"
- SOURCE_DIR ${EP_SOURCE_DIR}
- BINARY_DIR ${EP_BINARY_DIR}
- CMAKE_CACHE_ARGS
- # Compiler settings
- -DCMAKE_C_COMPILER:FILEPATH=${CMAKE_C_COMPILER}
- -DCMAKE_C_FLAGS:STRING=${ep_common_c_flags}
- -DCMAKE_CXX_COMPILER:FILEPATH=${CMAKE_CXX_COMPILER}
- -DCMAKE_CXX_FLAGS:STRING=${ep_common_cxx_flags}
- -DCMAKE_CXX_STANDARD:STRING=${CMAKE_CXX_STANDARD}
- -DCMAKE_CXX_STANDARD_REQUIRED:BOOL=${CMAKE_CXX_STANDARD_REQUIRED}
- -DCMAKE_CXX_EXTENSIONS:BOOL=${CMAKE_CXX_EXTENSIONS}
- # Output directories
- -DCMAKE_RUNTIME_OUTPUT_DIRECTORY:PATH=${CMAKE_BINARY_DIR}/${Slicer_THIRDPARTY_BIN_DIR}
- -DCMAKE_LIBRARY_OUTPUT_DIRECTORY:PATH=${CMAKE_BINARY_DIR}/${Slicer_THIRDPARTY_LIB_DIR}
- -DCMAKE_ARCHIVE_OUTPUT_DIRECTORY:PATH=${CMAKE_ARCHIVE_OUTPUT_DIRECTORY}
- # Install directories
- # XXX The following two variables should be updated to match the
- # requirements of a real CMake based external project
- # XXX Then, this comment and the one above should be removed. Really.
- -DFOO_INSTALL_RUNTIME_DIR:STRING=${Slicer_INSTALL_THIRDPARTY_LIB_DIR}
- -DFOO_INSTALL_LIBRARY_DIR:STRING=${Slicer_INSTALL_THIRDPARTY_LIB_DIR}
- # Output directories for CLIs
- #-DSlicerExecutionModel_DEFAULT_CLI_RUNTIME_OUTPUT_DIRECTORY:PATH=${SlicerExecutionModel_DEFAULT_CLI_RUNTIME_OUTPUT_DIRECTORY}
- #-DSlicerExecutionModel_DEFAULT_CLI_RUNTIME_LIBRARY_DIRECTORY:PATH=${SlicerExecutionModel_DEFAULT_CLI_LIBRARY_OUTPUT_DIRECTORY}
- #-DSlicerExecutionModel_DEFAULT_CLI_RUNTIME_ARCHIVE_DIRECTORY:PATH=${SlicerExecutionModel_DEFAULT_CLI_ARCHIVE_OUTPUT_DIRECTORY}
- # Options
- -DBUILD_TESTING:BOOL=OFF
- # Dependencies
- # -DBar_DIR:PATH=${Bar_DIR}
- CONFIGURE_COMMAND ${CMAKE_COMMAND} -E echo
- "This CONFIGURE_COMMAND is just here as a placeholder."
- "Remove this line to enable configuring of a real CMake based external project"
- BUILD_COMMAND ${CMAKE_COMMAND} -E echo
- "This BUILD_COMMAND is just here as a placeholder."
- "Remove this line to enable building of a real CMake based external project"
- INSTALL_COMMAND ""
- DEPENDS
- ${${proj}_DEPENDS}
- )
- set(${proj}_DIR ${EP_BINARY_DIR})
-
-else()
- ExternalProject_Add_Empty(${proj} DEPENDS ${${proj}_DEPENDS})
-endif()
-
-mark_as_superbuild(${proj}_DIR:PATH)
diff --git a/src/modules/SuperBuild/External_python-requirements.cmake b/src/modules/SuperBuild/External_python-requirements.cmake
deleted file mode 100644
index c330bbc..0000000
--- a/src/modules/SuperBuild/External_python-requirements.cmake
+++ /dev/null
@@ -1,61 +0,0 @@
-set(proj python-dicom-requirements)
-
-# Set dependency list
-set(${proj}_DEPENDENCIES python python-setuptools python-pip)
-
-set(requirements_file ${CMAKE_BINARY_DIR}/${proj}-requirements.txt)
-file(WRITE ${requirements_file} [===[
-numpy==1.17.4
-tensorflow==2.0.0
-tensorflow-gpu==2.0.0
-pyzmq==18.1.0
-scikit-image==0.16.2
-scikit-learn==0.22.0
-scipy==1.3.1
-natsort==6.2.0
-cudnn-python-wrappers==1.0.0
-array_split==0.2.0
-joblib==0.14.0
-ray==0.8.0
-psutil==5.6.7
-]===])
-
-if(NOT DEFINED Slicer_USE_SYSTEM_${proj})
- set(Slicer_USE_SYSTEM_${proj} ${Slicer_USE_SYSTEM_python})
-endif()
-
-# Include dependent projects if any
-ExternalProject_Include_Dependencies(${proj} PROJECT_VAR proj DEPENDS_VAR ${proj}_DEPENDENCIES)
-
-if(Slicer_USE_SYSTEM_${proj})
- foreach(module_name IN ITEMS pydicom numpy pillow six certifi idna chardet urllib3 requests dicomweb_client)
- ExternalProject_FindPythonPackage(
- MODULE_NAME "${module_name}"
- REQUIRED
- )
- endforeach()
-endif()
-
-if(NOT Slicer_USE_SYSTEM_${proj})
-
- ExternalProject_Add(${proj}
- ${${proj}_EP_ARGS}
- DOWNLOAD_COMMAND ""
- SOURCE_DIR ${CMAKE_BINARY_DIR}/${proj}
- BUILD_IN_SOURCE 1
- CONFIGURE_COMMAND ""
- BUILD_COMMAND ""
- INSTALL_COMMAND ${PYTHON_EXECUTABLE} -m pip install --require-hashes -r ${requirements_file}
- LOG_INSTALL 1
- DEPENDS
- ${${proj}_DEPENDENCIES}
- )
-
- ExternalProject_GenerateProjectDescription_Step(${proj}
- VERSION ${_version}
- )
-
-else()
- ExternalProject_Add_Empty(${proj} DEPENDS ${${proj}_DEPENDENCIES})
-endif()
-
diff --git a/src/modules/TableFilter/Resources/Icons/TableFilter.png b/src/modules/TableFilter/Resources/Icons/TableFilter.png
deleted file mode 100644
index d282644..0000000
Binary files a/src/modules/TableFilter/Resources/Icons/TableFilter.png and /dev/null differ
diff --git a/src/modules/TableFilter/Resources/Icons/TableFilter.svg b/src/modules/TableFilter/Resources/Icons/TableFilter.svg
new file mode 100644
index 0000000..7a7b6f7
--- /dev/null
+++ b/src/modules/TableFilter/Resources/Icons/TableFilter.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/modules/TableFilter/TableFilter.py b/src/modules/TableFilter/TableFilter.py
index 60a3d41..84c418a 100644
--- a/src/modules/TableFilter/TableFilter.py
+++ b/src/modules/TableFilter/TableFilter.py
@@ -147,7 +147,7 @@ class TableFilter(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
self.parent.title = "Table Filter"
- self.parent.categories = [""]
+ self.parent.categories = ["Tools"]
self.parent.dependencies = []
self.parent.contributors = ["LTrace Geophysics Team"]
self.parent.helpText = ""
@@ -415,7 +415,7 @@ def on_apply_clicked(self):
def openDialog(self):
self.logic.takeSnapshot()
item = 0 if self.logic._colorBy == -1 else self.logic.currentColumnColoredBy()
- d = LabelDialog(slicer.util.mainWindow(), self.logic, self.getCurrentMasterNode(), item)
+ d = LabelDialog(slicer.modules.AppContextInstance.mainWindow, self.logic, self.getCurrentMasterNode(), item)
if d.exec_() == 1:
# Force default column as color parameter
if self.logic._colorBy == -1:
@@ -440,7 +440,7 @@ def onFilterListItemDoubleClicked(self, item):
def addFilter(self):
self.logic.takeSnapshot()
- d = FilterDialog(slicer.util.mainWindow(), self.logic, self.getCurrentMasterNode(), 0)
+ d = FilterDialog(slicer.modules.AppContextInstance.mainWindow, self.logic, self.getCurrentMasterNode(), 0)
if d.exec_() == 1 and d.currentFilter is not None:
filter_ = d.currentFilter
self.filterList.addItem(repr(filter_))
@@ -463,7 +463,7 @@ def editFilter(self):
column = filter_.column + 1 # add None position
self.logic.takeSnapshot()
d = FilterDialog(
- slicer.util.mainWindow(),
+ slicer.modules.AppContextInstance.mainWindow,
self.logic,
self.getCurrentMasterNode(),
column,
@@ -494,7 +494,7 @@ def setTableReference(self, item):
try:
if self.logic is not None and len(self.logic._filters) > 0 and self.logic._node != node:
answer = qt.QMessageBox.question(
- slicer.util.mainWindow(),
+ slicer.modules.AppContextInstance.mainWindow,
"Table Filter",
"There are changes in the current table selection. Are you sure you want to change table? All filters will be erased.",
qt.QMessageBox.Yes | qt.QMessageBox.No, # | qt.QMessageBox.Cancel,
diff --git a/src/modules/ThinSectionAutoRegistration/Resources/Icons/ThinSectionAutoRegistration.png b/src/modules/ThinSectionAutoRegistration/Resources/Icons/ThinSectionAutoRegistration.png
deleted file mode 100644
index b89476c..0000000
Binary files a/src/modules/ThinSectionAutoRegistration/Resources/Icons/ThinSectionAutoRegistration.png and /dev/null differ
diff --git a/src/modules/ThinSectionAutoRegistration/Resources/Icons/ThinSectionAutoRegistration.svg b/src/modules/ThinSectionAutoRegistration/Resources/Icons/ThinSectionAutoRegistration.svg
new file mode 100644
index 0000000..013d931
--- /dev/null
+++ b/src/modules/ThinSectionAutoRegistration/Resources/Icons/ThinSectionAutoRegistration.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/modules/ThinSectionAutoRegistration/ThinSectionAutoRegistration.py b/src/modules/ThinSectionAutoRegistration/ThinSectionAutoRegistration.py
index c6975c5..ed2585c 100644
--- a/src/modules/ThinSectionAutoRegistration/ThinSectionAutoRegistration.py
+++ b/src/modules/ThinSectionAutoRegistration/ThinSectionAutoRegistration.py
@@ -8,16 +8,24 @@
import numpy as np
import qt
import slicer
+from scipy.ndimage import zoom
+
+from ltrace.slicer import ui
from ltrace.slicer.helpers import (
triggerNodeModified,
highlight_error,
reset_style_on_valid_node,
reset_style_on_valid_text,
)
-from ltrace.slicer.widgets import SingleShotInputWidget
from ltrace.slicer.widget.global_progress_bar import LocalProgressBar
-from ltrace.slicer_utils import slicer_is_in_developer_mode, LTracePlugin, LTracePluginWidget, LTracePluginLogic
-from scipy.ndimage import zoom
+from ltrace.slicer.widgets import SingleShotInputWidget
+from ltrace.slicer_utils import (
+ slicer_is_in_developer_mode,
+ LTracePlugin,
+ LTracePluginWidget,
+ LTracePluginLogic,
+ getResourcePath,
+)
class ThinSectionAutoRegistration(LTracePlugin):
@@ -28,11 +36,13 @@ class ThinSectionAutoRegistration(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
- self.parent.title = "Thin Section Auto Registration"
- self.parent.categories = ["Registration"]
+ self.parent.title = "Auto Registration"
+ self.parent.categories = ["Registration", "Thin Section"]
self.parent.dependencies = []
self.parent.contributors = ["LTrace Geophysical Solutions"]
- self.parent.helpText = ThinSectionAutoRegistration.help()
+ self.parent.helpText = (
+ f"file:///{(getResourcePath('manual') / 'Modules/Thin_section/AutoRegistration.html').as_posix()}"
+ )
@classmethod
def readme_path(cls):
@@ -125,18 +135,18 @@ def setup(self):
outputFormLayout.addRow(" ", None)
reset_style_on_valid_text(self.outputPrefixLineEdit)
- self.registerButton = qt.QPushButton("Apply")
- self.registerButton.setFixedHeight(40)
- self.registerButton.clicked.connect(self.onRegisterButtonClicked)
-
- self.cancelButton = qt.QPushButton("Cancel")
- self.cancelButton.setFixedHeight(40)
- self.cancelButton.clicked.connect(self.onCancelButtonClicked)
-
- buttonsHBoxLayout = qt.QHBoxLayout()
- buttonsHBoxLayout.addWidget(self.registerButton)
- buttonsHBoxLayout.addWidget(self.cancelButton)
- formLayout.addRow(buttonsHBoxLayout)
+ self.applyCancelButtons = ui.ApplyCancelButtons(
+ onApplyClick=self.onRegisterButtonClicked,
+ onCancelClick=self.onCancelButtonClicked,
+ applyTooltip="Apply changes",
+ cancelTooltip="Cancel",
+ applyText="Apply",
+ cancelText="Cancel",
+ enabled=True,
+ applyObjectName="Apply Button",
+ cancelObjectName=None,
+ )
+ self.layout.addWidget(self.applyCancelButtons)
self.layout.addWidget(self.progressBar)
diff --git a/src/modules/ThinSectionEnv/ThinSectionEnv.py b/src/modules/ThinSectionEnv/ThinSectionEnv.py
index 1365f8c..5963430 100644
--- a/src/modules/ThinSectionEnv/ThinSectionEnv.py
+++ b/src/modules/ThinSectionEnv/ThinSectionEnv.py
@@ -1,17 +1,11 @@
import os
from pathlib import Path
-import qt
import slicer
-from ltrace.slicer_utils import LTracePlugin, LTracePluginWidget
-from CustomizedCropVolume import CustomizedCropVolume
-from CustomizedData import CustomizedData
-from ImageTools import ImageTools
-from QEMSCANLoader import QEMSCANLoader
-from ThinSectionLoader import ThinSectionLoader
-from ThinSectionRegistration import ThinSectionRegistration
-from SegmentationEnv import SegmentationEnv
+from ltrace.slicer.helpers import svgToQIcon
+from ltrace.slicer.widget.custom_toolbar_buttons import addAction, addMenu
+from ltrace.slicer_utils import LTracePlugin, LTracePluginLogic, getResourcePath, LTraceEnvironmentMixin
class ThinSectionEnv(LTracePlugin):
@@ -22,106 +16,217 @@ class ThinSectionEnv(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
self.parent.title = "Thin Section Environment"
- self.parent.categories = ["Environments"]
+ self.parent.categories = ["Environment", "Thin Section"]
+ self.parent.hidden = True
self.parent.dependencies = []
self.parent.contributors = ["LTrace Geophysical Solutions"]
- self.parent.helpText = (
- ThinSectionEnv.help()
- + CustomizedData.help()
- + ThinSectionLoader.help()
- + QEMSCANLoader.help()
- + CustomizedCropVolume.help()
- + ImageTools.help()
- + ThinSectionRegistration.help()
- + SegmentationEnv.help()
- )
+ self.parent.helpText = ""
+
+ self.environment = ThinSectionEnvLogic()
@classmethod
def readme_path(cls):
return str(cls.MODULE_DIR / "README.md")
-class ThinSectionEnvWidget(LTracePluginWidget):
- def __init__(self, parent):
- LTracePluginWidget.__init__(self, parent)
- self.previousLayout = None
-
- def setup(self):
- LTracePluginWidget.setup(self)
-
- self.mainTab = qt.QTabWidget()
-
- self.dataTab = qt.QTabWidget()
- self.dataTab.addTab(slicer.modules.customizeddata.createNewWidgetRepresentation(), "Explorer")
- self.dataTab.addTab(slicer.modules.thinsectionloader.createNewWidgetRepresentation(), "Import")
- self.dataTab.addTab(slicer.modules.qemscanloader.createNewWidgetRepresentation(), "Import QEMSCAN")
- self.dataTab.addTab(slicer.modules.thinsectionexport.createNewWidgetRepresentation(), "Export")
- self.dataTab.addTab(slicer.modules.thinsectionflows.createNewWidgetRepresentation(), "Flows")
- self.mainTab.addTab(self.dataTab, "Data")
- self.mainTab.addTab(slicer.modules.customizedcropvolume.createNewWidgetRepresentation(), "Crop")
- self.mainTab.addTab(slicer.modules.imagetools.createNewWidgetRepresentation(), "Image Tools")
- segEnv = slicer.modules.thinsectionsegmentationenv.createNewWidgetRepresentation()
- self.mainTab.addTab(segEnv, "Segmentation") # remove histogram from thin section
-
- # Registration tab
- thinSectionRegistrationWidget = slicer.modules.thinsectionregistration.createNewWidgetRepresentation()
- thinSectionAutoRegistrationWidget = slicer.modules.thinsectionautoregistration.widgetRepresentation()
- self.registrationTab = qt.QTabWidget()
- self.registrationTab.addTab(thinSectionRegistrationWidget, "Manual")
- self.registrationTab.addTab(thinSectionAutoRegistrationWidget, "Automatic")
- self.mainTab.addTab(self.registrationTab, "Registration")
-
- self.multipleImageAnalysisWidget = slicer.modules.multipleimageanalysis.createNewWidgetRepresentation()
- self.mainTab.addTab(self.multipleImageAnalysisWidget, "Multi-Image Analysis")
-
- self.lastAccessedWidget = self.dataTab.widget(0)
-
- self.dataTab.tabBarClicked.connect(self.onDataTabClicked)
- self.mainTab.tabBarClicked.connect(self.onMainTabClicked)
- self.registrationTab.tabBarClicked.connect(self.onRegistrationTabClicked)
-
- self.layout.addWidget(self.mainTab)
-
- # Configure manual segment editor effects
- segEnv.self().segmentEditorWidget.self().selectParameterNodeByTag(ThinSectionEnv.SETTING_KEY)
- segEnv.self().segmentEditorWidget.self().configureEffectsForThinSectionEnvironment()
-
- def onMainTabClicked(self, index) -> None:
- if self.lastAccessedWidget != self.mainTab.widget(
- index
- ): # To avoid calling exit by clicking over the active tab
- self.lastAccessedWidget.exit()
- self.lastAccessedWidget = self.mainTab.widget(index)
- if type(self.lastAccessedWidget) is qt.QTabWidget:
- self.lastAccessedWidget = self.lastAccessedWidget.currentWidget()
- self.lastAccessedWidget.enter()
-
- def onDataTabClicked(self, index) -> None:
- self.lastAccessedWidget.exit()
- self.lastAccessedWidget = self.dataTab.widget(index)
- self.lastAccessedWidget.enter()
-
- def onRegistrationTabClicked(self, index) -> None:
- self.lastAccessedWidget.exit()
- self.lastAccessedWidget = self.registrationTab.widget(index)
- self.lastAccessedWidget.enter()
-
- def enter(self) -> None:
- super().enter()
- self.layoutNode = slicer.app.layoutManager().layoutLogic().GetLayoutNode()
- self.previousLayout = self.layoutNode.GetViewArrangement()
- self.layoutNode.SetViewArrangement(slicer.vtkMRMLLayoutNode.SlicerLayoutOneUpRedSliceView)
- self.lastAccessedWidget.enter()
-
- def exit(self):
- self.lastAccessedWidget.exit()
+class ThinSectionEnvLogic(LTracePluginLogic, LTraceEnvironmentMixin):
+ def __init__(self):
+ super().__init__()
+ self.__modulesToolbar = None
+
+ def setupEnvironment(self):
+ relatedModules = self.getModuleManager().fetchByCategory([self.category])
+
+ addAction(relatedModules["CustomizedData"], self.modulesToolbar)
+ addAction(relatedModules["ThinSectionLoader"], self.modulesToolbar)
+ addAction(relatedModules["QEMSCANLoader"], self.modulesToolbar)
+ addAction(relatedModules["CustomizedCropVolume"], self.modulesToolbar)
+ addAction(relatedModules["ImageTools"], self.modulesToolbar)
+ addMenu(
+ svgToQIcon(getResourcePath("Icons") / "IconSet-dark" / "Register.svg"),
+ "Register",
+ [relatedModules["ThinSectionRegistration"], relatedModules["ThinSectionAutoRegistration"]],
+ self.modulesToolbar,
+ )
- if not self.previousLayout:
- return
+ self.setupSegmentation()
+
+ addAction(relatedModules["ThinSectionFlows"], self.modulesToolbar)
+ addAction(relatedModules["MultipleImageAnalysis"], self.modulesToolbar)
+ addAction(relatedModules["ThinSectionExport"], self.modulesToolbar)
+
+ self.setupTools()
+ self.setupLoaders()
+
+ self.getModuleManager().setEnvironment(("Thin Section", "ThinSectionEnv"))
+
+ def setupSegmentation(self):
+ modules = self.getModuleManager().fetchByCategory(("Thin Section",), intersectWith="Segmentation")
+
+ addMenu(
+ svgToQIcon(getResourcePath("Icons") / "IconSet-dark" / "Layers.svg"),
+ "Segmentation",
+ [
+ modules["CustomizedSegmentEditor"],
+ modules["Segmenter"],
+ modules["SegmentInspector"],
+ # modules["ThinSectionInstanceSegmenter"],
+ # modules["ThinSectionInstanceEditor"],
+ modules["LabelMapEditor"],
+ modules["PoreStats"],
+ ],
+ self.modulesToolbar,
+ )
- # If layout was not changed from red slice, restore to previous one
- if self.layoutNode.GetViewArrangement() == slicer.vtkMRMLLayoutNode.SlicerLayoutOneUpRedSliceView:
- self.layoutNode.SetViewArrangement(self.previousLayout)
+ segmentEditor = slicer.util.getModuleWidget("CustomizedSegmentEditor")
+ segmentEditor.configureEffectsForThinSectionEnvironment()
- def switchToMultipleImageAnalysis(self):
- self.mainTab.setCurrentWidget(self.multipleImageAnalysisWidget)
+ def enter(self) -> None:
+ layoutNode = slicer.app.layoutManager().layoutLogic().GetLayoutNode()
+ if layoutNode.GetViewArrangement() != slicer.vtkMRMLLayoutNode.SlicerLayoutOneUpRedSliceView:
+ layoutNode.SetViewArrangement(slicer.vtkMRMLLayoutNode.SlicerLayoutOneUpRedSliceView)
+
+ # def setupEnviron(self):
+ # addAction(self.__modulesInfo["CustomizedData"], self.modulesToolbar)
+ # addAction(self.__modulesInfo["ThinSectionLoader"], self.modulesToolbar)
+ # addAction(self.__modulesInfo["QEMSCANLoader"], self.modulesToolbar)
+ # addAction(self.__modulesInfo["CustomizedCropVolume"], self.modulesToolbar)
+ # addAction(self.__modulesInfo["ImageTools"], self.modulesToolbar)
+ # addMenu(
+ # svgToQIcon(getResourcePath("Icons") / "IconSet-dark" / "Register.svg"),
+ # "Register",
+ # [self.__modulesInfo["ThinSectionRegistration"], self.__modulesInfo["ThinSectionAutoRegistration"]],
+ # self.modulesToolbar,
+ # )
+ #
+ # segmentation = getattr(slicer.modules, "SegmentationEnvInstance")
+ # segmentation.environment.modulesToolbar = self.modulesToolbar
+ # segmentation.environment.setupEnv(self.__modulesInfo) # TODO Move sets to args maybe?
+ #
+ # addAction(self.__modulesInfo["ThinSectionFlows"], self.modulesToolbar)
+ # addAction(self.__modulesInfo["MultipleImageAnalysis"], self.modulesToolbar)
+ # addAction(self.__modulesInfo["ThinSectionExport"], self.modulesToolbar)
+
+ # @classmethod
+ # def addAction(cls, module, toolbar, parent=None):
+ # if not parent:
+ # parent = toolbar
+ # m = getattr(slicer.modules, module.key.lower())
+ # button = CustomToolButton(parent)
+ # action = qt.QAction(m.icon, m.title, parent)
+ # action.setToolTip(m.title)
+ # action.triggered.connect(lambda _, name=module.key: slicer.util.selectModule(name))
+ # button.setDefaultAction(action)
+ # toolbar.addWidget(button)
+ #
+ # @classmethod
+ # def addMenuEntry(cls, module, menu, parent=None):
+ # if not parent:
+ # parent = menu
+ # m = getattr(slicer.modules, module.key.lower())
+ # action = qt.QAction(m.icon, m.title, parent)
+ # action.triggered.connect(lambda _, name=module.key: slicer.util.selectModule(name))
+ # #menu.setToolButtonStyle(qt.Qt.ToolButtonTextBesideIcon)
+ # menu.addAction(action)
+ #
+ # @classmethod
+ # def addMenu(cls, icon, folder, modules, parent):
+ # tool_button = CustomToolButton(parent)
+ # tool_button.setIcon(icon)
+ # tool_button.setText(folder)
+ # tool_button.setToolTip(folder)
+ #
+ # menu = qt.QMenu(tool_button)
+ # for module in modules:
+ # cls.addMenuEntry(module, menu, parent)
+ # tool_button.setMenu(menu)
+ #
+ # tool_button.setPopupMode(qt.QToolButton.MenuButtonPopup)
+ #
+ # parent.addWidget(tool_button)
+
+
+# class ThinSectionEnvWidget(LTracePluginWidget):
+# def __init__(self, parent):
+# LTracePluginWidget.__init__(self, parent)
+# self.previousLayout = None
+#
+# def setup(self):
+# LTracePluginWidget.setup(self)
+#
+# self.mainTab = qt.QTabWidget()
+#
+# self.dataTab = qt.QTabWidget()
+# self.dataTab.addTab(slicer.modules.customizeddata.createNewWidgetRepresentation(), "Explorer")
+# self.dataTab.addTab(slicer.modules.thinsectionloader.createNewWidgetRepresentation(), "Import")
+# self.dataTab.addTab(slicer.modules.qemscanloader.createNewWidgetRepresentation(), "Import QEMSCAN")
+# self.dataTab.addTab(slicer.modules.thinsectionexport.createNewWidgetRepresentation(), "Export")
+# self.dataTab.addTab(slicer.modules.thinsectionflows.createNewWidgetRepresentation(), "Flows")
+# self.mainTab.addTab(self.dataTab, "Data")
+# self.mainTab.addTab(slicer.modules.customizedcropvolume.createNewWidgetRepresentation(), "Crop")
+# self.mainTab.addTab(slicer.modules.imagetools.createNewWidgetRepresentation(), "Image Tools")
+# segEnv = slicer.modules.thinsectionsegmentationenv.createNewWidgetRepresentation()
+# self.mainTab.addTab(segEnv, "Segmentation") # remove histogram from thin section
+#
+# # Registration tab
+# thinSectionRegistrationWidget = slicer.modules.thinsectionregistration.createNewWidgetRepresentation()
+# thinSectionAutoRegistrationWidget = slicer.modules.thinsectionautoregistration.widgetRepresentation()
+# self.registrationTab = qt.QTabWidget()
+# self.registrationTab.addTab(thinSectionRegistrationWidget, "Manual")
+# self.registrationTab.addTab(thinSectionAutoRegistrationWidget, "Automatic")
+# self.mainTab.addTab(self.registrationTab, "Registration")
+#
+# self.multipleImageAnalysisWidget = slicer.modules.multipleimageanalysis.widgetRepresentation()
+# self.mainTab.addTab(self.multipleImageAnalysisWidget, "Multi-Image Analysis")
+#
+# self.lastAccessedWidget = self.dataTab.widget(0)
+#
+# self.dataTab.tabBarClicked.connect(self.onDataTabClicked)
+# self.mainTab.tabBarClicked.connect(self.onMainTabClicked)
+# self.registrationTab.tabBarClicked.connect(self.onRegistrationTabClicked)
+#
+# self.layout.addWidget(self.mainTab)
+#
+# # Configure manual segment editor effects
+# segEnv.self().segmentEditorWidget.self().selectParameterNodeByTag(ThinSectionEnv.SETTING_KEY)
+# segEnv.self().segmentEditorWidget.self().configureEffectsForThinSectionEnvironment()
+#
+# def onMainTabClicked(self, index) -> None:
+# if self.lastAccessedWidget != self.mainTab.widget(
+# index
+# ): # To avoid calling exit by clicking over the active tab
+# self.lastAccessedWidget.exit()
+# self.lastAccessedWidget = self.mainTab.widget(index)
+# if type(self.lastAccessedWidget) is qt.QTabWidget:
+# self.lastAccessedWidget = self.lastAccessedWidget.currentWidget()
+# self.lastAccessedWidget.enter()
+#
+# def onDataTabClicked(self, index) -> None:
+# self.lastAccessedWidget.exit()
+# self.lastAccessedWidget = self.dataTab.widget(index)
+# self.lastAccessedWidget.enter()
+#
+# def onRegistrationTabClicked(self, index) -> None:
+# self.lastAccessedWidget.exit()
+# self.lastAccessedWidget = self.registrationTab.widget(index)
+# self.lastAccessedWidget.enter()
+#
+# def enter(self) -> None:
+# super().enter()
+# self.layoutNode = slicer.app.layoutManager().layoutLogic().GetLayoutNode()
+# self.previousLayout = self.layoutNode.GetViewArrangement()
+# self.layoutNode.SetViewArrangement(slicer.vtkMRMLLayoutNode.SlicerLayoutOneUpRedSliceView)
+# self.lastAccessedWidget.enter()
+#
+# def exit(self):
+# self.lastAccessedWidget.exit()
+#
+# if not self.previousLayout:
+# return
+#
+# # If layout was not changed from red slice, restore to previous one
+# if self.layoutNode.GetViewArrangement() == slicer.vtkMRMLLayoutNode.SlicerLayoutOneUpRedSliceView:
+# self.layoutNode.SetViewArrangement(self.previousLayout)
+#
+# def switchToMultipleImageAnalysis(self):
+# self.mainTab.setCurrentWidget(self.multipleImageAnalysisWidget)
diff --git a/src/modules/ThinSectionExport/Resources/Icons/ThinSectionExport.png b/src/modules/ThinSectionExport/Resources/Icons/ThinSectionExport.png
deleted file mode 100644
index b89476c..0000000
Binary files a/src/modules/ThinSectionExport/Resources/Icons/ThinSectionExport.png and /dev/null differ
diff --git a/src/modules/ThinSectionExport/Resources/Icons/ThinSectionExport.svg b/src/modules/ThinSectionExport/Resources/Icons/ThinSectionExport.svg
new file mode 100644
index 0000000..85a4c2f
--- /dev/null
+++ b/src/modules/ThinSectionExport/Resources/Icons/ThinSectionExport.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/modules/ThinSectionExport/ThinSectionExport.py b/src/modules/ThinSectionExport/ThinSectionExport.py
index 5e269e3..536c452 100644
--- a/src/modules/ThinSectionExport/ThinSectionExport.py
+++ b/src/modules/ThinSectionExport/ThinSectionExport.py
@@ -1,12 +1,15 @@
import os
+from pathlib import Path
+
+import ctk
import qt
import slicer
-import ctk
import vtk
+
+from ltrace.slicer import export
+from ltrace.slicer import ui
from ltrace.slicer.helpers import getNodeDataPath
-from ltrace.slicer_utils import LTracePlugin, LTracePluginWidget, LTracePluginLogic
-from pathlib import Path
-from Export import ExportLogic, checkUniqueNames
+from ltrace.slicer_utils import LTracePlugin, LTracePluginWidget, LTracePluginLogic, getResourcePath
class ThinSectionExport(LTracePlugin):
@@ -21,10 +24,10 @@ class ThinSectionExport(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
- self.parent.title = "Thin Section Export"
+ self.parent.title = "Export"
self.parent.categories = ["Thin Section"]
self.parent.contributors = ["LTrace Geophysics Team"]
- self.parent.helpText = ThinSectionExport.help()
+ self.parent.helpText = f"file:///{(getResourcePath('manual') / 'Modules/Thin_section/Export.html').as_posix()}"
@classmethod
def readme_path(cls):
@@ -74,6 +77,7 @@ def setup(self):
)
self.directorySelector = ctk.ctkDirectoryButton()
+ self.directorySelector.setMaximumWidth(374)
self.directorySelector.caption = "Export directory"
self.directorySelector.directory = ThinSectionExport.get_setting(
self.EXPORT_DIR, Path(slicer.mrmlScene.GetRootDirectory()).parent
@@ -83,21 +87,28 @@ def setup(self):
self.progressBar.setValue(0)
self.progressBar.hide()
- self.cancelButton = qt.QPushButton("Cancel")
- self.cancelButton.hide()
-
- progressLayout = qt.QHBoxLayout()
+ progressLayout = qt.QVBoxLayout()
progressLayout.addWidget(self.progressBar)
- progressLayout.addWidget(self.cancelButton)
+ statusLayout = qt.QHBoxLayout()
self.statusLabel = qt.QLabel()
+ statusLayout.addStretch()
+ statusLayout.addWidget(self.statusLabel)
+ progressLayout.addLayout(statusLayout)
+
+ self.applyCancelButtons = ui.ApplyCancelButtons(
+ onApplyClick=self.onExportClicked,
+ onCancelClick=self.onCancelClicked,
+ applyTooltip="Export",
+ cancelTooltip="Cancel",
+ applyText="Export",
+ cancelText="Cancel",
+ enabled=True,
+ applyObjectName=None,
+ cancelObjectName=None,
+ )
+ self.applyCancelButtons.applyBtn.setEnabled(False)
- self.exportButton = qt.QPushButton("Export")
- self.exportButton.setFixedHeight(40)
- self.exportButton.enabled = False
-
- self.exportButton.clicked.connect(self.onExportClicked)
- self.cancelButton.clicked.connect(self.onCancelClicked)
self.subjectHierarchyTreeView.currentItemChanged.connect(self.onSelectionChanged)
formatGroup = qt.QGroupBox()
@@ -110,33 +121,31 @@ def setup(self):
formLayout.addRow("Ignore directory structure:", self.ignoreDirStructureCheckbox)
formLayout.addRow("Export directory:", self.directorySelector)
formLayout.addRow("Export format:", formatGroup)
- formLayout.addRow(progressLayout)
- formLayout.addRow(self.statusLabel)
- formLayout.addRow(self.exportButton)
self.layout.addLayout(formLayout)
+ self.layout.addWidget(self.applyCancelButtons)
+ self.layout.addLayout(progressLayout)
self.layout.addStretch(1)
def _startExport(self):
self.progressBar.setValue(0)
self.progressBar.show()
- self.cancelButton.show()
self.cancel = False
- self.cancelButton.enabled = True
- self.exportButton.enabled = False
+ self.applyCancelButtons.cancelBtn.setEnabled(True)
+ self.applyCancelButtons.applyBtn.setEnabled(False)
def _stopExport(self):
- self.cancelButton.enabled = False
+ self.applyCancelButtons.cancelBtn.setEnabled(False)
self._updateNodesAndExportButton()
def _updateNodesAndExportButton(self):
items = vtk.vtkIdList()
self.subjectHierarchyTreeView.currentItems(items)
- self.nodes = ExportLogic().getDataNodes(items, self.EXPORTABLE_TYPES)
- self.exportButton.enabled = self.nodes
+ self.nodes = export.getDataNodes(items, self.EXPORTABLE_TYPES)
+ self.applyCancelButtons.applyBtn.setEnabled(self.nodes)
def onExportClicked(self):
- checkUniqueNames(self.nodes)
+ export.checkUniqueNames(self.nodes)
outputDir = self.directorySelector.directory
ignoreDirStructure = self.ignoreDirStructureCheckbox.checked
imageFormat = self.imageFormatBox.currentText
@@ -185,29 +194,28 @@ def __init__(self):
LTracePluginLogic.__init__(self)
def export(self, node, outputDir, ignoreDirStructure, imageFormat, tableFormat):
- logic = ExportLogic()
nodeDir = Path(outputDir) if ignoreDirStructure else Path(outputDir) / getNodeDataPath(node).parent
if isinstance(node, slicer.vtkMRMLSegmentationNode):
format_ = {
- ThinSectionExport.FORMAT_PNG: ExportLogic.SEGMENTATION_FORMAT_PNG,
- ThinSectionExport.FORMAT_TIF: ExportLogic.SEGMENTATION_FORMAT_TIF,
+ ThinSectionExport.FORMAT_PNG: export.SEGMENTATION_FORMAT_PNG,
+ ThinSectionExport.FORMAT_TIF: export.SEGMENTATION_FORMAT_TIF,
}[imageFormat]
- logic.exportSegmentation(node, outputDir, nodeDir, format_)
+ export.exportSegmentation(node, outputDir, nodeDir, format_)
elif isinstance(node, slicer.vtkMRMLLabelMapVolumeNode):
format_ = {
- ThinSectionExport.FORMAT_PNG: ExportLogic.LABEL_MAP_FORMAT_PNG,
- ThinSectionExport.FORMAT_TIF: ExportLogic.LABEL_MAP_FORMAT_TIF,
+ ThinSectionExport.FORMAT_PNG: export.LABEL_MAP_FORMAT_PNG,
+ ThinSectionExport.FORMAT_TIF: export.LABEL_MAP_FORMAT_TIF,
}[imageFormat]
- logic.exportLabelMap(node, outputDir, nodeDir, format_)
+ export.exportLabelMap(node, outputDir, nodeDir, format_)
elif isinstance(node, slicer.vtkMRMLScalarVolumeNode):
format_ = {
- ThinSectionExport.FORMAT_PNG: ExportLogic.IMAGE_FORMAT_PNG,
- ThinSectionExport.FORMAT_TIF: ExportLogic.IMAGE_FORMAT_TIF,
+ ThinSectionExport.FORMAT_PNG: export.IMAGE_FORMAT_PNG,
+ ThinSectionExport.FORMAT_TIF: export.IMAGE_FORMAT_TIF,
}[imageFormat]
- logic.exportImage(node, outputDir, nodeDir, format_)
+ export.exportImage(node, outputDir, nodeDir, format_)
elif isinstance(node, slicer.vtkMRMLTableNode):
format_ = {
- ThinSectionExport.FORMAT_CSV: ExportLogic.TABLE_FORMAT_CSV,
- ThinSectionExport.FORMAT_LAS: ExportLogic.TABLE_FORMAT_LAS,
+ ThinSectionExport.FORMAT_CSV: export.TABLE_FORMAT_CSV,
+ ThinSectionExport.FORMAT_LAS: export.TABLE_FORMAT_LAS,
}[tableFormat]
- logic.exportTable(node, outputDir, nodeDir, format_)
+ export.exportTable(node, outputDir, nodeDir, format_)
diff --git a/src/modules/ThinSectionFlows/Resources/Icons/ThinSectionFlows.png b/src/modules/ThinSectionFlows/Resources/Icons/ThinSectionFlows.png
deleted file mode 100644
index b89476c..0000000
Binary files a/src/modules/ThinSectionFlows/Resources/Icons/ThinSectionFlows.png and /dev/null differ
diff --git a/src/modules/ThinSectionFlows/Resources/Icons/ThinSectionFlows.svg b/src/modules/ThinSectionFlows/Resources/Icons/ThinSectionFlows.svg
new file mode 100644
index 0000000..001e90d
--- /dev/null
+++ b/src/modules/ThinSectionFlows/Resources/Icons/ThinSectionFlows.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/modules/ThinSectionFlows/Resources/Icons/pp.png b/src/modules/ThinSectionFlows/Resources/Icons/pp.png
index ca11a82..c98cb61 100644
Binary files a/src/modules/ThinSectionFlows/Resources/Icons/pp.png and b/src/modules/ThinSectionFlows/Resources/Icons/pp.png differ
diff --git a/src/modules/ThinSectionFlows/Resources/Icons/pppx.png b/src/modules/ThinSectionFlows/Resources/Icons/pppx.png
index 7d55dfb..03053b5 100644
Binary files a/src/modules/ThinSectionFlows/Resources/Icons/pppx.png and b/src/modules/ThinSectionFlows/Resources/Icons/pppx.png differ
diff --git a/src/modules/ThinSectionFlows/Resources/Icons/qs.png b/src/modules/ThinSectionFlows/Resources/Icons/qs.png
index f11738b..4e41d73 100644
Binary files a/src/modules/ThinSectionFlows/Resources/Icons/qs.png and b/src/modules/ThinSectionFlows/Resources/Icons/qs.png differ
diff --git a/src/modules/ThinSectionFlows/ThinSectionFlows.py b/src/modules/ThinSectionFlows/ThinSectionFlows.py
index 72e6298..f345ae1 100644
--- a/src/modules/ThinSectionFlows/ThinSectionFlows.py
+++ b/src/modules/ThinSectionFlows/ThinSectionFlows.py
@@ -1,4 +1,5 @@
from ltrace.slicer_utils import LTracePlugin, LTracePluginWidget
+from ltrace.slicer_utils import getResourcePath
from pathlib import Path
import qt
import os
@@ -33,9 +34,11 @@ class ThinSectionFlows(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
self.parent.title = "Thin Section Flows"
- self.parent.categories = ["LTrace Tools"]
+ self.parent.categories = ["Tools", "Thin Section"]
self.parent.contributors = ["LTrace Geophysics Team"]
- self.parent.helpText = ThinSectionFlows.help()
+ self.parent.helpText = (
+ f"file:///{(getResourcePath('manual') / 'Modules/Thin_section/Fluxo%20PP.html').as_posix()}"
+ )
@classmethod
def readme_path(cls):
diff --git a/src/modules/ThinSectionInstanceEditor/Resources/Icons/EraseIcon.png b/src/modules/ThinSectionInstanceEditor/Resources/Icons/EraseIcon.png
deleted file mode 100644
index a6e30f2..0000000
Binary files a/src/modules/ThinSectionInstanceEditor/Resources/Icons/EraseIcon.png and /dev/null differ
diff --git a/src/modules/ThinSectionInstanceEditor/Resources/Icons/ThinSectionInstanceEditor.png b/src/modules/ThinSectionInstanceEditor/Resources/Icons/ThinSectionInstanceEditor.png
deleted file mode 100644
index b89476c..0000000
Binary files a/src/modules/ThinSectionInstanceEditor/Resources/Icons/ThinSectionInstanceEditor.png and /dev/null differ
diff --git a/src/modules/ThinSectionInstanceEditor/Resources/Icons/ThinSectionInstanceEditor.svg b/src/modules/ThinSectionInstanceEditor/Resources/Icons/ThinSectionInstanceEditor.svg
new file mode 100644
index 0000000..4a8a604
--- /dev/null
+++ b/src/modules/ThinSectionInstanceEditor/Resources/Icons/ThinSectionInstanceEditor.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/modules/ThinSectionInstanceEditor/ThinSectionInstanceEditor.py b/src/modules/ThinSectionInstanceEditor/ThinSectionInstanceEditor.py
index 7dbbc46..3e6341d 100644
--- a/src/modules/ThinSectionInstanceEditor/ThinSectionInstanceEditor.py
+++ b/src/modules/ThinSectionInstanceEditor/ThinSectionInstanceEditor.py
@@ -1,28 +1,25 @@
+import logging
import os
+from enum import Enum
from pathlib import Path
import ctk
-from Customizer import Customizer
import cv2
-import mrml
import numpy as np
-import pandas as pd
import qt
+import scipy
import slicer
import vtk
-import scipy
-import logging
from ThinSectionInstanceEditorLib.widget.FilterableTableWidgets import GenericTableWidget
+from ltrace.algorithms.measurements import LabelStatistics2D
from ltrace.slicer.helpers import highlight_error, reset_style_on_valid_text, tryGetNode
from ltrace.slicer.node_attributes import ImageLogDataSelectable
-from ltrace.slicer.ui import hierarchyVolumeInput
-from ltrace.slicer_utils import LTracePlugin, LTracePluginWidget, LTracePluginLogic
+from ltrace.slicer.volume_operator import VolumeOperator
+from ltrace.slicer_utils import LTracePlugin, LTracePluginWidget, LTracePluginLogic, getResourcePath
+from ltrace.slicer import ui
from ltrace.slicer_utils import dataFrameToTableNode
-from ltrace.slicer.volume_operator import VolumeOperator, SegmentOperator
-from ltrace.algorithms.measurements import LabelStatistics2D
from ltrace.transforms import transformPoints
-from enum import Enum
def calculate_instance_properties(mask, node):
@@ -49,11 +46,11 @@ def calculate_instance_properties(mask, node):
# Statistics
mask_filt = mask.squeeze()
voxel_area = np.product(spacing)
-
+ # TODO check that the inspector API is not being used correctly
volumeOperator = VolumeOperator(node)
operator = LabelStatistics2D(mask_filt, spacing, direction=None, size_filter=0)
pointsInRAS = np.array(np.where(mask_filt)).T
- stats = operator(mask_filt, pointsInRAS)
+ stats = operator.strict_calculate(mask_filt, pointsInRAS)
statistics["area (mm^2)"] = stats[2]
statistics["max_feret (mm)"] = stats[4] * spacing[0]
statistics["min_feret (mm)"] = stats[5] * spacing[0]
@@ -80,8 +77,8 @@ class ThinSectionInstanceEditor(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
- self.parent.title = "Thin Section Instance Editor"
- self.parent.categories = ["Segmentation"]
+ self.parent.title = "Instance Editor"
+ self.parent.categories = ["Segmentation", "Thin Section"]
self.parent.dependencies = []
self.parent.contributors = ["LTrace Geophysical Solutions"]
self.parent.helpText = ThinSectionInstanceEditor.help()
@@ -124,7 +121,7 @@ def setup(self):
inputFormLayout = qt.QFormLayout(inputCollapsibleButton)
inputFormLayout.setLabelAlignment(qt.Qt.AlignRight)
- self.inputTableNodeComboBox = hierarchyVolumeInput(
+ self.inputTableNodeComboBox = ui.hierarchyVolumeInput(
nodeTypes=["vtkMRMLTableNode"], onChange=self.onInputTableNodeChanged
)
self.inputTableNodeComboBox.setToolTip("Select the instance report table.")
@@ -155,29 +152,29 @@ def setup(self):
editFormLayout = qt.QFormLayout(self.editCollapsibleButton)
self.addSegmentButton = qt.QPushButton("Add")
- self.addSegmentButton.setIcon(qt.QIcon(str(Customizer.ADD_ICON_PATH)))
+ self.addSegmentButton.setIcon(qt.QIcon(getResourcePath("Icons") / "Add.png"))
self.addSegmentButton.clicked.connect(self.onAddSegmentButtonClicked)
self.paintButton = qt.QPushButton("Paint")
- self.paintButton.setIcon(qt.QIcon(str(Customizer.EDIT_ICON_PATH)))
+ self.paintButton.setIcon(qt.QIcon(getResourcePath("Icons") / "Edit.png"))
self.paintButton.clicked.connect(self.onPaintButtonClicked)
self.eraseButton = qt.QPushButton("Erase")
- self.eraseButton.setIcon(qt.QIcon(str(ThinSectionInstanceEditor.RES_DIR / "Icons" / "EraseIcon.png")))
+ self.eraseButton.setIcon(qt.QIcon(getResourcePath("Icons") / "IconSet-dark" / "Eraser.png"))
self.eraseButton.clicked.connect(self.onEraseButtonClicked)
self.applySegmentButton = qt.QPushButton("Apply")
- self.applySegmentButton.setIcon(qt.QIcon(str(Customizer.APPLY_ICON_PATH)))
+ self.applySegmentButton.setIcon(qt.QIcon(getResourcePath("Icons") / "Apply.png"))
self.applySegmentButton.setEnabled(False)
self.applySegmentButton.clicked.connect(self.onApplySegmentButtonClicked)
self.cancelSegmentButton = qt.QPushButton("Cancel")
- self.cancelSegmentButton.setIcon(qt.QIcon(str(Customizer.CANCEL_ICON_PATH)))
+ self.cancelSegmentButton.setIcon(qt.QIcon(getResourcePath("Icons") / "Cancel.png"))
self.cancelSegmentButton.setEnabled(False)
self.cancelSegmentButton.clicked.connect(self.onCancelSegmentButtonClicked)
self.declineSegmentButton = qt.QPushButton("Decline")
- self.declineSegmentButton.setIcon(qt.QIcon(str(Customizer.DELETE_ICON_PATH)))
+ self.declineSegmentButton.setIcon(qt.QIcon(getResourcePath("Icons") / "Delete.png"))
self.declineSegmentButton.clicked.connect(self.onDeclineSegmentButtonClicked)
editButtonsHBoxLayout = qt.QHBoxLayout()
@@ -210,21 +207,21 @@ def setup(self):
outputFormLayout.addRow(" ", None)
reset_style_on_valid_text(self.outputSuffixLineEdit)
- self.applyButton = qt.QPushButton("Apply")
- self.applyButton.setFixedHeight(40)
- self.applyButton.clicked.connect(self.onApplyButtonClicked)
-
- self.cancelButton = qt.QPushButton("Cancel")
- self.cancelButton.setFixedHeight(40)
- self.cancelButton.clicked.connect(self.onCancelButtonClicked)
-
- buttonsHBoxLayout = qt.QHBoxLayout()
- buttonsHBoxLayout.addWidget(self.applyButton)
- buttonsHBoxLayout.addWidget(self.cancelButton)
- formLayout.addRow(buttonsHBoxLayout)
+ self.applyCancelButtons = ui.ApplyCancelButtons(
+ onApplyClick=self.onApplyButtonClicked,
+ onCancelClick=self.onCancelButtonClicked,
+ applyTooltip="Apply changes",
+ cancelTooltip="Cancel",
+ applyText="Apply",
+ cancelText="Cancel",
+ enabled=True,
+ applyObjectName="Apply Button",
+ cancelObjectName=None,
+ )
+ formLayout.addWidget(self.applyCancelButtons)
self.spacerItem = qt.QSpacerItem(10, 10, qt.QSizePolicy.Minimum, qt.QSizePolicy.Expanding)
- self.layout.addItem(self.spacerItem)
+ formLayout.addItem(self.spacerItem)
self.switchEditionMode(editMode=None)
@@ -291,9 +288,9 @@ def onLabelChanged(self, observer, event):
)
if self.applySegmentButton.enabled == True:
self.applySegmentButton.click()
- self.applyButton.click()
+ self.applyCancelButtons.applyBtn.click()
else:
- self.cancelButton.click()
+ self.applyCancelButtons.cancelBtn.click()
labelVolumeNode = slicer.mrmlScene.GetNodeByID(labelVolumeID)
tableNodeID = labelVolumeNode.GetAttribute("ThinSectionInstanceTableNode")
@@ -436,17 +433,17 @@ def onParametersCollapsibleButtonClicked(self):
self.layout.removeItem(self.spacerItem)
def selectReferenceToTableNode(self, tableNode):
- dialog = qt.QDialog(slicer.util.mainWindow())
+ dialog = qt.QDialog(slicer.modules.AppContextInstance.mainWindow)
dialog.setWindowFlags(dialog.windowFlags() & ~qt.Qt.WindowContextHelpButtonHint)
dialog.setWindowTitle("Select corresponding reference and labelmap node")
formLayout = qt.QFormLayout()
- referenceNodeComboBox = hierarchyVolumeInput(
+ referenceNodeComboBox = ui.hierarchyVolumeInput(
nodeTypes=["vtkMRMLVectorVolumeNode"],
)
referenceNodeComboBox.setToolTip("Select the corresponding reference node.")
- labelMapNodeComboBox = hierarchyVolumeInput(
+ labelMapNodeComboBox = ui.hierarchyVolumeInput(
nodeTypes=["vtkMRMLLabelMapVolumeNode"],
)
labelMapNodeComboBox.setToolTip("Select the corresponding labelmap node.")
@@ -755,7 +752,9 @@ def apply(self, dataFrame, outputSuffix):
updatedLabelMapNode.SetAttribute(ImageLogDataSelectable.name(), ImageLogDataSelectable.TRUE.value)
updatedDataFrame = dataFrame
- updatedDataFrame.drop(updatedDataFrame[updatedDataFrame.label.isin(self.declinedLabels)].index, inplace=True)
+ updatedDataFrame = updatedDataFrame.drop(
+ updatedDataFrame[updatedDataFrame.label.isin(self.declinedLabels)].index
+ )
updatedTableNode = dataFrameToTableNode(updatedDataFrame)
updatedTableNode.SetName(slicer.mrmlScene.GenerateUniqueName(tableNode.GetName() + "_" + outputSuffix))
updatedTableNode.SetAttribute("InstanceEditor", tableNode.GetAttribute("InstanceEditor"))
@@ -822,7 +821,9 @@ def applySegment(self, dataFrame):
self.editedLabelMapNodeArray[0][mask == True] = new_label
k += 1
- except TypeError: # when single-pixel island happens a TypeError is catched by calculate_instance_properties()
+ except (
+ TypeError
+ ): # when single-pixel island happens a TypeError is catched by calculate_instance_properties()
self.editedLabelMapNodeArray[0][mask == True] = 0
return rowsData
@@ -893,7 +894,7 @@ def brushOverwritingAnotherInstance(self, array, value, brushRegion):
return np.any([instanceIndex not in [0, value] for instanceIndex in instancesUnderBrush])
def setMouseInteractionToViewTransform(self):
- mouseModeToolBar = slicer.util.findChild(slicer.util.mainWindow(), "MouseModeToolBar")
+ mouseModeToolBar = slicer.util.findChild(slicer.modules.AppContextInstance.mainWindow, "MouseModeToolBar")
mouseModeToolBar.interactionNode().SetCurrentInteractionMode(slicer.vtkMRMLInteractionNode.ViewTransform)
def onMouseButtonClickedOrHeld(self, *args):
diff --git a/src/modules/ThinSectionInstanceEditor/ThinSectionInstanceEditorLib/widget/Base.py b/src/modules/ThinSectionInstanceEditor/ThinSectionInstanceEditorLib/widget/Base.py
index 393a1ab..0ef9cc8 100644
--- a/src/modules/ThinSectionInstanceEditor/ThinSectionInstanceEditorLib/widget/Base.py
+++ b/src/modules/ThinSectionInstanceEditor/ThinSectionInstanceEditorLib/widget/Base.py
@@ -1,6 +1,7 @@
import qt
import slicer
-from Customizer import Customizer
+
+from ltrace.slicer_utils import getResourcePath
class TableWidget(qt.QWidget):
@@ -28,15 +29,15 @@ def setup(self):
self.layout().addRow(self.tableView)
self.previousButton = qt.QPushButton("Previous")
- self.previousButton.setIcon(qt.QIcon(str(Customizer.UNDO_ICON_PATH)))
+ self.previousButton.setIcon(qt.QIcon(getResourcePath("Icons") / "Undo.png"))
self.previousButton.clicked.connect(self.onPreviousButtonClicked)
self.nextButton = qt.QPushButton("Next")
- self.nextButton.setIcon(qt.QIcon(str(Customizer.REDO_ICON_PATH)))
+ self.nextButton.setIcon(qt.QIcon(getResourcePath("Icons") / "Redo.png"))
self.nextButton.clicked.connect(self.onNextButtonClicked)
self.resetFiltersButton = qt.QPushButton("Reset filters")
- self.resetFiltersButton.setIcon(qt.QIcon(str(Customizer.RESET_ICON_PATH)))
+ self.resetFiltersButton.setIcon(qt.QIcon(getResourcePath("Icons") / "Reset.png"))
self.resetFiltersButton.clicked.connect(lambda: self.setAllRangeWidgetValues(resetValues=True))
buttonsLayout = qt.QHBoxLayout()
diff --git a/src/modules/ThinSectionInstanceEditor/ThinSectionInstanceEditorLib/widget/FilterableTableWidgets.py b/src/modules/ThinSectionInstanceEditor/ThinSectionInstanceEditorLib/widget/FilterableTableWidgets.py
index 02344a7..9831eb6 100644
--- a/src/modules/ThinSectionInstanceEditor/ThinSectionInstanceEditorLib/widget/FilterableTableWidgets.py
+++ b/src/modules/ThinSectionInstanceEditor/ThinSectionInstanceEditorLib/widget/FilterableTableWidgets.py
@@ -121,8 +121,8 @@ def __init__(self, data, parent=None):
def removeRows(self, row, count, parent=None):
self.beginRemoveRows(parent, row, row + count - 1)
- self._data.drop(index=[i for i in range(row, row + count)], inplace=True)
- self._data.reset_index(drop=True, inplace=True)
+ self._data = self._data.drop(index=[i for i in range(row, row + count)])
+ self._data = self._data.reset_index(drop=True)
self.endRemoveRows()
return True
@@ -152,8 +152,8 @@ def getLabelByRow(self, row):
return int(float(label))
def sort(self, column, descending):
- self._data.sort_values(by=self._data.columns[column], inplace=True, ascending=not descending)
- self._data.reset_index(drop=True, inplace=True)
+ self._data = self._data.sort_values(by=self._data.columns[column], ascending=not descending)
+ self._data = self._data.reset_index(drop=True)
self.sortingColumn = [column, descending]
self.dataChanged.emit(qt.QModelIndex(), qt.QModelIndex())
@@ -161,8 +161,8 @@ def sortDefault(self):
self.sort(self.sortingColumn[0], self.sortingColumn[1])
def sortByMultipleColumns(self, columnIds, ascending=True):
- self._data.sort_values(by=columnIds, inplace=True, ascending=ascending)
- self._data.reset_index(drop=True, inplace=True)
+ self._data = self._data.sort_values(by=columnIds, ascending=ascending)
+ self._data = self._data.reset_index(drop=True)
self.dataChanged.emit(qt.QModelIndex(), qt.QModelIndex())
def setFilteredLabels(self, filteredLabels):
diff --git a/src/modules/ThinSectionInstanceSegmenter/Resources/Icons/ThinSectionInstanceSegmenter.png b/src/modules/ThinSectionInstanceSegmenter/Resources/Icons/ThinSectionInstanceSegmenter.png
deleted file mode 100644
index b89476c..0000000
Binary files a/src/modules/ThinSectionInstanceSegmenter/Resources/Icons/ThinSectionInstanceSegmenter.png and /dev/null differ
diff --git a/src/modules/ThinSectionInstanceSegmenter/Resources/Icons/ThinSectionInstanceSegmenter.svg b/src/modules/ThinSectionInstanceSegmenter/Resources/Icons/ThinSectionInstanceSegmenter.svg
new file mode 100644
index 0000000..ff95637
--- /dev/null
+++ b/src/modules/ThinSectionInstanceSegmenter/Resources/Icons/ThinSectionInstanceSegmenter.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/modules/ThinSectionInstanceSegmenter/ThinSectionInstanceSegmenter.py b/src/modules/ThinSectionInstanceSegmenter/ThinSectionInstanceSegmenter.py
index 0113e68..39a165d 100644
--- a/src/modules/ThinSectionInstanceSegmenter/ThinSectionInstanceSegmenter.py
+++ b/src/modules/ThinSectionInstanceSegmenter/ThinSectionInstanceSegmenter.py
@@ -205,8 +205,8 @@ class ThinSectionInstanceSegmenter(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
- self.parent.title = "ThinSection Instance Segmenter"
- self.parent.categories = ["LTrace Tools"]
+ self.parent.title = "Instance Segmenter"
+ self.parent.categories = ["Segmentation", "Thin Section"]
self.parent.contributors = ["LTrace Geophysics Team"]
self.parent.helpText = ThinSectionInstanceSegmenter.help()
self.parent.dependencies = []
@@ -299,7 +299,7 @@ def _setupInputsSection(self):
self.inputsSelector.soiInput.objectName = "SOI ComboBox"
self.inputsSelector.referenceInput.enabled = True
self.inputsSelector.referenceInput.objectName = "Input Volume ComboBox"
- self.inputsSelector.referenceInput.setNodeTypes(["vtkMRMLVectorVolumeNode"])
+ self.inputsSelector.referenceInput.selectorWidget.setNodeTypes(["vtkMRMLVectorVolumeNode"])
hbox = qt.QHBoxLayout(widget)
inferenceTypeLabel = qt.QLabel("Inference:")
@@ -411,20 +411,22 @@ def _setupApplySection(self):
widget = qt.QWidget()
vlayout = qt.QVBoxLayout(widget)
- self.applyButton = ui.ButtonWidget(
- text="Apply", tooltip="Run segmenter on input data limited by ROI", onClick=self._onApplyClicked
+ self.applyCancelButtons = ui.ApplyCancelButtons(
+ onApplyClick=self._onApplyClicked,
+ onCancelClick=self._onCancel,
+ applyTooltip="Run segmenter on input data limited by ROI",
+ cancelTooltip="Cancel",
+ applyText="Apply",
+ cancelText="Cancel",
+ enabled=False,
+ applyObjectName="Apply Button",
+ cancelObjectName=None,
)
- self.applyButton.objectName = "Apply Button"
-
- self.applyButton.setStyleSheet("QPushButton {font-size: 11px; font-weight: bold; padding: 8px; margin: 0px}")
- self.applyButton.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding)
-
- self.applyButton.enabled = False
self.progressBar = LocalProgressBar()
hlayout = qt.QHBoxLayout()
- hlayout.addWidget(self.applyButton)
+ hlayout.addWidget(self.applyCancelButtons)
hlayout.setContentsMargins(0, 8, 0, 8)
vlayout.addLayout(hlayout)
@@ -457,8 +459,14 @@ def _onReferenceSelected(self, node):
self.chunk_size_spinbox.setMaximum(max_size)
def _checkRequirementsForApply(self):
- if self.cliNode == None or not self.cliNode.IsBusy():
- self.applyButton.enabled = self.refNode is not None
+ if self.cliNode is None or not self.cliNode.IsBusy():
+ self.applyCancelButtons.setEnabled(self.refNode is not None)
+
+ def _onCancel(self):
+ if self.cliNode is None:
+ return
+ self.cliNode.Cancel()
+ self.resetUI()
def _onApplyClicked(self):
if self.outputPrefix.text.strip() == "":
@@ -469,7 +477,8 @@ def _onApplyClicked(self):
slicer.util.errorDisplay("Please select an input node.")
return
- self.applyButton.enabled = False
+ self.applyCancelButtons.applyBtn.setEnabled(False)
+ self.applyCancelButtons.cancelBtn.setEnabled(True)
prefix = self.outputPrefix.text + "_{type}"
@@ -494,7 +503,8 @@ def _onApplyClicked(self):
model_dir = model_dir.as_posix()
if self.remoteRadioButton.checked:
self.cliNode = logic.dispatch(model_dir, refNode, soiNode, prefix, params, classes)
- self.applyButton.enabled = True
+ self.applyCancelButtons.applyBtn.setEnabled(True)
+ self.applyCancelButtons.cancelBtn.setEnabled(False)
else:
self.cliNode = logic.run(model_dir, refNode, soiNode, prefix, params, classes)
if self.cliNode:
@@ -507,7 +517,8 @@ def _onApplyClicked(self):
slicer.util.errorDisplay(f"Failed to complete execution. {e}")
tmpPrefix = prefix.replace("{type}", "TMP_*")
clearPattern(tmpPrefix)
- self.applyButton.enabled = True
+ self.applyCancelButtons.applyBtn.setEnabled(True)
+ self.applyCancelButtons.cancelBtn.setEnabled(False)
raise
def resetUI(self):
@@ -520,12 +531,9 @@ def enter(self) -> None:
self._addPretrainedModelsIfAvailable()
def _addPretrainedModelsIfAvailable(self):
- env = slicer.util.selectedModule()
- envs = tuple(map(lambda x: x.value, NodeEnvironment))
-
- if env not in envs:
- return
+ env = helpers.getCurrentEnvironment().value
+ assert env is not None, "Missing environment definition"
if self.modelInput.count == 0:
try:
model_dirs = get_trained_models_with_metadata(env)
diff --git a/src/modules/ThinSectionInstanceSegmenter/ThinSectionInstanceSegmenterCLI/ThinSectionInstanceSegmenterCLI.py b/src/modules/ThinSectionInstanceSegmenter/ThinSectionInstanceSegmenterCLI/ThinSectionInstanceSegmenterCLI.py
index a81eaa0..514ff87 100644
--- a/src/modules/ThinSectionInstanceSegmenter/ThinSectionInstanceSegmenterCLI/ThinSectionInstanceSegmenterCLI.py
+++ b/src/modules/ThinSectionInstanceSegmenter/ThinSectionInstanceSegmenterCLI/ThinSectionInstanceSegmenterCLI.py
@@ -471,7 +471,7 @@ def calculate_statistics(df, instances, class_ids, classes, scale, spacing):
callback=lambda i, total: None,
)
if len(df_stats) > 0:
- df_stats.set_axis(operator.ATTRIBUTES, axis=1, inplace=True)
+ df_stats = df_stats.set_axis(operator.ATTRIBUTES, axis=1)
for col in df_stats.select_dtypes(include=["float"]).columns:
df_stats[col] = df_stats[col].round(5)
@@ -552,8 +552,7 @@ def runcli(args):
scale_percent = params["resize_ratio"]
chunk_size = params.get("chunk_size")
chunk_overlap = params.get("chunk_overlap")
- chunk_overlap = chunk_overlap / 100.0 if chunk_overlap else None
-
+ chunk_overlap = chunk_overlap / 100.0 if chunk_overlap is not None else 0
image = channels[0]
model_path = Path(args.input_model)
diff --git a/src/modules/ThinSectionLoader/Resources/Icons/ThinSectionLoader.png b/src/modules/ThinSectionLoader/Resources/Icons/ThinSectionLoader.png
deleted file mode 100644
index b89476c..0000000
Binary files a/src/modules/ThinSectionLoader/Resources/Icons/ThinSectionLoader.png and /dev/null differ
diff --git a/src/modules/ThinSectionLoader/Resources/Icons/ThinSectionLoader.svg b/src/modules/ThinSectionLoader/Resources/Icons/ThinSectionLoader.svg
new file mode 100644
index 0000000..14b2e8c
--- /dev/null
+++ b/src/modules/ThinSectionLoader/Resources/Icons/ThinSectionLoader.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/modules/ThinSectionLoader/ThinSectionLoader.py b/src/modules/ThinSectionLoader/ThinSectionLoader.py
index c176173..552b6ec 100644
--- a/src/modules/ThinSectionLoader/ThinSectionLoader.py
+++ b/src/modules/ThinSectionLoader/ThinSectionLoader.py
@@ -1,30 +1,27 @@
+import logging
import os
import re
-from collections import namedtuple
+from dataclasses import dataclass
from pathlib import Path
import ctk
import cv2
-import logging
import numpy as np
import pytesseract
import qt
import slicer
-from Customizer import Customizer
-from dataclasses import dataclass
+from Libs.scale_detect_rect import detect_scale, image_corners
from ltrace.image.io import volume_from_image
-from ltrace.slicer.widget.pixel_size_editor import PixelSizeEditor
+from ltrace.slicer import loader, ui
from ltrace.slicer.helpers import getTesseractCmd, save_path, isImageFile, tryGetNode
+from ltrace.slicer.node_attributes import LosslessAttribute
+from ltrace.slicer.node_observer import NodeObserver
+from ltrace.slicer.widget.pixel_size_editor import PixelSizeEditor
from ltrace.slicer.widget.status_panel import StatusPanel
from ltrace.slicer_utils import *
-from ltrace.slicer import loader
-from ltrace.slicer.node_observer import NodeObserver
-from ltrace.utils.Markup import MarkupLine
+from ltrace.slicer_utils import getResourcePath
from ltrace.units import global_unit_registry as ureg, SLICER_LENGTH_UNIT # ureg comes from pint library
-from ltrace.slicer.node_attributes import LosslessAttribute
-
-from Libs.scale_detect_rect import detect_scale, image_corners
# Checks if closed source code is available
try:
@@ -32,7 +29,6 @@
except ImportError:
ThinSectionLoaderTest = None
-os.environ["TESSDATA_PREFIX"] = f"{slicer.app.slicerHome}/bin/Tesseract-OCR/tessdata/"
pytesseract.pytesseract.tesseract_cmd = getTesseractCmd()
from PIL import Image
@@ -53,10 +49,10 @@ class ThinSectionLoader(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
self.parent.title = "Thin Section Loader"
- self.parent.categories = ["Thin Section"]
+ self.parent.categories = ["Thin Section", "Loader"]
self.parent.dependencies = []
self.parent.contributors = ["LTrace Geophysical Solutions"]
- self.parent.helpText = ThinSectionLoader.help()
+ self.parent.helpText = f"file:///{(getResourcePath('manual') / 'Modules/Thin_section/Loader.html').as_posix()}"
@classmethod
def readme_path(cls):
@@ -122,6 +118,7 @@ def setup(self):
self.inputFileSelector.currentPathChanged.connect(self.__on_file_selected)
self.inputFileSelector.settingKey = "ThinSectionLoader/InputFile"
self.inputFileSelector.objectName = "Input File Selector"
+
inputFormLayout = qt.QFormLayout(inputCollapsibleButton)
inputFormLayout.setLabelAlignment(qt.Qt.AlignRight)
inputFormLayout.addRow(self.__getFormattedLabel("Input file:"), self.inputFileSelector)
@@ -149,12 +146,18 @@ def setup(self):
self.advancedFormLayout.addRow(self.__getFormattedLabel(""), self.losslessCheckBox)
self.loadFormLayout.addRow(self.advancedCollapsibleButton)
- # Load button
- self.loadButton = qt.QPushButton("Load thin section")
- self.loadButton.objectName = "Load Thin Section Button"
- self.loadButton.setFixedHeight(40)
- self.loadButton.clicked.connect(self.onLoadButtonClicked)
-
+ self.applyCancelButtons = ui.ApplyCancelButtons(
+ onApplyClick=self.onLoadButtonClicked,
+ onCancelClick=self.onCancelButtonClicked,
+ applyTooltip="Load thin section",
+ cancelTooltip="Cancel",
+ applyText="Load thin section",
+ cancelText="Cancel",
+ enabled=True,
+ applyObjectName="Load Thin Section Button",
+ cancelObjectName=None,
+ )
+ self.loadFormLayout.addWidget(self.applyCancelButtons)
self.pixelSizeEditor = PixelSizeEditor()
self.layout.addWidget(self.pixelSizeEditor)
self.pixelSizeEditor.scaleSizeInputChanged.connect(
@@ -163,7 +166,6 @@ def setup(self):
self.pixelSizeEditor.imageSpacingSet.connect(
lambda: self.status_panel.set_instruction("Thin section updated successfully")
)
- self.loadFormLayout.addRow(self.loadButton)
# Parameters section
self.parametersCollapsibleButton = ctk.ctkCollapsibleButton()
@@ -189,7 +191,14 @@ def setup(self):
self.__update_scalesize_parameters_visibility()
self.__on_file_selected()
+ def onCancelButtonClicked(self):
+ self.logic.onCancel()
+ self.cancel = True
+
def onLoadButtonClicked(self):
+ self.applyCancelButtons.applyBtn.setEnabled(False)
+ self.applyCancelButtons.cancelBtn.setEnabled(True)
+ self.cancel = False
if not self.inputFileSelector.currentPath:
self.status_panel.set_instruction("Select an input file first", True)
return
@@ -210,6 +219,9 @@ def onLoadButtonClicked(self):
)
self.updateStatus(f"Loading {Path(path).name}...")
detection_data = self.logic.load(loadParameters)
+ if self.cancel:
+ slicer.mrmlScene.RemoveNode(self.logic.getCurrentNode())
+ return
self.logic.loaded = True
self.pixelSizeEditor.currentNode = self.logic.getCurrentNode()
failed_detection = "scale_size_mm" not in detection_data
@@ -224,6 +236,11 @@ def onLoadButtonClicked(self):
slicer.util.infoDisplay(str(e))
return
finally:
+ if self.cancel:
+ self.updateStatus("Cancelled")
+ self.applyCancelButtons.applyBtn.setEnabled(True)
+ self.applyCancelButtons.cancelBtn.setEnabled(False)
+ pass
self.updateStatus("")
if failed_detection:
slicer.util.warningDisplay("Failed to detect scale in the selected image")
@@ -234,12 +251,15 @@ def onLoadButtonClicked(self):
else:
self.status_panel.set_instruction("Manually define scale in px and mm")
self.__update_scalesize_parameters_visibility()
+ self.applyCancelButtons.applyBtn.setEnabled(True)
+ self.applyCancelButtons.cancelBtn.setEnabled(False)
def updateStatus(self, message):
self.currentStatusLabel.text = message
slicer.app.processEvents()
def __on_file_selected(self):
+ self.applyCancelButtons.setEnabled(True)
self.logic.loaded = False
self.logic.reset()
self.pixelSizeEditor.currentNode = self.logic.getCurrentNode()
@@ -288,12 +308,16 @@ def onNodeRemoved(self):
self.loaded = False
self.reset()
+ def onCancel(self):
+ self.cancel = True
+
def load(self, p, baseName=None):
path = Path(p.path)
baseName = baseName or path.parent.name
return self.loadImage(path, p, baseName)
def loadImage(self, file, p, baseName):
+ self.cancel = False
node = volume_from_image(str(file))
self.nodeObserver = NodeObserver(node=node, parent=None)
self.nodeObserver.removedSignal.connect(self.onNodeRemoved)
@@ -322,6 +346,12 @@ def loadImage(self, file, p, baseName):
lossless = file.suffix.lower() not in [".jpg", ".jpeg"]
else:
lossless = p.lossless
+ if self.cancel:
+ nodeId = self.getCurrentNode()
+ slicer.mrmlScene.RemoveNode(nodeId)
+ self.onNodeRemoved()
+ slicer.util.resetSliceViews()
+ return
losslessAttributeValue = LosslessAttribute.TRUE.value if lossless is True else LosslessAttribute.FALSE.value
node.SetAttribute(LosslessAttribute.name(), losslessAttributeValue)
image_info["node"] = node
@@ -329,6 +359,7 @@ def loadImage(self, file, p, baseName):
loader.configureInitialNodeMetadata(self.ROOT_DATASET_DIRECTORY_NAME, baseName, node)
slicer.util.resetSliceViews()
+
return image_info
def parse_tesseract_result(self, results, tolerance=-0.1):
@@ -461,7 +492,7 @@ def extract_scale(self, image):
gray_view = cv2.cvtColor(gray_view, cv2.COLOR_BGR2GRAY)
if not using_rect_detection:
left, top, right, bottom = parsed_scale["extended_bbox"]
- gray_view = gray_view[top : min(w, bottom), left : min(h, right)]
+ gray_view = gray_view[top : min(w, bottom), left : min(h, right)].astype("uint8")
edge_view = cv2.Canny(gray_view, 100, 200)
edge_view = cv2.dilate(edge_view, np.ones((3, 3)))
diff --git a/src/modules/ThinSectionRegistration/Resources/Icons/ThinSectionRegistration.png b/src/modules/ThinSectionRegistration/Resources/Icons/ThinSectionRegistration.png
deleted file mode 100644
index b89476c..0000000
Binary files a/src/modules/ThinSectionRegistration/Resources/Icons/ThinSectionRegistration.png and /dev/null differ
diff --git a/src/modules/ThinSectionRegistration/Resources/Icons/ThinSectionRegistration.svg b/src/modules/ThinSectionRegistration/Resources/Icons/ThinSectionRegistration.svg
new file mode 100644
index 0000000..3b1d6c3
--- /dev/null
+++ b/src/modules/ThinSectionRegistration/Resources/Icons/ThinSectionRegistration.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/modules/ThinSectionRegistration/ThinSectionRegistration.py b/src/modules/ThinSectionRegistration/ThinSectionRegistration.py
index 6697f4c..d666797 100644
--- a/src/modules/ThinSectionRegistration/ThinSectionRegistration.py
+++ b/src/modules/ThinSectionRegistration/ThinSectionRegistration.py
@@ -1,24 +1,25 @@
-import ctk
-import qt
-import slicer
-import vtk
-
import logging
-import numpy as np
import os
import re
-import RegistrationLib
-import ThinSectionRegistrationLib
import time
-
from collections import Counter
-from ltrace.utils.ProgressBarProc import ProgressBarProc
-from ltrace.slicer_utils import *
-from ltrace.transforms import getRoundedInteger
from pathlib import Path
-from skimage.transform import resize
from typing import Dict, List, Union
+import RegistrationLib
+import numpy as np
+import qt
+import slicer
+import vtk
+from skimage.transform import resize
+
+import ThinSectionRegistrationLib
+from ltrace.slicer import ui
+from ltrace.slicer_utils import *
+from ltrace.slicer_utils import getResourcePath
+from ltrace.transforms import getRoundedInteger
+from ltrace.utils.ProgressBarProc import ProgressBarProc
+
try:
from Test.ThinSectionRegistrationTest import ThinSectionRegistrationTest
except ImportError:
@@ -33,11 +34,13 @@ class ThinSectionRegistration(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
- self.parent.title = "Thin Section Registration"
- self.parent.categories = ["Thin Section"]
+ self.parent.title = "Registration"
+ self.parent.categories = ["Registration", "Thin Section"]
self.parent.dependencies = []
self.parent.contributors = ["LTrace Geophysical Solutions"]
- self.parent.helpText = ThinSectionRegistration.help()
+ self.parent.helpText = (
+ f"file:///{(getResourcePath('manual') / 'Modules/Thin_section/Registration.html').as_posix()}"
+ )
@classmethod
def readme_path(cls):
@@ -69,6 +72,7 @@ def setup(self):
self.selectVolumesButton = qt.QPushButton("Select the images to register")
self.selectVolumesButton.objectName = "Select Volumes Button"
self.selectVolumesButton.setFixedHeight(40)
+ self.selectVolumesButton.setProperty("class", "actionButtonBackground")
self.selectVolumesButton.connect("clicked(bool)", self.setupDialog)
self.layout.addWidget(self.selectVolumesButton)
@@ -117,22 +121,19 @@ def setup(self):
self.landmarksWidget = ThinSectionRegistrationLib.LandmarksWidget(self.logic)
parametersFormLayout.addRow(self.landmarksWidget.widget)
- self.finishRegistrationButton = qt.QPushButton("Finish registration")
- self.finishRegistrationButton.objectName = "Finish Registration Button"
- self.finishRegistrationButton.setFixedHeight(40)
- self.finishRegistrationButton.clicked.connect(self.finishRegistration)
-
- self.cancelRegistrationButton = qt.QPushButton("Cancel registration")
- self.cancelRegistrationButton.objectName = "Cancel Registration Button"
- self.cancelRegistrationButton.setFixedHeight(40)
- self.cancelRegistrationButton.clicked.connect(self.cancelRegistration)
-
- self.buttonsFrame = qt.QFrame()
- hBoxLayout = qt.QHBoxLayout(self.buttonsFrame)
- hBoxLayout.addWidget(self.finishRegistrationButton)
- hBoxLayout.addWidget(self.cancelRegistrationButton)
- parametersFormLayout.addRow(self.buttonsFrame)
+ self.applyCancelButtons = ui.ApplyCancelButtons(
+ onApplyClick=self.finishRegistration,
+ onCancelClick=self.cancelRegistration,
+ applyTooltip="Finish registration",
+ cancelTooltip="Cancel registration",
+ applyText="Finish registration",
+ cancelText="Cancel registration",
+ enabled=True,
+ applyObjectName="Finish Registration Button",
+ cancelObjectName="Cancel Registration Button",
+ )
+ parametersFormLayout.addRow(self.applyCancelButtons)
# Add vertical spacer
self.layout.addStretch(1)
@@ -256,7 +257,8 @@ def setupDialog(self) -> None:
if not self.volumeSelectDialog:
self.volumeSelectDialog = qt.QDialog(self.parent)
self.volumeSelectDialog.setModal(True)
- self.volumeSelectDialog.setFixedSize(550, 150)
+ self.volumeSelectDialog.setMinimumSize(400, 180)
+ self.volumeSelectDialog.setMaximumSize(800, 280)
self.volumeSelectDialog.objectName = "Thin Section Registration Volume Select"
self.volumeSelectDialog.setLayout(qt.QVBoxLayout())
@@ -296,18 +298,19 @@ def setupDialog(self) -> None:
self.volumeButtonFrame.setLayout(qt.QHBoxLayout())
self.volumeSelectDialog.layout().addWidget(self.volumeButtonFrame)
- self.volumeDialogApply = qt.QPushButton("Apply", self.volumeButtonFrame)
- self.volumeDialogApply.objectName = "Volume Dialog Apply"
- self.volumeDialogApply.setToolTip("Use currently selected images.")
- self.volumeButtonFrame.layout().addWidget(self.volumeDialogApply)
-
- self.volumeDialogCancel = qt.QPushButton("Cancel", self.volumeButtonFrame)
- self.volumeDialogCancel.objectName = "Volume Dialog Cancel"
- self.volumeDialogCancel.setToolTip("Cancel current operation.")
- self.volumeButtonFrame.layout().addWidget(self.volumeDialogCancel)
-
- self.volumeDialogApply.connect("clicked()", self.onVolumeDialogApply)
- self.volumeDialogCancel.connect("clicked()", self.volumeSelectDialog.hide)
+ self.volumeDialogApplyCancelButtons = ui.ApplyCancelButtons(
+ onApplyClick=self.onVolumeDialogApply,
+ onCancelClick=lambda: self.volumeSelectDialog.hide(),
+ applyTooltip="Use currently selected images.",
+ cancelTooltip="Cancel current operation.",
+ applyText="Apply",
+ cancelText="Cancel",
+ enabled=True,
+ applyObjectName="Volume Dialog Apply",
+ cancelObjectName="Cancel Registration Button",
+ parent=self.volumeButtonFrame,
+ )
+ self.volumeButtonFrame.layout().addWidget(self.volumeDialogApplyCancelButtons)
if self.volumeDialogSelectors["Fixed"].nodeCount() <= 1:
slicer.util.warningDisplay("You need at least two different images to register.")
diff --git a/src/modules/ThinSectionRegistration/ThinSectionRegistrationLib/Landmarks.py b/src/modules/ThinSectionRegistration/ThinSectionRegistrationLib/Landmarks.py
index 235f757..9e3c406 100644
--- a/src/modules/ThinSectionRegistration/ThinSectionRegistrationLib/Landmarks.py
+++ b/src/modules/ThinSectionRegistration/ThinSectionRegistrationLib/Landmarks.py
@@ -203,7 +203,9 @@ def renameLandmark(self):
landmarks = self.logic.landmarksForVolumes(self.volumeNodes)
if self.selectedLandmark in landmarks:
newName = qt.QInputDialog.getText(
- slicer.util.mainWindow(), "Rename Landmark", "New name for landmark '%s'?" % self.selectedLandmark
+ slicer.modules.AppContextInstance.mainWindow,
+ "Rename Landmark",
+ "New name for landmark '%s'?" % self.selectedLandmark,
)
if newName != "":
for fiducialNode, index in landmarks[self.selectedLandmark]:
@@ -229,7 +231,7 @@ def wrappedNodeAddedUpdate(self):
traceback.print_exc()
qt.QMessageBox.warning(
- slicer.util.mainWindow(),
+ slicer.modules.AppContextInstance.mainWindow,
"Node Added",
"Exception!\n\n" + str(e) + "\n\nSee Python Console for Stack Trace",
)
diff --git a/src/modules/UnwrapRegistration/Resources/Icons/UnwrapRegistration.png b/src/modules/UnwrapRegistration/Resources/Icons/UnwrapRegistration.png
deleted file mode 100644
index b89476c..0000000
Binary files a/src/modules/UnwrapRegistration/Resources/Icons/UnwrapRegistration.png and /dev/null differ
diff --git a/src/modules/UnwrapRegistration/Resources/Icons/UnwrapRegistration.svg b/src/modules/UnwrapRegistration/Resources/Icons/UnwrapRegistration.svg
new file mode 100644
index 0000000..1da8f03
--- /dev/null
+++ b/src/modules/UnwrapRegistration/Resources/Icons/UnwrapRegistration.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/modules/UnwrapRegistration/UnwrapRegistration.py b/src/modules/UnwrapRegistration/UnwrapRegistration.py
index 0c17b20..3045be5 100644
--- a/src/modules/UnwrapRegistration/UnwrapRegistration.py
+++ b/src/modules/UnwrapRegistration/UnwrapRegistration.py
@@ -8,12 +8,10 @@
import qt
import slicer
import vtk
-from Customizer import Customizer
-from Multicore import MulticoreLogic
from ltrace.slicer.helpers import triggerNodeModified
from ltrace.slicer.ui import hierarchyVolumeInput
-from ltrace.slicer_utils import LTracePlugin, LTracePluginWidget, LTracePluginLogic
+from ltrace.slicer_utils import LTracePlugin, LTracePluginWidget, LTracePluginLogic, getResourcePath
from ltrace.units import global_unit_registry as ureg, SLICER_LENGTH_UNIT
@@ -26,7 +24,7 @@ class UnwrapRegistration(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
self.parent.title = "Unwrap Registration"
- self.parent.categories = ["LTrace Tools"]
+ self.parent.categories = ["Tools", "ImageLog"]
self.parent.dependencies = []
self.parent.contributors = ["LTrace Geophysical Solutions"]
self.parent.helpText = UnwrapRegistration.help()
@@ -114,19 +112,19 @@ def setup(self):
parametersFormLayout.addRow(" ", None)
self.applyButton = qt.QPushButton("Apply")
- self.applyButton.setIcon(qt.QIcon(str(Customizer.APPLY_ICON_PATH)))
+ self.applyButton.setIcon(qt.QIcon(getResourcePath("Icons") / "Apply.png"))
self.applyButton.setToolTip("Apply the current changes. These changes can be undone, unless you click Save.")
self.applyButton.enabled = False
self.applyButton.clicked.connect(self.onApplyButtonClicked)
self.cancelButton = qt.QPushButton("Cancel")
- self.cancelButton.setIcon(qt.QIcon(str(Customizer.CANCEL_ICON_PATH)))
+ self.cancelButton.setIcon(qt.QIcon(getResourcePath("Icons") / "Cancel.png"))
self.cancelButton.setToolTip("Cancel the current changes.")
self.cancelButton.enabled = False
self.cancelButton.clicked.connect(self.onCancelButtonClicked)
self.undoButton = qt.QPushButton("Undo")
- self.undoButton.setIcon(qt.QIcon(str(Customizer.UNDO_ICON_PATH)))
+ self.undoButton.setIcon(qt.QIcon(getResourcePath("Icons") / "Undo.png"))
self.undoButton.setToolTip(
"Undo the applied changes. The earliest undo is where the volume was loaded or saved."
)
@@ -134,7 +132,7 @@ def setup(self):
self.undoButton.clicked.connect(self.onUndoButtonClicked)
self.redoButton = qt.QPushButton("Redo")
- self.redoButton.setIcon(qt.QIcon(str(Customizer.REDO_ICON_PATH)))
+ self.redoButton.setIcon(qt.QIcon(getResourcePath("Icons") / "Redo.png"))
self.redoButton.setToolTip("Redo the applied changes.")
self.redoButton.enabled = False
self.redoButton.clicked.connect(self.onRedoButtonClicked)
@@ -147,13 +145,13 @@ def setup(self):
formLayout.addRow(buttonsHBoxLayout)
self.saveButton = qt.QPushButton("Save")
- self.saveButton.setIcon(qt.QIcon(str(Customizer.SAVE_ICON_PATH)))
+ self.saveButton.setIcon(qt.QIcon(getResourcePath("Icons") / "Save.png"))
self.saveButton.setToolTip("Save the applied changes. This action cannot be undone.")
self.saveButton.enabled = False
self.saveButton.clicked.connect(self.onSaveButtonClicked)
self.resetButton = qt.QPushButton("Reset")
- self.resetButton.setIcon(qt.QIcon(str(Customizer.RESET_ICON_PATH)))
+ self.resetButton.setIcon(qt.QIcon(getResourcePath("Icons") / "Reset.png"))
self.resetButton.setToolTip("Reset the applied changes to the last saved state.")
self.resetButton.enabled = False
self.resetButton.clicked.connect(self.onResetButtonClicked)
@@ -338,6 +336,8 @@ def __init__(self):
LTracePluginLogic.__init__(self)
def save(self, imageNode, depthIncrement, orientationIncrement):
+ from Multicore import MulticoreLogic
+
imageNode.HardenTransform()
transformArray = self.getRegistrationTransformArray(depthIncrement, orientationIncrement)
diff --git a/src/modules/VariogramAnalysis/VariogramAnalysis.py b/src/modules/VariogramAnalysis/VariogramAnalysis.py
index d763c52..0924785 100644
--- a/src/modules/VariogramAnalysis/VariogramAnalysis.py
+++ b/src/modules/VariogramAnalysis/VariogramAnalysis.py
@@ -255,6 +255,7 @@ def setup(self):
outputGroup.text = "Output"
outputLayout = qt.QFormLayout(outputGroup)
self.exportDirectoryButton = ctk.ctkDirectoryButton()
+ self.exportDirectoryButton.setMaximumWidth(374)
self.exportDirectoryButton.caption = "Export directory"
self.exportDirectoryButton.directory = VariogramAnalysis.get_setting(
self.EXPORT_DIRECTORY, default=str(Path.home())
diff --git a/src/modules/VolumeCalculator/Resources/Icons/VolumeCalculator.png b/src/modules/VolumeCalculator/Resources/Icons/VolumeCalculator.png
deleted file mode 100644
index 5859a1d..0000000
Binary files a/src/modules/VolumeCalculator/Resources/Icons/VolumeCalculator.png and /dev/null differ
diff --git a/src/modules/VolumeCalculator/Resources/Icons/VolumeCalculator.svg b/src/modules/VolumeCalculator/Resources/Icons/VolumeCalculator.svg
new file mode 100644
index 0000000..29a8c62
--- /dev/null
+++ b/src/modules/VolumeCalculator/Resources/Icons/VolumeCalculator.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/modules/VolumeCalculator/VolumeCalculator.py b/src/modules/VolumeCalculator/VolumeCalculator.py
index 94f72e4..867422a 100644
--- a/src/modules/VolumeCalculator/VolumeCalculator.py
+++ b/src/modules/VolumeCalculator/VolumeCalculator.py
@@ -29,7 +29,7 @@ class VolumeCalculator(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
self.parent.title = "Volume Calculator"
- self.parent.categories = ["LTrace Tools"]
+ self.parent.categories = ["Tools"]
self.parent.dependencies = []
self.parent.contributors = ["LTrace Geophysical Solutions"]
self.parent.helpText = VolumeCalculator.help()
diff --git a/src/modules/WelcomeGeoSlicer/Resources/AutoSegmentIcon.png b/src/modules/WelcomeGeoSlicer/Resources/AutoSegmentIcon.png
deleted file mode 100644
index cff7140..0000000
Binary files a/src/modules/WelcomeGeoSlicer/Resources/AutoSegmentIcon.png and /dev/null differ
diff --git a/src/modules/WelcomeGeoSlicer/Resources/Charts.png b/src/modules/WelcomeGeoSlicer/Resources/Charts.png
deleted file mode 100644
index 7c3c02b..0000000
Binary files a/src/modules/WelcomeGeoSlicer/Resources/Charts.png and /dev/null differ
diff --git a/src/modules/WelcomeGeoSlicer/Resources/CustomizedData.png b/src/modules/WelcomeGeoSlicer/Resources/CustomizedData.png
deleted file mode 100644
index 3a31d56..0000000
Binary files a/src/modules/WelcomeGeoSlicer/Resources/CustomizedData.png and /dev/null differ
diff --git a/src/modules/WelcomeGeoSlicer/Resources/Data.png b/src/modules/WelcomeGeoSlicer/Resources/Data.png
deleted file mode 100644
index b4f7b83..0000000
Binary files a/src/modules/WelcomeGeoSlicer/Resources/Data.png and /dev/null differ
diff --git a/src/modules/WelcomeGeoSlicer/Resources/Export.png b/src/modules/WelcomeGeoSlicer/Resources/Export.png
deleted file mode 100644
index 3361d1e..0000000
Binary files a/src/modules/WelcomeGeoSlicer/Resources/Export.png and /dev/null differ
diff --git a/src/modules/WelcomeGeoSlicer/Resources/Icons/WelcomeGeoSlicer.png b/src/modules/WelcomeGeoSlicer/Resources/Icons/WelcomeGeoSlicer.png
deleted file mode 100644
index 4ea34c4..0000000
Binary files a/src/modules/WelcomeGeoSlicer/Resources/Icons/WelcomeGeoSlicer.png and /dev/null differ
diff --git a/src/modules/WelcomeGeoSlicer/Resources/ImageTools.png b/src/modules/WelcomeGeoSlicer/Resources/ImageTools.png
deleted file mode 100644
index 8714f49..0000000
Binary files a/src/modules/WelcomeGeoSlicer/Resources/ImageTools.png and /dev/null differ
diff --git a/src/modules/WelcomeGeoSlicer/Resources/MultipleImageAnalysis.png b/src/modules/WelcomeGeoSlicer/Resources/MultipleImageAnalysis.png
deleted file mode 100644
index 14054b2..0000000
Binary files a/src/modules/WelcomeGeoSlicer/Resources/MultipleImageAnalysis.png and /dev/null differ
diff --git a/src/modules/WelcomeGeoSlicer/Resources/OpenRockData.png b/src/modules/WelcomeGeoSlicer/Resources/OpenRockData.png
deleted file mode 100644
index 9ea54c3..0000000
Binary files a/src/modules/WelcomeGeoSlicer/Resources/OpenRockData.png and /dev/null differ
diff --git a/src/modules/WelcomeGeoSlicer/Resources/Segmentation.png b/src/modules/WelcomeGeoSlicer/Resources/Segmentation.png
deleted file mode 100644
index 56d11da..0000000
Binary files a/src/modules/WelcomeGeoSlicer/Resources/Segmentation.png and /dev/null differ
diff --git a/src/modules/WelcomeGeoSlicer/Resources/Shortcuts.png b/src/modules/WelcomeGeoSlicer/Resources/Shortcuts.png
deleted file mode 100644
index ac21505..0000000
Binary files a/src/modules/WelcomeGeoSlicer/Resources/Shortcuts.png and /dev/null differ
diff --git a/src/modules/WelcomeGeoSlicer/Resources/TableFilter.png b/src/modules/WelcomeGeoSlicer/Resources/TableFilter.png
deleted file mode 100644
index d282644..0000000
Binary files a/src/modules/WelcomeGeoSlicer/Resources/TableFilter.png and /dev/null differ
diff --git a/src/modules/WelcomeGeoSlicer/Resources/Tables.png b/src/modules/WelcomeGeoSlicer/Resources/Tables.png
deleted file mode 100644
index 6dcc840..0000000
Binary files a/src/modules/WelcomeGeoSlicer/Resources/Tables.png and /dev/null differ
diff --git a/src/modules/WelcomeGeoSlicer/Resources/VariogramAnalysis.png b/src/modules/WelcomeGeoSlicer/Resources/VariogramAnalysis.png
deleted file mode 100644
index 5cb93cf..0000000
Binary files a/src/modules/WelcomeGeoSlicer/Resources/VariogramAnalysis.png and /dev/null differ
diff --git a/src/modules/WelcomeGeoSlicer/Resources/VolumeCalculator.png b/src/modules/WelcomeGeoSlicer/Resources/VolumeCalculator.png
deleted file mode 100644
index 5859a1d..0000000
Binary files a/src/modules/WelcomeGeoSlicer/Resources/VolumeCalculator.png and /dev/null differ
diff --git a/src/modules/WelcomeGeoSlicer/Resources/VolumeRendering.png b/src/modules/WelcomeGeoSlicer/Resources/VolumeRendering.png
deleted file mode 100644
index 6fc7eea..0000000
Binary files a/src/modules/WelcomeGeoSlicer/Resources/VolumeRendering.png and /dev/null differ
diff --git a/src/modules/WelcomeGeoSlicer/Resources/Workflow.png b/src/modules/WelcomeGeoSlicer/Resources/Workflow.png
deleted file mode 100644
index 38b3c2e..0000000
Binary files a/src/modules/WelcomeGeoSlicer/Resources/Workflow.png and /dev/null differ
diff --git a/src/modules/WelcomeGeoSlicer/Resources/icon_geolog.svg b/src/modules/WelcomeGeoSlicer/Resources/icon_geolog.svg
deleted file mode 100644
index 546dbe0..0000000
--- a/src/modules/WelcomeGeoSlicer/Resources/icon_geolog.svg
+++ /dev/null
@@ -1,51 +0,0 @@
-
diff --git a/src/modules/WelcomeGeoSlicer/Resources/upscaling.png b/src/modules/WelcomeGeoSlicer/Resources/upscaling.png
deleted file mode 100644
index 8fec3b7..0000000
Binary files a/src/modules/WelcomeGeoSlicer/Resources/upscaling.png and /dev/null differ
diff --git a/src/modules/WelcomeGeoSlicer/WelcomeGeoSlicer.py b/src/modules/WelcomeGeoSlicer/WelcomeGeoSlicer.py
deleted file mode 100644
index 2814522..0000000
--- a/src/modules/WelcomeGeoSlicer/WelcomeGeoSlicer.py
+++ /dev/null
@@ -1,255 +0,0 @@
-import qt
-import slicer
-import logging
-import traceback
-import os
-
-from collections import OrderedDict
-from dataclasses import dataclass
-from ltrace.slicer_utils import *
-from ltrace.workflow import WorkflowWidget
-from pathlib import Path
-from typing import Callable, Tuple
-from typing import OrderedDict as OrderedDictType
-
-try:
- from Test.WelcomeGeoslicerTest import WelcomeGeoslicerTest
-except ImportError as error:
- WelcomeSlicerTest = None
-
-RESOURCES_PATH = Path(__file__).parent.absolute() / "Resources"
-
-
-class WelcomeGeoSlicer(LTracePlugin):
- SETTING_KEY = "WelcomeGeoSlicer"
-
- def __init__(self, parent):
- LTracePlugin.__init__(self, parent)
- self.parent.title = "Welcome GeoSlicer"
- self.parent.categories = [""]
- self.parent.dependencies = []
- self.parent.contributors = ["LTrace Geophysical Solutions"]
- self.parent.helpText = ""
-
-
-def getFeatures() -> OrderedDictType:
- return OrderedDict(
- {
- "Environments": [
- Feature("Image Log", "ImageLogEnv.png", "ImageLogEnv"),
- Feature("Core", "CoreEnv.png", "CoreEnv"),
- Feature("Micro CT", "MicroCTEnv.png", "MicroCTEnv"),
- Feature("Thin Section", "ThinSectionEnv.png", "ThinSectionEnv"),
- ],
- "Tools": [
- Feature("2D Color Scales", "CustomizedData.png", "CustomizedData"),
- Feature("3D Color Scales", "VolumeRendering.png", "VolumeRendering"),
- Feature("Volume Calculator", "VolumeCalculator.png", "VolumeCalculator"),
- Feature("Tables", "Tables.png", "CustomizedTables"),
- Feature("Segmentation Tools", "Segmentation.png", "SegmentationEnv"),
- Feature("Charts", "Charts.png", "Charts"),
- Feature("Table Filter", "TableFilter.png", "TableFilter"),
- Feature("NetCDF", "NetCDF.png", "NetCDF"),
- Feature("Workflow (Beta)", "Workflow.png", None, customActionWorkflow),
- Feature("BIAEP Browser", "BIAEPBrowser.png", "BIAEPBrowser"),
- (
- Feature("Digital Rocks Portal", "OpenRockData.png", "OpenRockData")
- if os.getenv("GEOSLICER_MODE") != "Remote"
- else None
- ), # Not working in cluster
- Feature(
- "Multiple\nImage Analysis",
- "MultipleImageAnalysis.png",
- "ThinSectionEnv",
- customActionMultipleImageAnalysis,
- ),
- Feature("Representative\nVolume", "VariogramAnalysis.png", "VariogramAnalysis"),
- Feature("Geolog integration", "icon_geolog.svg", "GeologEnv"),
- ],
- }
- )
-
-
-def customActionMultipleImageAnalysis():
- slicer.modules.ThinSectionEnvWidget.switchToMultipleImageAnalysis()
-
-
-def customActionWorkflow():
- workflowWidget = WorkflowWidget(slicer.util.mainWindow())
- workflowWidget.show()
-
-
-class FeatureToolButton(qt.QToolButton):
- def __init__(
- self,
- text: str,
- moduleName: str,
- image: str,
- parent: qt.QWidget = None,
- gridPosition=None,
- customAction: Callable[[None], None] = None,
- *args,
- **kwargs,
- ) -> None:
- super().__init__(*args, **kwargs)
- self.__updateStyleSheet()
- self.__moduleName = moduleName
- self.__customAction = customAction
- formatedName = text.replace("\n", " ")
- self.objectName = f"{formatedName} Tool Button"
-
- icon = qt.QIcon((RESOURCES_PATH / image).as_posix())
- self.setToolButtonStyle(qt.Qt.ToolButtonTextUnderIcon)
- self.setIcon(icon)
- self.setText(text)
- self.setIconSize(qt.QSize(60, 60))
- self.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding)
-
- if parent is not None:
- if gridPosition is None:
- parent.addWidget(self)
- else:
- parent.addWidget(self, *gridPosition)
-
- self.clicked.connect(self._action)
-
- def __updateStyleSheet(self) -> None:
- self.setStyleSheet(
- "QToolButton {\
- background-color: transparent;\
- border: none;\
- padding-top: 8px;\
- padding-bottom: 8px;\
- }\
- QToolButton:hover {\
- background-color: gray;\
- border-radius: 3px;\
- }\
- QToolButton:pressed {\
- background-color: #6B6B6B;\
- }"
- )
-
- def _action(self):
- try:
- if self.__moduleName is not None:
- slicer.util.selectModule(self.__moduleName)
-
- if self.__customAction is not None:
- self.__customAction()
- except Exception as error:
- logging.debug(f"Error in {self.__moduleName} shortcut: {error}. Traceback:\n{traceback.print_exc()}")
-
-
-@dataclass
-class Feature:
- text: str
- image: str
- moduleName: str
- customAction: Callable[[None], None] = None
-
- def createToolButton(self, parent: qt.QWidget = None, gridPosition: Tuple[int, int] = None) -> FeatureToolButton:
- if self.moduleName is not None and slicer.app.moduleManager().module(self.moduleName) is None:
- return None
-
- return FeatureToolButton(
- text=self.text,
- moduleName=self.moduleName,
- image=self.image,
- customAction=self.customAction,
- parent=parent,
- gridPosition=gridPosition,
- )
-
-
-class WelcomeGeoSlicerWidget(LTracePluginWidget):
- def __init__(self, parent):
- LTracePluginWidget.__init__(self, parent)
-
- def setup(self):
- LTracePluginWidget.setup(self)
-
- self.gridLayout = qt.QVBoxLayout()
- self.gridLayout.setContentsMargins(10, 10, 10, 0)
- self.gridLayout.setSpacing(10)
- frame = qt.QFrame()
- frame.setLayout(self.gridLayout)
- self.layout.addWidget(frame)
-
- generalInformationTextEdit = qt.QLabel()
- generalInformationTextEdit.setStyleSheet("QWidget {font-size: 12px;}")
- generalInformationTextEdit.setText(
- "Welcome to GeoSlicer, an integrated digital rocks platform developed by LTrace."
- )
- self.gridLayout.addWidget(generalInformationTextEdit)
-
- maxColumns = 4
- for groupName, features in getFeatures().items():
- groupBox, vBoxLayout = self.createFeaturesSectionFrame(groupName)
- currentColumn = 0
- currentRow = 0
- for feature in features:
- if not feature:
- continue
- gridPosition = (currentRow, currentColumn)
-
- button = feature.createToolButton(parent=vBoxLayout, gridPosition=gridPosition)
- if button is None:
- continue
-
- currentColumn += 1
- if currentColumn >= maxColumns:
- currentColumn = 0
- currentRow += 1
-
- self.gridLayout.addWidget(groupBox)
-
- self.gridLayout.addStretch(1)
- self.__mainWindow = slicer.util.mainWindow()
-
- def createFeaturesSectionFrame(self, title: str) -> Tuple[qt.QGroupBox, qt.QGridLayout]:
- hBoxLayout = qt.QGridLayout()
- groupBox = qt.QGroupBox(title)
- groupBox.setStyleSheet("QGroupBox {font-size: 20px;}")
- groupBox.setLayout(hBoxLayout)
- return groupBox, hBoxLayout
-
- def enter(self) -> None:
- super().enter()
- try:
- self.setDataProbeVisible(False)
- self.showLTraceLogo(False)
- except Exception as error:
- logging.debug("WelcomeGeoSlicer error: {}".format(error))
- return
-
- def exit(self):
- try:
- self.setDataProbeVisible(True)
- self.showLTraceLogo(False)
- except Exception as error:
- logging.debug("WelcomeGeoSlicer error: {}".format(error))
- return
-
- def showLTraceLogo(self, show):
- try:
- if self.__mainWindow is None:
- return
- dockWidgetContents = self.__mainWindow.findChild(qt.QObject, "dockWidgetContents")
- slicerLogoLabel = dockWidgetContents.findChild(qt.QLabel, "LogoLabel")
- if slicerLogoLabel:
- slicerLogoLabel.setVisible(show)
- except Exception as error:
- logging.debug("WelcomeGeoSlicer error: {}".format(error))
- return
-
- def setDataProbeVisible(self, visible):
- widget = slicer.util.findChild(self.__mainWindow, "DataProbeCollapsibleWidget")
- if not widget:
- return
- widget.setVisible(visible)
-
-
-class WelcomeGeoSlicerLogic(LTracePluginLogic):
- def __init__(self):
- LTracePluginLogic.__init__(self)
diff --git a/src/modules/WidgetTracking/WidgetTracking.py b/src/modules/WidgetTracking/WidgetTracking.py
index 41b42b5..d4f8c33 100644
--- a/src/modules/WidgetTracking/WidgetTracking.py
+++ b/src/modules/WidgetTracking/WidgetTracking.py
@@ -20,7 +20,7 @@ class WidgetTracking(LTracePlugin):
def __init__(self, parent):
LTracePlugin.__init__(self, parent)
self.parent.title = "Widget Tracking"
- self.parent.categories = ["LTrace Tools"]
+ self.parent.categories = ["Tools"]
self.parent.contributors = ["LTrace Geophysics Team"]
self.parent.helpText = WidgetTracking.help()
# self.parent.hidden = True
@@ -269,6 +269,22 @@ def tablesWidget(self) -> qt.QWidget:
return mainWidget
+ def slidersWidget(self) -> qt.QWidget:
+ mainWidget = qt.QWidget()
+ mainLayout = qt.QFormLayout()
+ mainWidget.setLayout(mainLayout)
+
+ rangeWidget = ctk.ctkRangeWidget()
+ rangeWidget.objectName = "CTK Range Widget"
+
+ windowLevelWidget = slicer.qMRMLWindowLevelWidget()
+ windowLevelWidget.objectName = "Window Level Widget"
+
+ mainLayout.addRow("Window Level:", windowLevelWidget)
+ mainLayout.addRow("Range:", rangeWidget)
+
+ return mainWidget
+
def setup(self):
LTracePluginWidget.setup(self)
@@ -289,9 +305,13 @@ def setup(self):
self.listsWidgetTab = self.listsWidget()
self.listsWidgetTab.objectName = "Lists Tab"
+
self.tablesWidgetTab = self.tablesWidget()
self.tablesWidgetTab.objectName = "Tables Tab"
+ self.slidersWidgetTab = self.slidersWidget()
+ self.slidersWidgetTab.objectName = "Sliders Tab"
+
self.mainTab = qt.QTabWidget()
self.mainTab.objectName = "Main Tab"
self.mainTab.addTab(self.buttonsWidgetTab, "Buttons")
@@ -301,6 +321,7 @@ def setup(self):
self.mainTab.addTab(self.comboBoxesWidgetTab, "ComboBoxes")
self.mainTab.addTab(self.listsWidgetTab, "Lists")
self.mainTab.addTab(self.tablesWidgetTab, "Tables")
+ self.mainTab.addTab(self.slidersWidgetTab, "Sliders")
# Update layout
self.layout.addWidget(self.mainTab)
diff --git a/src/modules/parse_globals.py b/src/modules/parse_globals.py
deleted file mode 100644
index 9f287b7..0000000
--- a/src/modules/parse_globals.py
+++ /dev/null
@@ -1,38 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: UTF-8 -*-
-
-import os
-import re
-from pathlib import Path
-import csv
-
-
-def parse(dirpath: Path):
- for item in os.listdir(dirpath):
- subdir = dirpath / item
- for file in os.listdir(subdir):
- filepath = str(subdir / file)
- with open(filepath, "r") as fp:
- content = fp.readlines()
-
- # detect missing area
- new_content = []
- for line in content:
- if re.search(r"\t0$", line):
- print(subdir)
-
- # replace squared symbol
- nline = re.sub("\(mm.*\)", "mm^2", line)
- new_content.append(nline)
-
- with open(filepath, "w") as fp:
- fp.writelines(new_content)
-
-
-if __name__ == "__main__":
- import sys
-
- if len(sys.argv) < 2:
- print("Provide a directory path")
-
- parse(Path(sys.argv[1]))
diff --git a/src/submodules/py_pore_flow b/src/submodules/py_pore_flow
new file mode 160000
index 0000000..af53a6f
--- /dev/null
+++ b/src/submodules/py_pore_flow
@@ -0,0 +1 @@
+Subproject commit af53a6f9b2f43368861acf951d2682d55e718bbe
diff --git a/src/submodules/pyflowsolver b/src/submodules/pyflowsolver
new file mode 160000
index 0000000..8eda74b
--- /dev/null
+++ b/src/submodules/pyflowsolver
@@ -0,0 +1 @@
+Subproject commit 8eda74b1493a500f380e08808ce0905daefd9325
diff --git a/tools/deploy/Customizer.py b/tools/deploy/Customizer.py
index e12780f..bb95919 100644
--- a/tools/deploy/Customizer.py
+++ b/tools/deploy/Customizer.py
@@ -17,7 +17,7 @@
from ltrace.slicer.lazy import lazy
from ltrace.slicer.helpers import themeIsDark, BlockSignals
from ltrace.slicer.modules_help_menu import ModulesHelpMenu
-from ltrace.slicer.project_manager import ProjectManager, BUGFIX_handle_copy_suffix_on_cloned_nodes
+from ltrace.slicer.project_manager import ProjectManager, handleCopySuffixOnClonedNodes
from ltrace.slicer.application_observables import ApplicationObservables
from ltrace.slicer.custom_main_window_event_filter import CustomizerEventFilter
from ltrace.slicer.custom_export_to_file import customize_export_to_file
@@ -165,7 +165,7 @@ def __init__(self, parent):
self.parent.helpText = ""
self.parent.acknowledgementText = ""
self.ltraceBugReportDialog = None
- self.__projectManager = ProjectManager(folder_icon_path=self.FOLDER_ICON_PATH)
+ self.__projectManager = ProjectManager(folderIconPath=self.FOLDER_ICON_PATH)
self.__layout_menu = None
self.popup_widget = None
@@ -193,6 +193,7 @@ def register_all_effects(self):
slicer.modules.MaskVolumeEffectInstance.registerEditorEffect()
slicer.modules.MultiThresholdEffectInstance.registerEditorEffect()
slicer.modules.SampleSegmentationEffectInstance.registerEditorEffect()
+ slicer.modules.SmartForegroundEffectInstance.registerEditorEffect()
def on_load_finished(self):
slicer.app.setRenderPaused(True)
@@ -337,7 +338,7 @@ def __on_node_added(self, caller, eventId, callData):
self.noInterpolate()
if callData and callData.IsA("vtkMRMLVolumeArchetypeStorageNode"):
- BUGFIX_handle_copy_suffix_on_cloned_nodes(callData)
+ handleCopySuffixOnClonedNodes(callData)
# disable interpolation of the volumes by default
def noInterpolate(self, *args):
@@ -579,7 +580,7 @@ def _saveSceneAs(self):
if not path:
return SaveStatus.CANCELLED # Nothing to do
- status = self.__projectManager.save_as(path)
+ status = self.__projectManager.saveAs(path)
if status == SaveStatus.FAILED:
slicer.util.errorDisplay(
@@ -650,6 +651,7 @@ def ltraceBugReport(self):
layout.addRow(" ", None)
self.ltraceBugReportDirectoryButton = ctk.ctkDirectoryButton()
+ self.ltraceBugReportDirectoryButton.setMaximumWidth(374)
self.ltraceBugReportDirectoryButton.caption = "Select a directory to save the report"
layout.addRow("Report destination directory:", None)
layout.addRow(self.ltraceBugReportDirectoryButton)
diff --git a/tools/deploy/GeoSlicerManual/.gitattributes b/tools/deploy/GeoSlicerManual/.gitattributes
new file mode 100644
index 0000000..2067259
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/.gitattributes
@@ -0,0 +1,2 @@
+*.webm filter=lfs diff=lfs merge=lfs -text
+*.mp4 filter=lfs diff=lfs merge=lfs -text
diff --git a/tools/deploy/GeoSlicerManual/build.py b/tools/deploy/GeoSlicerManual/build.py
index d05045b..62b3d96 100644
--- a/tools/deploy/GeoSlicerManual/build.py
+++ b/tools/deploy/GeoSlicerManual/build.py
@@ -7,4 +7,4 @@
import webbrowser
-webbrowser.open(Path(os.getcwd()) / "site" / "index.html")
+webbrowser.open(str(Path(os.getcwd()) / "site" / "index.html"))
diff --git a/tools/deploy/GeoSlicerManual/docs/Core CT/Usabilidade/usabilidade.md b/tools/deploy/GeoSlicerManual/docs/Core CT/Usabilidade/usabilidade.md
deleted file mode 100644
index 1ffc408..0000000
--- a/tools/deploy/GeoSlicerManual/docs/Core CT/Usabilidade/usabilidade.md
+++ /dev/null
@@ -1,214 +0,0 @@
-# Core Environment
-
-Ambiente para trabalhar com Tomografias Médicas de testemunhos (*Core CT*).
-
-Módulos:
-
-- Data
-- Multicore
-- Transforms (Multicore Transforms)
-- Crop (Crop Volume)
-- Segmentation
-
-## Data
-
-Módulo _GeoSlicer_ para visualizar os dados sendo trabalhados e suas propriedades.
-
-## Multicore
-
-Módulo _GeoSlicer_ para processar, orientar e extrair perfis tomográficos em lote.
-
-Demo (versão antiga): [https://youtu.be/JBkeHx6obTY](https://youtu.be/JBkeHx6obTY)
-
-Siga os passos abaixo para processar dados de core, orientar os cores e unwrap-os. Pode-se também exportar o resultados.
-
-Números decimais usam ponto como separador, não vírgula.
-
-### Process
-
-1. Use o botão _Add directories_ para adicionar diretórios contendo dados de core. Esses diretórios aparecerão na área _Data to be processed_ (ao processar, uma busca por dados de core nesses diretórios ocorrá em subdiretórios abaixo em no máximo um nível). Pode-se também remover entradas indesejadas selecionando-as e clicando em _Remove_.
-
-2. Escolha uma das maneiras de definir as produndidades do CoreCT: _Initial depth and core length_ ou _Core boundaries CSV file_. Para _Initial depth and core length_, insira a profundidade inicial (_Initial depth_) e o comprimento do core (_Core length_). Para _Core boundaries CSV file_, use o botão _..._ para adicionar o arquivo CSV contendo as boundaries do core (em metros). Um exemplo de arquivo CSV para dois cores seria:
-
- 5000.00, 5000.90
-
- 5000.90, 5001.80
-
- O CSV é um arquivo com duas colunas em que cada linha se refere a um core (em ordem de processamento, ver item 7), e as colunas se referem às profundidades limítrofes superior e inferior, sequencialmente.
-
-3. Para _Core diameter_, insira o diâmetro aproximado do core (em milímetros).
-
-4. Para _Core radial correction_, cheque para corrigir efeitos de atenuação CT transversais do core. Pode ser usado para corrigir efeitos como *beam hardening*. O objetivo é multiplicar um fator de correção a todas as fatias da imagem (fatias transversais, plano xy) para uniformizar os valores de atenuação em termos da coordenação radial. O fator de correção é calculado baseado na média de todas as fatias e depende apenas do raio relacionado ao centro das fatias.
-
-5. Para _Smooth core surface_, cheque para analisar a superfície do core; ficará mais suave (antialiased) com essa opção ativada.
-
-6. Para _Keep original volumes_, cheque para manter os dados carregados originais.
-
-7. Clique no botão _Process cores_ e aguarde a finalização. A ordem de processamento é a seguintes: a ordem dos diretórios adicionados na área _Data to be loaded_, e cada subdiretório, se aplicável, é processado em ordem alfabética. Pode-se inspecionar os cores carregados na aba _Data_ do _Core Environment_, dentro do diretório _Core_.
-
-#### __Detalhes sobre alinhamento e extração de cores__
-
-Nas fatias dos dados de core originais, pode-se frequentemente encontrar três círculos, que são (do maior para o menor): a superfície exterior do *liner*, a superfície interior do *liner* e a superfície do core. A Transformada Circular de Hough é usada para detectar esses círculos, e então o menor é escolhido como representação da superfície do core, com informação sobre seu raio e posição. Usa-se a posição do círculo a partir das fatias para calcular a melhor linha que passa pelo centro do core (eixo longitudinal), usando SVD (decomposição em valores singulares). Isso permite construir um vetor unitário que deve ser rotacionado ao eixo Z applicando a matriz de transformação (rotação). Uma vez calculada, essa matriz de rotação é então aplicada ao dado. Uma matriz de translação também é usada para mover o centro do core à origem do sistema de coordenadas, e mais tarde, à sua profundidade configurada no eixo Z.
-
-Após ser feito o alinhamento, todos os pontos fora de um cilindro envolvendo o core recebem o menor valor de intensidade do dado original. O raio desse cilindro é igual ao raio médio dos circulos do core detectados nas fatias
-
-### Orient
-
-1. Selecione o algoritmo de orientação em _Orientation algorithm_:
-
- - _Surface_ - a orientação é baseada no ângulo de corte da serra nas extremidades longitudinais do core. Esta opção funciona melhor se o ângulo de corte não for muito raso e as extremidades do core estiverem bem preservadas (ou seja, superfícies de corte limpas).
-
- - _Sinusoid_ - utiliza o perfil tomográfico para encontrar os padrões de sinusóides criados pelas camadas deposicionais/acamamento para orientação. Esta opção é boa se as camadas deposicionais estiverem bem pronunciadas no lote de cores.
-
- - _Surface + Sinusoid_ - se o algoritmo _Surface_ for capaz de encontrar um alinhamento, ele será usado, caso contrário, o algoritmo _Sinusoid_ será aplicado
-
-2. Clique no botão _Orient cores_ e aguarde a conclusão. Os cores serão rotacionados ao longo de seu eixo longitudinal, de acordo com o algoritmo de orientação selecionado. O primeiro core (o de menor profundidade) dita a orientação dos subseqüentes.
-
-### Unwrap (Perfil tomográfico)
-
-1. Para _Unwrap radial depth_, insira um valor de 0 ao raio do core, em milímetros. De fora do core até o centro, ao longo do eixo radial, é a profundidade na qual o unwrap será gerado. Valores pequenos gerarão unwraps próximos à superfície do core.
-
-2. Para _Well diameter_, insira o valor aproximado do diâmetro do poço (maior que o diâmetro do core) que será usado para projetar a image do core na parede do poço.
-
-3. Clique _Unwrap cores_ e aguarde a finalização. Unwraps de cores individuais e o unwrap global do poço serão gerados. As imagens de unwrap preservam as escalas originais do core em todos os eixos. Portanto, o tamanho/upscaling do pixel não depende do raio do core, i.e. o ângulo delta usado no processo iterativo de coleta dos voxels do unwrap são definidos como pixel_size/radius.
-
-Pode-se também realizar todos os passos acima clicando no botão _Apply all_.
-
-### Export
-
-Opcionalmente, pode-se exportar o sumário _Multicore_, as fatias centrais do core em cada eixo, e os unwraps do core clicando em seus respectivos botões na aba _Export_. Essa aba exporta um arquivo CSV de duas colunas, onde a primeira representa as profundidades e a segunda as intensidades de CT da imagem. Esse formado pode ser importado diretamente no software Techlog.
-
-Outra maneira de exportar as imagens é usando o módulo _Export_ do _GeoSlicer_. É sugerível usar a extensão .nc pois tanto a imagem quanto o espaçamento/tamanho do pixel e a profundidade inicial são exportados.
-
-## Multicore Transforms
-
-Módulo _GeoSlicer_ para alterar orientação e profundidade de cores manualmente, conforme descrito nos passos abaixo:
-
-1. Selecione cores a serem transformados na área _Available volumes_ e clique na seta verde para a direita para movê-los para a área _Selected volumes_, à direita. Pode-se também remover cores da área _Selected volumes_ usando a seta para a esquerda.
-
-2. Ajuste a translação e a rotação conforme necessário. Pode-se prever essas mudanças em _Slice views_.
-
-3. Clique em _Apply_ para salvar as mudanças. Clicar em _Cancel_ desfará a transformação.
-
-## Crop Volume
-
-Módulo _GeoSlicer_ para cortar um volume, conforme descrito nos passos abaixo:
-
-1. Selecione o volume em _Volume to be cropped_.
-
-2. Ajuste a posição e tamanho desejados da ROI nas slice views.
-
-3. Clique em _Crop_ e aguarde a finalização. O volume cortado aparecerá no mesmo diretório que o volume original.
-
-## Segmentation
-
-### Manual Segmentation
-
-1. Selecione/crie um nodo de segmentação de saída. O usuário pode criar uma nova segmentação ou editar uma segmentação previamente definida.
-2. Selecione a imagem de entrada a ser segmentada.
-3. Clique em _Add_ para adicionar segmentos.
-4. Selecione um segmento da lista a ser editado.
-5. Selecione uma ferramenta dentre as opção sob a lista de segmentos.
-6. Mais instruções sobre cada ferramenta de segmentação pode ser encontrada após selecionada clicando em _Show details._
-
-### Smart Segmentation
-
-Esse módulo provê métodos avançados para segmentação automática e supervisionada de vários tipos de imagem, tais como seção delgada e tomografia permitindo múltiplas imagens de entrada.
-
-#### __Inputs__
-
-1. __Annotations__: Selecione o nodo de segmentação que contém as anotações feitas na imagem para treinar o método de segmentação escolido.
-2. __Region (SOI)__: Selecione o nodo de segmentação em que o primeiro segmento delimita a região de interesse onde a segmentação será realizada.
-3. __Input image__: Selecione a imagem a ser segmentada. Vários tipos são aceitos, como imagens RGB e tomográficas.
-
-#### __Parameters__
-
-1. __Method__: Selecione o algoritmo para realizará a segmentação.
- 1. __Random Forest__: Florestas aleatórias são um método de aprendizado por agrupamento para classificação que opera construindo múltiplas árvores de decisão em tempo de treinamento. A __entrada__ é uma combinação de:
- * Entrada quantificada (RGB reduzido a um valor de 8 bits)
- * HSV puro
- * Múltiplos kernels gaussianos (tamanho e número de kernels são definidos pelo parâmetro __Radius__)
- * Se selecionado, kernels de __Variância__ são calculados (Ver __Use variation__).
- * If selected, kernels de __Sobel__ são calculados (Ver __Use contours__).
- 2. __Colored K-Means__: Um método de quantificação vetorial que busca particionar __n observações__ em __k clusters__ onde cada observação pertence ao cluster com a média (centro ou centroide) mais próxima. _Colored_ significa que o algoritmo funciona em espaço de cor tridimensional, especialmente HSV.
- * __Seed Initializer__: Algoritmo usado para escolher protótipos de clusters iniciais.
- * __Random__: Escolhe uma semente aleatória a partir das anotações, uma para cada segmento diferente.
- * __Smooth Centroid__: Para cada segmento, combina todas as amostras anotadas para geral uma semente mais geral.
-
-#### __Output__
-
-1. __Output prefix__: Digite um nome para ser usado como prefixo dos resultados.
-
-### Segment Inspector
-
-Para uma discussão mais detalhada sobre o algoritmo watershed, por favor cheque a seguinte [seção](../../Inspector/Watershed/estudos_de_porosidade.md) do manual do GeoSlicer.
-
-Este módulo provê múltiplos métodos para analisar uma imagem segmentada. Particularmente, algoritmos Watershed e Islands permite fragmentar a segmentação em diversas partições, ou diversos segmentos. Normalmente é aplicado a segmentação de espaço de poros para computar as métricas de cada elemento de poro. A entrada é um nodo de segmentação ou volume labelmap, uma região de interesse (definida por um nodo de segmentação) e a imagem/volume mestre. A saída é um labelmap onde cada partição (elemento de poro) está em uma cor diferente, uma tabela com parâmetros globais e uma tabela com as diferentes métricas para cada partição.
-
-#### __Inputs__
-
-1. __Selecionar__ single-shot (segmentação única) ou Batch (múltiplas amostras definidas por múltiplos projetos GeoSlicer).
-2. __Segmentation__: Selecionar um nodo de segmentação ou um labelmap para ser inspecionado.
-3. __Region__: Selecionar um nodo de segmentação para definir uma região de interesse (opcional).
-4. __Image__: Selecionar a imagem/volume mestre ao qual a segmentação é relacionada.
-
-#### __Parameters__
-
-1. __Method__: Selecionar um método a ser aplicado. Com algoritmo island, a segmentação é fragmentada de acordo com conexões diretas. Com watershed, a segmentação é fragmentada de acordo com a transformada de distância e os parâmetros da seção _Advanced_.
-2. __Size Filter__: Filtrar partições espúrias com eixo principal (feret_max) menor que o valor _Size Filter_.
-3. __Smooth factor__: Fator de suavização, que é o desvio padrão do filtro gaussiano aplicado à transformada de distância. Conforme aumenta, menos partições serão criadas. Use valores menores para resultados mais confiáveis.
-4. __Minimum distance__: Distância mínima separando picos em uma região de 2 * min_distance + 1 (i.e. picos são separados por no mínimo min_distance). Para encontrar o número máximo de picos, use min_distance = 0.
-5. __Orientation line__: Selecionar a linha para ser usada para cálculo de ângulo de orientação.
-
-#### __Output__
-
-Digite um nome para ser usado como prefixo dos resultados (labelmap onde cada partição (elemento de poro) está em uma cor diferente, uma tabela com parâmetros globais e uma tabela com as diferentes métricas para cada partição).
-
-#### Propriedades / Métricas:
-
-1. __Label__: Identificador rotular da partição.
-2. __mean__: Valor médio da imagem/volume de entrada dentro da região da partição (poro/grão).
-3. __median__: Valor mediano da imagem/volume de entrada dentro da região da partição (poro/grão).
-4. __stddev__: Desvio padrão da imagem/volume de entrada dentro da região da partição (poro/grão).
-5. __voxelCount__: Número total de pixels/voxels da região da partição (poro/grão).
-6. __area__: Área total da partição (poro/grão). Unidade: mm^2.
-7. __angle__: Ângulo em graus (entre 270 e 90) relacionado à linha de orientação (opcional; se nenhuma linha for selecionada, a orientação de referência é superior horizontal).
-8. __max_feret__: Eixo de caliper de Feret máximo. Unidade: mm.
-9. __min_feret__: Eixo de caliper de Feret mínimo. Unidade: mm.
-10. __mean_feret__: Média entre os calipers mínimo e máximo.
-11. __aspect_ratio__: min_feret / max_feret.
-12. __elongation__: max_feret / min_feret.
-13. __eccentricity__: sqrt(1 - min_feret / max_feret) relacionado à elipse equivalente (0 <= e < 1), igual a 0 para círculos.
-14. __ellipse_perimeter__: Perímetro da elipse equivalente (com eixo dado por caliper de Feret mínimo e máximo). Unidade: mm.
-15. __ellipse_area__: Área da elipse equivalente (com eixo dado por caliper de Feret mínimo e máximo). Unidade: mm^2.
-16. __ellipse_perimeter_over_ellipse_area__: Perímetro da elipse equivalente dividido pela área.
-17. __perimeter__: Perímetro real da partição (poro/grão). Unidade: mm.
-18. __perimeter_over_area__: Perímetro real dividido pela área da partição (poro/grão).
-19. __gamma__: "Redondeza" de uma área calculada como 'gamma = perimeter / (2 * sqrt(PI * area))'.
-20. __pore_size_class__: Símbolo/código/id da classe do poro.
-21. __pore_size_class_label__: Rótulo da classe do poro.
-
-#### Definição das classes de poro:
-
-* __Microporo__: classe 0, max_feret menor que 0.062 mm.
-* __Mesoporo mto pequeno__: classe 1, max_feret entre 0.062 e 0.125 mm.
-* __Mesoporo pequeno__: classe 2, max_feret entre 0.125 e 0.25 mm.
-* __Mesoporo médio__: classe 3, max_feret entre 0.25 e 0.5 mm.
-* __Mesoporo grande__: classe 4, max_feret entre 0.5 e 1 mm.
-* __Mesoporo muito grande__: classe 5, max_feret entre 1 e 4 mm.
-* __Megaporo pequeno__: classe 6, max_feret entre 4 e 32 mm.
-* __Megaporo grande__: classe 7, max_feret maior que 32mm.
-
-### Label Map Editor
-
-Realiza separação e aglutinação manual de objetos rotulados.
-
-#### __Atalhos para ferramentas__
-
-- m: Mesclar dois rótulos
-- a: Dividir rótulo automaticamente usando watershed
-- s: Dividir rótulo com uma linha reta
-- c: Cortar rótulo no ponteiro do mouse
-- z: Desfazer última edição
-- x: Refazer edição desfeita
-- Esc: Cancelar operação
diff --git a/tools/deploy/GeoSlicerManual/docs/Data_loading/img.png b/tools/deploy/GeoSlicerManual/docs/Data_loading/img.png
new file mode 100644
index 0000000..c5e2356
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Data_loading/img.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Data_loading/img_1.png b/tools/deploy/GeoSlicerManual/docs/Data_loading/img_1.png
new file mode 100644
index 0000000..aeb5d51
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Data_loading/img_1.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Data_loading/intro.md b/tools/deploy/GeoSlicerManual/docs/Data_loading/intro.md
new file mode 100644
index 0000000..a1c8fee
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Data_loading/intro.md
@@ -0,0 +1,32 @@
+# Introdução
+
+O GeoSlicer consegue abrir diversos tipos de arquivos, dentre eles, RAW, TIFF, PNG, JPG são os principais quando se
+trata de imagens 2D para lâminas delgadas. Para imagens 3D, o GeoSlicer consegue abrir arquivos em formato NetCDF, RAW e
+até mesmo
+diretórios com imagens 2D (PNG, JPG, TIFF) compondo um volume 3D.
+
+## Abrir Imagem
+
+Cada tipo de projeto carrega em seu ambiente um módulo específico para carregamento de imagens. Em todos os ambientes, o
+módulo que irá aparecer primeiro no lado esquerdo da tela é o **_Loader_** principal daquele ambiente. Alguns ambientes
+possuem mais
+de um módulo de carregamento, como o ambiente de **_Thin Section_** que possui o **_Loader_** e o **_QEMSCAN Loader_**.
+
+Os módulos de carregamento existentes são:
+
+- **_Thin Section_**:
+ - **_Loader_**: Carrega imagens de lâminas delgadas.
+ - **_QEMSCAN Loader_**: Carrega imagens de lâminas delgadas obtidas por QEMSCAN.
+- **_Volumes_**:
+ - **_Micro CT Loader_**: Carrega imagens de micro CT.
+- **_Well Log_**:
+ - **_Loader_**: Carrega imagens de perfis de poços
+ - **_Importer_**: Carrega perfis de poços em CSV, JPG, PNG e TIFF.
+- **_Core_**:
+ - **_Multicore_**: Carrega imagens de testemunhos de poços em lote.
+- **_Multiscale_**:
+ - **_Loader_**: Carrega imagens de perfis de poços
+ - **_Importer_**: Carrega perfis de poços em CSV, JPG, PNG e TIFF.
+ - **_Micro CT Loader_**: Carrega imagens de micro CT.
+ - **_Core Photograph Loader_**: Carrega a imagem central das fotografias das caixas centrais e construa um volume
+ com a imagem completa do núcleo.
\ No newline at end of file
diff --git a/tools/deploy/GeoSlicerManual/docs/Data_loading/load_bigimage.md b/tools/deploy/GeoSlicerManual/docs/Data_loading/load_bigimage.md
new file mode 100644
index 0000000..12e33fd
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Data_loading/load_bigimage.md
@@ -0,0 +1,3 @@
+O módulo **_Big Image_** é um kit de ferramentas para trabalhar com imagens grandes usando memória limitada. Visualize fatias, corte, reduza a
+resolução e converta o tipo sem carregar a imagem inteira na RAM. Atualmente, suporta o carregamento de imagens NetCDF
+de vários arquivos do sistema de arquivos local ou o carregamento de imagens NetCDF MicroCT do módulo BIAEP Browser.
\ No newline at end of file
diff --git a/tools/deploy/GeoSlicerManual/docs/Data_loading/load_corect.md b/tools/deploy/GeoSlicerManual/docs/Data_loading/load_corect.md
new file mode 100644
index 0000000..e69de29
diff --git a/tools/deploy/GeoSlicerManual/docs/Data_loading/load_microct.md b/tools/deploy/GeoSlicerManual/docs/Data_loading/load_microct.md
new file mode 100644
index 0000000..cf1a3fb
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Data_loading/load_microct.md
@@ -0,0 +1,77 @@
+O ambiente de micro CT possui apenas um **_Loader_** para carregar volumes 3D. Ele é capaz de reconhecer diversos tipos
+de dados, como:
+
+- **_RAW_**: Arquivos de imagem em formato RAW.
+- **_TIFF_**: Arquivos de imagem em formato TIFF.
+- **_PNG_**: Arquivos de imagem em formato PNG.
+- **_JPG_**: Arquivos de imagem em formato JPG.
+
+Com exceção do formato RAW, o **_Loader_** requer um diretório contendo as imagens 2D que compõem o
+volume. O **_Loader_** irá reconhecer as imagens e montar o volume automaticamente. Abaixo vamos detalhar esses dois
+modos
+de funcionamento.
+
+## Visualização de formatos TIFF, PNG e JPG
+
+1. Clique no botão **_Choose folder_** para selecionar o diretório contendo imagens de micro CT. Nessa opção o diretório
+ precisa conter imagens 2D para compor o volume 3D.
+2. Ao selecionar o diretório, o módulo informa quantas imagens ele encontrou, para que o usuário confirme que todas as
+ imagens foram detectadas corretamente. Em caso de faltar imagens, o usuário pode verificar se o nome de alguma delas
+ não está fora do
+ padrão, causando a falha na detecção.
+3. Automáticamente o módulo tenta detectar o tamanho do pixel das imagens através do nome do arquivo(ex. prefixo "_
+ 28000nm). Caso o valor detectado não esteja correto, o usuário pode alterar o valor manualmente.
+4. Clique no botão **_Load micro CTs_** para carregar as imagens. O volume 3D será montado e disponibilizado na **_View_
+ ** principal e acessível via **_Explorer_**.
+
+## Visualização de formato RAW
+
+1. Clique no botão **_Choose file_** para selecionar o arquivo RAW.
+2. Como o arquivo .RAW não possui informações sobre a imagem diretamente no formato, o módulo tenta inferir as
+ configurações do dado pelo nome do arquivo. Por exemplo, **AFLORAMENTO_III_LIMPA_B1_BIN_1000_1000_1000_02214nm.raw**
+ - Tamanho do pixel: 2214nm
+ - Tamanho do volume: 1000x1000x1000
+ - Tipo de pixel: BIN (8 bit unsigned)
+3. O usuário pode alterar manualmente as configurações do volume, como tamanho do pixel, tamanho do volume e tipo do
+ dado, caso alguma informação não tenha sido detectada corretamente ou simplesmente não exista.
+4. Clique no botão **_Load_** para carregar o volume. O volume 3D será montado e disponibilizado na **_View_** principal
+ e acessível via **_Explorer_**.
+
+### Explorando os Parâmetros de Importação
+
+Existe a opção **Real-time update** que permite que o volume seja atualizado automaticamente conforme as configurações
+são alteradas. No entanto, recomendamos que o usuário não utilize essa opção para volumes muito grandes.
+
+1. Mude o campo _X dimension_ até colunas retas aparecerem na image (se as colunas estiverem ligeiramente inclinadas
+ então o valor está próximo de estar correto). Tente com diferentes valores de endianness e tipo de pixel se nenhum
+ valor em _X dimension_ parece fazer sentido.
+2. Mova _Header size_ até a primeira linha da imagem aparecer no topo.
+3. Altere o valor do campo _Z dimension_ para algumas dezenas de fatias para tornar
+ mais fácil ver quando o valor de _Y dimension_ está correto.
+4. Altere o valor de _Y dimension_ até a última linha da imagem aparecer na parte mais baixa.
+5. Altere o slider _Z dimension_ até todas as fatias da imagem estarem inclusas.
+
+## Recursos Avançados de Importação
+
+![img.png](img.png)
+
+### Auto-crop
+
+O usuário pode ativar a opção **_Auto-crop Rock Cylinder_** para cortar a imagem automaticamente. Essa solução é
+aplicada para casos onde o volume de estudo é um cilindro, normalmente envolto por outras camadas de outros materiais.
+Essa opção, tenta detectar o cilindro relevante e gerar uma região para recortar (crop). Antes de aplicar o recorte, o
+módulo apresenta a região detectada para que o usuário possa confirmar se a região está correta ou fazer ajustes para
+enquadrar a região de interesse.
+
+![img_1.png](img_1.png)
+
+### Normalização de Imagens
+
+Algumas imagens podem ser importadas usando um arquivo de normalização dos valores, o PCR. Para isso, o usuário deve
+selecionar a opção **_Normalize min/max with PCR file_** e selecionar o arquivo PCR correspondente. As opções de uso do
+arquivo são:
+
+- **_Normalize min/max (float)_**: Normaliza os valores da imagem com base no arquivo PCR.
+- **_Divide by alumminum median (float)_**: Divide os valores da imagem pela mediana do alumínio.
+- **_Alumminum contrast range (uint8)_**: Define o intervalo de contraste do alumínio.
+
diff --git a/tools/deploy/GeoSlicerManual/docs/Data_loading/load_netcdf.md b/tools/deploy/GeoSlicerManual/docs/Data_loading/load_netcdf.md
new file mode 100644
index 0000000..e69de29
diff --git a/tools/deploy/GeoSlicerManual/docs/Data_loading/load_thin_section.md b/tools/deploy/GeoSlicerManual/docs/Data_loading/load_thin_section.md
new file mode 100644
index 0000000..1baf13e
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Data_loading/load_thin_section.md
@@ -0,0 +1,58 @@
+
+
+
PP / PX / Outros
+
Utilize o Módulo Loader para carregar imagens de seção delgada, conforme descrito nos passos abaixo:
+
+
+
Use o botão Add directories para adicionar diretórios contendo dados de seção delgada. Esses diretórios apareceção na área Data to be loaded (uma busca por dados de seção delgada nesses diretórios ocorrerá em subdiretórios abaixo em no máximo um nível). Pode-se também remover entradas indesejadas selecionando-as e clicando em Remove.
+
+
+
Defina o tamanho do pixel (Pixel size) em milímetros.
+
+
+
Opcionalmente, ative Try to automatically detect pixel size. Se funcionar, o tamanho de pixel detectado substituirá o valor configurado em Pixel size.
+
+
+
Clique no botão Load thin sections e aguarde o carregamento ser finalizado. As imagens carregadas podem ser acessadas na aba Data, dentro do diretório Thin Section.
+
+
+
+
+
+
Video: Thin Section Loader
+
+
+
+
+
+
QEMSCAN Loader
+
Utilize o Módulo QEMSCAN Loader para carregar imagens QEMSCAN, conforme descrito nos passos abaixo:
+
+
+
Use o botão Add directories para adicionar diretórios contendo dados QEMSCAN. Esses diretórios aparecerão na área Data to be loaded (uma busca por dados QEMSCAN nesses diretórios ocorrerá em subdiretórios abaixo em no máximo um nível). Pode-se também remover entradas indesejadas selecionando-as e clicando em Remove.
+
+
+
Selecione a tabela de cores (Lookup color table). Pode-se selecionar a tabela padrão (Default mineral colors) ou adicionar uma nova tabela clicando no botão Add new e selecionando um arquivo CSV. Tem-se também a opção de fazer o carregador buscar por um arquivo CSV no mesmo diretório que o arquivo QEMSCAN sendo carregado. Também há a opção Fill missing values from "Default mineral colors" lookup table para preencher valores faltantes.
+
+
+
Defina o tamanho do pixel (Pixel size) em milímetros.
+
+
+
Clique no botão Load QEMSCANs e aguarde o carregamento ser finalizado. Os QEMSCANs carregados podem ser acessados na aba Data, dentro do diretório QEMSCAN.
+
+
+
+
+
+
Video: QEMSCAN Loader
+
+
+
+
+
diff --git a/tools/deploy/GeoSlicerManual/docs/Data_loading/load_well_log.md b/tools/deploy/GeoSlicerManual/docs/Data_loading/load_well_log.md
new file mode 100644
index 0000000..600c8b5
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Data_loading/load_well_log.md
@@ -0,0 +1,56 @@
+## Import
+
+Módulo _GeoSlicer_ para carregar dados de perfis em DLIS, LAS e CSV, conforme descrito nos passos abaixo:
+
+1. Selecione o arquivo de log de poço em _Well Log File_.
+
+2. Edite _Null values list_ e adicione ou remova valores da lista de possíveis valores nulos.
+
+3. Escolha os perfis desejados para serem carregados no GeoSlicer, de acordo com cada opção descrita abaixo:
+
+ - Dados 1-D devem ser carregados sem selecionar as opções “As Table“ e “LabelMap” e são exibidos como gráficos de
+ linha na Image Log View.
+ ![ImageLog Environment aberto. À direita, uma view (1) com dados que foram carregados da curva 'GR/Gamma API', selecionada à esquerda (2). Os dados foram importados de um arquivo DLIS (3).](../assets/images/imagelog/curve-1D.png)
+ - Dados 2-D podem ser carregadas como volumes, tabelas ou LabelMap no GeoSlicer, sendo cada uma das opções
+ detalhadas a seguir:
+
+ - Volume: Esta é a opção padrão do GeoSlicer, sem marcar 'As Table' e 'LabelMap', sendo o dado carregado como
+ volume e exibido como imagem na Image Log View.
+ ![À direita, uma view (1) com uma *borehole
+ image* carregada da curva 'AMPEQ', selecionada à esquerda (2). Os dados foram importados de um arquivo DLIS (3).](../assets/images/imagelog/curve-2D-BHI.png)
+ - Tabelas: Nesta opção, marcando a checkbox 'As Table', o dado é carregado como tabela e é exibido como vários
+ histogramas, como no caso do dado t2_dist da Figura 3 abaixo:
+ ![À direita, uma view com dados de NMR (1), que foram carregados da curva T2_DIST (2), selecionada à esquerda, marcada com a opção 'As Table' (3). Os dados foram importados de um arquivo DLIS (4).](../assets/images/imagelog/NMR_1_annotated.png)
+ - LabelMap: Nesta opção, marcando a checkbox 'LabelMap', assumindo um dado segmentado, ele é carregado como um
+ LabelMap e exibido como imagem segmentada.
+ ![Dados segmentados (LabelMap) à direita (1), que foram carregados marcando a coluna com 'LabelMap' (2). Os dados foram carregados de um arquivo CSV (3).](../assets/images/imagelog/curve-2D-labelmap.png)
+
+## Formatação dos arquivos a serem carregados
+
+### LAS
+
+Curvas em sequência com mnemônicos no formato `mnemonic[number]` serão agrupadas em dados 2-D. Por exemplo, os
+mnemônicos de uma imagem com 200 colunas:
+
+```
+AMP[0]
+…
+AMP[199]
+```
+
+Um mesmo arquivo pode conter dados 1-D e 2-D. No exemplo a seguir, AMP é uma imagem, enquanto LMF1 e LMF2 são dados 1-D:
+
+```
+AMP[0]
+…
+AMP[199]
+LMF1
+LMF2
+```
+
+### CSV
+
+Para o GeoSlicer interpretar dados CVS como 2-D, basta os mnemônicos serem de mesmo nome seguidos de um índice entre
+colchetes, como no 1º exemplo da seção LAS acima (AMP[0], …, AMP[199]).
+Diferentemente do caso LAS, o arquivo deve ter somente os dados 2-D.
+
diff --git a/tools/deploy/GeoSlicerManual/docs/Micro CT/Gradient Anisotropic Diffusion/diffusion_parameters-figura_1.png b/tools/deploy/GeoSlicerManual/docs/Filtering_and_Segmentation/Filters/Gradient Anisotropic Diffusion/diffusion_parameters-figura_1.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Micro CT/Gradient Anisotropic Diffusion/diffusion_parameters-figura_1.png
rename to tools/deploy/GeoSlicerManual/docs/Filtering_and_Segmentation/Filters/Gradient Anisotropic Diffusion/diffusion_parameters-figura_1.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Micro CT/Gradient Anisotropic Diffusion/diffusion_parameters-figura_2.png b/tools/deploy/GeoSlicerManual/docs/Filtering_and_Segmentation/Filters/Gradient Anisotropic Diffusion/diffusion_parameters-figura_2.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Micro CT/Gradient Anisotropic Diffusion/diffusion_parameters-figura_2.png
rename to tools/deploy/GeoSlicerManual/docs/Filtering_and_Segmentation/Filters/Gradient Anisotropic Diffusion/diffusion_parameters-figura_2.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Micro CT/Gradient Anisotropic Diffusion/diffusion_parameters-figura_3.png b/tools/deploy/GeoSlicerManual/docs/Filtering_and_Segmentation/Filters/Gradient Anisotropic Diffusion/diffusion_parameters-figura_3.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Micro CT/Gradient Anisotropic Diffusion/diffusion_parameters-figura_3.png
rename to tools/deploy/GeoSlicerManual/docs/Filtering_and_Segmentation/Filters/Gradient Anisotropic Diffusion/diffusion_parameters-figura_3.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Micro CT/Gradient Anisotropic Diffusion/diffusion_parameters-figura_4.png b/tools/deploy/GeoSlicerManual/docs/Filtering_and_Segmentation/Filters/Gradient Anisotropic Diffusion/diffusion_parameters-figura_4.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Micro CT/Gradient Anisotropic Diffusion/diffusion_parameters-figura_4.png
rename to tools/deploy/GeoSlicerManual/docs/Filtering_and_Segmentation/Filters/Gradient Anisotropic Diffusion/diffusion_parameters-figura_4.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Micro CT/Gradient Anisotropic Diffusion/gradient_anisotropic_diffusion.md b/tools/deploy/GeoSlicerManual/docs/Filtering_and_Segmentation/Filters/Gradient Anisotropic Diffusion/gradient_anisotropic_diffusion.md
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Micro CT/Gradient Anisotropic Diffusion/gradient_anisotropic_diffusion.md
rename to tools/deploy/GeoSlicerManual/docs/Filtering_and_Segmentation/Filters/Gradient Anisotropic Diffusion/gradient_anisotropic_diffusion.md
diff --git a/tools/deploy/GeoSlicerManual/docs/Micro CT/Polynomial Shading Correction/beam_hardening-figura_1.png b/tools/deploy/GeoSlicerManual/docs/Filtering_and_Segmentation/Filters/Polynomial Shading Correction/beam_hardening-figura_1.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Micro CT/Polynomial Shading Correction/beam_hardening-figura_1.png
rename to tools/deploy/GeoSlicerManual/docs/Filtering_and_Segmentation/Filters/Polynomial Shading Correction/beam_hardening-figura_1.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Micro CT/Polynomial Shading Correction/polynomial_shading_correction.md b/tools/deploy/GeoSlicerManual/docs/Filtering_and_Segmentation/Filters/Polynomial Shading Correction/polynomial_shading_correction.md
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Micro CT/Polynomial Shading Correction/polynomial_shading_correction.md
rename to tools/deploy/GeoSlicerManual/docs/Filtering_and_Segmentation/Filters/Polynomial Shading Correction/polynomial_shading_correction.md
diff --git a/tools/deploy/GeoSlicerManual/docs/Filtering_and_Segmentation/Filters/apply_filters.md b/tools/deploy/GeoSlicerManual/docs/Filtering_and_Segmentation/Filters/apply_filters.md
new file mode 100644
index 0000000..b1c1e70
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Filtering_and_Segmentation/Filters/apply_filters.md
@@ -0,0 +1,77 @@
+## Filtering Tools
+
+Módulo _GeoSlicer_ que permite filtragem de imagens, conforme descrito abaixo:
+
+1. Selecione uma ferramenta em _Filtering tool_.
+
+2. Preencha as entradas necessárias e aplique.
+
+### Gradient Anisotropic Diffusion
+
+Módulo _GeoSlicer_ para aplicar filtro de difusão anisotrópica de gradiente a imagens, conforme descrito nos passos abaixo:
+
+1. Selecione a imagem a ser filtrada em _Input image_.
+
+2. Defina o parâmetro de condutância em _Conductance_. A condutância controla a sensibilidade do termo de condutância. Como regra geral, quanto menor o valor, mais fortemente o filtro preservará as bordas. Um alto valor causará difusão (suavização) das bordas. Note que o número de iterações controla o quanto haverá de suavização dentro de regiões delimitadas pelas bordas.
+
+3. Defina o parâmetro de número de iterações em _Iterations_. Quanto mais iterações, maior suavização. Cada iteração leva a mesma quantidade de tempo. Se uma iteração leva 10 segundos, 10 iterações levam 100 segundos. Note que a condutância controla o quanto cada iteração suavizará as bordas.
+
+4. Defina o parâmetro de passo temporal em _Time step_. O passo temporal depende da dimensionalidade da imagem. Para imagens tridimensionais, o valor padrão de de 0.0625 fornece uma solução estável.
+
+5. Defina o nome de saída em _Output image name_.
+
+6. Clique no botão _Apply_ e aguarde a finalização. O volume de saída filtrado estará localizado no mesmo diretório que o volume de entrada.
+
+### Curvature Anisotropic Diffusion
+
+Módulo _GeoSlicer_ para plicar filtro de difusão anisotrópica de curvatura em imagens, conforme descrito nos passos abaixo:
+
+1. Selecione a imagem a ser filtrada em _Input image_.
+
+2. Defina o parâmetro de condutância em _Conductance_. A condutância controla a sensibilidade do termo de condutância. Como regra geral, quanto menor o valor, mais fortemente o filtro preservará as bordas. Um alto valor causará difusão (suavização) das bordas. Note que o número de iterações controla o quanto haverá de suavização dentro de regiões delimitadas pelas bordas.
+
+3. Defina o parâmetro de número de iterações em _Iterations_. Quanto mais iterações, maior suavização. Cada iteração leva a mesma quantidade de tempo. Se uma iteração leva 10 segundos, 10 iterações levam 100 segundos. Note que a condutância controla o quanto cada iteração suavizará as bordas.
+
+4. Defina o parâmetro de passo temporal em _Time step_. O passo temporal depende da dimensionalidade da imagem. Para imagens tridimensionais, o valor padrão de de 0.0625 fornece uma solução estável.
+
+5. Defina o nome de saída em _Output image name_.
+
+6. Clique no botão _Apply_ e aguarde a finalização. O volume de saída filtrado estará localizado no mesmo diretório que o volume de entrada.
+
+### Gaussian Blur Image Filter
+
+Módulo _GeoSlicer para aplicar filtro de desfoque gaussiano a imagens, conforme descrito nos passos abaixo:
+
+1. Selecione a imagem a ser filtrada em _Input image_.
+
+2. Defina o parâmetro _Sigma_, o valor em unidades físicas (e.g. mm) do kernel gaussiano.
+
+3. Defina o nome de saída em _Output image name_.
+
+4. Clique no botão _Apply_ e aguarde a finalização. O volume de saída filtrado estará localizado no mesmo diretório que o volume de entrada.
+
+### Median Image Filter
+
+Módulo _GeoSlicer_ para aplicar filtro mediano a imagens, conforme descrito nos passos abaixo:
+
+1. Selecione a imagem a ser filtrada em _Input image_.
+
+2. Defina o parâmetro _Neighborhood size_, o tamanho da vizinhança em cada dimensão.
+
+3. Defina o nome de saída em _Output image name_.
+
+4. Clique no botão _Apply_ e aguarde a finalização. O volume de saída filtrado estará localizado no mesmo diretório que o volume de entrada.
+
+### Shading Correction (temporariamente apenas para usuários Windows)
+
+Módulo _GeoSlicer_ para aplicar correção de sombreamento a imagens, conforme descrito nos passos abaixo:
+
+1. Selecione a imagem a ser corrigida em _Input image_.
+
+2. Selecione a máscara de entrada em _Input mask LabelMap_ para ajustar as bordas do dado corrigido final.
+
+3. Selecione a máscara de sombreamento em _Input shading mask LabelMap_, que proverá o intervalo de intensidades usado no cálculo de fundo.
+
+4. Defina o o raio da bola do algoritmo rolling ball em _Ball Radius_.
+
+5. Clique no botão _Apply_ e aguarde a finalização. O volume de saída filtrado estará localizado no mesmo diretório que o volume de entrada.
\ No newline at end of file
diff --git a/tools/deploy/GeoSlicerManual/docs/Filtering_and_Segmentation/Segmentation/auto_segmentation.md b/tools/deploy/GeoSlicerManual/docs/Filtering_and_Segmentation/Segmentation/auto_segmentation.md
new file mode 100644
index 0000000..9a6d055
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Filtering_and_Segmentation/Segmentation/auto_segmentation.md
@@ -0,0 +1,41 @@
+
+
+
Módulo Segmenter para segmentar aumomaticamente uma imagem, conforme descrito nos passos abaixo:
+
+
+
Entre na seção de segmentação Smart-seg do ambiente.
+
+
+
Selecione o Pre-trained models.
+
+
+
O modelo Carbonate Multiphase(Unet) foi utlizado como exemplo.
+
+
+
Check Model inputs and outputs.
+
+
+
Selecione um SOI (Segment of interest) criado previamente ao parâmetro Region SOI
+
+
+
Selecione uma imagem PP (Plane polarized light) ao parâmetro PP
+
+
+
Selecione uma imagem PX (Crossed polarized light) ao parâmetro PX
+
+
+
Um prefixo para o nome da segmentação resultante é gerado mas esse pode ser modificado na area de Output Prefix.
+
+
+
Clique em Apply e aguarde a finalização. Um nó de segmentação aparecerá e sua visualização poderá ser alterada no Explorer.
+
+
+
+
+
+
Video: Segmentação automática com modelo pré treinado
+
+
\ No newline at end of file
diff --git a/tools/deploy/GeoSlicerManual/docs/Filtering_and_Segmentation/Segmentation/manual_segmentation.md b/tools/deploy/GeoSlicerManual/docs/Filtering_and_Segmentation/Segmentation/manual_segmentation.md
new file mode 100644
index 0000000..3be8b83
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Filtering_and_Segmentation/Segmentation/manual_segmentation.md
@@ -0,0 +1,38 @@
+
+
+
Módulo Segment editor para segmentar uma imagem, conforme descrito nos passos abaixo:
+
+
+
Entre na seção de segmentação manual do ambiente.
+
+
+
Selecione create new segmentation em Output segmentation.
+
+
+
Selecione uma imagem de referência em Input image.
+
+
+
Click em Add para adicionar o numero de segmentos
+
+
+
A visibilidade (icone de olho), cor e o nome dos segmentos pode ser alterada pela tabela de segmentos.
+
+
+
Selecione o effeito desejado no menu lateral esquerdo à tabela de segmentos, no video, o color threshold e o paint sâo utilizados.
+
+
+
Parametrize o efeito desejado e escolha a regiao a ser segmentada.
+
+
+
Clique em Apply e aguarde a finalização. Um nó de segmentação aparecerá e sua visualização poderá ser alterada no Explorer.
+
+
+
+
+
+
Video: Segmentação Manual
+
+
\ No newline at end of file
diff --git a/tools/deploy/GeoSlicerManual/docs/Getting_started/User_interface/application.md b/tools/deploy/GeoSlicerManual/docs/Getting_started/User_interface/application.md
new file mode 100644
index 0000000..b998516
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Getting_started/User_interface/application.md
@@ -0,0 +1,106 @@
+# Interface do Usuário
+
+A interface do GeoSlicer foi recentemente atualizada para melhorar a experiência do usuário.
+A interface foi redesenhada para ser mais intuitiva e fácil de usar. A seguir, vamos abordar as principais mudanças
+e funcionalidades da nova interface.
+
+## Tela Inicial (Welcome Screen)
+
+O GeoSlicer apresenta uma tela inicial de Boas-Vindas sempre que for aberto. Nela, você pode escolher entre criar um
+novo projeto, abrir um projeto existente ou acessar a documentação.
+
+![Tela Inicial](onboard_screen.png)
+
+Nela são apresentadas as seguintes opções:
+
+- **Open Project**: Abra um projeto existente.
+- **Volumes**: Crie um novo projeto para processar imagens 3D, como tomografias.
+- **Thin Section**: Crie um novo projeto para processar imagens de lâminas delgadas.
+- **Well Log**: Crie um novo projeto para processar imagens de perfis de poços.
+- **Core**: Crie um novo projeto para processar imagens de testemunhos de poços.
+- **Multiscale**: Crie um novo projeto para processar imagens de diferentes escalas.
+- **NetCDF**: Crie um novo projeto para processar arquivos NetCDF.
+
+
+Ao selecionar um tipo de projeto, você será direcionado para a tela principal do GeoSlicer e apenas os módulos
+relacionados a este projeto serão exibidos
+na barra de módulos. Caso o usuário precise acessar módulos de outros projetos, basta clicar no ícone de (TODO ICONE) no
+canto inferior direito da tela e o menu inicial irá abrir novamente.
+Outra maneira é acessar o menu de projetos na barra superior, que indica o projeto atual e permite a troca para outros
+tipos de projeto.
+
+## Tela Principal
+
+A tela principal do GeoSlicer é onde você irá interagir com as imagens e realizar as análises. Ela é dividida em três
+partes principais:
+
+1. **Barra de Módulos**: No lado esquerdo da tela, contém os módulos disponíveis para o projeto de acordo com a opção
+ escolhida na tela inicial.
+2. **Barra de Ferramentas**: No lado direito da *view* central, contém as ferramentas de interação com a imagem.
+3. ***View* Central**: Onde a imagem é exibida e as análises são realizadas. O *layout* varia de acordo com o tipo de
+ dado ou objetivo de visualização.
+4. **Barra de Menu**: No topo da tela, contém as opções de menu da aplicação.
+5. **Barra de Ferramentas de Visualização**: No topo da *view* central, contém as ferramentas de controle da
+ visualização da imagem (por exemplo, centralizar, escala, relacionar eixos e camadas)
+6. **Módulo Atual**: No lado esquerdo da *view* central, apresenta o módulo atual em execução e suas opções.
+7. **Barra de Sistema**: No lado direito da tela, contém ferramentas de interação com a aplicação, como: console
+ python, gerenciador de contas, reportar bugs e outros.
+8. **Barra de Status**: Na parte inferior da tela, contém informações sobre a aplicação (e.g uso de memória), processos
+ em execução e notificações.
+
+![Tela Principal](main_screen_labeled.png)
+
+## Principais Mudanças
+
+A seguir, vamos abordar essas alterações e como elas impactam a experiência do usuário.
+
+### Conceito e Usabilidade
+
+A maioria das alterações na interface seguem o principio de manter a ação próxima ao objeto de interesse ou área que
+será afetada. A ideia é manter elementos gráficos relacionados próximos, reduzindo o número de cliques e movimentos do
+mouse necessários para realizar uma tarefa.
+
+Outro conceito aplicado é de deixar o tipo do projeto conduzir o que está na interface, ou seja, apenas os módulos
+relacionados ao tipo de projeto selecionado serão exibidos, ajudando a manter a interface coerente e evitando que o
+usuário
+se perca em um mar de opções.
+
+O usuário poderá navegar entre os ambientes de projeto de forma rápida e intuitiva, através do menu de ambientes na
+barra superior.
+
+Conforme o usuário entra em um novo ambiente, os módulos desse ambiente são carregados e exibidos na barra de módulos.
+Caso o usuário retorne a um ambiente já aberto, os módulos serão apresentados instantaneamente, sem a necessidade de
+recarregá-los. Isso é especialmente útil para usuários que trabalham com diferentes tipos de dados. Para o caso especial
+de trabalhar com perfis de poços, testemunhos e micro CTs, o usuário pode utilizar o ambiente **_Multiscale_**, que dá suporte
+especial aos fluxos de trabalhoe envolvendo esses três tipos de dados.
+
+### Barra de Módulos
+
+Os módulos disponíveis agora estão organizados em uma barra lateral, facilitando a navegação e acesso. Alguns icones são
+submenus para mais opções de módulos. Como regra estabelecemos que haverá sempre apenas um nível de submenus, dessa
+maneira as opções permancem concisas e rapidamente acessíveis. Nessa barra, apenas os módulos relacionados ao tipo de
+projeto
+selecionado serão exibidos, caso o usuário deseje acessar módulos de outros projetos, basta clicar no ícone de (TODO
+ICONE) e
+o itens da barra serão atualizados para o módulos do tipo de projeto selecionado.
+
+### Barra de Ferramentas
+
+A barra de ferramentas foi reposicionada para o lado direito da tela, ficando próxima da *view* central, uma vez que
+essas ferramentas são utilizadas para interagir com a imagem. A ferramentas ali presente são as mesmas das versões
+anteriores
+e a maioria são ferramentas nativas do GeoSlicer, por isso os ícones foram mantidos.
+
+### Barra de Sistema
+
+Esta barra agrupa um tipo especial de elementos, que são ferramentas de interação com a aplicação, como: console python,
+gerenciador de contas, reportar bugs e outros. Essas funcionalidades tem em comum ativar novas telas, normalmente
+diálogos e não tem como objetivo principal a interação com a imagem.
+
+### Barra de Status
+
+A barra de status tem três funções principais: apresentar notificações (ex.: se houver algum subprocesso que precisa
+comunicar alguma mensagem),
+visualização do progresso de processsos em execução, assim como acesso rápido ao módulo que disparou o processo (através
+da botão ao lado da barra de progresso). Também
+é apresentada a quantidade de memória que o processo do GeoSlicer está consumindo naquele momento.
\ No newline at end of file
diff --git a/tools/deploy/GeoSlicerManual/docs/Getting_started/User_interface/layouts.md b/tools/deploy/GeoSlicerManual/docs/Getting_started/User_interface/layouts.md
new file mode 100644
index 0000000..bb2e69d
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Getting_started/User_interface/layouts.md
@@ -0,0 +1 @@
+# Layouts
\ No newline at end of file
diff --git a/tools/deploy/GeoSlicerManual/docs/Getting_started/User_interface/main_screen_labeled.png b/tools/deploy/GeoSlicerManual/docs/Getting_started/User_interface/main_screen_labeled.png
new file mode 100644
index 0000000..139e9b4
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Getting_started/User_interface/main_screen_labeled.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Getting_started/User_interface/modules.md b/tools/deploy/GeoSlicerManual/docs/Getting_started/User_interface/modules.md
new file mode 100644
index 0000000..38109bd
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Getting_started/User_interface/modules.md
@@ -0,0 +1,51 @@
+# Módulos
+
+O GeoSlicer é uma aplicação modular, ou seja, cada função específica adicionada ao _software_ é feita através de um novo
+módulo.
+Isso permite que o GeoSlicer seja facilmente expandido e personalizado para atender a diferentes necessidades de
+projeto. Inclusive
+o usuário pode desenvolver seus próprios módulos e integrá-los ao GeoSlicer.
+
+### Design
+
+Os módulos do GeoSlicer são desenvolvidos seguindo alguns padrões de design, que visam estabelecer uma interface coesa e
+intuitiva
+independente do módulo. A seguir, vamos abordar esses padrões e como eles impactam a experiência do usuário.
+
+### Entradas / Configuração / Saída
+
+A grande maioria dos módulos é estruturada em três partes: entradas, configuração e saída. As entradas são os dados que
+o módulo
+precisa para rodar a tarefa que ele implementa. A configuração são os parâmetros que o usuário pode ajustar para
+personalizar a execução.
+E a saída é o resultado da execução do módulo, normalmente sendo requisitado apenas um sufixo para o nome do nodo/dado
+resultante. A figura a seguir exemplifica essa divisão:
+
+![Divisão do módulo](images/module_io.png)
+
+### Flows
+
+Os fluxos de trabalho mais repetitivos e comuns são implementados na forma de fluxos (_flows_). Um fluxo é uma sequência
+específica
+de módulos pré-configurados, que ao serem executados passo-a-passo, implementam um fluxo de trabalho. O GeoSlicer já tem
+alguns
+fluxos implementados:
+
+- Lâminas Delgadas:
+ - **Fluxo de Segmentação**: Fluxos implementados para PP, PP/PX e QEMSCAN. Realizam o fluxo completo de análise das lâminas com segmentação, particionamento e quantificação das imagens.
+- Micro CT:
+ - **Fluxo de Modelagem de Permeabilidade**: Fluxo executa todas as etapas até a modelagem de permeabilidade.
+ - **Fluxo de Segmentação Microporosidade para Imagens Grandes**: Fluxo executa todas as etapas até a segmentação em imagens grandes.
+
+Abaixo um exemplo da interface do _Modelling Flow_:
+
+![Fluxo de Modelagem](images/modelling_flow.png)
+
+### Custom
+
+Alguns módulos são customizados para atender a necessidades específicas de um projeto. Nem sempre esses módulos vão seguir os padrões
+acima descritos, devido a alguma especificidade do problema ou característica da aplicação. Um exemplo é o módulo _Manual Segmentation_, que oferece
+uma gama de ferramentas para segmentação manual de imagens. Por ser extremamente interativo, esse módulo requer uma interface própria que permita que
+o usuário alterne entre as ferramentas facilmente.
+
+![Manual Segmentation](images/manual_segmentation.png)
\ No newline at end of file
diff --git a/tools/deploy/GeoSlicerManual/docs/Getting_started/User_interface/onboard_screen.png b/tools/deploy/GeoSlicerManual/docs/Getting_started/User_interface/onboard_screen.png
new file mode 100644
index 0000000..3f5b882
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Getting_started/User_interface/onboard_screen.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Getting_started/first_steps.md b/tools/deploy/GeoSlicerManual/docs/Getting_started/first_steps.md
new file mode 100644
index 0000000..482cc25
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Getting_started/first_steps.md
@@ -0,0 +1,54 @@
+# Primeiros Passos
+
+Como primeiros passos, vamos abordar um fluxo simples de segmentação que pode ser realizado tanto com imagens de lâminas
+delgadas quanto com imagens de micro CT.
+
+Para começar, abra o GeoSlicer e escolha um tipo de projeto na tela inicial. O ambiente escolhido precisa ser compatível
+com o tipo de imagem que você usará de exemplo. Escolha **_Volumes_** para imagens de micro CT e **_Thin Section_** para
+imagens de lâminas delgadas.
+
+## 1. Abrir Imagem
+
+O primeiro passo é abrir a imagem que você deseja segmentar. Ao selecionar uma ambiente no menu inicial, o módulo que
+irá aparecer, no lado esquerdo, é o **_Loader_** daquele ambiente.
+
+- Para micro CT veja essa etapa em [Abrir Imagem](open_image.md).
+- Para lâminas delgadas veja essa etapa em [Abrir Imagem](open_image.md).
+
+## 2. Segmentar Imagem
+
+Após abrir a imagem, o próximo passo é segmentá-la. A segmentação é o processo de dividir a imagem em regiões de
+interesse. A imagem a seguir mostra um exemplo de segmentação de uma lâmina em duas regiões, poro e não-poro. Essa
+segmentação pode ser feita no módulo **_Segmentation -> Manual Segmentation_**.
+
+- Para micro CT veja essa etapa em [Abrir Imagem](open_image.md).
+- Para lâminas delgadas veja essa etapa em [Abrir Imagem](open_image.md).
+
+Nessa etapa, você pode optar por criar uma segunda segmentação para representar a região de interesse, ou seja, a área
+que
+você de fato quer analisar. Normalmente esse passo é feito quando há alguma sujeira ou região que não interessa na
+imagem.
+
+## 3. Analisar Imagem
+
+Uma vez feita a segmentação, você pode quantificar as regiões segmentadas, fazer analises como distribuição de tamanho
+de poros.
+Para isso, vamos focar na região de interesse que você segmentou como Poro. Utilize o **_Segment Inspector_** para
+inspecionar a imagem.
+
+O **_Segment Inspector_** funciona de forma similar para ambos os tipos de imagem. Ele faz o particionamento da região
+de interesse
+de acordo com a configuração que o usuário escolher. No caso, como vamos analisar a região porosa, ele vai particionar a
+segmentaçao identificando as gargantas e separando os poros. Como resultado final, além da imagem particionada, você
+obtêm uma tabela com diversas estatísticas sobre os poros (sufixo '_Report').
+
+
+## 4. Exportar Resultados
+
+Por fim, você pode exportar os resultados da análise. Além de simplesmente salvar o projeto no formato do GeoSlicer,
+você pode exportar os resultados da análise em diversos formatos, como CSV, NetCDF e RAW. Assim você pode compartilhar
+esses resultados ou até mesmo carregar em outro sofware. Para isso, utilize o módulo **_Exporter_**. Teste todos os
+formatos para aprender como cada um se comporta.
+
+
+
diff --git a/tools/deploy/GeoSlicerManual/docs/Getting_started/installation_guide.md b/tools/deploy/GeoSlicerManual/docs/Getting_started/installation_guide.md
new file mode 100644
index 0000000..c0ec925
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Getting_started/installation_guide.md
@@ -0,0 +1,56 @@
+# Guia de Instalação
+
+A seguir, os passos para instalação do GeoSlicer. Observe atentamente os itens
+destacados, são dicas para contornar algumas situações comuns.
+
+## Pré-requisitos
+
+O GeoSlicer roda em qualquer computador Windows ou Linux lançado nos últimos 5
+anos. Computadores mais antigos podem funcionar (dependendo principalmente dos
+recursos gráficos). Os requisitos **mínimos** são:
+
+- Sistema Operacional: Windows 10 ou Ubuntu 20.04 LTS
+- RAM: 8 GB
+- Resolução de tela: 1024x768 (recomendamos 1280x1024 ou superior)
+- Placa de vídeo: 4 GB de RAM, suporte a OpenGL 3.2 (recomendamos pelo menos o dobro do tamanho do maior dado que será utilizado)
+- Armazenamento: > 15GB de espaço livre em disco. Recomendamos um SSD para melhor desempenho.
+
+!!! tip
+ Dê preferência para discos SSD e locais, evite discos de rede (NAS). Ter mais de 15GB livres para
+ armazenar o software e os dados utilizados no experimento.
+
+## Instalação
+
+#### 1. Preparação
+
+Escolha um disco para instalação, dê preferência para discos locais e que sejam SSD.
+
+!!! tip
+ (Opcional) Instale a ferramenta 7zip. O instalador do GeoSlicer detecta a presença dessa
+ e a utiliza para fazer a descompressão da instalação com mais eficiência. Como o
+ GeoSlicer é uma aplicação grande, a descompressão é um processo oneroso e que se for
+ realizado pela ferramenta nativa do Windows irá demorar mais.
+
+#### 2. Download
+
+Baixe o instalador do GeoSlicer. Se você tem acesso a um ambiente privado com a versão fechada do GeoSlicer,
+como a Petrobrás, você pode baixar via o [sharepoint](https://petrobrasbr.sharepoint.com.mcas.ms/teams/LTRACE/SitePages/Home.aspx) da LTrace ou diretamente no Teams, entrando em contato
+com algum membro da equipe da LTrace. Caso queira baixar a versão opensource, acesse o link [GeoSlicer Installer](https://objectstorage.sa-saopaulo-1.oraclecloud.com/p/KV_6G_jhvYnygJs-FLigs706yoMdiOaYsBnUMvoP3RjnJ2CJlsZMmobRXPyKoc1t/n/grrjnyzvhu1t/b/General_ltrace_files/o/GeoSlicer/builds/windows/GeoSlicer-2.4.10-public.exe).
+
+#### 3. Instalação
+
+Execute o instalador do GeoSlicer (GeoSlicer-*.exe) e ele irá pedir o local da instalação. Selecione
+um lugar no disco escolhido na etapa de preparação (Passo 1) e clique em Extrair.
+
+Em seguida a descompressão dos arquivos começa, e você poderá acompanhar o progresso da
+instalação. Caso você tenha instalado o 7zip, uma tela similar a essa irá aparecer para você. Do
+contrário, será a barra de progresso nativa do sistema operacional.
+
+#### 3. Execução
+
+Após finalizada a instalação, vá até a pasta que foi escolhida no passo anterior, e execute o
+GeoSlicer.exe. Essa primeira execução faz a configuração da aplicação.
+
+Na primeira execução, após concluir a configuração da aplicação o GeoSlicer irá reiniciar para
+finalizar a instalação. Nas versões abaixo da 2.5 essa etapa é automática, mas não se assuste,
+após esse reinicio a aplicação está pronta para ser usada.
\ No newline at end of file
diff --git a/tools/deploy/GeoSlicerManual/docs/Image Log/Usabilidade/usabilidade.md b/tools/deploy/GeoSlicerManual/docs/Image Log/Usabilidade/usabilidade.md
deleted file mode 100644
index c72af8a..0000000
--- a/tools/deploy/GeoSlicerManual/docs/Image Log/Usabilidade/usabilidade.md
+++ /dev/null
@@ -1,294 +0,0 @@
-# Image Log Environment
-
-Ambiente para trabalhar com Perfis de imagens de poços (ou *Image Logs*).
-
-Módulos:
-
-- **Data**: Explorer, Import, Export
-- **Processing**: Eccentricity, Spiral Filter, Quality Indicator
-- **Segmentation**: Manual, Instance, Instance Editor, Inspector
-- **Registration**: Unwrap Registration
-- **Modeling**: Permeability Modeling
-
-## Explorer
-
-Módulo _GeoSlicer_ para visualizar os dados sendo trabalhados e suas propriedades.
-
-## Import
-
-Módulo _GeoSlicer_ para carregar dados de perfis em DLIS, LAS e CSV, conforme descrito nos passos abaixo:
-
-1. Selecione o arquivo de log de poço em _Well Log File_.
-
-2. Edite _Null values list_ e adicione ou remova valores da lista de possíveis valores nulos.
-
-3. Escolha os perfis desejados para serem carregados no GeoSlicer, de acordo com cada opção descrita abaixo:
-
- \- Dados 1-D devem ser carregados sem selecionar as opções “As Table“ e “LabelMap” e são exibidos como gráficos de linha na Image Log View.
-
- | ![Figura 1](curve-1D.png) |
- |:-----------------------------------------------:|
- |
Figura 1: ImageLog Environment aberto. À direita, uma view (1) com dados que foram carregados da curva 'GR/Gamma API', selecionada à esquerda (2). Os dados foram importados de um arquivo DLIS (3).
|
-
- \- Dados 2-D podem ser carregadas como volumes, tabelas ou LabelMap no GeoSlicer, sendo cada uma das opções detalhadas a seguir:
-
- - Volume: Esta é a opção padrão do GeoSlicer, sem marcar 'As Table' e 'LabelMap', sendo o dado carregado como volume e exibido como imagem na Image Log View;
-
- | ![Figura 2](curve-2D-BHI.png) |
- |:-----------------------------------------------:|
- |
Figura 2: À direita, uma view (1) com uma *borehole image* carregada da curva 'AMPEQ', selecionada à esquerda (2). Os dados foram importados de um arquivo DLIS (3).
|
-
- - Tabelas: Nesta opção, marcando a checkbox 'As Table', o dado é carregado como tabela e é exibido como vários histogramas, como no caso do dado t2_dist da Figura 3 abaixo:
-
- | ![Figura 3](NMR_1_annotated.png) |
- |:-----------------------------------------------:|
- |
Figura 3: À direita, uma view com dados de NMR (1), que foram carregados da curva T2_DIST (2), selecionada à esquerda, marcada com a opção 'As Table' (3). Os dados foram importados de um arquivo DLIS (4).
|
-
- - LabelMap: Nesta opção, marcando a checkbox 'LabelMap', assumindo um dado segmentado, ele é carregado como um LabelMap e exibido como imagem segmentada.
-
- | ![Figura 4](curve-2D-labelmap.png) |
- |:-----------------------------------------------:|
- |
Figura 4: Dados segmentados (LabelMap) à direita (1), que foram carregados marcando a coluna com 'LabelMap' (2). Os dados foram carregados de um arquivo CSV (3).
|
-
-### Formatação dos arquivos a serem carregados
-#### LAS
-Curvas em sequência com mnemônicos no formato `mnemonic[number]` serão agrupadas em dados 2-D. Por exemplo, os mnemônicos de uma imagem com 200 colunas:
-
-AMP[0]
-…
-AMP[199]
-
-
-Um mesmo arquivo pode conter dados 1-D e 2-D. No exemplo a seguir, AMP é uma imagem, enquanto LMF1 e LMF2 são dados 1-D:
-
-AMP[0]
-…
-AMP[199]
-LMF1
-LMF2
-
-
-#### CSV
-Para o GeoSlicer interpretar dados CVS como 2-D, basta os mnemônicos serem de mesmo nome seguidos de um índice entre colchetes, como no 1º exemplo da seção LAS acima (AMP[0], …, AMP[199]).
-Diferentemente do caso LAS, o arquivo deve ter somente os dados 2-D.
-
-## Image Log Export
-
-Módulo _GeoSlicer_ para exportar dados track, conforme descrito nos passos abaixo:
-
-1. Selecione os dados a serem exportados.
-
-2. Selecione o formato desejado e a pasta de saída.
-
-## Eccentricity
-
-O módulo Eccentricity é baseado na patente entitulada Method to correct eccentricity in ultrasonic image profiles, Pub. No.: US2017/0082767, pela aplicante Petrobras e os inventores Menezes, C., Compan, A. L. M. and Surmas, R.
-
-É um método para corrigir a excentricidade de perfis de imagens ultrassônicas baseada nas medidas de tempo de trânsito. A correção é realizada ponto a ponto baseada no modelo de decaimento de amplitude exponencial e seu parâmetro "tau". Em geral, o valor tau que produz a melhor correção é obtido minimizando um dos momentos estatísticos desvio padrão, assimetria e curtose.
-
-1. __Amplitude__: selecione um image log de _Amplitude_ a ser corrigido.
-2. __Transit time__: selecione um image log de tempo de trânsito (_Transit time_) para ser usado na correção.
-3. __Tau__: digite o valor do tau a ser usado na correção.
-4. Aba __Tau Optimization__:
- - _No optimization_: A ferramenta apenas aplicará o tau de entrada ao processo de correção de imagem.
- - _Minimize STD_: A ferramenta encontrará o melhor valor tau que minimiza o desvio padrão da imagem corrigida.
- - _Minimize Skew_: A ferramenta encontrará o melhor tau que minimiza a assimetria absoluta da imagem corrigida.
- - _Minimize Kurtosis_: A ferramenta encontrará o melhor valor tau que minimiza a curtose da imagem corrigida.
-5. Aba __Advanced Settings__:
- - _Missing value_: O valor/rótulo nulo no dado será ignorado durante o processo de otimização.
- - _Minimum amplitude_: O valor de amplitude mínima da imagem de referência a ser considerado na otimização. Pixels/pontos abaixo da "amplitude mínima" serão ignorados na otimização.
- - _Maximum amplitude_: O valor de amplitude máxima da imagem de referência a ser considerado na otimização. Pixels/pontos acima da "amplitude máxima" serão ignorados na otimização.
- - _Minimum transit time_: O valor de tempo de trânsito mínimo da imagem de referência a ser considerado na otimização. Pixels/pontos abaixo do "tempo de trânsito mínimo" serão ignorados na otimização.
- - _Maximum transit time_: O valor de tempo de trânsito máximo da imagem de referência a ser considerado na otimização. Pixels/pontos acima do "tempo de trânsito máximo" serão ignorados na otimização.
-
-## Spiral filter
-
-Módulo _GeoSlicer_ para remover o efeito de espiralamento e excentricidade de dados image log.
-
-O processo de filtragem é computado baseado em um filtro de rejeição de banda nas frequências de Fourier 2D da imagem. A banda comumente associadas a excentricidade e espiralamento é entre 4 e 100 metros de comprimentos de onda verticais e 360 graus de comprimento de onda horizontal.
-
-Os comprimentos de onda mínimo e máximo exatos podem ser medidos a partir do dado pelo usuário usando a ferramenta __Ruler__.
-
-1. Selecione a imagem de entrada em _Input image_.
-
-2. Configure os parâmetros:
- - _Minimum wavelength_: Comprimento de onda vertical mínimo do efeito de espiralamento em metros.
- - _Maximum wavelength_: Comprimento de onda vertical máximo do efeito de espiralamento em metros.
- - _Filtering factor_: Fator multiplicativo do filtro. 0 resulta em filtragem nenhuma. 1 resulta em filtragem máxima.
- - _Band spectrum step length_: tamanho do passo/degrau da banda do espectro do filtro. Quanto maior o valor, mais suave o passo da largura da banda.
-
-3. Defina o nome da imagem de saída em _Output image name_.
-
-4. Clique em _Apply_.
-
-## Quality Indicator
-
-Módulo _GeoSlicer_ para indicar a qualidade de dados image log em termos de nível de excentricidade e efeitos de espiralamento.
-
-A saída é uma imagem em que os valores próximos de 1 indicam um alto nível de excentricidade e espiralamento, enquanto em valores próximos de 0 indicam um baixo nível.
-
-O indicador é computado baseado na transformada de Fourier 2D da imagem. Seus valores são definidos pelo espectro de amplitude média da banda comumente associada a excentricidade e espiralamento (comprimentos de onda verticais entre 4 e 100 metros e comprimentos de onda horizontais de 360 graus).
-
-1. Selecione o volume de entrada em _Input volume_.
-
-2. Configure os parâmetros:
- - _Window size_: Tamanho em metros da janela móvel usada para computer o indicador.
- - _Minimum wavelength_: Comprimento de onda vertical mínimo do efeito de espiralamento em metros.
- - _Maximum wavelength_: Comprimento de onda vertical máximo do efeito de espiralamento em metros.
- - _Filtering factor_: Fator multiplicativo do filtro. 0 resulta em filtragem nenhuma. 1 resulta em filtragem máxima.
- - _Band spectrum step length_: tamanho do passo da banda de espectro de filtro. Quanto maior o valor, mais suave o passo da largura da banda.
-
-3. Defina o nome da imagem de saída em _Output image name_.
-
-4. Clique em _Apply_.
-
-## Manual segmentation
-
-Módulo _GeoSlicer_ para segmentar imagens, conforme descrito nos passos abaixo:
-
-1. Selecione a segmentação de saída em _Output segmentation_.
-
-2. Selecione a imagem a ser segmentada em _Input image_.
-
-3. Clique em _Add_ para adicionar segmentos.
-
-4. Selecione um segmento da lista a ser editado.
-
-5. Selecione uma ferramenta dentre as opção sob a lista de segmentos.
-
-Mais instruções sobre cada ferramenta de segmentação pode ser encontrada após selecionada clicando em _Show details._
-
-## Image Log Instance Segmenter
-
-Módulo _GeoSlicer_ para aplicar segmentação de instância a image logs, conforme descrito nos passos abaixo. Para uma descrição mais detalhada dos métodos, consulte a seguinte [seção](../../Image Log/Instance Segmenter/instance_segmenter.md) do manual do GeoSlicer.
-
-1. Selecione o modelo em _Model_, que determina o tipo do artefato a ser detectado.
-
-2. Selecione as imagens necessárias:
- - Modelos sidewall sample: selecione a imagem de amplitude (_Amplitude image_) e a imagem de tempo de trânsito (_Transit time image_).
- - Modelos stops: selecione a imagem de tempo de trânsito em _Transit time image_.
-
-3. Defina os parâmetros:
- - Modelos sidewall sample: selecione o arquivo de profundidades nominais em _Nominal depths file_ (opcional).
- - Modelos stops: defina os parâmetros limiar (_Threshold_), tamanho (_Size_) e _Sigma_.
-
-4. Defina o prefixo de saída em _Output prefix_ (sugerido automaticamente ao serem selecionadas as imagens de entrada).
-
-5. Clique no botão _Segment_ e aguarde a finalização.
-
-## Instance Segmenter Editor
-
-Módulo _GeoSlicer_ para visualizar e editar resultados do segmentador de instância.
-
-### Visualizar
-
-1. Defina o image log e o labelmap de segmentação (gerado pelo _Image Log Instance Segmenter_) nas views do _Image Log Environment_.
-
-2. Selecione a tabela de report correspondete em _Report table_, também gerada pelo _Image Log Instance Segmenter_.
-
-3. As instâncias detectadas podem ser inspecionadas clicando em qualquer uma das colunas da tabela _Parameters_. A instância selecionada será centrada nas views.
-
-4. As instâncias detectadas também podem ser filtradas por algumas propriedades movendo os sliders na seção _Parameters_. Isso ajuda a escolher quais instâncias são boas candidatas.
-
-### Editar
-
-1. Abra a seção _Edit_ para adicionar, editar ou deletar instâncias.
-
-2. Para editar, selecione uma instância databela e clique em _Edit_. Um cursor de mira aparecerá ao mover o mouse sobre as views. Clique para pintar uma instância no image log (pode-set também definir o tamanho do pincel em _Brush size_). Após terminar, clique em _Apply_.
-
-3. Para deletar uma instância, simplesmente selecione na tabela, clique em _Decline_ e confirme.
-
-4. Após terminar de editar, pode-se clicar em _Apply_, na parte inferior do módulo, para gerar outra tabela de relatório com as modificações com o prefixo escolhido em _Output prefix_. Clicar em _Cancel_ reverterá todas as modificações da tabela de relatório atual.
-
-## Segment Inspector
-
-Para uma discussão mais detalhada sobre o algoritmo watershed, por favor cheque a seguinte [seção](../../Inspector/Watershed/estudos_de_porosidade.md) do manual do GeoSlicer.
-
-Este módulo provê múltiplos métodos para analisar uma imagem segmentada. Particularmente, algoritmos Watershed e Islands permite fragmentar a segmentação em diversas partições, ou diversos segmentos. Normalmente é aplicado a segmentação de espaço de poros para computar as métricas de cada elemento de poro. A entrada é um nodo de segmentação ou volume labelmap, uma região de interesse (definida por um nodo de segmentação) e a imagem/volume mestre. A saída é um labelmap onde cada partição (elemento de poro) está em uma cor diferente, uma tabela com parâmetros globais e uma tabela com as diferentes métricas para cada partição.
-
-### Inputs
-
-1. __Selecionar__ single-shot (segmentação única) ou Batch (múltiplas amostras definidas por múltiplos projetos GeoSlicer).
-2. __Segmentation__: Selecionar um nodo de segmentação ou um labelmap para ser inspecionado.
-3. __Region__: Selecionar um nodo de segmentação para definir uma região de interesse (opcional).
-4. __Image__: Selecionar a imagem/volume mestre ao qual a segmentação é relacionada.
-
-### Parameters
-
-1. __Method__: Selecionar um método a ser aplicado. Com algoritmo island, a segmentação é fragmentada de acordo com conexões diretas. Com watershed, a segmentação é fragmentada de acordo com a transformada de distância e os parâmetros da seção _Advanced_.
-2. __Size Filter__: Filtrar partições espúrias com eixo principal (feret_max) menor que o valor _Size Filter_.
-3. __Smooth factor__: Fator de suavização, que é o desvio padrão do filtro gaussiano aplicado à transformada de distância. Conforme aumenta, menos partições serão criadas. Use valores menores para resultados mais confiáveis.
-4. __Minimum distance__: Distância mínima separando picos em uma região de 2 * min_distance + 1 (i.e. picos são separados por no mínimo min_distance). Para encontrar o número máximo de picos, use min_distance = 0.
-5. __Orientation line__: Selecionar a linha para ser usada para cálculo de ângulo de orientação.
-
-### Output
-
-Digite um nome para ser usado como prefixo dos resultados (labelmap onde cada partição (elemento de poro) está em uma cor diferente, uma tabela com parâmetros globais e uma tabela com as diferentes métricas para cada partição).
-
-#### __Propriedades / Métricas__:
-
-1. __Label__: Identificador rotular da partição.
-2. __mean__: Valor médio da imagem/volume de entrada dentro da região da partição (poro/grão).
-3. __median__: Valor mediano da imagem/volume de entrada dentro da região da partição (poro/grão).
-4. __stddev__: Desvio padrão da imagem/volume de entrada dentro da região da partição (poro/grão).
-5. __voxelCount__: Número total de pixels/voxels da região da partição (poro/grão).
-6. __area__: Área total da partição (poro/grão). Unidade: mm^2.
-7. __angle__: Ângulo em graus (entre 270 e 90) relacionado à linha de orientação (opcional; se nenhuma linha for selecionada, a orientação de referência é superior horizontal).
-8. __max_feret__: Eixo de caliper de Feret máximo. Unidade: mm.
-9. __min_feret__: Eixo de caliper de Feret mínimo. Unidade: mm.
-10. __mean_feret__: Média entre os calipers mínimo e máximo.
-11. __aspect_ratio__: min_feret / max_feret.
-12. __elongation__: max_feret / min_feret.
-13. __eccentricity__: sqrt(1 - min_feret / max_feret) relacionado à elipse equivalente (0 <= e < 1), igual a 0 para círculos.
-14. __ellipse_perimeter__: Perímetro da elipse equivalente (com eixo dado por caliper de Feret mínimo e máximo). Unidade: mm.
-15. __ellipse_area__: Área da elipse equivalente (com eixo dado por caliper de Feret mínimo e máximo). Unidade: mm^2.
-16. __ellipse_perimeter_over_ellipse_area__: Perímetro da elipse equivalente dividido pela área.
-17. __perimeter__: Perímetro real da partição (poro/grão). Unidade: mm.
-18. __perimeter_over_area__: Perímetro real dividido pela área da partição (poro/grão).
-19. __gamma__: "Redondeza" de uma área calculada como 'gamma = perimeter / (2 * sqrt(PI * area))'.
-20. __pore_size_class__: Símbolo/código/id da classe do poro.
-21. __pore_size_class_label__: Rótulo da classe do poro.
-
-#### __Definição das classes de poro__:
-
-* __Microporo__: classe 0, max_feret menor que 0.062 mm.
-* __Mesoporo mto pequeno__: classe 1, max_feret entre 0.062 e 0.125 mm.
-* __Mesoporo pequeno__: classe 2, max_feret entre 0.125 e 0.25 mm.
-* __Mesoporo médio__: classe 3, max_feret entre 0.25 e 0.5 mm.
-* __Mesoporo grande__: classe 4, max_feret entre 0.5 e 1 mm.
-* __Mesoporo muito grande__: classe 5, max_feret entre 1 e 4 mm.
-* __Megaporo pequeno__: classe 6, max_feret entre 4 e 32 mm.
-* __Megaporo grande__: classe 7, max_feret maior que 32mm.
-
-## Unwrap Registration
-
-Módulo _GeoSlicer_ para registrar manual unwraps de core em image logs, conforme descrito nos passos abaixo:
-
-1. Selecione a imagem de entrada em _Input unwrap image_.
-
-2. Mude os valores de profundidade (_Depth_) e orientação (_Orientation_) conforme desejado.
-
-3. Clique em _Apply_.
-
-4. Para aplicar permanentemente os resultados, clique em _Save_. _Reset_ reverterá a imagem ao último estado salvo.
-
-## Permeability Modeling
-
-O módulo de modelagem é baseado na referência Menezes de Jesus, C., Compan, A. L. M. and Surmas, R., Permeability Estimation Using Ultrasonic Borehole Image Logs in Dual-Porosity Carbonate Reservoirs, 2016.
-
-É um método para modelagem de permeabilidade usando um image log segmentado e um log de porosidade. A porosidade total é pesada por frações de cada uma das classes segmentadas de entrada extraídas dos image logs.
-
-A permeabilidade é definida por
-
-K = (A1 * F1* Phi ^B1) + (A2 * F2* Phi ^B2) + ... + (An * Fn* Phi ^Bn) + (Am * Fm* Phi),
-
-onde A e B são os parâmetros da equação, F são as frações das n classes de segmento e m é o segmento de macroporo.
-
-1. Depth Log: Selecione o log de profundidade do arquivo LAS relacionado ao log de porosidade.
-2. Porosity Log: Selecione o log de porosidade importado do arquivo LAS.
-3. Depth Image: Selecione a profundidade relacionada ao image log segmentado.
-4. Segmented Image: Selecione o image log segmentado.
-5. Macro Pore Segment class: Selecione a classe de segmento relacionada ao segmento de macroporo.
-6. Ignore class: Selecione a classe de segmento relacionada à classe nula.
-7. Plugs Depth Log: Selecione a profundidade das medidas do plugue.
-8. Plugs Permeability Log: Selecione as medidas de permeabilidade dos plugues.
diff --git a/tools/deploy/GeoSlicerManual/docs/Inspector/Watershed/estudos_de_porosidade-figura_1.png b/tools/deploy/GeoSlicerManual/docs/Inspector/Watershed/estudos_de_porosidade-figura_1.png
deleted file mode 100644
index d8ce6f5..0000000
Binary files a/tools/deploy/GeoSlicerManual/docs/Inspector/Watershed/estudos_de_porosidade-figura_1.png and /dev/null differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Inspector/Watershed/estudos_de_porosidade-figura_2.png b/tools/deploy/GeoSlicerManual/docs/Inspector/Watershed/estudos_de_porosidade-figura_2.png
deleted file mode 100644
index 8d05000..0000000
Binary files a/tools/deploy/GeoSlicerManual/docs/Inspector/Watershed/estudos_de_porosidade-figura_2.png and /dev/null differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Inspector/Watershed/estudos_de_porosidade-figura_3.png b/tools/deploy/GeoSlicerManual/docs/Inspector/Watershed/estudos_de_porosidade-figura_3.png
deleted file mode 100644
index e49ce27..0000000
Binary files a/tools/deploy/GeoSlicerManual/docs/Inspector/Watershed/estudos_de_porosidade-figura_3.png and /dev/null differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Inspector/Watershed/estudos_de_porosidade-figura_4.png b/tools/deploy/GeoSlicerManual/docs/Inspector/Watershed/estudos_de_porosidade-figura_4.png
deleted file mode 100644
index 75eaa7f..0000000
Binary files a/tools/deploy/GeoSlicerManual/docs/Inspector/Watershed/estudos_de_porosidade-figura_4.png and /dev/null differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Inspector/Watershed/estudos_de_porosidade-figura_5.png b/tools/deploy/GeoSlicerManual/docs/Inspector/Watershed/estudos_de_porosidade-figura_5.png
deleted file mode 100644
index a34a483..0000000
Binary files a/tools/deploy/GeoSlicerManual/docs/Inspector/Watershed/estudos_de_porosidade-figura_5.png and /dev/null differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Inspector/Watershed/estudos_de_porosidade-figura_6.png b/tools/deploy/GeoSlicerManual/docs/Inspector/Watershed/estudos_de_porosidade-figura_6.png
deleted file mode 100644
index f3ccc25..0000000
Binary files a/tools/deploy/GeoSlicerManual/docs/Inspector/Watershed/estudos_de_porosidade-figura_6.png and /dev/null differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Inspector/Watershed/estudos_de_porosidade.md b/tools/deploy/GeoSlicerManual/docs/Inspector/Watershed/estudos_de_porosidade.md
deleted file mode 100644
index 992d9c6..0000000
--- a/tools/deploy/GeoSlicerManual/docs/Inspector/Watershed/estudos_de_porosidade.md
+++ /dev/null
@@ -1,57 +0,0 @@
-# Estudos de porosidade
-
-## PNM, PIA e a importância de se obter parâmetros geométricos confiáveis para a porosidade
-
-PNM (Pore Network Modelling, ou “Modelagem de Trama Porosa” em tradução livre) é uma
-técnica muito disseminada no estudo de materiais que tem como objetivo a descrição de
-propriedades de fluxos multifásicos em meio a uma trama de vazios tridimensionalmente
-distribuídos. Iniciada com estudos a partir da década de 70, vem ganhando robustez com a
-incorporação de novas técnicas e capacidade computacional, promovendo a criação de
-estruturas de porosidade cada vez mais complexas e fiéis às amostras, em velocidades de
-aquisição progressivamente maiores. De acordo com a bibliografia, são inúmeras as técnicas
-utilizadas para a construção desses modelos: ensaios de porosimetria, MEV, Microtomografia,
-e avaliação petrográfica são os mais consagrados.
-
-Dentre as possibilidades petrográficas, o módulo Thin Section do Geoslicer possibilita o desenvolvimento de estudos avançados de fotomicrografias, que se enquadram-no conceito de PIA (Petrographic Image Analysis, ou “Análise de Imagem Petrográfica” em tradução livre), com a obtenção de propriedades geométricas e quantificação do segmento identificado como porosidade. Cunhada por Ehrlich et al. (1984), e evoluída por diversos autores (Gostick, ....) a técnica aqui proposta visa a identificação, segmentação, fragmentação e análise dos elementos. Foram elaboradas diversas propostas que visam otimizar a operação e os resultados, e variam de acordo com a complexidade do dado ou necessidade de detalhamento dos outputs. Na versão atual do software, consideram-se as seguintes questões:
-
-**Identificação**: refere-se a capacidade de reconhecer determinado objeto, ou característica (no caso, as áreas associadas a porosidade). Para os referidos estudos, é suficiente o reconhecimento do que é poro e o que não é (ou seja, é sólido). Como costume, a porosidade de rocha é impregnada por resina de coloração azul, que confere essa tonalidade característica às áreas de porosidade, porém há questões associadas às paredes dos poros, que, a depender de características como ângulo de contato e tamanho dos cristais, podem gerar uma variação de tonalidades (Figura 1 – esquema 3D em perspectiva de uma lâmina, e foto representativa dessa variação de cores).
-
-| ![Figura 1](estudos_de_porosidade-figura_1.png) |
-|:-----------------------------------------------:|
-| Figura 1: esquema de lâmina petrográfica. Notar, nos destaques a direita, as variações de tonalidade provocadas pela ponderação entre resina e fases minerais, conforme ângulo de contato e características dos cristais dos constituintes |
-
-**Segmentação**: como definido por Ehlich et al (1984) é o ato de determinar quais pixels do arranjo pertencem a mesma categoria. Para a análise da área porosa, basta a discretização de dois segmentos (“poro” e “não poro”), processo comumente chamado de “binarização”. É possível de se realizar, no Geoslicer, a partir de abordagem manual, com adoção de filtros nos sistemas de cores (thresholds), ou através de semi-automatização, onde cabe ao usuário realizar as anotações dos segmentos (amostrar o que é “poro” e o que é “não-poro”) e definir o algoritmo de extrapolação dessas informações para uma área de interesse (ROI – Region of Interest). Sugere-se previamente realizar eventuais correções da imagem, através de ferramentas de brilho, contraste e/ou equalização do histograma de distribuição de cores (Figura 2).
-
-| ![Figura 2](estudos_de_porosidade-figura_2.png) |
-|:-----------------------------------------------:|
-| Figura 2: exemplo de aplicação de um dos filtros pré-segmenação, disponíveis na aba "Image Tools". No caso, foi aplicada a equalização de histograma de cores, e observa-se uma melhor definição e homogeneização das cores das diferentes fases |
-
-Isso tem como vantagem a homogeneização das características dos pixels, e maior efetividade na construção de segmentos por thresholds ou anotações. Após a imagem ser binarizada, sugere-se a aplicação de filtros de suavização (smoothing) no segmento, evitando, assim, serrilhamento excessivo da superfície, e textura “salt and pepper” (zona de classificação de pixels altamente variável, comum nos halos limites entre o que é poro e o que não é) (Figura 3). Com esses dados já é possível de se obter uma estimativa de porosidade, que nada mais é do que a porcentagem de área ocupada por pixels identificados como poros. Diferente de Ehrlich et al (1984), considera-se, aqui, a porosidade total, cabendo ao usuário a determinação de filtros de tamanho para exclusão de determinada faixa de poros identificados.
-
-| ![Figura 3](estudos_de_porosidade-figura_3.png) |
-|:-----------------------------------------------:|
-| Figura 3: exemplo de suavização da superfície do segmento de porosidde (em vermelho). em 1), segmento sem qualquer suavização. Notar serrilhamento e identação evidenciando "confusão" na rotulação de pixels, principalmente associados às bordas dos elementos; em 2), após aplicada iteração de suavização (definida pelo usuário em “Segmentation > Manual – smoothing”). Notar superfícies mais regulares. Como desvantagem, atentar ao desaparecimento de alguns elementos, em virtude de serem menores que os limites do filtro aplicado. |
-
-**Fragmentação**: a completa extração de informações sobre o sistema poroso, a nível bidimensional, carece de maior detalhamento do segmento de porosidade. O meio poroso é uma trama 3D composta por espaços de maiores dimensões (câmaras porosas) conectadas por espaços menores, que indicam restrições (gargantas de poro). Em uma seção bidimensional (como é o caso de lâminas petrográficas), esse sistema é representado por um conjunto de áreas discretas preenchidos por resina. Admite-se que, embora essas áreas não necessariamente estejam conectadas no plano da seção delgada, essa conexão é efetiva devido a impregnação por resina. Uma área discreta, contínua, preenchida por resina, é denominada PorEl (“elemento de porosidade”, por Ehrlich et al., 1991a), e pode ser composta por uma ou mais câmaras porosas e suas conexões (Figura 4). Grande esforço foi realizado, durante os anos, para a correta fragmentação desses PorEls em constituintes do sistema poroso, através de protocolos de erosão-dilatação (Ehrlich et al. 1991b, dentre outros), razão de eixos, algoritmo SNOW (Gostick, 2017) e Watershed. Atualmente, o Geoslicer contempla uma customização do protocolo de Watershed, através da personalização de filtros e outros parâmetros vinculados a geometria de gargantas.
-
-| ![Figura 4](estudos_de_porosidade-figura_4.png) |
-|:-----------------------------------------------:|
-| Figura 4: Esquema representando binarização (segmentação poro - não-poro) de fotomicrografia. Notar que a porosidade registrada é dividida no que é chamado, por Ehrlich et al. (1991a), de PorEl (ou "elemento de porosidade'). PorEls são áreas discretas, contínuas, de pixels classificados como “poro”. Apesar de não conectados, tendem a serem considerados partes da porosidade efetiva em virtude da impregnação por resina. Vale ressaltar que esses elementos podem ser compostos por diferentes componentes do sistema poroso (ilustrados nessa projeção bidimensional). |
-
-A abordagem adotada aqui considera a garganta de poro como um plano, definido na seção de maior estrangulamento da área identificada como porosidade (nos dados bidimensionais) (Figura 5 – comparativo de garganta como linha e como área). As limitações de tamanho de gargantas estão vinculadas ao tamanho dos eixos dos poros que elas conectam. Admite-se que os valores absolutos obtidos provavelmente não coincidem com os resultados de outras análises (como a porosimetria), porém bibliografia sugere que os dados obtidos apresentam boas correlações com os ensaios realizados para as mesmas amostras.
-
-| ![Figura 5](estudos_de_porosidade-figura_5.png) |
-|:-----------------------------------------------:|
-| Figura 5: comparativo entre duas abordagens utilizadas em bibliografia para identificação de gargantas de poro. em 1) considera-se a garganta como uma área (em 2D), ou volume (em 3D), balizada por ângulos formados por linhas tangenciais às paredes dos poros; em 2) considera-se garganta uma área (em 3D), ou segmento de reta (em 2D) unindo os pontos mais próximos das paredes de poro. No Geoslicer, o conceito 2) é abordado pois, além da maior facilidade em se adquirir os dados de garganta, as informações obtidas (tamanho, orientação) são correlacionáveis aos dados adquiridos através de outras análises (porosimetria, por exemplo). |
-
-Por fim, a fragmentação do segmento de poro nos prováveis constituintes do sistema poroso visa adquirir, de maneira mais fiel, as propriedades geométricas de (seção das) câmaras porosas e gargantas, estabelecendo valores razoáveis de perímetro, área, rugosidade, tamanho e orientação de eixos, etc... Nota-se que a não fragmentação, ou superfragmentação geram dados que não caracterizam a porosidade conforme suas propriedades (materializadas na seção 2D exposta)
-
-P.S.: define-se como “garganta de poro” as restrições que promovem impacto na distribuição espacial de poros, e provável modificação do comportamento dinâmico de fluidos. Essas atribuições estão associadas a uma proporcionalidade entre o tamanho da restrição e o tamanho do maior eixo paralelo medido dentro da câmara porosa (Figura 6).
-
-| ![Figura 6](estudos_de_porosidade-figura_6.png) |
-|:-----------------------------------------------:|
-| Figura 6: PorEl fragmentado. Em 1) todas as reentrâncias de porosidade (feições que alteram a continuidade da função distância e transformada para construção do watershed) são identificadas e utilizadas para fragmentar a área discretizada. Isso gera maior quantidade de câmaras porosas (CP´s) e gargantas de poros de grandes dimensões; em 2), nem todas as reentrâncias são consideradas gargantas de poro (GP´s). Esse diagnóstico está associado a relação espacial e de tamanho entre a reentrância e as câmaras porosas conectadas por ela. Uma vez que essa fragmentação é executada de maneira coerente, as propriedades geométricas dos constituintes do sistema poroso (CP´s e GP´s) podem ser adquiridos de maneira mais confiável. Vale ressaltar que essas informações apresentam a limitação de serem dados bidimensionais, extraídos de seções aleatórias (não controladas) de sistemas espaciais geralmente heterogêneos. |
-
-Embora, em bibliografia, muitas vezes as gargantas serem representadas como “pipes”, aqui a figura geométrica representativa é uma linha (perpendicular às paredes de poro), cujas propriedades medidas são o comprimento e orientação. Em se tratando de comprimento, o comparativo com os raios de garganta de poro, obtidos em porosimetria, é mais direto, pois se trata da mesma unidade de medida. Esse cuidado que deve ser tomado visa honrar possíveis irregularidades das paredes de poro, muito comuns em litotipos carbonáticos (alta reatividade mineralógica, que facilita a ocorrência de processos diagenéticos de modificação do espaço poroso). Em outras palavras, toda garganta é uma reentrância de parede de poro, mas nem toda reentrância pode ser considerada uma garganta de poro.
-
-**Análise**: uma vez definidos os parâmetros e coletados os dados vinculados às câmaras porosas e gargantas de poro (propriedades geométricas como dimensões, orientação, distribuição, etc...), os outputs visam ilustrar, da melhor maneira possível, as características do sistema poroso, tanto na forma de mapas de distribuição (imagens do segmento fragmentado) quanto pela documentação das informações geométricas levantadas, na forma de tabelas (editáveis) e gráficos (plots binários, roseta e histogramas). A confecção de gráficos visa otimizar a visualização das distribuições e buscar correlações entre grandezas, com alto grau de customização por parte do usuário. Como exemplo de atividades, histogramas de distribuição de tamanhos de gargantas de poros podem ser associados aos resultados dos testes de porosimetria; parâmetros como TSD (Throat Size Distribution, ou “Distribuição de Tamanho de Gargantas’, em tradução livre) e MPS (Mean Pore Size, ou “Tamanho Médio de Poros”, em tradução livre) podem ser obtidos e correlacionados de acordo com a necessidade.
diff --git a/tools/deploy/GeoSlicerManual/docs/Micro CT/Usabilidade/usabilidade.md b/tools/deploy/GeoSlicerManual/docs/Micro CT/Usabilidade/usabilidade.md
deleted file mode 100644
index 5bddb83..0000000
--- a/tools/deploy/GeoSlicerManual/docs/Micro CT/Usabilidade/usabilidade.md
+++ /dev/null
@@ -1,332 +0,0 @@
-# Micro CT Environment
-
-Ambiente para trabalhar com micro-CTs.
-
-Módulos:
-
-- Data
-- Loader (Micro CT Loader)
-- Raw Loader
-- Crop (Crop Volume)
-- Registration (Manual and Auto Registration)
-- Segmentation
-- Simulation (Microtom and Pore Network)
-
-## Data
-
-Módulo _GeoSlicer_ para visualizar os dados sendo trabalhados e suas propriedades.
-
-## Micro CT Loader
-
-Módulo _GeoSlicer_ para carregar imagens de micro-CT em lotes, conformed descrito no passos abaixo:
-
-1. Use o botão _Add directories_ para adicionar diretórios contendo dados de micro-CT (atualmente, as extensões de arquivo aceitas são: tif, png e jpg). Esses diretórios aparecerão na área _Data to be loaded_ (uma procura por dados de micro-CT nesses diretórios ocorrá em subdiretórios abaixo em no máximo um nível). Pode-se também remover entradas indesejadas selecionando-as e clicando em _Remove_.
-
-2. Defina o tamanho do pixel (_Pixel size_) em milímetros.
-
-3. Clique no botão _Load micro CTs_ e aguarde o carregamento ser finalizado. As imagens carregadas podem ser acessadas na aba _Data_, dentro do diretório _Micro CT_.
-
-## Raw Loader
-
-Módulo _GeoSlicer_ para carregar imagens armazenadas em um formato de arquivo desconhecido ao permitir rapidamente tentar vários tipos de voxel e tamanhos de imagem, conforme descrito nos passos abaixo:
-
-1. Selecione o arquivo de entrada em _Input file_.
-
-2. Caso não souver as informações do volumes, tente adivinhar os parâmetros da imagem baseado em informações disponíveis.
-
-3. Clique em _Load_ para ver uma prévia da imagem que pode ser carregada.
-
-4. Experimente com os parâmetros da imagem (clique na caixa no botão _Load_ para atualizar automaticamente o volume de saída quando algum parâmetro for alterado).
-
-5. Mova o slider _X dimension_ até colunas retas aparecerem na image (se as colunas estiverem ligeiramente inclinadas então o valor está próximo de estar correto). Tente com diferentes valores de endianness e tipo de pixel se nenhum valor em _X dimension_ parece fazer sentido.
-
-6. Mova _Header size_ até a primeira linha da imagem aparecer no topo.
-
-7. Se estiver carregando um volume 3D: Altere o valor do slider _Z dimension_ para algumas dezenas de fatias para tornar mais fácil ver quando o valor de _Y dimension_ está correto.
-
-8. Mova o slider _Y dimension_ até a última linha da imagem aparecer na parte mais baixa.
-
-9. Se estiver carregando um volume 3D: Mova o slider _Z dimension_ até todas as fatias da imagem estarem inclusas.
-
-10. Quando a combinação correta de parâmetros for encontrada salve a saída atual ou clique em _Generate NRRD header_ para criar um arquivo de cabeçalho que pode ser carregado diretamente no Slicer.
-
-### Mais informações sobre os formatos de exportação
-
-**RAW** - para carregar esse formato exportado pelo módulo *Export*, estes parâmetros precisam ser definidos:
-
- - *Endianness*: Little endian
- - *X dimension*, *Y dimension*, *Z dimension*: as dimensões do dado
- - Para volumes escalares e imagens:
- - *Pixel type*: 16 bit unsigned
- - Para labelmaps e segmentações:
- - *Pixel type*: 8 bit unsigned
-
-## Crop Volume
-
-Módulo _GeoSlicer_ para cortar um volume, conforme descrito nos passos abaixo:
-
-1. Selecione o volume em _Volume to be cropped_.
-
-2. Ajuste a posição e tamanho desejados da ROI nas slice views .
-
-3. Clique em _Crop_ e aguarde a finalização. O volume cortado aparecerá no mesmo diretório que o volume original.
-
-## Filtering Tools
-
-Módulo _GeoSlicer_ que permite filtragem de imagens, conforme descrito abaixo:
-
-1. Selecione uma ferramenta em _Filtering tool_.
-
-2. Preencha as entradas necessárias e aplique.
-
-### Gradient Anisotropic Diffusion
-
-Módulo _GeoSlicer_ para aplicar filtro de difusão anisotrópica de gradiente a imagens, conforme descrito nos passos abaixo:
-
-1. Selecione a imagem a ser filtrada em _Input image_.
-
-2. Defina o parâmetro de condutância em _Conductance_. A condutância controla a sensibilidade do termo de condutância. Como regra geral, quanto menor o valor, mais fortemente o filtro preservará as bordas. Um alto valor causará difusão (suavização) das bordas. Note que o número de iterações controla o quanto haverá de suavização dentro de regiões delimitadas pelas bordas.
-
-3. Defina o parâmetro de número de iterações em _Iterations_. Quanto mais iterações, maior suavização. Cada iteração leva a mesma quantidade de tempo. Se uma iteração leva 10 segundos, 10 iterações levam 100 segundos. Note que a condutância controla o quanto cada iteração suavizará as bordas.
-
-4. Defina o parâmetro de passo temporal em _Time step_. O passo temporal depende da dimensionalidade da imagem. Para imagens tridimensionais, o valor padrão de de 0.0625 fornece uma solução estável.
-
-5. Defina o nome de saída em _Output image name_.
-
-6. Clique no botão _Apply_ e aguarde a finalização. O volume de saída filtrado estará localizado no mesmo diretório que o volume de entrada.
-
-### Curvature Anisotropic Diffusion
-
-Módulo _GeoSlicer_ para plicar filtro de difusão anisotrópica de curvatura em imagens, conforme descrito nos passos abaixo:
-
-1. Selecione a imagem a ser filtrada em _Input image_.
-
-2. Defina o parâmetro de condutância em _Conductance_. A condutância controla a sensibilidade do termo de condutância. Como regra geral, quanto menor o valor, mais fortemente o filtro preservará as bordas. Um alto valor causará difusão (suavização) das bordas. Note que o número de iterações controla o quanto haverá de suavização dentro de regiões delimitadas pelas bordas.
-
-3. Defina o parâmetro de número de iterações em _Iterations_. Quanto mais iterações, maior suavização. Cada iteração leva a mesma quantidade de tempo. Se uma iteração leva 10 segundos, 10 iterações levam 100 segundos. Note que a condutância controla o quanto cada iteração suavizará as bordas.
-
-4. Defina o parâmetro de passo temporal em _Time step_. O passo temporal depende da dimensionalidade da imagem. Para imagens tridimensionais, o valor padrão de de 0.0625 fornece uma solução estável.
-
-5. Defina o nome de saída em _Output image name_.
-
-6. Clique no botão _Apply_ e aguarde a finalização. O volume de saída filtrado estará localizado no mesmo diretório que o volume de entrada.
-
-### Gaussian Blur Image Filter
-
-Módulo _GeoSlicer para aplicar filtro de desfoque gaussiano a imagens, conforme descrito nos passos abaixo:
-
-1. Selecione a imagem a ser filtrada em _Input image_.
-
-2. Defina o parâmetro _Sigma_, o valor em unidades físicas (e.g. mm) do kernel gaussiano.
-
-3. Defina o nome de saída em _Output image name_.
-
-4. Clique no botão _Apply_ e aguarde a finalização. O volume de saída filtrado estará localizado no mesmo diretório que o volume de entrada.
-
-### Median Image Filter
-
-Módulo _GeoSlicer_ para aplicar filtro mediano a imagens, conforme descrito nos passos abaixo:
-
-1. Selecione a imagem a ser filtrada em _Input image_.
-
-2. Defina o parâmetro _Neighborhood size_, o tamanho da vizinhança em cada dimensão.
-
-3. Defina o nome de saída em _Output image name_.
-
-4. Clique no botão _Apply_ e aguarde a finalização. O volume de saída filtrado estará localizado no mesmo diretório que o volume de entrada.
-
-### Shading Correction (temporariamente apenas para usuários Windows)
-
-Módulo _GeoSlicer_ para aplicar correção de sombreamento a imagens, conforme descrito nos passos abaixo:
-
-1. Selecione a imagem a ser corrigida em _Input image_.
-
-2. Selecione a máscara de entrada em _Input mask LabelMap_ para ajustar as bordas do dado corrigido final.
-
-3. Selecione a máscara de sombreamento em _Input shading mask LabelMap_, que proverá o intervalo de intensidades usado no cálculo de fundo.
-
-4. Defina o o raio da bola do algoritmo rolling ball em _Ball Radius_.
-
-5. Clique no botão _Apply_ e aguarde a finalização. O volume de saída filtrado estará localizado no mesmo diretório que o volume de entrada.
-
-## Registration
-
-### Micro CT Transforms
-
-Esse módulo provê transformadas manuais de translação e rotação para o usuário realizar registro manual de imagens.
-
-1. Seleciones micro-CTs a serem transformadas na área _Available volumes_ e clique na seta verde para a direita para movê-las para a área _Selected volumes_, à direita. Pode-se também remover micro-CTs da área _Selected volumes_ usando a seta para a esquerda.
-
-2. Ajuste a translação e a rotação conforme necessário. Pode-se prever essas mudanças em _Slice views_.
-
-3. Após definir a transformação, o usuário pode clicar em _Apply_ para aplicar as mudanças e reiniciar os parâmetros de transformação. Os botões _Undo_, _Redo_ e _Cancel_ também estão disponíveis próximo ao botão _Apply_. A transformada final é aplicada ao dado apenas após clicar em _Save_.
-
-### CT Auto Registration
-
-Módulo _GeoSlicer_ para registrar automaticamente imagens de CT tridimensionais, conforme descrito nos passos abaixo:
-
-1. Selecione o volume fixo (_Fixed volume_) e o volume móvel (_Moving volume_). Transformações serão aplicadas à imagem móvel para corresponder à imagem fixa, e o resultado será salvo em um novo volume transformado, preservando os volumes fixo e móvel.
-
-2. Defina o raio da amostra em _Sample radius_, em milímetros. Esse raio será usado para criar a máscara que identificará o dado relevante a ser registrado.
-
-3. Defina a fração de amostragem em _Sampling fraction_, a fração dos voxels do volume fixo que serão usados para o registro. O numero deve ser maior que zero e menor ou igual a 1. Valores maiores aumentam o tempo de computação mas podem render resultados mais precisos.
-
-4. Defina tamanho do passo mínimo em _Minimum step length_, um valor maior ou igual a 10-8. Cada passo na otimização terá no mínimo esse tamanho. Quando nenhum for possível, o registro estará completo. Valores menores permite que o otimizador faça ajustes menores, mas o tempo do registro pode aumentar.
-
-5. Defina o número de iterações em _Number of iterations_, que determina o número máximo de iteração para tentar antes de parar a otimização. Ao ser usado um valor menor (500-1000) o registro é forçado a terminar antes, mas há um risco maior de parar antes de uma solução ótima ser obtida.
-
-6. Defina um fator de downsampling. Esse parâmetro afeta diretamente a eficiẽncia do algoritmo. Valores altos (~1) podem exigir um alto tempo de execução para finalizar o registro. Valores intermediários, como 0.3, foram encontrados como valores ótimos para obter-se um bom resultado sem um grande custo computacional.
-
-7. Selecione ao menos uma das fase de registro em _Registration phases_. Cada fase de registro será usada para inicializar a próxima fase.
-
-8. Clique no botão _Register_ e aguarde completar. O volume registrado (transformado) pode ser acessado pela aba _Data_, dentro do mesmo diretório que o volume móvel. O nodo de transformada e as máscaras de labelmap criadas também estarão disponíveis para inspeção pelo usuário, mas podem ser deletadas.
-
-## Segmentation
-
-### Manual Segmentation
-
-1. Selecione/crie o nodo de segmentação de saída. O usuário pode criar uma nova segmentação ou editar uma segmentação previamente definida.
-2. Selecione a imagem de entrada a ser segmentada.
-3. Clique em _Add_ para adicionar segmentos.
-4. Selecione um segmento da lista a ser editado.
-5. Selecione uma ferramenta dentre as opção sob a lista de segmentos.
-6. Mais instruções sobre cada ferramenta de segmentação pode ser encontrada após selecionada clicando em _Show details._
-
-### Smart Segmentation
-
-Esse módulo provê métodos avançados para segmentação automática e supervisionada de vários tipos de imagem, tais como seção delgada e tomografia permitindo múltiplas imagens de entrada.
-
-#### __Inputs__
-
-1. __Annotations__: Selecione o nodo de segmentação que contém as anotações feitas na imagem para treinar o método de segmentação escolido.
-2. __Region (SOI)__: Selecione o nodo de segmentação em que o primeiro segmento delimita a região de interesse onde a segmentação será realizada.
-3. __Input image__: Selecione a imagem a ser segmentada. Vários tipos são aceitos, como imagens RGB e tomográficas.
-
-#### __Parameters__
-
-1. __Method__: Selecione o algoritmo para realizará a segmentação.
- 1. __Random Forest__: Florestas aleatórias são um método de aprendizado por agrupamento para classificação que opera construindo múltiplas árvores de decisão em tempo de treinamento. A __entrada__ é uma combinação de:
- * Entrada quantificada (RGB reduzido a um valor de 8 bits)
- * HSV puro
- * Múltiplos kernels gaussianos (tamanho e número de kernels são definidos pelo parâmetro __Radius__)
- * Se selecionado, kernels de __Variância__ são calculados (Ver __Use variation__).
- * If selected, kernels de __Sobel__ são calculados (Ver __Use contours__).
- 2. __Colored K-Means__: Um método de quantificação vetorial que busca particionar __n observações__ em __k clusters__ onde cada observação pertence ao cluster com a média (centro ou centroide) mais próxima. _Colored_ significa que o algoritmo funciona em espaço de cor tridimensional, especialmente HSV.
- * __Seed Initializer__: Algoritmo usado para escolher protótipos de clusters iniciais.
- * __Random__: Escolhe uma semente aleatória a partir das anotações, uma para cada segmento diferente.
- * __Smooth Centroid__: Para cada segmento, combina todas as amostras anotadas para geral uma semente mais geral.
-
-#### __Output__
-
-1. __Output prefix__: Digite um nome para ser usado como prefixo dos resultados.
-
-### Segment Inspector
-
-Para uma discussão mais detalhada sobre o algoritmo watershed, por favor cheque a seguinte [seção](../../Inspector/Watershed/estudos_de_porosidade.md) do manual do GeoSlicer.
-
-Este módulo provê múltiplos métodos para analisar uma imagem segmentada. Particularmente, algoritmos Watershed e Islands permite fragmentar a segmentação em diversas partições, ou diversos segmentos. Normalmente é aplicado a segmentação de espaço de poros para computar as métricas de cada elemento de poro. A entrada é um nodo de segmentação ou volume labelmap, uma região de interesse (definida por um nodo de segmentação) e a imagem/volume mestre. A saída é um labelmap onde cada partição (elemento de poro) está em uma cor diferente, uma tabela com parâmetros globais e uma tabela com as diferentes métricas para cada partição.
-
-#### __Inputs__
-
-1. __Selecionar__ single-shot (segmentação única) ou Batch (múltiplas amostras definidas por múltiplos projetos GeoSlicer).
-2. __Segmentation__: Selecionar um nodo de segmentação ou um labelmap para ser inspecionado.
-3. __Region__: Selecionar um nodo de segmentação para definir uma região de interesse (opcional).
-4. __Image__: Selecionar a imagem/volume mestre ao qual a segmentação é relacionada.
-
-#### __Parameters__
-
-1. __Method__: Selecionar um método a ser aplicado. Com algoritmo island, a segmentação é fragmentada de acordo com conexões diretas. Com watershed, a segmentação é fragmentada de acordo com a transformada de distância e os parâmetros da seção _Advanced_.
-2. __Size Filter__: Filtrar partições espúrias com eixo principal (feret_max) menor que o valor _Size Filter_.
-3. __Smooth factor__: Fator de suavização, que é o desvio padrão do filtro gaussiano aplicado à transformada de distância. Conforme aumenta, menos partições serão criadas. Use valores menores para resultados mais confiáveis.
-4. __Minimum distance__: Distância mínima separando picos em uma região de 2 * min_distance + 1 (i.e. picos são separados por no mínimo min_distance). Para encontrar o número máximo de picos, use min_distance = 0.
-5. __Orientation line__: Selecionar a linha para ser usada para cálculo de ângulo de orientação.
-
-#### __Output__
-
-Digite um nome para ser usado como prefixo dos resultados (labelmap onde cada partição (elemento de poro) está em uma cor diferente, uma tabela com parâmetros globais e uma tabela com as diferentes métricas para cada partição).
-
-#### Propriedades / Métricas:
-
-1. __Label__: Identificador rotular da partição.
-2. __mean__: Valor médio da imagem/volume de entrada dentro da região da partição (poro/grão).
-3. __median__: Valor mediano da imagem/volume de entrada dentro da região da partição (poro/grão).
-4. __stddev__: Desvio padrão da imagem/volume de entrada dentro da região da partição (poro/grão).
-5. __voxelCount__: Número total de pixels/voxels da região da partição (poro/grão).
-6. __area__: Área total da partição (poro/grão). Unidade: mm^2.
-7. __angle__: Ângulo em graus (entre 270 e 90) relacionado à linha de orientação (opcional; se nenhuma linha for selecionada, a orientação de referência é superior horizontal).
-8. __max_feret__: Eixo de caliper de Feret máximo. Unidade: mm.
-9. __min_feret__: Eixo de caliper de Feret mínimo. Unidade: mm.
-10. __mean_feret__: Média entre os calipers mínimo e máximo.
-11. __aspect_ratio__: min_feret / max_feret.
-12. __elongation__: max_feret / min_feret.
-13. __eccentricity__: sqrt(1 - min_feret / max_feret) relacionado à elipse equivalente (0 <= e < 1), igual a 0 para círculos.
-14. __ellipse_perimeter__: Perímetro da elipse equivalente (com eixo dado por caliper de Feret mínimo e máximo). Unidade: mm.
-15. __ellipse_area__: Área da elipse equivalente (com eixo dado por caliper de Feret mínimo e máximo). Unidade: mm^2.
-16. __ellipse_perimeter_over_ellipse_area__: Perímetro da elipse equivalente dividido pela área.
-17. __perimeter__: Perímetro real da partição (poro/grão). Unidade: mm.
-18. __perimeter_over_area__: Perímetro real dividido pela área da partição (poro/grão).
-19. __gamma__: "Redondeza" de uma área calculada como 'gamma = perimeter / (2 * sqrt(PI * area))'.
-20. __pore_size_class__: Símbolo/código/id da classe do poro.
-21. __pore_size_class_label__: Rótulo da classe do poro.
-
-#### Definição das classes de poro:
-
-* __Microporo__: classe 0, max_feret menor que 0.062 mm.
-* __Mesoporo mto pequeno__: classe 1, max_feret entre 0.062 e 0.125 mm.
-* __Mesoporo pequeno__: classe 2, max_feret entre 0.125 e 0.25 mm.
-* __Mesoporo médio__: classe 3, max_feret entre 0.25 e 0.5 mm.
-* __Mesoporo grande__: classe 4, max_feret entre 0.5 e 1 mm.
-* __Mesoporo muito grande__: classe 5, max_feret entre 1 e 4 mm.
-* __Megaporo pequeno__: classe 6, max_feret entre 4 e 32 mm.
-* __Megaporo grande__: classe 7, max_feret maior que 32mm.
-
-### Label Map Editor
-
-Realiza separação e aglutinação manual de objetos rotulados.
-
-#### __Atalhos para ferramentas__
-
-- m: Mesclar dois rótulos
-- a: Dividir rótulo automaticamente usando watershed
-- s: Dividir rótulo com uma linha reta
-- c: Cortar rótulo no ponteiro do mouse
-- z: Desfazer última edição
-- x: Refazer edição desfeita
-- Esc: Cancelar operação
-
-## MicroTom
-
-Esse módulo permite que usuários do _GeoSlicer_ usem algoritmos e métodos da biblioteca MicroTom, desenvolvida pela Petrobras.
-
-__Métodos disponíveis__
-
-* Pore Size Distribution
-* Hierarchical Pore Size Distribution
-* Mercury Injection capillary Pressure
-* Stokes-Kabs Absolute Permeability on the Pore Scale
-
-### Interface
-
-#### __Inputs__
-
-1. __Segmentation__: Selecione o labelmap ao qual o algoritmo de microtom será aplicado. Deve ser necessariamente um labelmap; nodos de segmentação não são aceitos (qualquer nodo de segmentação pode ser transformado em um labelmap na aba _Data_ clicando com o botão esquerdo do mouse no nodo).
-2. __Region (SOI)__: Selecione um nodo de segmentação em que o primeiro segmento delimita a região de interesse onde a segmentação será realizada.
-3. __Segments__: Selecione um segmento na lista para ser usado como o espaço de poro da rocha.
-
-#### __Parameters__
-
-1. __Select a Simulation__: Select one of the MicroTom algorithm in the list
-2. __Store result at__: (optional) The user can define a specific folder to store the files.
-3. __Execution Mode__: Local or Remote.
-4. __Show (Jobs list)__: By clicking on "Show", the user can see a list of process sent to the remote cluster.
-
-## Pore Network Environment
-
-Ambiente para realizar operações de modelo de rede de poros (Pore Network Model).
-
-Módulos:
-
-- PN Extraction: Cria um modelo de rede de poros a partir de um volume binário ou rotulado.
-- PN Simulation: Realiza simulações de fluxo monofásico ou bifásico em um PNM.
-- Cycles Visualization: Visualiza passes de simulação bifásica.
-- Production Prediction: Cria uma predição de produção com a equação de Buckley-Leverett a partir de resultados de Krel bifásico.
\ No newline at end of file
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/auto_label.md b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/auto_label.md
new file mode 100644
index 0000000..619edd5
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/auto_label.md
@@ -0,0 +1,24 @@
+## Auto-fragmentar
+
+Divida a segmentação atual em vários objetos usando o método escolhido.
+
+Escolha quais segmentos na segmentação atual dividir em objetos utilizando as caixas de seleção. Os segmentos selecionados serão considerados um só no algoritmo de fragmentação.
+
+**Módulo correspondente**: *[Thin Section Loader](../Modulos/SegmentInspector.md)*
+
+
+
+
+### Elementos da Interface
+
+![Auto-fragmentar](images/auto_label.png)
+
+- **Method:** Selecione o método desejado para dividir os segmentos. As opções incluem:
+ - **Watershed**: Divide os segmentos encontrando bacias nos valores da imagem subjacente.
+ - **Separate objects**: Divide segmentos em regiões contíguas. Os objetos não tocarão uns aos outros.
+
+- **Segments:**
+ - Lista de segmentos atualmente na imagem com caixas de seleção para escolher quais devem ser divididos.
+ - Cada segmento é representado por sua cor e nome.
+
+- **Calculate proportions:** Caixa de seleção para habilitar ou desabilitar o cálculo das proporções dos segmentos. Se habilitado, mostra a área de cada segmento em relação à região de interesse.
\ No newline at end of file
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/finish.md b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/finish.md
new file mode 100644
index 0000000..41f3b46
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/finish.md
@@ -0,0 +1,20 @@
+## Finalizar
+
+Este passo apresenta os resultados do fluxo de trabalho. Todas imagens do projeto são listadas, incluindo entradas, saídas e imagens intermediárias.
+
+Ao finalizar o fluxo, você pode:
+
+- Visualizar os resultados (Clique no ícone de olho na lista de resultados)
+- Salvar o projeto (Ctrl+S)
+- Exportar os resultados usando o módulo *Thin Section Export*
+- Clicar em `Next` para executar o fluxo com outra imagem.
+
+**Módulo correspondente**: *Explorer*
+
+### Elementos da Interface
+
+![Finalizar](images/finish.png)
+
+Todos os dados do projeto atual são listados, isso inclui tanto dados gerados neste fluxo ou em outros processos.
+
+O relatório é gerado no passo *Auto-label* e é recalculado opcionalmente no passo *Edit labels*. Ambas tabelas são listadas. O mesmo vale para o mapa de objetos (*labelmap*).
\ No newline at end of file
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/auto_label.png b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/auto_label.png
new file mode 100644
index 0000000..fb77591
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/auto_label.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/delete_marker.png b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/delete_marker.png
new file mode 100644
index 0000000..1830272
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/delete_marker.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/find_file.png b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/find_file.png
new file mode 100644
index 0000000..4c5d482
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/find_file.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/finish.png b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/finish.png
new file mode 100644
index 0000000..d999ae2
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/finish.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/label_editor.png b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/label_editor.png
new file mode 100644
index 0000000..c7e8519
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/label_editor.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/load_pp.png b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/load_pp.png
new file mode 100644
index 0000000..a0ea9dd
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/load_pp.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/load_pp_px.png b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/load_pp_px.png
new file mode 100644
index 0000000..8a5c325
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/load_pp_px.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/load_qemscan.png b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/load_qemscan.png
new file mode 100644
index 0000000..83d1cea
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/load_qemscan.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/manual_seg.png b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/manual_seg.png
new file mode 100644
index 0000000..ffdff70
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/manual_seg.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/no_editing.png b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/no_editing.png
new file mode 100644
index 0000000..0e80893
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/no_editing.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/register.png b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/register.png
new file mode 100644
index 0000000..17c97d7
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/register.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/scale.png b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/scale.png
new file mode 100644
index 0000000..8ff9d9a
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/scale.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/scissors.png b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/scissors.png
new file mode 100644
index 0000000..f5bab09
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/scissors.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/select_marker.png b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/select_marker.png
new file mode 100644
index 0000000..18c8fe6
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/select_marker.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/smartseg.png b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/smartseg.png
new file mode 100644
index 0000000..80f19a9
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/smartseg.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/soi.png b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/soi.png
new file mode 100644
index 0000000..5b11610
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/soi.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/undo_redo.png b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/undo_redo.png
new file mode 100644
index 0000000..54432a7
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/images/undo_redo.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/label_editor.md b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/label_editor.md
new file mode 100644
index 0000000..ca13c15
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/label_editor.md
@@ -0,0 +1,22 @@
+## Editar Objetos
+
+Separe ou una objetos detectados no passo anterior.
+
+Após a edição, pressione `Next` para recalcular as métricas de poro e gerar uma nova tabela de relatório. Pressione `Skip` para pular o cálculo, se não houver alterações.
+
+**Módulo correspondente**: *Label Editor*
+
+### Elementos da Interface
+
+![Editar Objetos](images/label_editor.png)
+
+Este passo mostra o mapa de objetos gerado no passo anterior (*auto-label*). Clique em uma operação e, em seguida, clique em um objeto na imagem para executá-la.
+
+- **Hold operation for next edition**: Selecione esta opção para realizar uma operação (ex.: `Merge`) várias vezes consecutivas sem precisar selecionar a operação novamente.
+- **Merge**: Clique em dois objetos para uni-los. Atalho: `m`.
+- **Auto Split**: Clique para dividir automaticamente um objeto usando a técnica de *watershed*. Atalho: `a`.
+- **Slice**: Clique para cortar o objeto com uma linha reta. Defina a linha com dois cliques na imagem. Atalho: `s`.
+- **Point cut**: Clique para cortar o objeto em um ponto específico. Atalho: `c`.
+- **Cancel**: Clique para cancelar a operação atual.
+- **Undo**: Clique para desfazer a última ação. Atalho: `z`.
+- **Redo**: Clique para refazer a última ação desfeita. Atalho: `x`.
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/load_pp.md b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/load_pp.md
new file mode 100644
index 0000000..dd3c178
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/load_pp.md
@@ -0,0 +1,23 @@
+## Carregar PP/PX
+
+Escolha os arquivos de imagem PP (polarizado plano) para carregar.
+
+**Módulo correspondente**: *[Thin Section Loader](../Modulos/Loader.md)*
+
+
+
+
+### Elementos da Interface
+
+![Carregar PP](images/load_pp.png)
+
+
+Especifique o caminho para a imagem no campo **PP**.
+
+Ao lado do campo, há um botão ![Procurar arquivo](images/find_file.png) que abre o explorador de arquivos do sistema, a fim de selecionar o arquivo.
+
+### Formatos aceitos
+
+- JPEG
+- TIFF
+- PNG
\ No newline at end of file
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/load_pp_px.md b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/load_pp_px.md
new file mode 100644
index 0000000..101fbbe
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/load_pp_px.md
@@ -0,0 +1,23 @@
+## Carregar PP/PX
+
+Escolha os arquivos de imagem PP (polarizado plano) e PX (polarizado cruzado) para carregar.
+
+**Módulo correspondente**: *[Thin Section Loader](../Modulos/Loader.md)*
+
+
+
+
+### Elementos da Interface
+
+![Carregar PP/PX](images/load_pp_px.png)
+
+
+Especifique o caminho para as imagens nos campos **PP** e **PX**.
+
+Ao lado de cada campo, há um botão ![Procurar arquivo](images/find_file.png) que abre o explorador de arquivos do sistema, a fim de selecionar o arquivo.
+
+### Formatos aceitos
+
+- JPEG
+- TIFF
+- PNG
\ No newline at end of file
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/load_qemscan.md b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/load_qemscan.md
new file mode 100644
index 0000000..a687cc0
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/load_qemscan.md
@@ -0,0 +1,28 @@
+## Carregar QEMSCAN
+
+Escolha o arquivo de imagem QEMSCAN a ser carregado.
+
+**Módulo correspondente**: *[QEMSCAN Loader](../Modulos/QemscanLoader.md)*
+
+
+
+
+### Elementos da Interface
+
+![Carregar QEMSCAN](images/load_qemscan.png)
+
+- QEMSCAN file
+Especifique o caminho para a imagem QEMSCAN.
+
+Ao lado deste campo, há um botão ![Procurar arquivo](images/find_file.png) que abre o explorador de arquivos do sistema, a fim de selecionar o arquivo.
+
+Se houver um único arquivo CSV na mesma pasta da imagem, este será usado para definir o nome e a cor de cada segmento da imagem. Se não houver, é usada uma tabela padrão QEMSCAN.
+
+- Pixel size (mm)
+
+Especifique o tamanho de cada pixel da imagem em milímetros.
+
+### Formatos aceitos
+
+- TIFF (imagem)
+- CSV (tabela de cores)
\ No newline at end of file
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/manual_seg.md b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/manual_seg.md
new file mode 100644
index 0000000..a60b5e4
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/manual_seg.md
@@ -0,0 +1,17 @@
+## Segmentação Manual
+
+Edite a segmentação com ferramentas manuais. Esse passo pode ser usado para editar a segmentação criada no passo anterior (*Smart-seg*), ou para editar uma segmentação nova.
+
+**Módulo correspondente**: *[Segment Editor](../Modulos/SegmentEditor.md)*
+
+
+
+
+### Elementos de Interface
+
+![Segmentação Manual](images/manual_seg.png)
+
+Página principal: *[Segment Editor](../Modulos/SegmentEditor.md)*
+
+
+
\ No newline at end of file
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/register.md b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/register.md
new file mode 100644
index 0000000..d218290
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/register.md
@@ -0,0 +1,43 @@
+## Registrar
+
+Registre a imagem PP com a imagem PX, garantindo que elas estejam espacialmente alinhadas.
+
+Confira se as imagens precisam de alinhamento, por exemplo, usando a função `Rock` Se elas já estiverem alinhadas, pule esse passo com a opção `Skip`
+
+Se o registro for necessário, clique no botão `Add` e, em seguida, clique na imagem para adicionar um marcador. Arraste o mesmo marcador na outra imagem de forma que ele esteja na mesma posição em ambas as imagens.
+
+Adicione mais marcadores até que as imagens estejam alinhadas.
+
+**Módulo correspondente**: *[Thin Section Registration](../Modulos/Registration.md)*
+
+
+
+
+### Elementos da Interface
+
+![Registrar](images/register.png)
+
+#### Visualização
+
+- **Display**:
+ - **Fixed**: Seleciona a visualização da imagem PP.
+ - **Moving**: Seleciona a visualização da imagem PX.
+ - **Transformed**: Seleciona a visualização da imagem *Transformed* Essa imagem mostra ambas imagens sobrepostas.
+
+- **Fade**: Ajusta a opacidade entre as imagens sobrepostas. Use a barra deslizante para alterar a opacidade e o campo numérico para definir um valor específico.
+
+- **Rock**: Ativa a visualização alternativa das imagens em um movimento de vai-e-vem.
+
+- **Flicker**: Alterna rapidamente entre as imagens.
+
+- **Views**:
+ - **Zoom in**: Aumenta o zoom na visualização de todas imagens.
+ - **Zoom out**: Reduz o zoom na visualização de todas imagens.
+ - **Fit**: Ajusta as imagens para caber na janela de visualização.
+
+#### Marcadores (Landmarks)
+
+- **Add**: Clique neste botão para adicionar um novo marcador na imagem.
+- **Lista de marcadores**: Exibe os marcadores adicionados. Cada marcador listado inclui:
+ - **Botão de seleção** ![Botão de seleção](images/select_marker.png): Seleciona o marcador.
+ - **Botão de exclusão** ![Botão de exclusão](images/delete_marker.png): Exclui o marcador.
\ No newline at end of file
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/scale.md b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/scale.md
new file mode 100644
index 0000000..ffaaf75
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/scale.md
@@ -0,0 +1,26 @@
+## Definir a Escala
+
+Defina o tamanho do pixel em milímetros para a imagem.
+
+Neste passo, a imagem PP deve estar visível. Se a imagem tiver uma barra de escala, ela pode ser detectada automaticamente, e os campos serão preenchidos automaticamente. Nesse caso, confira os valores e siga para o próximo passo.
+
+Se a detecção falhar, a seguinte mensagem aparecerá: `Could not detect scale automatically. Please define it manually.` Nesse caso, é necessário definir a escala manualmente. Se o tamanho do pixel for conhecido, preencha-o diretamente no campo `Pixel size (mm)`. Se não for, o tamanho do pixel pode ser calculado medindo a barra de escala na imagem.
+
+Definir a escala é importante para garantir que as métricas de poro calculadas posteriormente estejam fisicamente corretas.
+
+**Módulo correspondente**: *[Thin Section Loader](../Modulos/Loader.md)*
+
+
+
+
+### Elementos da Interface
+
+![Definir a Escala](images/scale.png)
+
+- **Scale size (px)**: Insira o tamanho da barra de escala em pixels (px). Use o botão `Measure bar` para medir diretamente na imagem.
+
+- **Measure bar**: Clique neste botão para usar a ferramenta de medida, que permitirá que você desenhe uma linha na imagem para medir a barra de escala em pixels. Com a ferramenta ativa, clique em uma das extremidades da barra, em seguida clique na outra extremidade. O campo `Scale size (px)` será preenchido com o tamanho medido.
+
+- **Scale size (mm)**: Insira o tamanho real da barra de escala em milímetros (mm). Esse valor deve estar visível na imagem, próximo a barra. Por exemplo, se estiver escrito "0,1 cm", insira "1" no campo.
+
+- **Pixel size (mm)**: Este campo exibe o valor calculado do tamanho do pixel em milímetros, com base nos valores inseridos nos campos `Scale size (px)` e `Scale size (mm)`. Esse valor é atualizado automaticamente quando os campos anteriores são preenchidos.
\ No newline at end of file
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/smartseg.md b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/smartseg.md
new file mode 100644
index 0000000..bb30194
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/smartseg.md
@@ -0,0 +1,13 @@
+## Segmentação Inteligente
+
+Segmente automaticamente a imagem usando um modelo de aprendizado de máquina. Se preferir segmentar a imagem manualmente, pule este passo com a opção `Skip`.
+
+**Módulo correspondente**: [Segmentação Automática (Thin Section)](../../Segmenter/Automatic/automatic_thinSection.md)
+
+
+
+### Elementos da Interface
+
+![Segmentação Inteligente](images/smartseg.png)
+
+- **Model**: Selecione o modelo a ser usado para segmentar a imagem. Após selecionar, informações sobre o modelo aparecerão, incluindo descrição e segmentos de saída. Cada modelo segmenta a imagem em um conjunto específico de classes.
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/soi.md b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/soi.md
new file mode 100644
index 0000000..f34f982
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Modules/FlowSteps/soi.md
@@ -0,0 +1,36 @@
+## Definir o Segmento de Interesse
+
+Selecione o segmento de interesse (SOI) para as imagens PP/PX.
+
+Algumas imagens podem incluir regiões que não fazem parte da rocha, em especial nas bordas. Esse passo permite definir a região relevante.
+
+Os passos posteriores *Smart-seg* e *Auto-label* serão executados apenas nesta região.
+
+Para definir o segmento, desenhe retângulos na imagem. Se preferir desenho livre, selecione a opção *Free-form*.
+
+**Módulo correspondente**: *[Segment Editor](../Modulos/SegmentEditor.md)*
+
+
+
+
+### Elementos da Interface
+
+![Definir o Segmento de Interesse](images/soi.png)
+
+#### Ferramentas de Segmentação
+
+- ![](images/no_editing.png) **Sem edição**: Use este ícone para interagir com a visualização (zoom, mover etc) em vez de desenhar o segmento.
+- ![](images/scissors.png) **Tesoura**: Use este ícone para ativar a ferramenta de tesoura, usada para editar o segmento de interesse.
+
+#### Opções da Tesoura
+
+- **Operações**:
+ - **Erase inside**: Apagar parte do segmento que estiver dentro da região desenhada.
+ - **Erase outside**: Apagar parte do segmento que estiver fora da região desenhada.
+ - **Fill inside**: Preencher segmento dentro da região desenhada.
+ - **Fill outside**: Preencher segmento fora da região desenhada.
+- **Formas**:
+ - **Free-form**: Desenhar desenhos livres.
+ - **Circle**: Desenhar círculos.
+ - **Rectangle**: Desenhar retângulos.
+- ![](images/undo_redo.png) **Undo/Redo**: Desfazer ou refazer a última alteração.
\ No newline at end of file
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/PNM/PNExtraction.md b/tools/deploy/GeoSlicerManual/docs/Modules/PNM/PNExtraction.md
new file mode 100644
index 0000000..a3e4262
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Modules/PNM/PNExtraction.md
@@ -0,0 +1,17 @@
+# Extração
+
+Esse módulo é utilizado para extrair a rede de poros e ligações a partir de: uma segmentação dos poros (_Label Map Volume_) realizada por um algoritmo de _watershed_, gerando uma rede uniescalar; ou por um mapa de porosidades (_Scalar Volume_), que gerará um modelo multiescalar com poros resolvidos e não-resolvidos.
+
+| |
+|:-----------------------------------------------------------------------:|
+| Figura 1: Interface do módulo de Extração. |
+
+Após a extração, ficará disponível na interface do GeoSlicer: as tabelas de poros e gargantas e também os modelos de visualização da rede. As tabelas geradas serão os dados usados na etapa seguinte de simulação.
+
+| |
+|:-----------------------------------------------------------------------:|
+| Figura 1: A esquerda Label Map utilizado como entrada na extração e a direita rede uniescalar extraída. |
+
+| |
+|:-----------------------------------------------------------------------:|
+| Figura 2: A esquerda Scalar Volume utilizado como entrada na extração e a direita rede multiescalar extraída, onde azul representa poros resolvidos, e rosa representa os poros não-resolvidos. |
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/PNM/PNSimulation.md b/tools/deploy/GeoSlicerManual/docs/Modules/PNM/PNSimulation.md
new file mode 100644
index 0000000..9876a39
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Modules/PNM/PNSimulation.md
@@ -0,0 +1,161 @@
+# Pore Network Simulation
+
+Este módulo pode realizar diferentes tipos de simulações a partir dos resultados da tabela de poros e ligações (criada no módulo de [Extração](./PNExtraction.md)). As simulações incluem: [_One-phase_](#one-phase), [_Two-phase_](#two-phase), [_Mercury injection_](#mercury-injection), explicados nas seções adiante.
+
+Todas as simulações possuem os mesmos argumentos de entrada: A tabela de poros gerada a partir da extração da rede e quando o volume for multiescalar, o [modelo subescala](#modelo-subscala) utilizado e sua parametrização.
+
+| ![Figura 1](../../assets/images/pnm/simulation.png) |
+|:----------------------------------------------------------------------------:|
+| Figura 1: Entrada da tabela de poros gerada com o módulo [Extração](./PNExtraction.md). |
+
+## _One-phase_
+
+A simulação de uma fase, é utilizada principalmente para determinar a propriedade de permeabilidade absoluta ($K_{abs}$) da amostra.
+
+| ![Figura 2](../../assets/images/pnm/one phase.png) |
+|:----------------------------------------------------------------------------:|
+| Figura 2: Simulação de uma fase. |
+
+Três diferentes _solvers_ podem ser usados para esse tipo de simulação:
+
+- pypardiso (recomendado) : rende os melhores resultados, convergindo mesmo em situações de raios muito discrepantes;
+- pyflowsolver : inclui uma opção de seleção do critério de parada, porém com performance menor que o pypardiso;
+- openpnm : opção mais tradicionalmente usada;
+
+| ![Figura 3](../../assets/images/pnm/one phase_solver.png) |
+|:----------------------------------------------------------------------------:|
+| Figura 3: Opção pyflowsolver e critério de parada. As demais opções (pypardiso e openpnm) não possuem esse critério. |
+
+Quando os valores de condutividade são muito discrepantes para a mesma amostra, e essa amostra percola mais pela subescala, podemos ter problemas na convergência para a solução, por conta disso, adicionamos uma opção para poder limitar os valores em altas condutividades.
+
+| ![Figura 4](../../assets/images/pnm/one phase_clip.png) |
+|:----------------------------------------------------------------------------:|
+| Figura 4: Opções de limitação dos valores de condutividade. |
+
+Além disso, a simulação de uma fase pode ser realizada em uma única direção, ou em múltiplas direções, a partir do parâmetro _Orientation scheme_.
+
+| ![Figura 5](../../assets/images/pnm/one phase_orientation scheme.png) |
+|:----------------------------------------------------------------------------:|
+| Figura 5: Esquema de orientação utilizado. |
+
+
+## _Two-phase_
+
+A simulação de duas fases consiste em inicialmente injetar óleo na amostra e aumentar a pressão do mesmo afim de que esse invada praticamente todos os poros, num processo que é conhecido como drenagem (_drainage_). Após essa primeira etapa, substitui-se o óleo por água e novamente aumenta-se a pressão, de forma a permitir que a água invada alguns dos poros que antes estavam com óleo, expulsando o último, esse segundo processo é conhecido como embebição (_imbibition_). Ao medirmos a permeabilidade da rocha em relação a permeabilidade absoluta, em função da saturação de água durante esse processo, obtemos a curva conhecida como curva de permeabilidade relativa ($K_{rel}$).
+
+Uma vez que cada rocha pode interagir físicamente ou quimicamente com o óleo e com a água de diferentes maneiras, precisamos de uma variedade bastante grande de parametros que permitam calibrar os resultados de forma a modelar e simplificar essa interação, afim de extrairmos algum significado físico das propriedades da rocha a partir da simulação. Abaixo, elencamos alguns parâmetros que podem ser encontrados na simulação de duas fases disponibilizada no GeoSlicer.
+
+Atualmente, temos disponível no GeoSlicer dois algoritmos para realizar a simulação de duas fases, a primeira delas é a do PNFlow que é um algoritmo padrão utilizado, implementado em C++, e a segunda é um algoritmo próprio desenvolvido pela LTrace em linguagem Python.
+
+### Salvar/Carregar tabela de seleção de parâmetros
+
+Para facilitar a reprodução de simulações que rodam no mesmo conjunto de parâmetros, a interface possui opções para salvar/carregar os parâmetros a partir de tabelas que são salvas junto ao projeto. Dessa forma, ao calibrar o conjunto de parâmetros, o usuário pode guardar essas informações para uma outra análise posterior, ou usar esses mesmos parâmetros em outra amostra.
+
+| ![Figura 6a](../../assets/images/pnm/two phase_load.png)![Figura 6b](../../assets/images/pnm/two phase_save.png) |
+|:----------------------------------------------------------------------------:|
+| Figura 6: Opções para salvar/carregar tabelas de seleção de parâmetros. |
+
+### _Fluid properties_
+
+Nessa seção é possível alterar os parâmetros dos fluidos (água e óleo) utilizados na simulação:
+
+| ![Figura 7](../../assets/images/pnm/two phase_fluid properties.png) |
+|:----------------------------------------------------------------------------:|
+| Figura 7: Entrada dos parâmetros dos fluidos (água e óleo). |
+
+### _Contact angle options_
+
+Uma das principais propriedades que afetam a interação de um líquido com um sólido é a molhabilidade, essa pode ser determinada a partir do ângulo de contato formado pelo primeiro quando em contato com o último. Assim, se o ângulo de contato for próximo de zero há uma forte interação que "prende" o líquido ao sólido, já quando o ângulo de contato é próximo a 180º, a interação do líquido com a superfície é fraca e esse pode escoar com mais facilidade pela mesma.
+
+| ![Figura 8](../../assets/images/pnm/molhabilidade.png) |
+|:----------------------------------------------------------------------------:|
+| Figura 8: Representação visual do conceito de molhabilidade e ângulo de contato. |
+
+Modelamos os ângulos de contato a partir de duas distribuições usadas em momentos distintos: a _Initial contact angle_ que controla o ângulo de contato dos poros antes da invasão por óleo; e a _Equilibrium contact angle_ que controla o ângulo de contato após a invasão por óleo. Além das distribuições base utilizadas em cada caso, há uma opção para adicionar uma segunda distribuição para cada uma delas, assim cada poro é atrelado a uma das duas distribuições, com o parâmetro "Fraction" sendo usado para determinar qual a porcentagem de poros vão seguir a segunda distribuição em relação a primeira.
+
+| ![Figura 9](../../assets/images/pnm/two phase_contact angle.png) |
+|:----------------------------------------------------------------------------:|
+| Figura 9: Parâmetros das distribuições de ângulo de contato. |
+
+Cada distribuição de ângulos, seja primária ou secundária, inicial ou de equilíbrio, tem uma série de parâmetros que a descreve:
+
+- _Model_: permite modelar as curvas de histerese entre ângulos de avanço/recuo a partir dos ângulos intrínsicos:
+
+ - _Equal angles_: ângulos de avanço/recuo idênticos ao ângulo intrinsico;
+ - _Constant difference_: diferença constante dos ângulos de avanço/recuo em relação ao ângulo intrínsico;
+ - _Morrow curve_: curvas de avanço/recuo determinadas pelas curvas de Morrow;
+
+| |
+|:---------------------------------------------------------------------:|
+| Figura 10: Curvas para cada um dos modelos de ângulo de contato implementados no GeoSlicer. Imagem retirada de N. R. Morrow, 1975 (https://doi.org/10.2118/75-04-04). |
+
+- _Contact angle distribution center_: Define o centro da distribuição de ângulo de contato;
+- _Contact angle distribution range_: Alcance da distribuição (center-range/2, center+range/2), com o ângulo mínimo/máximo sendo 0º/180º, respectivamente;
+- _Delta_, _Gamma_: Parâmetros da distribuição de Weibull truncada, se um número negativo é escolhido, usa uma distribuição uniforme;
+- _Contact angle correlation_: Escolhe como o ângulo de contato será correlacionado ao raio dos poros: _Positive radius_ define maiores ângulos de contato para raios maiores; _Negative radius_ faz o oposto, atribuindo maiores ângulos para raios menores; _Uncorrelated_ significa independência entre ângulos de contato com o raio do poro;
+- _Separation_: Se o modelo escolhido for _constant difference_, define a separação entre ângulos de avanço e recuo;
+
+Outros parâmetros estão definidos apenas para a segunda distribuição:
+
+- _Fraction_: Um valor entre 0 e 1 que controla qual a fração dos poros usará a segunda distribuição ao invés da primeira;
+- _Fraction distribution_: Define se a fração será determinada pela quantidade de poros ou volume total;
+- _Correlation diameter_: Se a _Fraction correlation_ for escolhida como _Spatially correlated_, define a distância mais provável de encontrar poros com mesma distribuição de ângulo de contato;
+- _Fraction correlation_: Define como a fração para a segunda distribuição será correlacionada, se correlacionada espacialmente, para maiores poros, menores poros ou aleatória;
+
+### _Simulation options_
+
+Essa seção de parâmetros é dedicada a controlar os parâmetros relacionados a própria simulação.
+
+| ![Figura 11](../../assets/images/pnm/two phase_simulation properties.png) |
+|:----------------------------------------------------------------------------:|
+| Figura 11: Parâmetros da simulação. |
+
+- _Minimum SWi_: Define o valor mínimo de SWi, interrompendo o ciclo de drenagem quando o valor de Sw é atingido (SWi pode ser maior se a água ficar presa);
+- _Final cycle Pc_: Interrompe o ciclo quando essa pressão capilar é alcançada;
+- _Sw step length_: Passo de Sw utilizado antes de verificar o novo valor de permeabilidade;
+- _Inject/Produce from_: Define por quais lados o fluido será injetado/produzido ao longo do eixo z, o mesmo lado pode tanto injetar como também produzir;
+- _Pore fill_: Determina qual mecanismo domina cada evento de preenchimento de poro individual;
+- _Lower/Upper box boundary_: Poros com distância relativa no eixo Z da borda até este valor do plano são considerados poros "à esquerda"/"à direita", respectivamente;
+- _Subresolution volume_: Considera que o volume contém essa fração de espaço poroso em subresolução que está sempre preenchido com água;
+- _Plot first injection cycle_: Se selecionado, o primeiro ciclo, injeção de óleo em um meio totalmente saturado de água, será incluído no gráfico de saída. A simulação será executada, independentemente da opção estar selecionada ou não;
+- _Create animation node_: Cria um nó de animação que pode ser usado na aba "Cycles Visualization";
+- _Keep temporary files_: Mantém os arquivos .vtu na pasta de arquivos temporários do GeoSlicer, um arquivo para cada etapa da simulação;
+- _Max subprocesses_: Quantidade máxima de subprocessos single-thread que devem ser executados; O valor recomendado para uma máquina ociosa é 2/3 do total de núcleos;
+
+Uma vez que temos uma vasta quantidade de parâmetros que podem ser modificados para modelar o experimento a partir da simulação, se torna útil variarmos tais parâmetros de forma mais sistemática para uma análise aprofundada da sua influência nos resultados obtidos.
+
+Para isso o usuário pode selecionar o botão "_Multi_" disponível em grande parte dos parâmetros, ao clicar em Multi, três caixas aparecem com opções de início, fim e passo, que podem ser usadas para rodar diversas simulações em uma tabela linearmente distribuída dos valores desses parâmetros. Se mais de um parâmetro é escolhido com múltiplos valores, simulações rodam com todas as combinações de parâmetros possíveis, isso pode aumentar consideravelmente a quantidade de simulações e o tempo para executar.
+
+Ao finalizar a execução do conjunto de simulações, o usuário pode realizar análises para entender as relações entre os resultados das simulações com os parâmetros escolhidos na aba _Krel EDA_.
+
+## _Mercury injection_
+
+Além das simulações de uma e duas fases, também temos disponível nesse módulo uma simulação do experimento de intrusão de Mercúrio.
+
+| ![Figura 12](../../assets/images/pnm/mercury.png) |
+|:----------------------------------------------------------------------------:|
+| Figura 12: Simulação de intrusão de Mercúrio. |
+
+A intrusão de mercúrio é um experimento no qual mercúrio líquido é injetado em uma amostra de rocha reservatório sob vácuo, com pressão crescente. O volume de mercúrio invadindo a amostra é medido em função da pressão de mercúrio. Uma vez que o ângulo de contato do mercúrio líquido com o vapor de mercúrio é aproximadamente independente do substrato, é possível utilizar modelos analíticos, como o modelo do feixe de tubos, para calcular a distribuição do tamanho dos poros da amostra.
+
+O ensaio de intrusão de mercúrio é relativamente acessível e sua principal relevância no contexto do PNM reside na capacidade de executar a simulação em uma amostra para a qual os resultados experimentais de curvas de Pressão Capilar por Intrusão de Mercúrio (MICP) estão disponíveis. Isso permite a comparação dos resultados para validar e calibrar a rede de poros extraída da amostra, que será usada nas simulações de uma e duas fases.
+
+Para facilitar as análises da atribuição dos raios dos poros sub-resolução, o código presente no GeoSlicer produzirá como saída, além dos gráficos obtidos pela simulação no OpenPNM, os gráficos das distribuições de raios de poros e gargantas e também das distribuições de volumes, separando em poros resolvidos (que não se alteram pela atribuição da subescala) e poros não resolvidos. Dessa forma, o usuário pode conferir se o modelo de subescala foi aplicado corretamente.
+
+## Modelo Subscala
+
+No caso da rede multiescalar, como os raios da subescala não podem ser determinados a partir da própria imagem, por estarem fora da resolução, é necessário definir um modelo para atribuição desses raios. Algumas opções disponíveis atualmente são:
+
+- _Fixed radius_: Todos os raios da subresolução tem o mesmo tamanho escolhido na interface;
+![Figura 13](../../assets/images/pnm/subscale_fixed.png)
+
+- _Leverett Function - Sample Permeability_: Atribui uma pressão de entrada com base na curva de J Leverett com base na permeabilidade da amostra;
+![Figura 14](../../assets/images/pnm/subscale_leverett.png)
+
+- _Leverett Function - Permeability curve_: Também utiliza a curva de J Leverett mas com uma curva definida para a permeabilidade ao invés de um valor;
+![Figura 15](../../assets/images/pnm/subscale_leverettCurve.png)
+
+- _Pressure Curve_ e _Throat Radius Curve_: Atribui os raios da subresolução com base na curva obtida por um experimento de injeção de mercúrio. Pode ser utilizado o dado da pressão de entrada pela fração do volume, ou então o raio equivalente em função da fração de volume;
+![Figura 16a](../../assets/images/pnm/subscale_pressure.png)![Figura 16b](../../assets/images/pnm/subscale_radius.png)
+
+O modelo de subescala escolhido não tem impacto nas simulações de redes uniescalares, uma vez que todos os raios já estão determinados.
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/PNM/cycles.md b/tools/deploy/GeoSlicerManual/docs/Modules/PNM/cycles.md
new file mode 100644
index 0000000..452f63e
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Modules/PNM/cycles.md
@@ -0,0 +1,25 @@
+# Cycles Visualization
+
+Esse módulo serve para controlar e visualizar as simulações de permeabilidade relativa criadas no módulo Pore Network Simulation com a opção "Create animation node" ativada.
+
+| |
+|:-----------------------------------------------------------------------:|
+| Figura 1: Entrada do nó de animação para visualização. |
+
+Ao selecionar o nó de animação, aparecerá na visualização 3D um modelo dos poros e ligações com setas nas regiões de inlet/outlet, indicando o sentido das invasões. Além disso os gráficos na seção "Information" mostraram as curvas do Krel e algumas informações extras.
+
+| |
+|:-----------------------------------------------------------------------:|
+| Figura 3: Curvas de permeabilidade relativa para o ciclo. |
+
+A partir da seção parâmetros na interface, o usuário pode então controlar a animação.
+
+| |
+|:-----------------------------------------------------------------------:|
+| Figura 3: Interface de seleção de parâmetros. |
+
+- Show zero log Krel: Coloca um valor não-nulo para os pontos na escala logarítmica;
+- Animations step: Seleciona um passo de tempo específico na animação;
+- Run animation: Atualiza incrementalmente o passo da simulação de forma automática;
+- Loop animation: Executa a atualização em loop, voltando ao início sempre que chegar ao fim;
+- Animation speed: Escolhe a velocidade da atualização automática;
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/PNM/krelEDA.md b/tools/deploy/GeoSlicerManual/docs/Modules/PNM/krelEDA.md
new file mode 100644
index 0000000..0ca4f07
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Modules/PNM/krelEDA.md
@@ -0,0 +1,91 @@
+# Krel EDA
+
+## EDA
+
+Para facilitar a análise do conjunto de simulações e entender como os diferentes parâmetros afetam os resultados obtidos, foi criado o módulo do Krel EDA.
+
+Após rodar diversas simulações usando o módulo Pore Network Simulation o usuário pode colocar como entrada a tabela com os resultados obtidos nesse módulo, para assim visualizar os gráficos da nuvem de curvas, e também fazer um pós-processamento dos seus resultados.
+
+| |
+|:-----------------------------------------------------------------------:|
+| Figura 1: Entrada do módulo Krel EDA. |
+
+Diversas ferramentas foram criadas para facilitar essas análises a partir de gráficos interativos.
+
+### Krel curves dispersion
+
+Ao selecionar o tipo de visualização como "Krel curves dispersion", será mostrado o gráfico de dispersão das curvas de krel das várias simulações, com a respectiva média dessas curvas.
+
+É possível escolher, através das caixas seletoras localizadas logo acima do gráfico, quais tipos de curva serão mostrados: curvas de drenagem ou embibição e curvas de água e óleo separadamente.
+
+| |
+|:-----------------------------------------------------------------------:|
+| Figura 2: Nuvem de curvas Krel. |
+
+Para entender como se dá a distribuição dessas curvas conforme algum dos parâmetros, pode ser selecionado em "Curves color scale" uma escala de cores para as curvas.
+
+#### Filtragem e adição de curvas de referência
+
+Também é possível filtrar a nuvem de curvas para mostrar apenas uma parte dos dados a partir do colapsável nomeado "Simulation filters":
+
+| |
+|:-----------------------------------------------------------------------:|
+| Figura 3: Nuvem de curvas Krel filtradas e com curva de referência. |
+
+Ali é possível adicionar um filtro com base em algum parâmetro, e também adicionar uma curva de referência, que pode ser o dado experimental por exemplo, para comparação.
+
+### Crossed error
+
+Esse tipo de visualização é indicado para comparar a correlação entre os erros nas medidas com algum parâmetro indicado pela escala de cores. A interface permite selecionar o erro de qual medida nos eixos x e y, e também qual parâmetro será indicado na escala de cores.
+
+| |
+|:-----------------------------------------------------------------------:|
+| Figura 4: Parâmetros da visualização de erro cruzado. |
+
+### Crossed parameters
+
+Esse tipo de visualização serve para comparar a correlação entre os parâmetros com o erro indicado pela escala de cores. A interface permite selecionar qual parâmetro será colocado nos eixos x e y, e também qual erro será indicado na escala de cores.
+
+| |
+|:-----------------------------------------------------------------------:|
+| Figura 5: Parâmetros da visualização de parâmetros cruzados. |
+
+### Parameters and Result correlation
+
+Nessa visualização, o usuário pode checar as correlações entre os resultados obtidos da simulação com os parâmetros colocados como entrada no algoritmo, assim pode-se saber quais dos parâmetros mais afetam os resultados.
+
+| |
+|:-----------------------------------------------------------------------:|
+| Figura 6: Correlação entre parâmetros e resultados. |
+
+### Parameters and Error correlation
+
+Da mesma forma, pode-se querer olhar para as correlações existentes entre os parâmetros e os erros da simulação. Esse tipo de visualização demonstra essa matriz de correlação.
+
+| |
+|:-----------------------------------------------------------------------:|
+| Figura 7: Correlação entre parâmetros e erros. |
+
+### Results selfcorrelation
+
+Para entender como os resultados se correlacionam entre si, ou seja, como a permeabilidade está sendo afetada pela saturação ou pela pressão obtidas na simulação, pode-se olhar para a matriz de autocorrelação dos resultados, apresentada nessa visualização.
+
+| |
+|:-----------------------------------------------------------------------:|
+| Figura 8: Matriz de autocorrelação dos resultados. |
+
+### Higher order interactions
+
+Na análise estatística, podemos também querer entender como são os coeficientes de confiabilidade da correlação, que representam interações de ordens mais altas do que a correlação. Estão disponíveis 3 tipos de visualização para interpretar essas dependência de ordem mais altas: "Second order interactions", "Second order interactions list", "Third order interactions list".
+
+| |
+|:-----------------------------------------------------------------------:|
+| Figura 9: Interações de segunda ordem. |
+
+## Import
+
+A aba Import pode ser usada para trazer resultados de uma tabela experimental (por exemplo) com curvas de krel, ao selecionar as colunas correspondentes a saturação, permeabilidade da água, permeabilidade do óleo e o ciclo, o usuário pode salvar uma tabela para ser usada no módulo EDA.
+
+| |
+|:-----------------------------------------------------------------------:|
+| Figura 10: Aba Import. |
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/PNM/production.md b/tools/deploy/GeoSlicerManual/docs/Modules/PNM/production.md
new file mode 100644
index 0000000..62a3762
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Modules/PNM/production.md
@@ -0,0 +1,29 @@
+# Production Prediction
+
+Esse módulo pode ser usado para estimar a quantidade de óleo que pode efetivamente ser extraído para uma dada amostra a partir da curva de permeabilidade relativa, usando a equação de Buckley-Leverett.
+
+## Single Krel
+
+A primeira opção disponível usa a curva de permeabilidade relativa construída para uma única simulação de duas fases.
+
+| |
+|:-----------------------------------------------------------------------:|
+| Figura 1: Parâmetros do módulo de produção. |
+
+Na interface, além da tabela com os resultados da simulação o usuário pode escolher os valores de viscosidade da água e do óleo que serão usados na estimativa, além de um fator de suavização da curva de krel.
+
+| |
+|:-----------------------------------------------------------------------:|
+| Figura 2: Curva de estimativa de produção para simulação única. |
+
+Os gráficos gerados correspondem então a curva da estimativa de produção de óleo (em volume produzido) com base na quantidade de água injetada. E abaixo a curva de permeabilidade relativa com indicação da onda de choque estimada.
+
+## Teste de sensibilidade
+
+A outra opção pode ser usada quando múltiplas curvas de permeabilidade relativa são geradas.
+
+Nesse caso uma nuvem de curvas será gerada e o algoritmo calcula também as previsões: otimista, pessimista e neutra.
+
+| |
+|:-----------------------------------------------------------------------:|
+| Figura 3: Curva de estimativa de produção para o teste de sensibilidade. |
diff --git a/tools/deploy/GeoSlicerManual/docs/Image Log/Instance Segmenter/instance_segmenter.md b/tools/deploy/GeoSlicerManual/docs/Modules/Quantification/instance_segmenter.md
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Image Log/Instance Segmenter/instance_segmenter.md
rename to tools/deploy/GeoSlicerManual/docs/Modules/Quantification/instance_segmenter.md
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/Quantification/segment_inspector.md b/tools/deploy/GeoSlicerManual/docs/Modules/Quantification/segment_inspector.md
new file mode 100644
index 0000000..4033403
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Modules/Quantification/segment_inspector.md
@@ -0,0 +1,150 @@
+# Segment Inspector
+
+Este módulo fornece vários métodos para analisar uma imagem segmentada. Particularmente, os algoritmos Watershed e Separate objects permitem fragmentar uma segmentação em várias partições, ou vários segmentos. Geralmente é aplicado à segmentação do espaço poroso para calcular as métricas de cada elemento poroso.
+A entrada é um nó de segmentação ou volume de labelmap, uma região de interesse (definida por um nó de segmentação) e a imagem/volume mestre. A saída é um labelmap onde cada partição (elemento poro) está em uma cor diferente, uma tabela com parâmetros globais e uma tabela com as diferentes métricas para cada partição.
+
+## Painéis e sua utilização
+
+| ![Figura 1](../../assets/images/thin_section/modulos/segment_inspector/interface.png) |
+|:-----------------------------------------------:|
+| Figura 1: Apresentação do módulo Segment Inspector. |
+
+
+### Principais opções:
+A interface do módulo Segment Inspector é composta por Inputs, Parameters e Output
+
+#### Single input
+
+| ![Figura 2](../../assets/images/thin_section/modulos/segment_inspector/inputs_single.png) |
+|:-----------------------------------------------:|
+| Figura 2: Apresentação dos inputs no módulo Segment Inspector. |
+
+ - _Segmentation_: Input para a segmentação utilizada na partição.
+
+ - _Region SOI_: Escolha Uma segmentação de interresse que contenha ao menos uma parte da segmentação utilizada em _Segmentation_.
+
+ - _Image_: Campo preenchido automaticamente com o nodo de referência da segmentação utilizada em _Segmentation_.
+
+#### Attributes
+
+| ![Figura 3](../../assets/images/thin_section/modulos/segment_inspector/attributes.png) |
+|:-----------------------------------------------:|
+| Figura 3: Atributos de segmentos no módulo Segment Inspector. |
+
+ - _Segments_: Segmentos contidos na segmentação selecionada em _Segmentation_. A lista informa sobre a visualização do segmento pelo icone do olho. Para a inicialização do metodo de fragmentação um segmento deve ser selecionado.
+
+ - _Calculate proportions_: Checkbox para apresentar as proporções de cada segmento na imagem.
+
+ - _Dimension(px)_: Apresenta as dimensões da imagem selecionada.
+
+### Parâmetros e Métodos
+
+#### Watershed
+
+O algoritmo de Watershed funciona simulando a expansão de "bacias hidrográficas" a partir de pontos marcados como mínimos locais. À medida que a "água" preenche os vales, ela define as fronteiras entre diferentes regiões. Essa abordagem é amplamente utilizada em aplicações onde é necessário separar objetos ou poros em materiais, aproveitando os contrastes entre regiões.
+
+| ![Figura 4](../../assets/images/thin_section/modulos/segment_inspector/watershed.png) |
+|:-----------------------------------------------:|
+| Figura 4: Watershed no módulo Segment Inspector. |
+
+ - _Size Filter(mm)_: Controla o alcance máximo de segmentação, influenciando diretamente o tamanho e a conectividade das regiões segmentadas. Valores pequenos são Usados quando você deseja segmentar muitos detalhes finos em contrapartida Valores grandes são usados quando o foco é em grandes áreas ou objetos conectados.
+
+ - _2D throat analysis(beta)_: Adiciona métricas de analise de gargantas 2d no report.
+
+ - _Smooth factor_: Parâmetro que ajusta o grau de suavidade nas bordas das regiões segmentadas, permitindo controle entre a preservação dos detalhes e a redução de ruído ou irregularidades. Com Fatores altos a segmentação será mais suave e simplificada, mas com perda de pequenos detalhes.
+
+ - _Minimun Distance_: parâmetro que determina a menor distância permitida entre dois máximos locais ou objetos segmentados. Um valor maior deste parâmetro fundirá objetos próximos, simplificando a segmentação, enquanto um valor menor permitirá a separação de objetos mais próximos, resultando em uma segmentação mais detalhada.
+
+ - _Orientation Line_: O parâmetro de orientação permite que o algoritmo alinhe-se adequadamente com as características da imagem, melhorando a precisão da segmentação
+
+#### Separate Objects
+
+ O método de segmentação por "Separate Objects
+" identifica regiões conectadas em uma matriz binária que representam objetos de informação. Este processo é especialmente útil em análise de porosidade, onde é importante distinguir diferentes regiões conectadas dentro de um volume.
+
+| ![Figura 5](../../assets/images/thin_section/modulos/segment_inspector/separate_objects.png) |
+|:-----------------------------------------------:|
+| Figura 5: Separate Objects no módulo Segment Inspector. |
+
+ - _Size Filter(mm)_: Controla o alcance máximo de segmentação, influenciando diretamente o tamanho e a conectividade das regiões segmentadas. Valores pequenos são Usados quando você deseja segmentar muitos detalhes finos em contrapartida Valores grandes são usados quando o foco é em grandes áreas ou objetos conectados.
+
+ - _Orientation Line_: O parâmetro de orientação permite que o algoritmo alinhe-se adequadamente com as características da imagem, melhorando a precisão da segmentação
+
+#### GPU Watershed
+
+A técnica de Deep Watershed combina o conceito tradicional de Watershed com redes neurais profundas para obter uma segmentação mais precisa e robusta. Utilizando a força de aprendizado profundo, o método aprimora a detecção de limites e objetos em cenários complexos, como a análise de materiais porosos com múltiplos níveis de sobreposição. Essa abordagem é particularmente eficaz para lidar com volumes tridimensionais e para realizar segmentações precisas em imagens ruidosas.
+
+| ![Figura 6](../../assets/images/thin_section/modulos/segment_inspector/gpu_watershed.png) |
+|:-----------------------------------------------:|
+| Figura 6: GPU Watershed no módulo Segment Inspector. |
+
+ - _Split Threshold(0-1)_: Controla o alcance máximo de segmentação, influenciando diretamente o tamanho e a conectividade das regiões segmentadas. Valores pequenos são usados quando você deseja segmentar muitos detalhes finos em contrapartida valores grandes são usados quando o foco é em grandes áreas ou objetos conectados.
+
+ - _2D throat analysis(beta)_: Adiciona métricas de analise de gargantas 2d no report.
+
+ - _Base volume (px)_: Esse parâmetro representa um valor base que pode ser relacionado ao tamanho ou à escala do volume que está sendo processado. Ele serve como uma referência para calcular a profundidade ou as camadas do volume que serão analisadas.
+
+ - _Intersection (px)_:Esse parâmetro é usado para ajustar o quanto as regiões dentro do volume podem se sobrepor umas às outras durante a segmentação.
+
+ - _Border (px)_: Esse parâmetro define o tamanho ou a espessura das bordas que serão consideradas ao calcular as camadas de profundidade no volume.
+
+ - _Background Threshold(0-1)_: Atua como um ponto de corte. Todos os valores abaixo desse limiar (threshold) são considerados como pertencentes ao fundo (ou background), enquanto valores acima do limiar são considerados como partes de objetos ou regiões significativas dentro da imagem ou volume.
+
+#### Transitions Analysis
+
+A Análise de Transições se concentra em examinar as mudanças entre regiões ou segmentos de uma imagem. Este método é empregado principalmente para estudar a mineralogia de amostras.
+
+| ![Figura 7](../../assets/images/thin_section/modulos/segment_inspector/transition_analysis.png) |
+|:-----------------------------------------------:|
+| Figura 7: Transitions Analysis no módulo Segment Inspector. |
+
+ - _Include Background_: Usa as dimensões totais da imagem de entrada para análise.
+
+#### Basic Petrophysics
+
+| ![Figura 8](../../assets/images/thin_section/modulos/segment_inspector/transition_analysis.png) |
+|:-----------------------------------------------:|
+| Figura 8: Transitions Analysis no módulo Segment Inspector. |
+
+ - _Include Background_: Usa as dimensões totais da imagem de entrada para análise.
+
+
+### Output
+
+Digite um nome para ser usado como prefixo para o objeto de resultados (mapa de rótulos onde cada partição (elemento de poro) está em uma cor diferente, uma tabela com parâmetros globais e uma tabela com as diferentes métricas para cada partição).
+
+### Propriedades / Métricas:
+
+1. __Label__: identificação do rótulo da partição.
+2. __mean__: valor médio da imagem/volume de entrada dentro da região da partição (poro/grão).
+3. __median__: valor mediano da imagem/volume de entrada dentro da região da partição (poro/grão).
+4. __stddev__: Desvio padrão do valor da imagem/volume de entrada dentro da região da partição (poro/grão).
+5. __voxelCount__: Número total de pixels/vóxels da região da partição (poro/grão).
+6. __area__: Área total da partição (poro/grão). Unidade: mm².
+7. __angle__: Ângulo em graus (entre 270 e 90) relacionado à linha de orientação (opcional, se nenhuma linha for selecionada, a orientação de referência é a horizontal superior).
+8. __max_feret__: Maior eixo de Feret. Unidade: mm.
+9. __min_feret__: Menor eixo de Feret. Unidade: mm.
+10. __mean_feret__: Média dos eixos mínimo e máximo.
+11. __aspect_ratio__: min_feret / max_feret.
+12. __elongation__: max_feret / min_feret.
+13. __eccentricity__: quadrado(1 - min_feret / max_feret), relacionado à elipse equivalente (0 ≤ e < 1), igual a 0 para círculos.
+14. __ellipse_perimeter__: Perímetro da elipse equivalente (elipse equivalente com eixos dados pelos eixos mínimo e máximo de Feret). Unidade: mm.
+15. __ellipse_area__: Área da elipse equivalente (elipse equivalente com eixos dados pelos eixos mínimo e máximo de Feret). Unidade: mm².
+16. __ellipse_perimeter_over_ellipse_area__: Perímetro da elipse equivalente dividido por sua área.
+17. __perimeter__: Perímetro real da partição (poro/grão). Unidade: mm.
+18. __perimeter_over_area__: Perímetro real dividido pela área da partição (poro/grão).
+19. __gamma__: Arredondamento de uma área calculado como 'gamma = perímetro / (2 * quadrado(PI * área))'.
+20. __pore_size_class__: Símbolo/código/id da classe do poro.
+21. __pore_size_class_label__: Rótulo da classe do poro.
+
+#### Definição das classes de poros:
+
+* __Microporo__: classe = 0, max_feret menor que 0,062 mm.
+* __Mesoporo muito pequeno__: classe = 1, max_feret entre 0,062 e 0,125 mm.
+* __Mesoporo pequeno__: classe = 2, max_feret entre 0,125 e 0,25 mm.
+* __Mesoporo médio__: classe = 3, max_feret entre 0,25 e 0,5 mm.
+* __Mesoporo grande__: classe = 4, max_feret entre 0,5 e 1 mm.
+* __Mesoporo muito grande__: classe = 5, max_feret entre 1 e 4 mm.
+* __Megaporo pequeno__: classe = 6, max_feret entre 4 e 32 mm.
+* __Megaporo grande__: classe = 7, max_feret maior que 32 mm.
+
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/Thin_section/AutoRegistration.md b/tools/deploy/GeoSlicerManual/docs/Modules/Thin_section/AutoRegistration.md
new file mode 100644
index 0000000..f168c2b
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Modules/Thin_section/AutoRegistration.md
@@ -0,0 +1,26 @@
+# Thin Section Automatic Registration
+
+O módulo Thin Section Auto Registration faz parte da categoria Registration e tem como objetivo registrar automaticamente imagens de seções delgadas, usando segmentações de volumes fixos e móveis. Ele permite definir imagens de entrada, configurar prefixos de saída e aplicar transformações para alinhar as imagens.
+
+## Painéis e sua utilização
+
+| ![Figura 1](../../assets/images/thin_section/modulos/auto_registration/interface.png) |
+|:-----------------------------------------------:|
+| Figura 1: Apresentação do módulo Auto Registration. |
+
+
+
+### Principais opções:
+A interface do módulo Auto Registration é composta por vários painéis, cada um projetado para simplificar o carregamento e o processamento de imagens QEMSCAN/RGB:
+
+ - _Fixed segmentation image_: Escolha a imagem de referência ou fixa.
+
+ - _Moving segmentation image_: Escolha a imagem que será transformada para se alinhar à imagem fixa.
+
+ - _Segments_: Escolha os segmentos da imagem a ser transformada para serem utilizados para otimização do registro.
+
+ - _Output prefix_: Defina um prefixo que será aplicado aos resultados, facilitando a organização e identificação dos arquivos de saída
+
+ - _Apply_: Inicia o processo de registro, aplicando transformações que alinham a segmentação móvel à segmentação fixa.
+
+ - _Cancel_: Interrompe o processo em qualquer momento, caso seja necessário.
\ No newline at end of file
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/Thin_section/Crop.md b/tools/deploy/GeoSlicerManual/docs/Modules/Thin_section/Crop.md
new file mode 100644
index 0000000..83c9a59
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Modules/Thin_section/Crop.md
@@ -0,0 +1,55 @@
+# Thin Section Crop
+
+O módulo Customized Crop Volume é uma ferramenta integrada ao GeoSlicer, projetada para permitir o recorte personalizado de volumes de imagem em coordenadas IJK, utilizando uma Região de Interesse (ROI) específica. O módulo é especialmente útil para focar em áreas específicas de volumes maiores, ajustando a dimensão do corte de acordo com as necessidades do usuário.
+
+## Painéis e sua utilização
+
+| ![Figura 1](../../assets/images/thin_section/modulos/crop/interface.png) |
+|:-----------------------------------------------:|
+| Figura 1: Apresentação do módulo Thin Section Crop. |
+
+### Principais opções:
+
+ - _Volume to be cropped_: Escolha a Imagem a ser Recortada.
+
+ - _Crop Size_: Definir o tamanho do recorte nas três dimensões (X, Y, Z), ajustando os limites da ROI de forma interativa. No caso de imagens @d o valor Z sempre será 1.
+
+ - _Crop/cancel_: Botões dedicados para iniciar o processo de recorte e cancelar as operações em andamento.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+_GeoSlicer_ module to crop a volume, as described in the steps bellow:
+
+1. Select the _Volume to be cropped_.
+
+2. Adjust the ROI in the slice views to the desired location and size.
+
+3. Click _Crop_ and wait for completion. The cropped volume will appear in the same directory as the original volume.
\ No newline at end of file
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/Thin_section/Export.md b/tools/deploy/GeoSlicerManual/docs/Modules/Thin_section/Export.md
new file mode 100644
index 0000000..c92798b
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Modules/Thin_section/Export.md
@@ -0,0 +1,19 @@
+# Thin Section Export
+
+O módulo Thin Section Export é usado para exportar diferentes tipos de dados, como imagens, mapas de rótulos e tabelas, em formatos como PNG, TIF, CSV e LAS. O módulo oferece uma interface gráfica onde o usuário pode escolher quais dados exportar, selecionar o formato desejado e definir a pasta de destino. Ele também mostra o progresso da exportação e permite cancelar o processo se necessário. O objetivo é facilitar a exportação de dados geológicos ou científicos de forma organizada.
+
+## Painéis e sua utilização
+
+| ![Figura 1](../../assets/images/thin_section/modulos/export/interface.png) |
+|:-----------------------------------------------:|
+| Figura 1: Apresentação do módulo Thin Section Export. |
+
+### Principais opções:
+
+ - _Explorer data_: Escolha a Imagem a ser Exportada.
+
+ - _Ignore directory structure_: Exporte todos os dados ignorando a estrutura de diretórios. Apenas um nó com o mesmo nome e tipo será exportado.
+
+ - _Export directory_: Caminho para os arquivos exportados
+
+ - _Export format_: Formatos como PNG, TIF, CSV e LAS são suportados
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/Thin_section/Fluxo PP PX.md b/tools/deploy/GeoSlicerManual/docs/Modules/Thin_section/Fluxo PP PX.md
new file mode 100644
index 0000000..bd4ef53
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Modules/Thin_section/Fluxo PP PX.md
@@ -0,0 +1,25 @@
+
+
+
Fluxo PP/PX
+
Este fluxo é utilizado para calcular métricas de poro a partir de uma imagem PP (polarização plana) e uma imagem PX (polarização cruzada).
+{% include-markdown "../FlowSteps/load_qemscan.md" %}
+{% include-markdown "../FlowSteps/soi.md" %}
+{% include-markdown "../FlowSteps/auto_label.md" %}
+
+Observação: como este fluxo não carrega uma foto de lâmina de referência, o algoritmo *watershed* usa apenas o segmento especificado como entrada.
+
+{% include-markdown "../FlowSteps/label_editor.md" %}
+{% include-markdown "../FlowSteps/finish.md" %}
+
+
+
+
+
Video: Executando o fluxo QEMSCAN
+
+
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/Thin_section/ImageTools.md b/tools/deploy/GeoSlicerManual/docs/Modules/Thin_section/ImageTools.md
new file mode 100644
index 0000000..c8cdd2c
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Modules/Thin_section/ImageTools.md
@@ -0,0 +1,45 @@
+# Thin Section Image Tools
+
+O módulo Thin Section Image Tools no GeoSlicer oferece um conjunto de funcionalidades voltadas para a manipulação e análise de imagens digitais de rochas. Este módulo serve como uma caixa de ferramentas versátil para usuários que precisam realizar operações como recorte, redimensionamento, ajustes de cor e filtragem de imagens, tudo dentro do ambiente GeoSlicer.
+
+## Painéis e sua utilização
+
+| ![Figura 1](../../assets/images/thin_section/modulos/image_tools/interface.png) |
+|:-----------------------------------------------:|
+| Figura 1: Apresentação do módulo Thin Section Image Tools. |
+
+### Principais opções:
+
+ - _Image_: Escolha a Imagem a ser modificada.
+
+ - _Tool_: Escolha a Ferramenta a ser utilizada.
+
+ - _Apply/Cancel_: Aplicar a ferramenta atual na imagem. Cancela o Processo Iniciado.
+
+ - _Undo/Redo_: Desfaz ou refaz os efeitos aplicados
+
+ - _Save/Reset_: Finaliza o processo impossibilitando o Undu/Redo ou desfaz todos os processos aplicados.
+
+### Brightness/Contrast:
+| ![Figura 2](../../assets/images/thin_section/modulos/image_tools/contrast.png) |
+|:-----------------------------------------------:|
+| Figura 2: Apresentação da interface para a opção Brightness/Contrast. |
+
+ - _Brightness_: Escolha o novo nivel de brilho da imagem.
+
+ - _Contrast_: Escolha o novo nivel de contraste da imagem.
+
+### Saturation:
+| ![Figura 3](../../assets/images/thin_section/modulos/image_tools/saturation.png) |
+|:-----------------------------------------------:|
+| Figura 3: Apresentação da interface para a opção Saturation. |
+
+ - _Saturation_: Escolha o novo nivel de Saturação de cores da imagem.
+
+### Histogram Equalization:
+
+A Equalização de Histograma é uma técnica de processamento de imagem que ajusta o contraste, redistribuindo os níveis de intensidade de forma mais uniforme. Isso melhora a visibilidade dos detalhes em áreas com baixo contraste, tornando a imagem mais clara e informativa.
+
+### Shading Correction:
+
+A Correção de Sombras (Shading Correction) é uma técnica de processamento de imagem que remove variações de iluminação e sombras indesejadas causadas por condições de iluminação desiguais durante a captura da imagem. Esse processo normaliza a intensidade da imagem, melhorando a uniformidade e a precisão dos dados
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/Thin_section/Loader.md b/tools/deploy/GeoSlicerManual/docs/Modules/Thin_section/Loader.md
new file mode 100644
index 0000000..4a4a1c3
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Modules/Thin_section/Loader.md
@@ -0,0 +1,24 @@
+# Thin Section Loader
+
+O módulo ThinSectionLoader foi projetado para carregar e processar com eficiência imagens de seção fina no ambiente _Thin section_. Ele importa imagens PNG, JPG/JPEG, BMP, GIF, e TIFF. Ele integra detecção automatizada de escala usando Tesseract OCR, garantindo uma estimativa precisa do tamanho do pixel. O módulo também inclui opções avançadas para manipulação de imagens sem perdas e permite ajustes manuais nos parâmetros de espaçamento das imagens, tornando-o adaptável a diversas necessidades de análise geológica.
+
+
+## Painéis e sua utilização
+
+| ![Figura 1](../../assets/images/thin_section/modulos/loader/interface.png) |
+|:-----------------------------------------------:|
+| Figura 1: Apresentação do módulo Thin Section Loader. |
+
+### Principais opções:
+
+ - _Input file_: Escolha o caminho da Imagem a ser importada. As extenções mais usadas são:PNG, JPG/JPEG, BMP, GIF, e TIFF.
+
+ - _Advanced Lossless_: Utilizar uma opção de compressão lossless garante que a qualidade original da imagem seja preservada integralmente, sem perda de dados durante o processo.
+
+ - _Scale size(px)_: Seção para definir a faixa de pixels usada na proporção de pixels por milímetro. O ícone de régua à direita permite adicionar uma marcação na escala da imagem, facilitando essa definição.
+
+ - _Scale size(mm)_: Seção para definir o tamanho em milímetros da faixa de pixels selecionada. Se a régua foi utilizada na escala da imagem, o valor deverá ser correspondente.
+
+ - _Pixel size(mm)_: Proporção entre pixels/mm pode ser definida ou resultante da medida dos campos _Scale size(px)_ e _Scale size(mm)_
+
+ - _Save pixel size_: Definida a proporção entre pixels/mm o botão atrela o valor à imagem.
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/Thin_section/MultipleImageAnalysis.md b/tools/deploy/GeoSlicerManual/docs/Modules/Thin_section/MultipleImageAnalysis.md
new file mode 100644
index 0000000..df53f82
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Modules/Thin_section/MultipleImageAnalysis.md
@@ -0,0 +1,57 @@
+# Multiple Image Analysis
+
+Crie diversos tipos de análise a partir dos conjuntos de dados incluídos em vários projetos do GeoSlicer. Para utilizá-la, forneça um caminho de diretório que contenha múltiplas pastas de projetos do GeoSlicer. As pastas de projeto inclusas devem ser adequadas para o seguinte padrão:
+
+`_`
+
+### Exemplo de Estrutura de Diretórios
+
+- **Projetos**
+ - `TAG_3000,00m`
+ - `TAG_3000,00m.mrml`
+ - **Data**
+ - `TAG_3010,00m`
+ - `TAG_3010,00m.mrml`
+ - **Data**
+
+## Análises
+
+### 1. Histograma por Profundidade
+
+Gera uma curva de histograma para cada valor de profundidade, com base em um parâmetro específico do relatório do plugin 'Segment Inspector'.
+
+| ![Figura 1](../../assets/images/thin_section/modulos/multiple_image_analysis/MIA-HiD.png) |
+|:-----------------------------------------------:|
+| Figura 1: Interface da análise de Histograma por Profundidade. |
+
+#### Opções de Configuração
+
+- **Histogram Input Parameter**: Define o parâmetro utilizado para criar o histograma.
+- **Histogram Weight Parameters**: Define o parâmetro usado como peso no histograma.
+- **Histogram Bins**: Especifica o número de bins no histograma.
+- **Histogram Height**: Ajusta a altura visual da curva do histograma.
+- **Normalize**: Aplica normalização aos valores do histograma. A normalização pode ser baseada pelo número de elementos ou por algum dos parâmetros disponíveis no relatório do Segment Inspector (e.g., Área de Voxel, Área de ROI).
+
+
+### 2. Média por Profundidade
+
+Calcula o valor médio para cada profundidade com base em um parâmetro específico do relatório do plugin 'Segment Inspector'.
+
+#### Opções de Configuração
+
+- **Mean Input Parameter**: Define o parâmetro a ser analisado.
+- **Mean Weight Parameters**: Define o parâmetro utilizado como peso durante a análise.
+
+
+| ![Figura 2](../../assets/images/thin_section/modulos/multiple_image_analysis/MIA-MiD.png) |
+|:-----------------------------------------------:|
+| Figura 2: Interface da análise de Média por Profundidade. |
+
+### 3. Petrofísica Básica
+
+Gera uma tabela que inclui os parâmetros relacionados ao método de Petrofísica Básica do plugin 'Segment Inspector', organizados por valor de profundidade.
+
+
+| ![Figura 3](../../assets/images/thin_section/modulos/multiple_image_analysis/MIA-BP.png) |
+|:-----------------------------------------------:|
+| Figura 3: Interface da análise de Petrofísica Básica. |
\ No newline at end of file
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/Thin_section/PoreStats.md b/tools/deploy/GeoSlicerManual/docs/Modules/Thin_section/PoreStats.md
new file mode 100644
index 0000000..7a1f7dd
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Modules/Thin_section/PoreStats.md
@@ -0,0 +1,188 @@
+# Pore Stats: estatísticas e propriedades de poros e partículas em seção delgada
+
+O módulo _Pore Stats_ oferece recursos para o cálculo de propriedades geológicas de poros em lotes de imagens de seção delgada de rocha, bem como estatísticas descritivas relacionadas. Dado um diretório de entrada contendo imagens relativas a um mesmo poço, o módulo é capaz de segmentar a região porosa, separar os diferentes poros, calcular diferentes propriedades de cada um e salvar relatórios e imagens sumarizando os resultados.
+
+## Interface e funcionamento
+
+O módulo está disponível no ambiente _Thin Section_, na aba _Segmentation_, sub-aba _Pore Stats_. A Figura 1 ilustra uma visualização geral do fluxo de funcionamento do módulo, enquanto a Figura 2 mostra a interface do módulo no GeoSlicer e aponta, para cada recurso, a subseção desta seção que o descreve.
+
+| ![Figura 1](../../assets/images/thin_section/modulos/pore_stats/overview.png) |
+|:-----------------------------------------------:|
+| Figura 1: visualização geral do fluxo de funcionamento do módulo. |
+
+
+| ![Figura 2](../../assets/images/thin_section/modulos/pore_stats/interface.png) |
+|:-----------------------------------------------:|
+| Figura 2: módulo *Pore Stats*. |
+
+### 1. Entrada
+
+O módulo é desenvolvido de modo a iterar sobre todas as imagens válidas que encontrar num dado diretório de entrada. Uma imagem é considerada válida caso o arquivo tenha formato PNG, JPEG ou TIFF e seu nome respeite o seguinte padrão: `_(-índice-opcional)_<…>_c1.`. O sufixo `c1` se refere à imagens em polarização direta (PP). Opcionalmente, contrapartes em polarização cruzada (PX) que constem no mesmo diretório também podem ser usadas para auxiliar em operações específicas, devendo ter o sufixo `c2`. Segue um exemplo de diretório de entrada:
+
+```
+Diretório_entrada
+ |__ ABC-123_3034.00m_2.5x_c1.jpg
+ |__ ABC-123_3034.00m_2.5x_c2.jpg
+ |__ ABC-123_3080.0-2m_c1.jpg
+ |__ ABC-123_3080.0-2m_c2.jpg
+ |__ ABC-123_3080.0m_c1.jpg
+ |__ ABC-123_3080.0m_c2.jpg
+ |__ ABC-123_3126.65m_2.5x_TG_c1.jpg
+ |__ ABC-123_3126.65m_2.5x_TG_c2.jpg
+```
+
+O exemplo descreve um diretório de entrada com 4 imagens JPEG do poço "ABC-123", correspondentes às profundidades de 3034, 3080 e 3126,65 metros, em ambas as versões PP/c1 e PX/c2. Como existem duas variações correspondentes à mesma profundidade (3080m), um índice opcional ("-2") consta em uma delas. Entre as informações de profundidade e polarização, algumas informações adicionais podem existir entre *underlines* (como "\_2.5x\_" e "\_TG\_").
+
+Além do diretório de entrada, também é necessário especificar a escala das imagens em mm, ou seja, quantos mm são representados pela distância entre um pixel e o pixel subsequente.
+
+O diretório de entrada pode ser definido através do seletor _Input directory_ na interface do módulo, dentro da seção _Inputs_ da interface. Já a escala das imagens deve ser especificada no campo _Pixel size (mm)_, na seção _Parameters_.
+
+
+| ![Figura 3](../../assets/images/thin_section/modulos/pore_stats/input.png) |
+|:-----------------------------------------------:|
+| Figura 3: seletor de diretório de entrada. |
+
+
+| ![Figura 4](../../assets/images/thin_section/modulos/pore_stats/pixel_size.png) |
+|:-----------------------------------------------:|
+| Figura 4: campo de escala mm/pixel. |
+
+
+### 2. Segmentação de poros
+
+Uma vez que uma imagem é carregada, sua região porosa é segmentada através dos **[modelos pré-treinados](../../Segmenter/Automatic/automatic_thinSection.md)**
+
+
+ disponíveis no GeoSlicer. Neste módulo, 3 modelos são disponibilizados:
+
+* Modelo bayesiano de _kernel_ pequeno;
+* Modelo bayesiano de _kernel_ grande;
+* Modelo neural convolucional U-Net.
+
+Escolha o modelo através do seletor _Input model_ na seção _Classifier_. A caixa de informações abaixo do seletor pode ser expandida para maiores detalhes sobre cada modelo.
+
+| ![Figura 5](../../assets/images/thin_section/modulos/pore_stats/models.png) |
+|:-----------------------------------------------:|
+| Figura 5: seletor de modelo de segmentação de poros. |
+
+### 3. Separação de fragmentos
+
+Muitas imagens possuem grandes regiões "vazias", preenchidas por resina de poro, que são detectadas pelo segmentador mas que não correspondem de fato à porosidade da rocha, mas apenas à região em volta de seu(s) fragmento(s) (vide exemplo da Figura 1). Em alguns casos, não todos mas apenas os _N_ maiores fragmentos da seção de rocha interessam. Para isolar os fragmentos úteis da rocha, a seguinte sequência de operações é aplicada:
+
+* Primeiramente, o maior fragmento, correspondente à toda área da seção, é isolado das bordas da imagem;
+* Então, toda porosidade detectada que toque a borda da imagem é também descartada, pois é interpretada como resina de poro visível ao redor da área útil da rocha;
+* Por fim, caso seja interessante apenas os _N_ maiores fragmentos, o tamanho (em pixeis) de cada fragmento da área útil restante é medido e apenas os _N_ maiores são mantidos.
+
+O módulo executa a separação de fragmentos automaticamente. Porém, é possível limitar a análise aos _N_ maiores marcando a caixa de seleção _Limit maximum number of fragments_, na seção _Parameters_, e definindo o valor de _N_ (entre 1 e 20).
+
+
+| ![Figura 6](../../assets/images/thin_section/modulos/pore_stats/max_frags.png) |
+|:-----------------------------------------------:|
+| Figura 6: limitador de fragmentos a serem analisados, do maior para o menor. |
+
+
+### 4. Limpeza de poros
+
+Este algoritmo é responsável por aplicar duas operações de "limpeza" na segmentação de poros convencional, que se não realizadas podem impactar negativamente nos resultados finais.
+
+**Remoção de poros espúrios**
+
+Os segmentadores de poro atuais do GeoSlicer tendem a gerar detecções espúrias em pequenas regiões compreendidas por rocha mas que por efeitos de iluminação/resolução/afins têm coloração parecida com a da resina azul de poro. O módulo executa um modelo capaz de reconhecer essas detecções e diferencia-las das corretas, com base nos valores de pixel dentro de um intervalo em torno do centróide de cada segmento. Todos os poros espúrios detectados são descartados.
+
+**Incorporação de bolhas e resíduos na resina de poro**
+
+É comum que se formem na resina de poro algumas bolhas de ar e resíduos relacionados. Os segmentadores não detectam esses artefatos, não interpretando-os como área de poro, o que influencia no tamanho e quantidade dos poros detectados. Este módulo visa "limpar" a resina, incluindo essas bolhas e resíduos ao corpo do poro correspondente. Basicamente, alguns critérios devem ser atendidos para que uma região da imagem seja interpretada como bolha/resíduo:
+
+1. Ter cor branca ou ter cor azul com pouca intensidade e saturação: em geral, as bolhas são brancas ou, quando cobertas de material, têm um tom de azul quase negro. Os resíduos que eventualmente circundam as bolhas também tem um nível de azul pouco intenso;
+2. Tocar na resina de poro: a transição entre a resina e os artefatos é normalmente direta e suave. Como o modelo de segmentação de poro detecta bem a região de resina, o artefato precisa tocar nessa região. Consequentemente, o algoritmo atual não consegue detectar casos menos comuns em que o artefato tome 100% da área do poro;
+3. Ser pouco visível na imagem PX/c2 (se disponível): alguns elementos da rocha podem ser parecidos com os artefatos e também ter contato com a resina. Porém, no geral, os artefatos são pouco ou nada visíveis nas imagens PX/c2, enquanto os demais elementos são geralmente notáveis. Este critério se faz mais efetivo quanto melhor for o registro (alinhamento espacial) entre as imagens PP e PX. O algoritmo tenta corrigir o registro das imagens automaticamente alinhando os centros das imagens. Esta etapa pode ser precedida por uma operação de enquadramento da área útil da rocha, descartando bordas excedentes.
+
+As operações de limpeza são recomendadas, porém podem levar um tempo. Por isso, é possível desabilitá-las desmarcando as caixas de seleção _Remove spurious_ para remoção de poros espúrios e _Clean resin_ para limpeza da resina, na seção _Parameters_. Caso esta última esteja habilitada, duas outras opções são disponibilizadas:
+
+* _Use PX_ para usar a imagem PX para análise do critério 3;
+* _Smart registration_ para decisão automática entre realizar ou não o enquadramento antes do registro automático. Não é recomendada caso a imagens já sejam naturalmente registradas.
+
+
+| ![Figura 7](../../assets/images/thin_section/modulos/pore_stats/pore_cleaning.png) |
+|:-----------------------------------------------:|
+| Figura 7: opções de limpeza de poros espúrios e artefatos na resina de poro |
+
+
+### 5. Separação de instâncias e cálculo de propriedades geológicas
+
+Uma vez que a região porosa esteja devidamente segmentada e "limpa", o recurso de **[inspeção](../../Inspector/Watershed/estudos_de_porosidade.md)**
+
+
+
+do GeoSlicer é utilizado para separação da segmentação em diferentes instâncias de poros e cálculo das propriedades geológicas de cada um. As seguintes propriedades são computadas:
+
+* Área (mm²);
+* Ângulo (°);
+* Máximo diâmetro de Feret (mm);
+* Mínimo diâmetro de Feret (mm);
+* Razão de aspecto;
+* Alongamento;
+* Excentricidade;
+* Perímetro da elipse (mm);
+* Área da elipse (mm²);
+* Elipse: perímetro sobre área (1/mm);
+* Perímetro (mm);
+* Perímetro sobre área;
+* _Gamma_.
+
+Dentro da seção _Parameters_, escolha o método de separação através do seletor _Partitioning method_, entre as opções _Islands_ para separação por conectividade simples de pixeis e _Over-Segmented Watershed_ para aplicação do algoritmo SNOW. Ambas as opções permitem filtragem extra de detecções espúrias através da escolha de valor mínimo aceitável (_Size Filter (mm)_) para o tamanho do maior eixo (diâmetro de Feret) da detecção. A seleção do _watershed_ disponibiliza as seguintes opções extras:
+
+* _2D Throat analysis (beta)_: caixa de seleção que permite incluir a análise de garganta de poros nos relatórios de saída da inspeção;
+* _Smooth Factor (mm)_: fator que regula a criação de mais ou menos partições. Valores pequenos são recomendados;
+* _Minimum Distance_: distância padrão mínima entre os picos de segmentação (pontos mais distantes das bordas) para serem considerados como pertencentes a diferentes instâncias.
+
+| ![Figura 8](../../assets/images/thin_section/modulos/pore_stats/inspector.png) |
+|:-----------------------------------------------:|
+| Figura 8: opções de separação da região porosa em diferentes instâncias de poros e cálculo de suas propriedades geológicas. |
+
+Depois de todos os parâmetros e opções de entrada definidos, pressione o botão _Apply_ para geração dos resultados.
+
+### 6. Saída
+
+Um diretório de saída também deve ser especificado. Nele, é criado um sub-diretório _pores_, dentro do qual é criada uma pasta para cada imagem processada, herdando o nome da imagem. Dentro dessa pasta, são criados três arquivos:
+
+* `AllStats__pores.xlsx`: planilha contendo os valores das propriedades geológicas de cada instância detectada;
+* `GroupsStats__pores.xlsx`: agrupa as instâncias detectadas por similaridade de área e disponibliza diversas estatísticas descritivas calculadas sobre as propriedades desses grupos;
+* `.png`: imagem que destaca as instâncias detectadas na imagem original colorindo-as aleatoriamente.
+
+Em cada sub-diretório também é criada uma pasta LAS, contendo arquivos .las que sumarizam estatísticas descritivas das instâncias do poço inteiro, separadas por profundidade.
+
+Finalmente, também são geradas imagens netCDF dos resultados. Elas estarão contidas no sub-diretório _netCDFs_.
+
+Exemplo:
+
+```
+Diretório_saída
+ |__ pores
+ | |__ LAS
+ | | |_ las_max.las
+ | | |_ las_mean.las
+ | | |_ las_median.las
+ | | |_ las_min.las
+ | | |_ las_std.las
+ | |__ ABC-123_3034.00m_2.5x_c1
+ | | |__ AllStats_ABC-123_3034.00m_2.5x_c1_pores.xlsx
+ | | |__ GroupsStats_ABC-123_3034.00m_2.5x_c1_pores.xlsx
+ | | |__ ABC-123_3034.00m_2.5x_c1.png
+ | |__ ...
+ |__ netCDFs
+ |__ ABC-123_3034.00m_2.5x_c1.nc
+ |__ ...
+```
+
+Defina o diretório de saída através no seletor _Output directory_ na seção _Output_. Caso inexistente, o diretório é criado automaticamente.
+
+| ![Figura 9](../../assets/images/thin_section/modulos/pore_stats/output.png) |
+|:-----------------------------------------------:|
+| Figura 9: seletor do diretório de saída. |
+
+Escolha se deseja ou não gerar os relatórios de saída através das caixas de seleção _Export images_, _Export sheets_ e _Export LAS_ na seção _Parameters_. Caso marcadas, garantem respectivamente a geração da imagem ilustrando as instâncias, das planilhas de propriedades e estatísticas e dos arquivos LAS de descrição do poço.
+
+| ![Figura 10](../../assets/images/thin_section/modulos/pore_stats/export.png) |
+|:-----------------------------------------------:|
+| Figura 10: exportação opcional das ilustrações das instâncias detectadas, das planilhas de propriedades e estatísticas e dos arquivos LAS de estatísticas por profundidade. |
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/Thin_section/QemscanLoader.md b/tools/deploy/GeoSlicerManual/docs/Modules/Thin_section/QemscanLoader.md
new file mode 100644
index 0000000..8681c60
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Modules/Thin_section/QemscanLoader.md
@@ -0,0 +1,25 @@
+# Qemscan Loader
+
+O módulo QEMSCAN Loader foi projetado especificamente para carregar e processar imagens QEMSCAN dentro do ambiente de Seção Delgada do GeoSlicer. Este módulo simplifica o processo de importação e visualização de dados complexos do QEMSCAN, oferecendo recursos como configurações personalizáveis de tamanho de pixel e tabelas de cores integradas para identificação eficiente de minerais.
+
+## Painéis e sua utilização
+
+| ![Figura 1](../../assets/images/thin_section/modulos/qemscan_loader/interface.png) |
+|:-----------------------------------------------:|
+| Figura 1: Apresentação do módulo Qemscan Loader. |
+
+### Principais opções:
+A interface do módulo QEMSCAN Loader é composta por vários painéis, cada um projetado para simplificar o carregamento e o processamento de imagens QEMSCAN:
+
+ - _Input file_: Este input permite selecionar o diretório que contém seus arquivos de imagem QEMSCAN.
+
+ - _Lookup color table_: Permite aos usuários criar e aplicar seus próprios mapeamentos de cores aos dados minerais ou a escolha entre um conjunto de tabelas de cores .csv predefinidas para atribuir cores a diferentes minerais com base em sua composição.
+
+ - _Add new_: Opção de permitir que o carregador procure um arquivo CSV no mesmo diretório do arquivo QEMSCAN que está sendo carregado. Você também tem a opção de caixa de seleção para utilizar a tabela de "Cores minerais padrão". **[Cores minerais padrão](../../../../Resources/QEMSCAN/LookupColorTables/Default%20mineral%20colors.csv)**
+
+
+
+
+ - _Pixel size(mm)_: Seção para definir a razão em px/milímetros. Se a imagem complementar em RGB ja foi importada, o valor deverá ser correspondente.
+
+ - _Load Qemscam_: Carregar _QEMSCANs_ .
\ No newline at end of file
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/Thin_section/Registration.md b/tools/deploy/GeoSlicerManual/docs/Modules/Thin_section/Registration.md
new file mode 100644
index 0000000..bddf55e
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Modules/Thin_section/Registration.md
@@ -0,0 +1,62 @@
+
+# Thin Section Manual Registration
+
+O módulo Thin Section Registration é projetado para registrar imagens de cortes delgados e QEMSCAN, permitindo ao usuário alinhar as imagens através da adição de marcos (landmarks) em ambas. Após a seleção dos marcos correspondentes nas imagens fixas e móveis, o módulo aplica transformações para garantir um registro preciso.
+
+## Inicialização
+
+| ![Figura 1](../../assets/images/thin_section/modulos/manual_registration/intro_interface.png) |
+|:-----------------------------------------------:|
+| Figura 1: Apresentação do módulo Manual Registration. |
+
+### Principais opções:
+A interface do módulo Manual Registration é composta por vários painéis, cada um projetado para simplificar o carregamento e o processamento de imagens QEMSCAN/RGB:
+
+ - _Select images to register_: Este botão inicia a seleção de imagens para registro.
+
+ - _Fixed image_: Escolha a imagem de referência ou fixa.
+
+ - _Moving_: Escolha a imagem que será transformada para se alinhar à imagem fixa.
+
+ - _Apply/Cancel_: Aceite ou cancele as escolhas de imagem.
+
+## Painéis e sua utilização
+
+| ![Figura 2](../../assets/images/thin_section/modulos/manual_registration/interface.png) |
+|:-----------------------------------------------:|
+| Figura 2: Apresentação do módulo Manual Registration. |
+
+
+
+### Principais opções:
+A interface do módulo Manual Registration é composta por vários painéis, cada um projetado para simplificar o carregamento e o processamento de imagens QEMSCAN/RGB:
+
+#### Vizualization
+
+| ![Figura 3](../../assets/images/thin_section/modulos/manual_registration/views.png) |
+|:-----------------------------------------------:|
+| Figura 3: Apresentação das views do módulo Manual Registration. |
+
+ - _Display_: Checkboxes para ferramentas de visualização. _Fixed image_: Apresentar a view com a imagem fixa, _Moving image_: Apresentar a view com a imagem que será transformada, _Transformed_: Apresentar a view interativa com a imagem transformada e _Reveal cursor_: Apresentar a regiao de transformação na view com a imagem transformada
+
+ - _Fade_: Escolha a transparência da imagem transformada na view interativa com a imagem transformada.
+
+ - _Rock/Flicker_: Efeitos de balanco ou de variação de transparência para a imagem transformada na view interativa com a imagem transformada.
+
+ - _Views_: _ZoomIn/Out_: Escolha o nível de zoom das views. _Fit_: Reseta o nível de zoom das views para apresentar toda a imagem.
+
+
+#### Landmarks
+
+| ![Figura 4](../../assets/images/thin_section/modulos/manual_registration/landmarks.png) |
+|:-----------------------------------------------:|
+| Figura 4: Apresentação da interface de landmarks. |
+
+- _Add_ ![](../../assets/images/thin_section/modulos/manual_registration/icon_add.png): Adicionar um ponto de referencia que será reproduzido nas views com a imagem fixa e na imagem que será transformada.
+
+- ![](../../assets/images/thin_section/modulos/manual_registration/icon_trash.png): Remover um ponto de referencia que será reproduzido nas views com a imagem fixa e na imagem que será transformada.
+
+- ![](../../assets/images/thin_section/modulos/manual_registration/icon_active.png)/ ![](../../assets/images/thin_section/modulos/manual_registration/icon_inactive.png): Ativa/Desativa a edição da posição um ponto de referencia que será reproduzido nas views com a imagem fixa e na imagem que será transformada. Somente uma pode ser editada por vez.
+
+- _Finish/Cancel registration_: Termine ou cancele as edições de registro. Em caso de finalização, utilize a imagem transformada para os próximos processos.
+
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/Thin_section/SegmentEditor.md b/tools/deploy/GeoSlicerManual/docs/Modules/Thin_section/SegmentEditor.md
new file mode 100644
index 0000000..85b73bf
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Modules/Thin_section/SegmentEditor.md
@@ -0,0 +1,225 @@
+# Segment Editor
+
+Este módulo é usado para especificar segmentos (estruturas de interesse) em imagens 2D/3D/4D. Algumas das ferramentas imitam uma interface de pintura, como o Photoshop ou o GIMP, mas operam em matrizes 3D de voxels em vez de pixels 2D. O módulo oferece edição de segmentos sobrepostos, exibição tanto em visualizações 2D quanto 3D, opções de visualização detalhadas, edição em visualizações 3D, criação de segmentações por interpolação ou extrapolação em algumas fatias, e edição em fatias em qualquer orientação.
+
+O Segment Editor não edita volumes de mapas de rótulos, mas as segmentações podem ser facilmente convertidas para/partir de volumes de mapas de rótulos usando a seção Explorer e utilizando o menu do botão secundário do mouse.
+
+## Segmentação e Segmento
+
+O resultado de uma segmentação é armazenado no nó segmentation no GeoSlicer. Um nó de segmentação consiste em vários segmentos.
+
+Um segmento especifica a região para uma estrutura única. Cada segmento possui um número de propriedades, como nome, cor de exibição preferencial, descrição do conteúdo (capaz de armazenar entradas codificadas padrão DICOM) e propriedades personalizadas. Os segmentos podem se sobrepor no espaço.
+
+## Representação de rótulos em mapa binário
+
+A representação em mapa binário de rótulos é provavelmente a representação mais comumente usada porque é a mais fácil de editar. A maioria dos softwares que utilizam essa representação armazena todos os segmentos em uma única matriz 3D, portanto, cada voxel pode pertencer a um único segmento: os segmentos não podem se sobrepor. No GeoSlicer, a sobreposição entre segmentos é permitida. Para armazenar segmentos sobrepostos em mapas binários, os segmentos são organizados em camadas. Cada camada é armazenada internamente como um volume 3D separado, e um volume pode ser compartilhado entre vários segmentos não sobrepostos para economizar memória.
+
+Em uma segmentação com a representação de origem definida como mapa binário, cada camada pode ter geometria diferente (origem, espaçamento, direções dos eixos, extensões) temporariamente - para permitir mover segmentos entre segmentações sem perda de qualidade desnecessária (cada reamostragem de um mapa binário pode levar a pequenas mudanças). Todas as camadas são forçadas a ter a mesma geometria durante certas operações de edição e quando a segmentação é salva em arquivo.
+
+## Painéis e sua utilização
+
+| ![Figura 1](../../assets/images/thin_section/modulos/segment_editor/segment_editor_fig1.png) |
+|:-----------------------------------------------:|
+| Figura 1: Apresentação do módulo segment editor. |
+
+
+### Principais opções:
+
+ - Segmentação: escolha a segmentação a ser editada.
+
+ - Volume de origem: escolha o volume a ser segmentado. O volume de origem selecionado na primeira vez após a segmentação ser criada é usado para determinar a geometria de representação do labelmap da segmentação (extensão, resolução, direções dos eixos, origem). O volume de origem é usado por todos os efeitos do editor que utilizam a intensidade do volume segmentado (por exemplo, limite, rastreamento de nível). O volume de origem pode ser alterado a qualquer momento durante o processo de segmentação.
+
+ - Adicionar: Adicione um novo segmento à segmentação e selecione-o.
+
+ - Remover: selecione o segmento que deseja excluir e clique em Remover segmento para excluir da segmentação.
+
+ - Mostrar 3D: exiba sua segmentação no visualizador 3D. Este é um botão de alternância. Quando ativado, a superfície é criada e atualizada automaticamente conforme o usuário segmenta. Quando desligado, a conversão não é contínua e o processo de segmentação é mais rápido. Para alterar os parâmetros de criação de superfície: clique na seta ao lado do botão siga para a opção "Smoothing factor" e na barra de valor para editar um valor de parâmetro de conversão. Definir o fator de suavização como 0 desativa a suavização, tornando as atualizações muito mais rápidas. Defina o fator de suavização como 0,1 para suavização fraca e 0,5 ou maior para suavização mais forte.
+
+## Tabela de segmentos
+
+Esta tabela exibe a lista de todos os segmentos.
+
+### Colunas da tabela:
+
+- Visibilidade (ícone de olho): Alterna a visibilidade do segmento. Para personalizar a visualização: abra os controles de visualização de fatia (clique nos ícones de botão e seta dupla na parte superior de um visualizador de fatia) ou vá para o módulo Segmentações.
+
+- Amostra de cores: defina a cor e atribua o segmento à terminologia padronizada.
+
+- Estado (ícone de bandeira): Esta coluna pode ser usada para definir o status de edição de cada segmento que pode ser usado para filtrar a tabela ou marcar segmentos para processamento posterior.
+Não iniciado: estado inicial padrão, indica que a edição ainda não ocorreu.
+Em andamento: quando um segmento “não iniciado” é editado, seu estado é automaticamente alterado para este
+Concluído: o usuário pode selecionar manualmente este estado para indicar que o segmentat está completo
+Sinalizado: o usuário pode selecionar manualmente esse estado para qualquer finalidade personalizada, por exemplo, para chamar a atenção de um revisor especialista para o segmento
+
+## Seção de efeitos
+
+| ![Figura 2](../../assets/images/thin_section/modulos/segment_editor/segment_editor_fig2.png) |
+|:-----------------------------------------------:|
+| Figura 2: Seção de efeitos do segment editor. |
+
+
+- Barra de ferramentas de efeitos: Selecione o efeito desejado aqui. Veja abaixo mais informações sobre cada efeito.
+
+- Opções: As opções para o efeito selecionado serão exibidas aqui.
+
+- Desfazer/Refazer: O módulo salva o estado da segmentação antes de cada efeito ser aplicado. Isso é útil para experimentação e correção de erros. Por padrão, os últimos 10 estados são lembrados.
+
+### Efeitos
+-------------------------------------------
+
+Os efeitos operam clicando no botão Aplicar na seção de opções do efeito ou clicando e/ou arrastando nas visualizações de fatias ou 3D.
+
+
+### ![](https://github.com/Slicer/Slicer/releases/download/docs-resources/module_segmenteditor_paint.png) Pintura
+
+* Escolha o raio (em milímetros) do pincel a ser aplicado.
+
+* Clique com o botão esquerdo para aplicar um círculo único.
+
+* Clique com o botão esquerdo e arraste para preencher uma região.
+
+* Uma trilha de círculos é deixada, que é aplicada quando o botão do mouse é liberado.
+
+* O modo Esfera aplica o raio às fatias acima e abaixo da fatia atual.
+
+
+
+### ![](https://github.com/Slicer/Slicer/releases/download/docs-resources/module_segmenteditor_draw.png) Desenho
+
+* Clique com o botão esquerdo para criar pontos individuais de um contorno.
+
+* Arraste com o botão esquerdo para criar uma linha contínua de pontos.
+
+* Clique duas vezes com o botão esquerdo para adicionar um ponto e preencher o contorno. Alternativamente, clique com o botão direito para preencher o contorno atual sem adicionar mais pontos.
+
+Nota
+
+O efeito Tesoura também pode ser usado para desenhar. O efeito Tesoura funciona tanto em visualizações de fatias quanto em 3D, pode ser configurado para desenhar em mais de uma fatia por vez, pode apagar também, pode ser restrito a desenhar linhas horizontais/verticais (usando o modo retângulo), etc.
+
+### ![](https://github.com/Slicer/Slicer/releases/download/docs-resources/module_segmenteditor_erase.png) Apagar
+
+Semelhante ao efeito Pintura, mas as regiões destacadas são removidas do segmento selecionado em vez de adicionadas.
+
+Se a Máscara / Área Editável estiver definida para um segmento específico, a região destacada é removida do segmento selecionado e adicionada ao segmento de máscara. Isso é útil quando uma parte de um segmento precisa ser separada em outro segmento.
+
+### ![](https://github.com/Slicer/Slicer/releases/download/docs-resources/module_segmenteditor_level_tracing.png) Rastreamento de Nível
+
+* Mover o mouse define um contorno onde os pixels têm o mesmo valor de fundo que o pixel de fundo atual.
+
+* Clicar com o botão esquerdo aplica esse contorno ao mapa de rótulos.
+
+
+### ![](https://github.com/Slicer/Slicer/releases/download/docs-resources/module_segmenteditor_grow_from_seeds.png) Crescer a partir de sementes
+
+Desenhe o segmento dentro de cada estrutura anatômica. Este método começará a partir dessas "sementes" e as expandirá para alcançar a segmentação completa.
+
+* Inicializar: Clique neste botão após a segmentação inicial ser concluída (usando outros efeitos de editor). O cálculo inicial pode levar mais tempo do que as atualizações subsequentes. O volume de origem e o método de preenchimento automático serão bloqueados após a inicialização, portanto, se algum desses precisar ser alterado, clique em Cancelar e inicialize novamente.
+
+* Atualizar: Atualize a segmentação concluída com base nas entradas alteradas.
+
+* Atualização automática: ative esta opção para atualizar automaticamente a visualização do resultado quando a segmentação for alterada.
+
+* Cancelar: Remover visualização do resultado. As sementes são mantidas inalteradas, portanto, os parâmetros podem ser alterados e a segmentação pode ser reiniciada clicando em Inicializar.
+
+* Aplicar: Substitua os segmentos de sementes pelos resultados visualizados.
+
+
+Notas:
+
+* Apenas segmentos visíveis são usados por este efeito.
+
+* Pelo menos dois segmentos são necessários.
+
+* Se uma parte de um segmento for apagada ou a pintura for removida usando Desfazer (e não for substituída por outro segmento), recomenda-se cancelar e inicializar novamente. A razão é que o efeito de adicionar mais informações (pintar mais sementes) pode ser propagado para toda a segmentação, mas remover informações (remover algumas regiões de sementes) não mudará a segmentação completa.
+
+* O método usa uma versão aprimorada do algoritmo grow-cut descrito em _Liangjia Zhu, Ivan Kolesov, Yi Gao, Ron Kikinis, Allen Tannenbaum. An Effective Interactive Medical Image Segmentation Method Using Fast GrowCut, International Conference on Medical Image Computing and Computer Assisted Intervention (MICCAI), Interactive Medical Image Computing Workshop, 2014_.
+
+
+### ![](https://github.com/Slicer/Slicer/releases/download/docs-resources/module_segmenteditor_margin.png) Margem
+
+Aumenta ou diminui o segmento selecionado pela margem especificada.
+
+Ao habilitar `Aplicar aos segmentos visíveis`, todos os segmentos visíveis da segmentação serão processados (na ordem da lista de segmentos).
+
+### ![](https://github.com/Slicer/Slicer/releases/download/docs-resources/module_segmenteditor_smoothing.png) Suavização
+
+Suaviza segmentos preenchendo buracos e/ou removendo extrusões.
+
+Por padrão, o segmento atual será Suavizado. Ao habilitar `Aplicar aos segmentos visíveis`, todos os segmentos visíveis da segmentação serão suavizado (na ordem da lista de segmentos). Esta operação pode ser demorada para segmentações complexas. O método `Suavização conjunta` sempre suaviza todos os segmentos visíveis.
+
+Clicando no botão `Aplicar`, toda a segmentação é suavizada. Para suavizar uma região específica, clique e arraste com o botão esquerdo em qualquer visualização de fatia ou 3D. O mesmo método e força de suavização são usados tanto no modo de segmentação inteira quanto no modo de suavização por região (o tamanho do pincel não afeta a força da Suavização, apenas facilita a designação de uma região maior).
+
+Métodos disponíveis:
+
+* Mediana: remove pequenas extrusões e preenche pequenos espaços enquanto mantém os contornos suaves praticamente inalterados. Aplicado apenas ao segmento selecionado.
+
+
+* Abertura: remove extrusões menores do que o tamanho do kernel especificado. Não adiciona nada ao segmento. Aplicado apenas ao segmento selecionado.
+
+
+* Fechamento: preenche cantos afiados e buracos menores do que o tamanho do kernel especificado. Não remove nada do segmento. Aplicado apenas ao segmento selecionado.
+
+
+* Gaussiano: suaviza todos os detalhes. A suavização mais forte possível, mas tende a diminuir o segmento. Aplicado apenas ao segmento selecionado.
+
+
+* Suavização conjunto: suaviza múltiplos segmentos de uma vez, preservando a interface estanque entre eles. Se os segmentos se sobrepuserem, o segmento mais alto na tabela de segmentos terá prioridade. Aplicado a todos os segmentos visíveis.
+
+
+### ![](https://github.com/Slicer/Slicer/releases/download/docs-resources/module_segmenteditor_scissors.png) Tesoura
+
+Recorta segmentos para a região especificada ou preenche regiões de um segmento (geralmente usado com mascaramento). As regiões podem ser desenhadas tanto na visualização de fatia quanto nas visualizações 3D.
+
+* Clique com o botão esquerdo para começar a desenhar (forma livre ou círculo/retângulo elástico)
+
+* Solte o botão para aplicar
+
+
+Ao habilitar `Aplicar aos segmentos visíveis`, todos os segmentos visíveis da segmentação serão processados (na ordem da lista de segmentos).
+
+### ![](https://github.com/Slicer/Slicer/releases/download/docs-resources/module_segmenteditor_islands.png) Ilhas
+
+Use esta ferramenta para processar “ilhas”, ou seja, regiões conectadas que são definidas como grupos de voxels não vazios que se tocam, mas são cercados por voxels vazios.
+
+* `Manter maior ilha`: mantém a maior região conectada.
+
+* `Remover pequenas ilhas`: mantém todas as regiões conectadas que são maiores que o `tamanho mínimo`.
+
+* `Dividir ilhas em segmentos`: cria um segmento único para cada região conectada do segmento selecionado.
+
+* `Manter ilha selecionada`: após selecionar este modo, clique em uma área não vazia na visualização de fatias para manter essa região e remover todas as outras regiões.
+
+* `Remover ilha selecionada`: após selecionar este modo, clique em uma área não vazia na visualização de fatias para remover essa região e preservar todas as outras regiões.
+
+* `Adicionar ilha selecionada`: após selecionar este modo, clique em uma área vazia na visualização de fatias para adicionar essa região vazia ao segmento (preencher buraco).
+
+### ![](https://github.com/Slicer/Slicer/releases/download/docs-resources/module_segmenteditor_logical_operators.png) Operadores lógicos
+
+Aplicar operações básicas de copiar, limpar, preencher e Booleanas ao(s) segmento(s) selecionado(s). Veja mais detalhes sobre os métodos clicando em “Mostrar detalhes” na descrição do efeito no Editor de Segmentos.
+
+### ![](https://github.com/Slicer/Slicer/releases/download/docs-resources/module_segmenteditor_mask_volume.png) Volume de máscara
+
+Apague dentro/fora de um segmento em um volume ou crie uma máscara binária. O resultado pode ser salvo em um novo volume ou sobrescrever o volume de entrada. Isso é útil para remover detalhes irrelevantes de uma imagem ou criar máscaras para operações de processamento de imagem (como registro ou correção de intensidade).
+
+* `Operação`:
+
+ * `Preencher dentro`: define todos os voxels do volume selecionado para o `Valor de preenchimento` especificado dentro do segmento selecionado.
+
+ * `Preencher fora`: define todos os voxels do volume selecionado para o `Valor de preenchimento` especificado fora do segmento selecionado.
+
+ * `Preencher dentro e fora`: cria um volume de mapa de etiquetas binário como saída, preenchido com o `Valor de preenchimento fora` e o `Valor de preenchimento dentro`. A maioria das operações de processamento de imagem exige que a região de fundo (fora, ignorada) seja preenchida com o valor 0.
+
+* `Borda suave`: se definido como >0, a transição entre dentro/fora da máscara é gradual. O valor especifica o desvio padrão da função de desfoque gaussiano. Valores maiores resultam em uma transição mais suave.
+
+* `Volume de entrada`: voxels deste volume serão usados como entrada para a máscara. A geometria e o tipo de voxel do volume de saída serão os mesmos que os deste volume.
+
+* `Volume de saída`: este volume armazenará o resultado da máscara. Embora possa ser o mesmo que o volume de entrada, geralmente é melhor usar um volume de saída diferente, porque assim as opções podem ser ajustadas e a máscara pode ser recalculada várias vezes.
+
+### ![](../../assets/images/thin_section/modulos/segment_editor/color_threshold_effect.png) Limiar de cores
+
+Esse efeito de edição de segmentação chamado "Color threshold", permite a segmentação de imagens com base em intervalos de cores definidos pelo usuário. O efeito pode operar em modos de cor HSV ou RGB, permitindo ajustes nos componentes de matiz, saturação e valor. Ainda possui ajuste nos níveis de vermelho, verde e azul. O efeito oferece uma visualização em tempo real da segmentação, utilizando um pulso de pré-visualização para ajudar o usuário a refinar os parâmetros antes de aplicar as alterações permanentemente. Além disso, o efeito inclui funcionalidades avançadas, como a conversão de espaços de cor e a manipulação de intervalos circulares, possibilitando uma segmentação precisa e customizada.
+
+
+### ![](../../assets/images/thin_section/modulos/segment_editor/conectivity_effect.png) Conectividade
+
+Esse efeito de "Conectividade" permite a seleção de segmentos no Geoslicer, permitindo aos usuários calcular regiões conectadas dentro de um segmento em uma direção específica. O efeito inclui parâmetros configuráveis como saltos de conectividade, direção e nome de saída, tornando-o uma ferramenta versátil para tarefas detalhadas de segmentação. Ele lida de forma eficiente com a análise de componentes conectados e gera um novo segmento baseado nas configurações definidas pelo usuário.
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/OpenRockData.md b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/OpenRockData.md
new file mode 100644
index 0000000..4c8acc5
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/OpenRockData.md
@@ -0,0 +1,31 @@
+
+# Open Data Rocks
+
+O módulo Open Data Rocks permite o acesso à imagens digitais de rochas normalmente adquiridas usando técnicas de imagem tridimensionais. O módulo utiliza a biblioteca **[drd](https://github.com/LukasMosser/digital_rocks_data)**
+
+
+ de Lukas-Mosser para acessar dados de microtomografia de diferentes fontes geológicas, incluindo o **[Digital Rocks Portal](https://www.digitalrocksportal.org/)**
+
+
+ e **[Imperial College London](https://www.imperial.ac.uk/earth-science/research/research-groups/pore-scale-modelling/micro-ct-images-and-networks/)**
+
+
+. Ele permite o download e a extração de datasets importantes, como Eleven Sandstones e ICL Sandstone Carbonates, facilitando o processamento e salvamento em formato NetCDF para análise posterior.
+
+
+## Painéis e sua utilização
+
+| ![Figura 1](../../assets/images/micro_ct/modulos/open_rock_data/interface.png) |
+|:-----------------------------------------------:|
+| Figura 1: Apresentação do módulo Auto Registration. |
+
+
+
+### Principais opções:
+
+ - _Datasets_: Escolha entre as imagens disponíveis para download
+
+ - _Download_: inicie o donwload da amostra selecionada
+
+ - _Painel de retorno_: Informações sobre o processo de download são retornadas através desse painel
+
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/PNExtraction.md b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/PNExtraction.md
new file mode 100644
index 0000000..316dcbc
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/PNExtraction.md
@@ -0,0 +1,17 @@
+# Extração
+
+Esse módulo é utilizado para extrair a rede de poros e ligações a partir de: uma segmentação dos poros (_Label Map Volume_) realizada por um algoritmo de _watershed_, gerando uma rede uniescalar; ou por um mapa de porosidades (_Scalar Volume_), que gerará um modelo multiescalar com poros resolvidos e não-resolvidos.
+
+| |
+|:-----------------------------------------------------------------------:|
+| Figura 1: Interface do módulo de Extração. |
+
+Após a extração, ficará disponível na interface do GeoSlicer: as tabelas de poros e gargantas e também os modelos de visualização da rede. As tabelas geradas serão os dados usados na etapa seguinte de simulação.
+
+| |
+|:-----------------------------------------------------------------------:|
+| Figura 1: A esquerda Label Map utilizado como entrada na extração e a direita rede uniescalar extraída. |
+
+| |
+|:-----------------------------------------------------------------------:|
+| Figura 2: A esquerda Scalar Volume utilizado como entrada na extração e a direita rede multiescalar extraída, onde azul representa poros resolvidos, e rosa representa os poros não-resolvidos. |
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/PNSimulation.md b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/PNSimulation.md
new file mode 100644
index 0000000..961a81b
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/PNSimulation.md
@@ -0,0 +1,124 @@
+# Pore Network Simulation
+
+Este módulo pode realizar diferentes tipos de simulações a partir dos resultados da tabela de poros e ligações (criada no módulo de [Extração](./PNExtraction.md)). As simulações incluem: [_One-phase_](#one-phase), [_Two-phase_](#two-phase), [_Mercury injection_](#mercury-injection), explicados nas seções adiante.
+
+Todas as simulações possuem os mesmos argumentos de entrada: A tabela de poros gerada a partir da extração da rede e quando o volume for multiescalar, o [modelo subescala](#modelo-subscala) utilizado e sua parametrização.
+
+| ![Figura 1](simulation.png) |
+|:----------------------------------------------------------------------------:|
+| Figura 1: Entrada da tabela de poros gerada com o módulo [Extração](./PNExtraction.md). |
+
+## _One-phase_
+
+A simulação de uma fase, é utilizada principalmente para determinar a propriedade de permeabilidade absoluta ($K_{abs}$) da amostra. Pode ser realizada em uma única direção ou em múltiplas direções a partir do parâmetro _Orientation scheme_.
+
+## _Two-phase_
+
+A simulação de duas fases consiste em inicialmente injetar óleo na amostra e aumentar a pressão do mesmo afim de que esse invada praticamente todos os poros, num processo que é conhecido como drenagem (_drainage_). Após essa primeira etapa, substitui-se o óleo por água e novamente aumenta-se a pressão, de forma a permitir que a água invada alguns dos poros que antes estavam com óleo, expulsando o último, esse segundo processo é conhecido como embebição (_imbibition_). Ao medirmos a permeabilidade da rocha em relação a permeabilidade absoluta, em função da saturação de água durante esse processo, obtemos a curva conhecida como curva de permeabilidade relativa ($K_{rel}$).
+
+Uma vez que cada rocha pode interagir físicamente ou quimicamente com o óleo e com a água de diferentes maneiras, precisamos de uma variedade bastante grande de parametros que permitam calibrar os resultados de forma a modelar e simplificar essa interação, afim de extrairmos algum significado físico das propriedades da rocha a partir da simulação. Abaixo, elencamos alguns parâmetros que podem ser encontrados na simulação de duas fases disponibilizada no GeoSlicer.
+
+Atualmente, temos disponível no GeoSlicer dois algoritmos para realizar a simulação de duas fases, a primeira delas é a do PNFlow que é um algoritmo padrão utilizado, implementado em C++, e a segunda é um algoritmo próprio desenvolvido pela LTrace em linguagem Python.
+
+### Salvar/Carregar tabela de seleção de parâmetros
+
+| ![Figura 2a](two phase_load.png)![Figura 2](two phase_save.png) |
+|:----------------------------------------------------------------------------:|
+| Figura 2: Opções para salvar/carregar tabelas de seleção de parâmetros. |
+
+### _Fluid properties_
+
+Nessa seção colocamos como entrada do modelo os parâmetros dos fluidos (água e óleo) utilizados:
+
+| ![Figura 3](two phase_fluid properties.png) |
+|:----------------------------------------------------------------------------:|
+| Figura 3: Entrada dos parâmetros dos fluidos (água e óleo). |
+
+### _Contact angle options_
+
+Uma das principais propriedades que afetam a interação de um líquido com um sólido é a molhabilidade, essa pode ser determinada a partir do ângulo de contato formado pelo primeiro quando em contato com o último. Assim, se o ângulo de contato for próximo de zero há uma forte interação que "prende" o líquido ao sólido, já quando o ângulo de contato é próximo a 180º, a interação do líquido com a superfície é fraca e esse pode escoar com mais facilidade pela mesma.
+
+| ![Figura 4](molhabilidade.png) |
+|:----------------------------------------------------------------------------:|
+| Figura 4: Representação visual do conceito de molhabilidade e ângulo de contato. |
+
+Modelamos os ângulos de contato a partir de duas distribuições usadas em momentos distintos: a _Initial contact angle_ que controla o ângulo de contato dos poros antes da invasão por óleo; e a _Equilibrium contact angle_ que controla o ângulo de contato após a invasão por óleo. Além das distribuições base utilizadas em cada caso, há uma opção para adicionar uma segunda distribuição para cada uma delas, assim cada poro é atrelado a uma das duas distribuições, com o parâmetro "Fraction" sendo usado para determinar qual a porcentagem de poros vão seguir a segunda distribuição em relação a primeira.
+
+| ![Figura 5](two phase_contact angle.png) |
+|:----------------------------------------------------------------------------:|
+| Figura 5: Parâmetros das distribuições de ângulo de contato. |
+
+Cada distribuição de ângulos, seja primária ou secundária, inicial ou de equilíbrio, tem uma série de parâmetros que a descreve:
+
+- _Model_: permite modelar as curvas de histerese entre ângulos de avanço/recuo a partir dos ângulos intrínsicos:
+
+ - _Equal angles_: ângulos de avanço/recuo idênticos ao ângulo intrinsico;
+ - _Constant difference_: diferença constante dos ângulos de avanço/recuo em relação ao ângulo intrínsico;
+ - _Morrow curve_: curvas de avanço/recuo determinadas pelas curvas de Morrow;
+
+| |
+|:---------------------------------------------------------------------:|
+| Figura 4: Curvas para cada um dos modelos de ângulo de contato implementados no GeoSlicer. |
+
+- _Contact angle distribution center_: Define o centro da distribuição de ângulo de contato;
+- _Contact angle distribution range_: Alcance da distribuição (center-range/2, center+range/2), com o ângulo mínimo/máximo sendo 0º/180º, respectivamente;
+- _Delta_, _Gamma_: Parâmetros da distribuição de Weibull truncada, se um número negativo é escolhido, usa uma distribuição uniforme;
+- _Contact angle correlation_: Escolhe como o ângulo de contato será correlacionado ao raio dos poros: _Positive radius_ define maiores ângulos de contato para raios maiores; _Negative radius_ faz o oposto, atribuindo maiores ângulos para raios menores; _Uncorrelated_ significa independência entre ângulos de contato com o raio do poro;
+- _Separation_: Se o modelo escolhido for _constant difference_, define a separação entre ângulos de avanço e recuo;
+
+Outros parâmetros estão definidos apenas para a segunda distribuição:
+
+- _Fraction_: Um valor entre 0 e 1 que controla qual a fração dos poros usará a segunda distribuição ao invés da primeira;
+- _Fraction distribution_: Define se a fração será determinada pela quantidade de poros ou volume total;
+- _Correlation diameter_: Se a _Fraction correlation_ for escolhida como _Spatially correlated_, define a distância mais provável de encontrar poros com mesma distribuição de ângulo de contato;
+- _Fraction correlation_: Define como a fração para a segunda distribuição será correlacionada, se correlacionada espacialmente, para maiores poros, menores poros ou aleatória;
+
+### _Simulation options_
+
+Alguns parâmetros relacionados a própria simulação podem ser escolhidos nessa seção, entre eles:
+
+- _Minimum SWi_: Define o valor mínimo de SWi, interrompendo o ciclo de drenagem quando o valor de Sw é atingido (SWi pode ser maior se a água ficar presa);
+- _Final cycle Pc_: Interrompe o ciclo quando essa pressão capilar é alcançada;
+- _Sw step length_: Passo de Sw utilizado antes de verificar o novo valor de permeabilidade;
+- _Inject/Produce from_: Define por quais lados o fluido será injetado/produzido ao longo do eixo z, o mesmo lado pode tanto injetar como também produzir;
+- _Pore fill_: Determina qual mecanismo domina cada evento de preenchimento de poro individual;
+- _Lower/Upper box boundary_: Poros com distância relativa no eixo Z da borda até este valor do plano são considerados poros "à esquerda"/"à direita", respectivamente;
+- _Subresolution volume_: Considera que o volume contém essa fração de espaço poroso em subresolução que está sempre preenchido com água;
+- _Plot first injection cycle_: Se selecionado, o primeiro ciclo, injeção de óleo em um meio totalmente saturado de água, será incluído no gráfico de saída. A simulação será executada, independentemente da opção estar selecionada ou não;
+- _Create animation node_: Cria um nó de animação que pode ser usado na aba "Cycles Visualization";
+- _Keep temporary files_: Mantém os arquivos .vtu na pasta de arquivos temporários do GeoSlicer, um arquivo para cada etapa da simulação;
+- _Max subprocesses_: Quantidade máxima de subprocessos single-thread que devem ser executados; O valor recomendado para uma máquina ociosa é 2/3 do total de núcleos;
+
+### _Sensitivity test_
+
+Uma vez que temos uma vasta quantidade de parâmetros que podem ser modificados para modelar o experimento a partir da simulação, se torna útil variarmos tais parâmetros de forma mais sistemática para uma análise aprofundada da sua influência nos resultados obtidos.
+
+Para isso o usuário pode selecionar o botão "_Multi_" disponível em grande parte dos parâmetros, ao clicar em Multi, três caixas aparecem com opções de início, fim e passo, que podem ser usadas para rodar diversas simulações em uma tabela linearmente distribuída dos valores desses parâmetros. Se mais de um parâmetro é escolhido com múltiplos valores, simulações rodam com todas as combinações de parâmetros possíveis, isso pode aumentar consideravelmente a quantidade de simulações e o tempo para executar.
+
+Ao finalizar a execução do conjunto de simulações, o usuário pode realizar análises para entender as relações entre os resultados das simulações com os parâmetros escolhidos na aba _Krel EDA_.
+
+## _Mercury injection_
+
+A intrusão de mercúrio é um experimento no qual mercúrio líquido é injetado em uma amostra de rocha reservatório sob vácuo, com pressão crescente. O volume de mercúrio invadindo a amostra é medido em função da pressão de mercúrio. Uma vez que o ângulo de contato do mercúrio líquido com o vapor de mercúrio é aproximadamente independente do substrato, é possível utilizar modelos analíticos, como o modelo do feixe de tubos, para calcular a distribuição do tamanho dos poros da amostra.
+
+O ensaio de intrusão de mercúrio é relativamente acessível e sua principal relevância no contexto do PNM reside na capacidade de executar a simulação em uma amostra para a qual os resultados experimentais de curvas de Pressão Capilar por Intrusão de Mercúrio (MICP) estão disponíveis. Isso permite a comparação dos resultados para validar e calibrar a rede de poros extraída da amostra, que será usada nas simulações de uma e duas fases.
+
+Para facilitar as análises da atribuição dos raios dos poros sub-resolução, o código presente no GeoSlicer produzirá como saída, além dos gráficos obtidos pela simulação no OpenPNM, os gráficos das distribuições de raios de poros e gargantas e também das distribuições de volumes, separando em poros resolvidos (que não se alteram pela atribuição da subescala) e poros não resolvidos. Dessa forma, o usuário pode conferir se o modelo de subescala foi aplicado corretamente.
+
+## Modelo Subscala
+
+No caso da rede multiescalar, como os raios da subescala não podem ser determinados a partir da própria imagem, por estarem fora da resolução, é necessário definir um modelo para atribuição desses raios. Algumas opções disponíveis atualmente são:
+
+- _Fixed radius_: Todos os raios da subresolução tem o mesmo tamanho escolhido na interface;
+![Figura 2](subscale_fixed.png)
+
+- _Leverett Function - Sample Permeability_: Atribui uma pressão de entrada com base na curva de J Leverett com base na permeabilidade da amostra;
+![Figura 3](subscale_leverett.png)
+
+- _Leverett Function - Permeability curve_: Também utiliza a curva de J Leverett mas com uma curva definida para a permeabilidade ao invés de um valor;
+![Figura 4](subscale_leverettCurve.png)
+
+- _Pressure Curve_ e _Throat Radius Curve_: Atribui os raios da subresolução com base na curva obtida por um experimento de injeção de mercúrio. Pode ser utilizado o dado da pressão de entrada pela fração do volume, ou então o raio equivalente em função da fração de volume;
+![Figura 5a](subscale_pressure.png)![Figura 5b](subscale_radius.png)
+
+O modelo de subescala escolhido não tem impacto nas simulações de redes uniescalares, uma vez que todos os raios já estão determinados.
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/StreamlinedSegmentation.md b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/StreamlinedSegmentation.md
new file mode 100644
index 0000000..7288823
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/StreamlinedSegmentation.md
@@ -0,0 +1,119 @@
+
+## Explore
+
+Fluxo para o obtenção de um mapa de porosidade a partir de um volume escalar.
+
+```mermaid
+%%{
+ init: {
+ 'themeVariables': {
+ 'lineColor': '#c4c4c4'
+ }
+ }
+}%%
+flowchart LR
+ Iniciar --> Importar
+ Importar --> Segmentacao
+ Segmentacao --> Modelagem
+ Modelagem --> Resultados
+
+ click Segmentacao "../../Segmentation/segmentation.html" "teste de imagem"
+ style Iniciar fill:#808080,stroke:#333,stroke-width:1px,color:#fff;
+ style Importar fill:#6d8873,stroke:#333,stroke-width:1px,color:#fff;
+ style Segmentacao fill:#5a9b87,stroke:#333,stroke-width:1px,color:#fff;
+ style Modelagem fill:#45ae97,stroke:#333,stroke-width:1px,color:#fff;
+ style Resultados fill:#2ea67e,stroke:#333,stroke-width:1px,color:#fff;
+
+ Segmentacao["Segmentação"]
+```
+
+1. Inicie o Geoslicer no ambiente MicroCT a partir da interface do aplicativo.
+
+2. Selecione o volume de entrada clicando em "Escolher pasta" ou "Escolher arquivo" e escolha os dados de importação desejados entre as opções disponíveis. Sugerimos testar primeiro os parâmetros padrão.
+
+3. Selecione o volume de entrada clicando em "Input:" Ajuste os parâmetros para diferentes efeitos de segmentação, como "Múltiplos Limiares," "Remoção de Fronteira," e "Expandir Segmentos." Ajuste as configurações para obter os resultados de segmentação desejados, usando o feedback da interface e as ferramentas de visualização.
+
+4. Revise e refine os dados segmentados. Ajuste os limites da segmentação, mescle ou divida segmentos, e aplique outras modificações para aprimorar o modelo de porosidade usando as ferramentas fornecidas.
+
+
+5. Salve o mapa de porosidade ou exporte o volume com as tabelas de parametros.
+
+## Saiba Mais...
+```mermaid
+%%{init: { 'logLevel': 'debug', 'theme': 'default','themeVariables': {
+ 'git0': '#808080',
+ 'git1': '#6d8873',
+ 'git2': '#5a9b87',
+ 'git3': '#45ae97',
+ 'git4': '#2ea67e',
+ 'git5': '#ffff00',
+ 'git6': '#ff00ff',
+ 'git7': '#00ffff',
+ 'gitBranchLabel0': '#ffffff',
+ 'gitBranchLabel1': '#ffffff',
+ 'gitBranchLabel2': '#ffffff',
+ 'gitBranchLabel3': '#ffffff',
+ 'gitBranchLabel4': '#ffffff',
+ 'gitBranchLabel5': '#ffffff',
+ 'gitBranchLabel6': '#ffffff',
+ 'gitBranchLabel7': '#ffffff',
+ 'gitBranchLabel8': '#ffffff',
+ 'gitBranchLabel9': '#ffffff',
+ 'commitLabelColor': '#afafaf',
+ 'commitLabelBackground': '#0000',
+ 'commitLabelFontSize': '13px'
+ }, 'gitGraph': {'showBranches': true, 'showCommitLabel':true,'mainBranchName': 'Inicio'}} }%%
+ gitGraph LR:
+ commit id:"Inicio"
+ commit id:"Micro CT ."
+ branch "Importar"
+ commit id:"Aba de Dados"
+ commit id:"Aba de Importação"
+ commit id:"Selecionar Arquivo"
+ commit id:"Parâmetros"
+ commit id:"Carregar ."
+ branch "Segmentação"
+ commit id:"Aba de Segmentação"
+ commit id:"Adicionar novo nó de segmentação"
+ commit id:"Criar pelo menos 4 segmentos"
+ commit id:"Adicionar ROI ."
+ branch Modelagem
+ commit id:"Aba de Modelagem"
+ commit id:"Segmentação"
+ commit id:"Selecionar volume"
+ commit id:"Selecionar segmentação"
+ commit id:"Aplicar ."
+ branch Resultados
+ commit id:"Gráficos"
+ commit id:"Imagens"
+ commit id:"Tabelas"
+ commit id:"Relatórios"
+```
+#### [Inicio](../../Bem Vindo/welcome)
+Inicie o Geoslicer no ambiente MicroCT a partir da interface do aplicativo.
+
+#### [Importar(TODO)](../../Bem Vindo/welcome)
+Selecione o volume de entrada clicando em "Escolher pasta" ou "Escolher arquivo" e escolha os dados de importação desejados entre as opções disponíveis. Sugerimos testar primeiro os parâmetros padrão.
+
+#### [Segmentação](../../Segmentation/segmentation.md)
+Selecione o volume de entrada clicando em "Entrada:" Ajuste os parâmetros para diferentes efeitos de segmentação, como:
+
+ 1. Múltiplos Limiares(TODO)
+ 2. Remoção de Fronteira(TODO)
+ 3. Expandir Segmentos(TODO)
+
+Ajuste as configurações para obter os resultados de segmentação desejados, usando o feedback da interface e as ferramentas de visualização.
+
+#### [Modelagem(TODO)](../../Bem Vindo/welcome)
+ Revise e refine os dados segmentados. Ajuste os limites da segmentação, mescle ou divida segmentos, e aplique outras modificações para aprimorar o modelo de segmentação usando as ferramentas fornecidas.(TODO)
+
+#### [Resultados(TODO)](../../Bem Vindo/welcome)
+Salve o projeto ou exporte o volume segmentado. Os resultados podem ser exibidos como:
+
+ 1. Imagem(Screenshot)(TODO)
+ 2. Graficos(Charts)(TODO)
+ 3. Servidos atraves de Relatórios (Streamlit)(TODO)
+
+## Ainda tem perguntas?
+
+#### [Entre em contato](https://www.ltrace.com.br/contact/)
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/extract.png b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/extract.png
new file mode 100644
index 0000000..1b0147a
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/extract.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/labelmap.png b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/labelmap.png
new file mode 100644
index 0000000..df88636
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/labelmap.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/mercury.png b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/mercury.png
new file mode 100644
index 0000000..0ea6802
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/mercury.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/molhabilidade.png b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/molhabilidade.png
new file mode 100644
index 0000000..0026741
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/molhabilidade.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/morrow.png b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/morrow.png
new file mode 100644
index 0000000..c6957b7
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/morrow.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/output.png b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/output.png
new file mode 100644
index 0000000..cf2e84e
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/output.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/rede_multiescala.png b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/rede_multiescala.png
new file mode 100644
index 0000000..b070746
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/rede_multiescala.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/rede_uniescalar.png b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/rede_uniescalar.png
new file mode 100644
index 0000000..4da5943
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/rede_uniescalar.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/scalar.png b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/scalar.png
new file mode 100644
index 0000000..7b7f51d
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/scalar.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/simulation.png b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/simulation.png
new file mode 100644
index 0000000..b2d240d
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/simulation.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/subscale_fixed.png b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/subscale_fixed.png
new file mode 100644
index 0000000..3cc849d
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/subscale_fixed.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/subscale_leverett.png b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/subscale_leverett.png
new file mode 100644
index 0000000..b4b4012
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/subscale_leverett.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/subscale_leverettCurve.png b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/subscale_leverettCurve.png
new file mode 100644
index 0000000..82bb981
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/subscale_leverettCurve.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/subscale_pressure.png b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/subscale_pressure.png
new file mode 100644
index 0000000..6320fdc
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/subscale_pressure.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/subscale_radius.png b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/subscale_radius.png
new file mode 100644
index 0000000..9fa3335
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/subscale_radius.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/two phase.png b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/two phase.png
new file mode 100644
index 0000000..5d2713a
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/two phase.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/two phase_contact angle.png b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/two phase_contact angle.png
new file mode 100644
index 0000000..6df7825
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/two phase_contact angle.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/two phase_fluid properties.png b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/two phase_fluid properties.png
new file mode 100644
index 0000000..af21c2d
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/two phase_fluid properties.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/two phase_input.png b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/two phase_input.png
new file mode 100644
index 0000000..6f70b4f
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/two phase_input.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/two phase_load.png b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/two phase_load.png
new file mode 100644
index 0000000..2d2bd63
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/two phase_load.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/two phase_save.png b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/two phase_save.png
new file mode 100644
index 0000000..bda445a
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/two phase_save.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/two phase_simulation properties.png b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/two phase_simulation properties.png
new file mode 100644
index 0000000..1d2945c
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/Modules/Volumes/two phase_simulation properties.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Overview/index.md b/tools/deploy/GeoSlicerManual/docs/Overview/index.md
new file mode 100644
index 0000000..6924f19
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Overview/index.md
@@ -0,0 +1,31 @@
+# GeoSlicer
+#### Plataforma integrada para visualização, análise e interpretação de imagens de rocha digital em multiescalas
+
+O termo rocha digital refere-se a imagens de rochas obtidas por diferentes técnicas de imagem, como fotografia,
+tomografia computadorizada e microscopia eletrônica. A interpretação dessas imagens é essencial para a compreensão
+de processos geológicos, como a formação de reservatórios de petróleo e gás. Para dar suporte a pesquisa e análise
+desse tipo de dado foi desenvolvida uma plataforma integrada
+projetada para atender diversos fluxos de trabalho envolvendo rocha digital. Essa plataforma é o _**GeoSlicer**_.
+
+O GeoSlicer é uma plataforma de código aberto, desenvolvida em Python e baseada no software 3D Slicer.
+
+## Tópicos
+
+
+
+- [Visualização de volumes 3D](../Data_loading/load_microct.md)
+- [Registro de Imagens](../Transforms/transforms.md)
+- [Segmentação utilizando IA](../Segmentation/auto_segmentation.md)
+- [Simulação de Redes de Poros](../Simulation/PNM/intro.md)
+
+
\ No newline at end of file
diff --git a/tools/deploy/GeoSlicerManual/docs/Quantification/instance_segmenter.md b/tools/deploy/GeoSlicerManual/docs/Quantification/instance_segmenter.md
new file mode 100644
index 0000000..03de7af
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Quantification/instance_segmenter.md
@@ -0,0 +1,75 @@
+# Instance Segmenter
+
+## Introdução
+
+A análise quantitativa de dados de perfis de imagens é de grande importância na indústria de óleo e gás. Esta análise se destaca na
+caracterização de reservatórios que apresentam grande variabilidade de escalas de poros, como os carbonatos do pré-sal. Frequentemente,
+apenas a porosidade matricial das rochas não é suficiente para caracterizar as propriedades de escoamento de fluidos nesses reservatórios.
+De fato, a presença de regiões sujeitas a intensa carstificação e de rochas com porosidade vugular significativa governam o comportamento
+dos fluidos nesses reservatórios. Por sua natureza e escala, os perfis de imagem se destacam como uma importante ferramenta para melhor
+entendimento e caracterização mais acurada dessas rochas.
+
+Entretanto, o uso quantitativo dessas imagens é um desafio devido ao grande número de artefatos como espiralamento, breakouts,
+excentricidade, batentes, marcas de broca ou de amostragem, entre outros. Portanto, a correta interpretação, identificação e correção desses
+artefatos é de crucial importância.
+
+Neste contexto, são utilizadas técnicas de aprendizado de máquina e visão computacional para automatização e auxílio nos processos de
+análise das imagens de perfis de poços.
+
+Image Log Instance Segmenter é um módulo do GeoSlicer que realiza a segmentação por instância de artefatos de interesse em imagens de log.
+Segmentação por instância significa que os objetos são separados individualmente uns dos outros, e não somente por classes. No GeoSlicer
+estão disponíveis por ora a detecção de dois tipos de artefatos:
+
+- Marcas de amostragem
+- Batentes
+
+Após executado, um modelo gera um nodo de segmentação (labelmap) e uma tabela com propriedades de cada instância detectada, que podem ser
+analisadas utilizando-se o módulo Instance Editor, presente no ambiente Image Log.
+
+## Marcas de amostragem
+
+Para marcas de amostragem, é utilizada a [Mask-RCNN](https://github.com/matterport/Mask_RCNN), uma rede convolucional de alto desempenho designada para detecção por instância em
+imagens tradicionais RGB. Apesar das imagens de perfil não possuírem os três canais RGB, a entrada da rede foi adaptada para analisar os
+logs de Tempo de Trânsito e de Amplitude como dois canais de cores para o treinamento da rede.
+
+Estão disponíveis dois modelos treinados para a detecção de marcas de amostragem: um modelo primário e um modelo secundário, de testes. O
+objetivo de existir um modelo secundário é permitir que os usuários confrontem os dois modelos de modo a determinar qual é o melhor,
+possibilitando o descarte do pior modelo e a inclusão de futuros modelos concorrentes em novas versões do GeoSlicer.
+
+Este modelo gera uma tabela com as seguintes propriedades para cada marca de amostragem detectada:
+
+- Depth (m): profundidade real em metros.
+
+- N depth (m): profundidade nominal em metros.
+
+- Desc: número de descida.
+
+- Cond: condição da marca.
+
+- Diam (cm): diâmetro equivalente, o diâmetro de um círculo com a mesma área de marca.
+$$D = 2 \sqrt{\frac{area}{\pi}}$$
+
+- Circularity: circularidade, uma medida de quão próxima de um círculo a marca é, quanto mais próximo de 1, dado pela equação:
+$$C=\frac{4 \pi \times area}{perimetro^2}$$
+
+- Solidity: solidez, uma medida da convexidade da marca, quanto mais próximo de 1, dada pela equação:
+$$S=\frac{area}{area\text{_}convexa}$$
+
+| ![Figura 1](../assets/images/imagelog/area_vs_areaconvexa.png) |
+|:--------------------------------------------------------------:|
+| Figura 1: Área versus área convexa (em azul). |
+
+- Azimuth: posição horizontal em graus ao longo da imagem, 0 a 360 graus.
+
+- Label: identificador da instância na segmentação resultante.
+
+## Batentes
+
+Para os batentes, são utilizadas técnicas de visão computacional tradicional, que identificam marcas diagonais nas imagens de tempo de
+trânsito, características dos batentes.
+
+- Depth (m): profundidade real em metros.
+- Steepness: medida em graus da inclinação do batente em relação ao eixo horizontal.
+- Area (cm²): área em centímetros quadrados.
+- Linearity: o quanto o batente se parece com uma linha reta.
+- Label: o identificador da instância na segmentação resultante.
diff --git a/tools/deploy/GeoSlicerManual/docs/Quantification/partitioning.md b/tools/deploy/GeoSlicerManual/docs/Quantification/partitioning.md
new file mode 100644
index 0000000..b0ba746
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Quantification/partitioning.md
@@ -0,0 +1,71 @@
+### Particionamento
+
+Este módulo provê múltiplos métodos para analisar uma imagem segmentada. Particularmente, algoritmos Watershed e Islands permite fragmentar a segmentação em diversas partições, ou diversos segmentos. Normalmente é aplicado a segmentação de espaço de poros para computar as métricas de cada elemento de poro. A entrada é um nodo de segmentação ou volume labelmap, uma região de interesse (definida por um nodo de segmentação) e a imagem/volume mestre. A saída é um labelmap onde cada partição (elemento de poro) está em uma cor diferente, uma tabela com parâmetros globais e uma tabela com as diferentes métricas para cada partição.
+
+#### __Inputs__
+
+1. __Selecionar__ single-shot (segmentação única) ou Batch (múltiplas amostras definidas por múltiplos projetos GeoSlicer).
+2. __Segmentation__: Selecionar um nodo de segmentação ou um labelmap para ser inspecionado.
+3. __Region__: Selecionar um nodo de segmentação para definir uma região de interesse (opcional).
+4. __Image__: Selecionar a imagem/volume mestre ao qual a segmentação é relacionada.
+
+#### __Parameters__
+
+1. __Method__: Selecionar um método a ser aplicado. Com algoritmo island, a segmentação é fragmentada de acordo com conexões diretas. Com watershed, a segmentação é fragmentada de acordo com a transformada de distância e os parâmetros da seção _Advanced_.
+2. __Size Filter__: Filtrar partições espúrias com eixo principal (feret_max) menor que o valor _Size Filter_.
+3. __Smooth factor__: Fator de suavização, que é o desvio padrão do filtro gaussiano aplicado à transformada de distância. Conforme aumenta, menos partições serão criadas. Use valores menores para resultados mais confiáveis.
+4. __Minimum distance__: Distância mínima separando picos em uma região de 2 * min_distance + 1 (i.e. picos são separados por no mínimo min_distance). Para encontrar o número máximo de picos, use min_distance = 0.
+5. __Orientation line__: Selecionar a linha para ser usada para cálculo de ângulo de orientação.
+
+#### __Output__
+
+Digite um nome para ser usado como prefixo dos resultados (labelmap onde cada partição (elemento de poro) está em uma cor diferente, uma tabela com parâmetros globais e uma tabela com as diferentes métricas para cada partição).
+
+#### Propriedades / Métricas:
+
+1. __Label__: Identificador rotular da partição.
+2. __mean__: Valor médio da imagem/volume de entrada dentro da região da partição (poro/grão).
+3. __median__: Valor mediano da imagem/volume de entrada dentro da região da partição (poro/grão).
+4. __stddev__: Desvio padrão da imagem/volume de entrada dentro da região da partição (poro/grão).
+5. __voxelCount__: Número total de pixels/voxels da região da partição (poro/grão).
+6. __area__: Área total da partição (poro/grão). Unidade: mm^2.
+7. __angle__: Ângulo em graus (entre 270 e 90) relacionado à linha de orientação (opcional; se nenhuma linha for selecionada, a orientação de referência é superior horizontal).
+8. __max_feret__: Eixo de caliper de Feret máximo. Unidade: mm.
+9. __min_feret__: Eixo de caliper de Feret mínimo. Unidade: mm.
+10. __mean_feret__: Média entre os calipers mínimo e máximo.
+11. __aspect_ratio__: min_feret / max_feret.
+12. __elongation__: max_feret / min_feret.
+13. __eccentricity__: sqrt(1 - min_feret / max_feret) relacionado à elipse equivalente (0 <= e < 1), igual a 0 para círculos.
+14. __ellipse_perimeter__: Perímetro da elipse equivalente (com eixo dado por caliper de Feret mínimo e máximo). Unidade: mm.
+15. __ellipse_area__: Área da elipse equivalente (com eixo dado por caliper de Feret mínimo e máximo). Unidade: mm^2.
+16. __ellipse_perimeter_over_ellipse_area__: Perímetro da elipse equivalente dividido pela área.
+17. __perimeter__: Perímetro real da partição (poro/grão). Unidade: mm.
+18. __perimeter_over_area__: Perímetro real dividido pela área da partição (poro/grão).
+19. __gamma__: "Redondeza" de uma área calculada como 'gamma = perimeter / (2 * sqrt(PI * area))'.
+20. __pore_size_class__: Símbolo/código/id da classe do poro.
+21. __pore_size_class_label__: Rótulo da classe do poro.
+
+#### Definição das classes de poro:
+
+* __Microporo__: classe 0, max_feret menor que 0.062 mm.
+* __Mesoporo mto pequeno__: classe 1, max_feret entre 0.062 e 0.125 mm.
+* __Mesoporo pequeno__: classe 2, max_feret entre 0.125 e 0.25 mm.
+* __Mesoporo médio__: classe 3, max_feret entre 0.25 e 0.5 mm.
+* __Mesoporo grande__: classe 4, max_feret entre 0.5 e 1 mm.
+* __Mesoporo muito grande__: classe 5, max_feret entre 1 e 4 mm.
+* __Megaporo pequeno__: classe 6, max_feret entre 4 e 32 mm.
+* __Megaporo grande__: classe 7, max_feret maior que 32mm.
+
+### Label Map Editor
+
+Realiza separação e aglutinação manual de objetos rotulados.
+
+#### __Atalhos para ferramentas__
+
+- m: Mesclar dois rótulos
+- a: Dividir rótulo automaticamente usando watershed
+- s: Dividir rótulo com uma linha reta
+- c: Cortar rótulo no ponteiro do mouse
+- z: Desfazer última edição
+- x: Refazer edição desfeita
+- Esc: Cancelar operação
\ No newline at end of file
diff --git a/tools/deploy/GeoSlicerManual/docs/Quantification/result_analysis.md b/tools/deploy/GeoSlicerManual/docs/Quantification/result_analysis.md
new file mode 100644
index 0000000..ad41ca4
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Quantification/result_analysis.md
@@ -0,0 +1,84 @@
+# Análise de Resultados
+
+
+
+
Tabelas
+
Além dos relatórios visuais, o Geoslicer permite a exportação de tabelas de dados derivadas das análises realizadas. Estas tabelas são vitais para a documentação detalhada dos resultados, permitindo uma análise quantitativa e comparativa, além de serem facilmente integradas com outros softwares de análise.
+
+
O ícone é usado para editar tabelas, conforme descrito nos passos abaixo:
+
+
+
Clique no ícone de tabela.
+
+
+
Navegue pelos dados da tabela pela janela interativa
+
+
+
Na seção Calculator descreva a operação entre as colunas e a coluna que sera modificada/adicionada
+
+
+
Click em Calculate para modificar a tabela de acordo com a formula do passo anterior.
+
+
+
+
+
+
Video: Uso básico de tabelas
+
+
+
+
+
+
Gráficos
+
O ícone é usado no GeoSlicer é usado para a visualização de dados através de vários tipos de gráficos, permitindo a geração de gráficos a partir de dados contidos em tabelas. Os tipos de gráficos disponíveis incluem crossplots, diagramas de roseta e gráficos de barras. A interface do módulo permite aos usuários selecionar tabelas de dados, escolher um tipo de gráfico e exibir múltiplos conjuntos de dados no mesmo gráfico. A interface do gráfico permite a personalização interativa, como ajustar escalas e selecionar propriedades para os eixos X, Y e Z.
+
+
+
+
Clique no ícone de Gráfico.
+
+
+
Selecione uma tabela com os dados que serao utilizado no plot
+
+
+
Em Plot type escolha o tipo ideal de plot a ser renderizado com seus dados
+
+
+
Clique em Plot
+
+
+
Nomeie seu plot e clique em Ok
+
+
+
Na seção data selecione a coluna a ser plotada
+
+
+
As opções Bins, Show mean and stantard deviation and Log mode podem ser usadas para melhorar a visualização dos dados
+
+
+
+
+
+
Video: Uso básico de gráficos
+
+
+
+
+
+
+
Módulo MultipleImageAnalysis
+ O módulo Multiple Image Analysis é uma ferramenta avançada do Geoslicer, projetada para gerar relatórios personalizados com base em diferentes tipos de análises de imagens. Ele suporta análises como histogramas, médias em profundidade e petrofísica básica. Os usuários podem selecionar o tipo de análise desejado através de uma interface intuitiva, carregar diretórios contendo imagens de seções delgadas, e configurar as opções específicas para cada tipo de análise. Após o processamento, o módulo gera relatórios detalhados que podem ser visualizados e exportados para uso posterior. A flexibilidade em escolher diferentes tipos de análises permite que os geólogos obtenham insights abrangentes e detalhados sobre as propriedades das rochas estudadas.
+
+
+
+
Video: MultipleImageAnalysis
+
+
\ No newline at end of file
diff --git a/tools/deploy/GeoSlicerManual/docs/Remote_computing/accounts.md b/tools/deploy/GeoSlicerManual/docs/Remote_computing/accounts.md
new file mode 100644
index 0000000..9e24364
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Remote_computing/accounts.md
@@ -0,0 +1 @@
+As contas cadastradas são gerenciadas
\ No newline at end of file
diff --git a/tools/deploy/GeoSlicerManual/docs/Remote_computing/intro.md b/tools/deploy/GeoSlicerManual/docs/Remote_computing/intro.md
new file mode 100644
index 0000000..f26d92b
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Remote_computing/intro.md
@@ -0,0 +1,32 @@
+O GeoSlicer permite que o usuário conecte-se a uma conta remota, via ssh, para acessar recursos computacionais como
+clusters. Atualmente o modelo implementado está bastante atrelado ao requisitos de execução da Petrobras, no entando
+quanlquer máquina pode ser acessada pelo módulo **_Job Monitor_**, desde que ela tenha acesso SSH liberado e acessível
+na rede.
+
+## Conexão
+
+A conta deve ser cadastrada, como pode ser visto na Figura 1. O usuário deve preencher os campos de acordo com as
+suas credencias de acesso. Caso o usuário tenha uma chave SSH, ele pode inserir o caminho para a chave clicando em **Add
+Key**.
+
+![Registro de Conta](../assets/images/register_screen.png)
+
+Observe que o usuário pode configurar outro paramêtros relativos ao que vai acontecer quando a conexão for estabelecida.
+Como por exemplo, o _"Command Setup"_ que é o comando que é executado imediatamente ao estabelecer a conexão. Os demais
+parametros são especificidades do cluster da Petrobras.
+
+## Contas
+
+As contas cadastradas são gerenciadas na tela de contas, como pode ser visto na Figura 2. Nela o usuário pode ver
+quais contas estão ativas, remover e editar as contas cadastradas.
+
+![Tela de Contas](../assets/images/account_screen.png)
+
+## Job Monitor
+
+O módulo **_Job Monitor_** é responsável por monitorar os jobs que estão sendo executados na máquina remota. Através
+dele o usuário pode acompanhar o progresso das tarefas em execução. Atualmente, apenas as tarefas do módulo *
+*_Microtom_** são enviadas remotamente. O **_Job Monitor_** é acessado através do barra de dialogos, no canto inferior
+direito.
+
+![Job Monitor](../assets/images/job_monitor_screen.png)
diff --git a/tools/deploy/GeoSlicerManual/docs/Remote_computing/job_monitor.md b/tools/deploy/GeoSlicerManual/docs/Remote_computing/job_monitor.md
new file mode 100644
index 0000000..e69de29
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_microCT.md b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_microCT.md
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_microCT.md
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_microCT.md
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_microCT_1.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_microCT_1.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_microCT_1.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_microCT_1.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_microCT_10.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_microCT_10.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_microCT_10.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_microCT_10.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_microCT_11.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_microCT_11.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_microCT_11.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_microCT_11.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_microCT_12.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_microCT_12.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_microCT_12.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_microCT_12.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_microCT_2.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_microCT_2.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_microCT_2.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_microCT_2.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_microCT_2b.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_microCT_2b.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_microCT_2b.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_microCT_2b.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_microCT_3.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_microCT_3.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_microCT_3.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_microCT_3.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_microCT_4.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_microCT_4.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_microCT_4.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_microCT_4.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_microCT_5.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_microCT_5.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_microCT_5.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_microCT_5.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_microCT_6.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_microCT_6.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_microCT_6.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_microCT_6.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_microCT_7.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_microCT_7.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_microCT_7.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_microCT_7.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_microCT_8.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_microCT_8.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_microCT_8.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_microCT_8.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_microCT_8b.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_microCT_8b.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_microCT_8b.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_microCT_8b.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_microCT_9.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_microCT_9.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_microCT_9.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_microCT_9.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection.md b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection.md
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection.md
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection.md
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_1.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_1.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_1.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_1.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_10.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_10.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_10.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_10.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_11.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_11.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_11.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_11.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_12.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_12.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_12.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_12.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_13.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_13.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_13.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_13.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_14.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_14.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_14.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_14.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_15.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_15.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_15.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_15.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_16.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_16.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_16.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_16.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_17.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_17.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_17.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_17.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_18.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_18.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_18.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_18.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_19.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_19.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_19.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_19.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_2.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_2.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_2.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_2.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_20.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_20.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_20.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_20.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_21.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_21.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_21.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_21.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_22.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_22.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_22.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_22.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_23.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_23.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_23.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_23.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_24.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_24.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_24.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_24.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_25.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_25.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_25.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_25.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_26.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_26.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_26.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_26.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_27.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_27.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_27.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_27.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_28.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_28.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_28.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_28.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_29.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_29.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_29.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_29.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_3.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_3.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_3.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_3.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_30.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_30.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_30.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_30.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_31.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_31.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_31.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_31.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_32.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_32.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_32.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_32.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_33.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_33.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_33.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_33.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_34.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_34.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_34.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_34.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_35.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_35.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_35.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_35.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_4.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_4.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_4.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_4.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_5.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_5.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_5.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_5.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_6.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_6.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_6.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_6.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_7.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_7.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_7.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_7.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_8.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_8.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_8.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_8.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_9.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_9.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Automatic/automatic_thinSection_9.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Automatic/automatic_thinSection_9.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Semiauto/semiauto-figura_1.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Semiauto/semiauto-figura_1.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Semiauto/semiauto-figura_1.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Semiauto/semiauto-figura_1.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Semiauto/semiauto-figura_2.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Semiauto/semiauto-figura_2.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Semiauto/semiauto-figura_2.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Semiauto/semiauto-figura_2.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Semiauto/semiauto-figura_3.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Semiauto/semiauto-figura_3.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Semiauto/semiauto-figura_3.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Semiauto/semiauto-figura_3.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Semiauto/semiauto-figura_4.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Semiauto/semiauto-figura_4.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Semiauto/semiauto-figura_4.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Semiauto/semiauto-figura_4.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Semiauto/semiauto-figura_5.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Semiauto/semiauto-figura_5.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Semiauto/semiauto-figura_5.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Semiauto/semiauto-figura_5.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Semiauto/semiauto-figura_a1.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Semiauto/semiauto-figura_a1.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Semiauto/semiauto-figura_a1.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Semiauto/semiauto-figura_a1.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Semiauto/semiauto-figura_a2.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Semiauto/semiauto-figura_a2.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Semiauto/semiauto-figura_a2.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Semiauto/semiauto-figura_a2.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Semiauto/semiauto-figura_a3.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Semiauto/semiauto-figura_a3.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Semiauto/semiauto-figura_a3.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Semiauto/semiauto-figura_a3.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Semiauto/semiauto-figura_a4.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Semiauto/semiauto-figura_a4.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Semiauto/semiauto-figura_a4.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Semiauto/semiauto-figura_a4.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Semiauto/semiauto-figura_a5.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Semiauto/semiauto-figura_a5.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Semiauto/semiauto-figura_a5.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Semiauto/semiauto-figura_a5.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Semiauto/semiauto-figura_mink_1.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Semiauto/semiauto-figura_mink_1.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Semiauto/semiauto-figura_mink_1.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Semiauto/semiauto-figura_mink_1.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Semiauto/semiauto-figura_mink_2.png b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Semiauto/semiauto-figura_mink_2.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Semiauto/semiauto-figura_mink_2.png
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Semiauto/semiauto-figura_mink_2.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Segmenter/Semiauto/semiauto.md b/tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Semiauto/semiauto.md
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Segmenter/Semiauto/semiauto.md
rename to tools/deploy/GeoSlicerManual/docs/Resources/Segmenter/Semiauto/semiauto.md
diff --git a/tools/deploy/GeoSlicerManual/docs/Simulation/PNM/intro.md b/tools/deploy/GeoSlicerManual/docs/Simulation/PNM/intro.md
new file mode 100644
index 0000000..92b51d8
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Simulation/PNM/intro.md
@@ -0,0 +1,120 @@
+# Pore Network Model (PNM)
+
+## Extração
+
+O fluxo do PNM começa a partir da "extração" da rede de poros e ligações que pode ser feita a partir de: uma segmentação dos poros (_Label Map Volume_) realizada por um algoritmo de _watershed_, que gerará a rede uniescalar; ou por um mapa de porosidades (_Scalar Volume_), que gerará um modelo multiescalar com poros resolvidos e não-resolvidos.
+
+Após a extração, ficará disponível na interface do GeoSlicer: as tabelas de poros e gargantas e também os modelos de visualização da rede. As tabelas geradas serão os dados usados na etapa seguinte de simulação.
+
+| |
+|:-----------------------------------------------------------------------:|
+| Figura 1: A esquerda Label Map utilizado como entrada na extração e a direita rede uniescalar extraída. |
+
+| |
+|:-----------------------------------------------------------------------:|
+| Figura 2: A esquerda Scalar Volume utilizado como entrada na extração e a direita rede multiescalar extraída, onde azul representa poros resolvidos, e rosa representa os poros não-resolvidos. |
+
+
+## Simulação
+
+A partir da tabela de poros pode-se realizar, através da aba _Simulation_ diferentes tipos de simulação: _One-phase_, _Two-phase_, _Mercury injection_, explicados adiante.
+
+Todas as simulações possuem os mesmos argumentos de entrada: A tabela de poros gerada a partir da extração da rede e quando o volume for multiescalar, o modelo subescala utilizado e sua parametrização.
+
+No caso da rede multiescalar, como os raios da subescala não podem ser determinados a partir da própria imagem, por estarem fora da resolução, é necessário definir um modelo para atribuição desses raios. Algumas opções disponíveis atualmente são:
+
+- _Fixed radius_: Todos os raios da subresolução tem o mesmo tamanho escolhido na interface;
+- _Leverett Function - Sample Permeability_: Atribui uma pressão de entrada com base na curva de J Leverett com base na permeabilidade da amostra;
+- _Leverett Function - Permeability curve_: Também utiliza a curva de J Leverett mas com uma curva definida para a permeabilidade ao invés de um valor;
+- _Pressure Curve_ e _Throat Radius Curve_: Atribui os raios da subresolução com base na curva obtida por um experimento de injeção de mercúrio. Pode ser utilizado o dado da pressão de entrada pela fração do volume, ou então o raio equivalente em função da fração de volume;
+
+O modelo de subescala escolhido não tem impacto nas simulações de redes uniescalares, uma vez que todos os raios já estão determinados.
+
+### _One-phase_
+
+A simulação de uma fase, é utilizada principalmente para determinar a propriedade de permeabilidade absoluta ($K_{abs}$) da amostra. Pode ser realizada em uma única direção ou em múltiplas direções a partir do parâmetro _Orientation scheme_.
+
+### _Two-phase_
+
+A simulação de duas fases consiste em inicialmente injetar óleo na amostra e aumentar a pressão do mesmo afim de que esse invada praticamente todos os poros, num processo que é conhecido como drenagem (_drainage_). Após essa primeira etapa, substitui-se o óleo por água e novamente aumenta-se a pressão, de forma a permitir que a água invada alguns dos poros que antes estavam com óleo, expulsando o último, esse segundo processo é conhecido como embebição (_imbibition_). Ao medirmos a permeabilidade da rocha em relação a permeabilidade absoluta, em função da saturação de água durante esse processo, obtemos a curva conhecida como curva de permeabilidade relativa ($K_{rel}$).
+
+Uma vez que cada rocha pode interagir físicamente ou quimicamente com o óleo e com a água de diferentes maneiras, precisamos de uma variedade bastante grande de parametros que permitam calibrar os resultados de forma a modelar e simplificar essa interação, afim de extrairmos algum significado físico das propriedades da rocha a partir da simulação. Abaixo, elencamos alguns parâmetros que podem ser encontrados na simulação de duas fases disponibilizada no GeoSlicer.
+
+Atualmente, temos disponível no GeoSlicer dois algoritmos para realizar a simulação de duas fases, a primeira delas é a do PNFlow que é um algoritmo padrão utilizado, implementado em C++, e a segunda é um algoritmo próprio desenvolvido pela LTrace em linguagem Python.
+
+#### _Fluid properties_
+
+Nessa seção separamos os parâmetros dos fluidos (água e óleo) utilizados:
+
+- Viscosidade da água (cP)
+- Densidade da água (Kg/m3)
+- Viscosidade do óleo (cP)
+- Densidade do óleo (Kg/m3)
+- Tensão superficial (N/m)
+
+#### _Contact angle options_
+
+Uma das principais propriedades que afetam a interação de um líquido com um sólido é a molhabilidade, essa pode ser determinada a partir do ângulo de contato formado pelo primeiro quando em contato com o último. Assim, se o ângulo de contato for próximo de zero há uma forte interação que "prende" o líquido ao sólido, já quando o ângulo de contato é próximo a 180º, a interação do líquido com a superfície é fraca e esse pode escoar com mais facilidade pela mesma.
+
+| ![Figura 3](../../assets/images/pnm/molhabilidade.png) |
+|:----------------------------------------------------------------------------:|
+| Figura 3: Representação visual do conceito de molhabilidade e ângulo de contato. |
+
+Modelamos os ângulos de contato a partir de duas distribuições usadas em momentos distintos: a _Initial contact angle_ que controla o ângulo de contato dos poros antes da invasão por óleo; e a _Equilibrium contact angle_ que controla o ângulo de contato após a invasão por óleo. Além das distribuições base utilizadas em cada caso, há uma opção para adicionar uma segunda distribuição para cada uma delas, assim cada poro é atrelado a uma das duas distribuições, com o parâmetro "Fraction" sendo usado para determinar qual a porcentagem de poros vão seguir a segunda distribuição em relação a primeira.
+
+Cada distribuição de ângulos, seja primária ou secundária, inicial ou de equilíbrio, tem uma série de parâmetros que a descreve:
+
+- _Model_: permite modelar as curvas de histerese entre ângulos de avanço/recuo a partir dos ângulos intrínsicos:
+
+ - _Equal angles_: ângulos de avanço/recuo idênticos ao ângulo intrinsico;
+ - _Constant difference_: diferença constante dos ângulos de avanço/recuo em relação ao ângulo intrínsico;
+ - _Morrow curve_: curvas de avanço/recuo determinadas pelas curvas de Morrow;
+
+| |
+|:---------------------------------------------------------------------:|
+| Figura 4: Curvas para cada um dos modelos de ângulo de contato implementados no GeoSlicer. |
+
+- _Contact angle distribution center_: Define o centro da distribuição de ângulo de contato;
+- _Contact angle distribution range_: Alcance da distribuição (center-range/2, center+range/2), com o ângulo mínimo/máximo sendo 0º/180º, respectivamente;
+- _Delta_, _Gamma_: Parâmetros da distribuição de Weibull truncada, se um número negativo é escolhido, usa uma distribuição uniforme;
+- _Contact angle correlation_: Escolhe como o ângulo de contato será correlacionado ao raio dos poros: _Positive radius_ define maiores ângulos de contato para raios maiores; _Negative radius_ faz o oposto, atribuindo maiores ângulos para raios menores; _Uncorrelated_ significa independência entre ângulos de contato com o raio do poro;
+- _Separation_: Se o modelo escolhido for _constant difference_, define a separação entre ângulos de avanço e recuo;
+
+Outros parâmetros estão definidos apenas para a segunda distribuição:
+
+- _Fraction_: Um valor entre 0 e 1 que controla qual a fração dos poros usará a segunda distribuição ao invés da primeira;
+- _Fraction distribution_: Define se a fração será determinada pela quantidade de poros ou volume total;
+- _Correlation diameter_: Se a _Fraction correlation_ for escolhida como _Spatially correlated_, define a distância mais provável de encontrar poros com mesma distribuição de ângulo de contato;
+- _Fraction correlation_: Define como a fração para a segunda distribuição será correlacionada, se correlacionada espacialmente, para maiores poros, menores poros ou aleatória;
+
+#### _Simulation options_
+
+Alguns parâmetros relacionados a própria simulação podem ser escolhidos nessa seção, entre eles:
+
+- _Minimum SWi_: Define o valor mínimo de SWi, interrompendo o ciclo de drenagem quando o valor de Sw é atingido (SWi pode ser maior se a água ficar presa);
+- _Final cycle Pc_: Interrompe o ciclo quando essa pressão capilar é alcançada;
+- _Sw step length_: Passo de Sw utilizado antes de verificar o novo valor de permeabilidade;
+- _Inject/Produce from_: Define por quais lados o fluido será injetado/produzido ao longo do eixo z, o mesmo lado pode tanto injetar como também produzir;
+- _Pore fill_: Determina qual mecanismo domina cada evento de preenchimento de poro individual;
+- _Lower/Upper box boundary_: Poros com distância relativa no eixo Z da borda até este valor do plano são considerados poros "à esquerda"/"à direita", respectivamente;
+- _Subresolution volume_: Considera que o volume contém essa fração de espaço poroso em subresolução que está sempre preenchido com água;
+- _Plot first injection cycle_: Se selecionado, o primeiro ciclo, injeção de óleo em um meio totalmente saturado de água, será incluído no gráfico de saída. A simulação será executada, independentemente da opção estar selecionada ou não;
+- _Create animation node_: Cria um nó de animação que pode ser usado na aba "Cycles Visualization";
+- _Keep temporary files_: Mantém os arquivos .vtu na pasta de arquivos temporários do GeoSlicer, um arquivo para cada etapa da simulação;
+- _Max subprocesses_: Quantidade máxima de subprocessos single-thread que devem ser executados; O valor recomendado para uma máquina ociosa é 2/3 do total de núcleos;
+
+#### _Sensitivity test_
+
+Uma vez que temos uma vasta quantidade de parâmetros que podem ser modificados para modelar o experimento a partir da simulação, se torna útil variarmos tais parâmetros de forma mais sistemática para uma análise aprofundada da sua influência nos resultados obtidos.
+
+Para isso o usuário pode selecionar o botão "_Multi_" disponível em grande parte dos parâmetros, ao clicar em Multi, três caixas aparecem com opções de início, fim e passo, que podem ser usadas para rodar diversas simulações em uma tabela linearmente distribuída dos valores desses parâmetros. Se mais de um parâmetro é escolhido com múltiplos valores, simulações rodam com todas as combinações de parâmetros possíveis, isso pode aumentar consideravelmente a quantidade de simulações e o tempo para executar.
+
+Ao finalizar a execução do conjunto de simulações, o usuário pode realizar análises para entender as relações entre os resultados das simulações com os parâmetros escolhidos na aba _Krel EDA_.
+
+### _Mercury injection_
+
+A intrusão de mercúrio é um experimento no qual mercúrio líquido é injetado em uma amostra de rocha reservatório sob vácuo, com pressão crescente. O volume de mercúrio invadindo a amostra é medido em função da pressão de mercúrio. Uma vez que o ângulo de contato do mercúrio líquido com o vapor de mercúrio é aproximadamente independente do substrato, é possível utilizar modelos analíticos, como o modelo do feixe de tubos, para calcular a distribuição do tamanho dos poros da amostra.
+
+O ensaio de intrusão de mercúrio é relativamente acessível e sua principal relevância no contexto do PNM reside na capacidade de executar a simulação em uma amostra para a qual os resultados experimentais de curvas de Pressão Capilar por Intrusão de Mercúrio (MICP) estão disponíveis. Isso permite a comparação dos resultados para validar e calibrar a rede de poros extraída da amostra, que será usada nas simulações de uma e duas fases.
+
+Para facilitar as análises da atribuição dos raios dos poros sub-resolução, o código presente no GeoSlicer produzirá como saída, além dos gráficos obtidos pela simulação no OpenPNM, os gráficos das distribuições de raios de poros e gargantas e também das distribuições de volumes, separando em poros resolvidos (que não se alteram pela atribuição da subescala) e poros não resolvidos. Dessa forma, o usuário pode conferir se o modelo de subescala foi aplicado corretamente.
diff --git a/tools/deploy/GeoSlicerManual/docs/Simulation/PNM/kabs.md b/tools/deploy/GeoSlicerManual/docs/Simulation/PNM/kabs.md
new file mode 100644
index 0000000..e3e5607
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Simulation/PNM/kabs.md
@@ -0,0 +1,32 @@
+# Simulação de Permeabilidade Absoluta (Kabs)
+
+
+
+
Simulação Kabs em escala única
+
O fluxo abaixo permite simular e obter um estimado da permeabilidade absoluta, em uma amostra de escala única, considerando todos os poros como resolvidos:
+
+
+
Carregue o volume no qual deseja executar a simulação;
+
+
+
Realize a Segmentação Manual utilizando um dos segmentos para designar a região porosa da rocha;
+
+
+
Separe os segmentos utilizando a aba Inspector, delimitando assim a região de cada um dos poros;
+
+
+
Utilize a aba Extraction para obter a rede de poros e ligações a partir do volume LabelMap gerado;
+
+
+
Na aba Simulation, escolha a tabela de poros, no seletor Simulation selecione "One-phase" para rodar a simulação de Kabs;
+
+
+
+
+
+
Video: Fluxo para simulação de permeabilidade absoluta.
+
+
diff --git a/tools/deploy/GeoSlicerManual/docs/Simulation/PNM/krel.md b/tools/deploy/GeoSlicerManual/docs/Simulation/PNM/krel.md
new file mode 100644
index 0000000..f6d4d4f
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Simulation/PNM/krel.md
@@ -0,0 +1,130 @@
+# Simulação de Permeabilidade Relativa (Krel)
+
+
+
+
Simulação única de Krel (animação)
+
O fluxo abaixo permite simular e obter uma animação dos processos de Drenagem e Embibição:
+
+
+
Carregue o volume no qual deseja executar a simulação;
+
+
+
Realize a Segmentação Manual utilizando um dos segmentos para designar a região porosa da rocha;
+
+
+
Separe os segmentos utilizando a aba Inspector, delimitando assim a região de cada um dos poros;
+
+
+
Utilize a aba Extraction para obter a rede de poros e ligações a partir do volume LabelMap gerado;
+
+
+
Na aba Simulation, escolha a tabela de poros, no seletor Simulation selecione "Two-phase";
+
+
+
Marque a opção "Create animation node" na caixa "Simulation options" e clique no botão "Apply";
+
+
+
Ao finalizar a simulação, vá até a aba "Cycles Visualization" e selecione o nó de animação para visualizar o ciclo e a curva gerada;
+
+
+
+
+
+
Video: Fluxo para simulação de permeabilidade relativa com animação.
+
+
+
+
+
+
+
Teste de Sensibilidade
+
O fluxo abaixo permite que o usuário simule e obtenha uma nuvem de curvas de Krel na qual ele pode fazer diferentes análises para determinar as propriedades que são mais sensíveis:
+
+
+
Carregue o volume no qual deseja executar a simulação;
+
+
+
Realize a Segmentação Manual utilizando um dos segmentos para designar a região porosa da rocha;
+
+
+
Separe os segmentos utilizando a aba Inspector, delimitando assim a região de cada um dos poros;
+
+
+
Utilize a aba Extraction para obter a rede de poros e ligações a partir do volume LabelMap gerado;
+
+
+
Na aba Simulation, escolha a tabela de poros, no seletor Simulation selecione "Two-phase";
+
+
+
Selecione múltiplos valores para alguns parâmetros clicando no botão "Multi" (como fizemos para o centro das distribuições dos ângulos de contato no vídeo) - Você pode encontrar mais informações sobre os parâmetros na seção "Two-phase";
+
+
+
(Opcional) Salve os parâmetros selecionados usando a seção "Save parameters";
+
+
+
Clique no botão "Apply" para rodar as várias simulações;
+
+
+
Ao finalizar a execução, vá até a aba "Krel EDA" e selecione a tabela de parâmetros gerada para fazer diferentes análises usando os recursos de visualização da interface (nuvem de curvas, correlações de parâmetros e resultados, etc);
+
+
+
+
+
+
Video: Fluxo para Teste de Sensibilidade (variando parâmetros para múltiplas simulações Krel).
+
+
+
+
+
+
+
Estimativa de Produção
+
O fluxo abaixo permite que o usuário simule e obtenha uma nuvem de curvas de Krel, em uma amostra de escala única:
+
+
+
Carregue o volume no qual deseja executar a simulação;
+
+
+
Realize a Segmentação Manual utilizando um dos segmentos para designar a região porosa da rocha;
+
+
+
Separe os segmentos utilizando a aba Inspector, delimitando assim a região de cada um dos poros;
+
+
+
Utilize a aba Extraction para obter a rede de poros e ligações a partir do volume LabelMap gerado;
+
+
+
Selecione múltiplos valores para alguns parâmetros clicando no botão "Multi" (como fizemos para o centro das distribuições dos ângulos de contato no vídeo) - Você pode encontrar mais informações sobre os parâmetros na seção "Two-phase";
+
+
+
(Opcional) Salve os parâmetros selecionados usando a seção "Save parameters";
+
+
+
Clique no botão "Apply" para rodar as várias simulações;
+
+
+
+ Ao finalizar a execução, vá até a aba "Production Prediction" e selecione a tabela de parâmetros gerada na simulação; Duas opções são disponíveis nessa interface:
+
+
A primeira delas "Single Krel" é uma análise de cada simulação individual;
+
A segunda "Sensitivity test" é uma estimativa da produção levando em conta todas as simulações feitas;
+
+
+
+
+
+
+
+
Video: Fluxo da estimativa de produção.
+
+
+
diff --git a/tools/deploy/GeoSlicerManual/docs/Simulation/PNM/micp.md b/tools/deploy/GeoSlicerManual/docs/Simulation/PNM/micp.md
new file mode 100644
index 0000000..508acda
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Simulation/PNM/micp.md
@@ -0,0 +1,35 @@
+# Simulação de Intrusão por Mercúrio
+
+
+
+
Simulação de MICP
+
O fluxo abaixo permite simular o experimento de Intrusão por Mercúrio na amostra:
+
+
+
Carregue o volume no qual deseja executar a simulação;
+
+
+
Realize a Segmentação Manual utilizando um dos segmentos para designar a região porosa da rocha;
+
+
+
Separe os segmentos utilizando a aba Inspector, delimitando assim a região de cada um dos poros;
+
+
+
Utilize a aba Extraction para obter a rede de poros e ligações a partir do volume LabelMap gerado;
+
+
+
Na aba Simulation, escolha a tabela de poros, no seletor Simulation selecione "Mercury injection";
+
+
+
Ao finalizar a simulação, os resultados podem ser visualizados na tabela gerada ou então nos gráficos criados;
+
+
+
+
+
+
Video: Fluxo para simulação de intrusão de mercúrio.
+
+
diff --git a/tools/deploy/GeoSlicerManual/docs/Simulation/PNM/report.md b/tools/deploy/GeoSlicerManual/docs/Simulation/PNM/report.md
new file mode 100644
index 0000000..009923a
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Simulation/PNM/report.md
@@ -0,0 +1,38 @@
+# Geração de relatórios
+
+
+
+
Geração de relatórios
+
O fluxo abaixo permite rodar um fluxo completo do PNM, realizando simulações de Kabs, Krel e MICP; Os resultados podem ser visualizados em uma interface web:
+
+
+
Carregue o volume no qual deseja executar a simulação;
+
+
+
Realize a Segmentação Manual utilizando um dos segmentos para designar a região porosa da rocha;
+
+
+
Separe os segmentos utilizando a aba Inspector, delimitando assim a região de cada um dos poros;
+
+
+
Utilize a aba Microtom e selecione "PNM Complete Workflow";
+
+
+
Selecione ou crie uma tabela de seleção de parâmetros do "Teste de Sensibilidade" clicando no botão "Edit";
+
+
+
Clique em "Apply" para rodar o fluxo;
+
+
+
Ao finalizar a simulação, clique no botão "Open Report Locally" para abrir o relatório;
+
+
+
+
+
+
Video: Execução do fluxo completo do PNM para geração de relatório.
+
+
diff --git a/tools/deploy/GeoSlicerManual/docs/Simulation/microtom.md b/tools/deploy/GeoSlicerManual/docs/Simulation/microtom.md
new file mode 100644
index 0000000..12bf47c
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Simulation/microtom.md
@@ -0,0 +1,23 @@
+# MicroTom
+
+Este módulo permite que usuários do _GeoSlicer_ usem algoritmos e métodos da biblioteca MicroTom, desenvolvida pela
+Petrobras.
+
+__Métodos disponíveis__
+
+* Distribuição do tamanho dos poros
+* Distribuição hierárquica do tamanho dos poros
+* Pressão capilar de injeção de mercúrio
+* Permeabilidade absoluta de Stokes-Kabs na escala dos poros
+
+## Interface
+
+### Entrada
+
+1. __Segmentação__: Selecione um labelmap no qual o algoritmo microtom será aplicado. Deve ser um labelmap, nó de
+ segmentação não é aceito (qualquer nó de segmentação pode ser transformado em um labelmap na aba _Dados_ clicando com
+ o botão esquerdo no nó. ).
+2. __Região (SOI)__: Selecione uma nota de segmentação onde o primeiro segmento delimita a região de interesse onde a
+ segmentação será realizada.
+3. __Segmentos__: Selecione um segmento na lista para ser usado como o espaço poroso da rocha.
+
diff --git a/tools/deploy/GeoSlicerManual/docs/Support_and_assistance/error_log.md b/tools/deploy/GeoSlicerManual/docs/Support_and_assistance/error_log.md
new file mode 100644
index 0000000..e69de29
diff --git a/tools/deploy/GeoSlicerManual/docs/Support_and_assistance/log_bundle.md b/tools/deploy/GeoSlicerManual/docs/Support_and_assistance/log_bundle.md
new file mode 100644
index 0000000..e69de29
diff --git a/tools/deploy/GeoSlicerManual/docs/Thin Section/Usabilidade/usabilidade.md b/tools/deploy/GeoSlicerManual/docs/Thin Section/Usabilidade/usabilidade.md
deleted file mode 100644
index ec42bc7..0000000
--- a/tools/deploy/GeoSlicerManual/docs/Thin Section/Usabilidade/usabilidade.md
+++ /dev/null
@@ -1,183 +0,0 @@
-# Ambiente Thin Section
-
-Ambiente para trabalhar com seções delgadas.
-
-Módulos:
-
-- Data
-- Loader (Thin Section Loader)
-- QEMSCAN Loader
-- Crop (Crop Volume)
-- Registration (Thin Section Registration)
-- Segmentation
-
-## Data
-
-Módulo _GeoSlicer_ para visualizar os dados sendo trabalhados e suas propriedades.
-
-## Thin Section Loader
-
-Módulo _GeoSlicer_ para carregar imagens de seção delgada em lotes, conforme descrito nos passos abaixo:
-
-1. Use o botão _Add directories_ para adicionar diretórios contendo dados de seção delgada. Esses diretórios apareceção na área _Data to be loaded_ (uma busca por dados de seção delgada nesses diretórios ocorrerá em subdiretórios abaixo em no máximo um nível). Pode-se também remover entradas indesejadas selecionando-as e clicando em _Remove_.
-
-2. Defina o tamanho do pixel (_Pixel size_) em milímetros.
-
-3. Opcionalmente, ative _Try to automatically detect pixel size_. Se funcionar, o tamanho de pixel detectado substituirá o valor configurado em _Pixel size_.
-
-4. Clique no botão _Load thin sections_ e aguarde o carregamento ser finalizado. As imagens carregadas podem ser acessadas na aba _Data_, dentro do diretório _Thin Section_.
-
-## QEMSCAN Loader
-
-Módulo _GeoSlicer_ para carregar imagens QEMSCAN em lotes, conforme descrito nos passos abaixo:
-
-1. Use o botão _Add directories_ para adicionar diretórios contendo dados QEMSCAN. Esses diretórios aparecerão na área _Data to be loaded_ (uma busca por dados QEMSCAN nesses diretórios ocorrerá em subdiretórios abaixo em no máximo um nível). Pode-se também remover entradas indesejadas selecionando-as e clicando em _Remove_.
-
-2. Selecione a tabela de cores (_Lookup color table_). Pode-se selecionar a tabela padrão (_Default mineral colors_) ou adicionar uma nova tabela clicando no botão _Add new_ e selecionando um arquivo CSV. Tem-se também a opção de fazer o carregador buscar por um arquivo CSV no mesmo diretório que o arquivo QEMSCAN sendo carregado. Também há a opção _Fill missing values from "Default mineral colors" lookup table_ para preencher valores faltantes.
-
-3. Defina o tamanho do pixel (_Pixel size_) em milímetros.
-
-4. Clique no botão _Load QEMSCANs_ e aguarde o carregamento ser finalizado. Os QEMSCANs carregados podem ser acessados na aba _Data_, dentro do diretório _QEMSCAN_.
-
-## Crop Volume
-
-Módulo _GeoSlicer_ para cortar um volume, conforme descrito nos passos abaixo:
-
-1. Selecione o volume em _Volume to be cropped_.
-
-2. Ajuste a posição e tamanho desejados da ROI nas slice views.
-
-3. Clique em _Crop_ e aguarde a finalização. O volume cortado aparecerá no mesmo diretório que o volume original.
-
-## Image Tools
-
-Módulo _GeoSlicer_ que permite manipulação de imagens, conforme descrito abaixo:
-
-1. Selecione a imagem em _Input image_.
-
-2. Selecione a ferramenta em _Tool_ e faça as mudanças desejadas.
-
-3. Clique no botão _Apply_ para confirmar as mudanças. Essas mudanças não são permanentes e podem ser desfeitas clicando no botão _Undo_; e serão descartadas se o módulo for deixado sem serem salvas ou for clicado o botão _Reset_ (isso reverterá a imagem ao seu último estado salvo). Mudanças podem ser tornadas permanentes clicando no botão _Save_ (isso alterará a imagem e não pode ser desfeito).
-
-## Thin Section Registration
-
-Módulo _GeoSlicer_ para registrar imagens de seção delgada e QEMSCAN, conforme descrito nos passos abaixo:
-
-1. Clique no botão _Select images to register_. Uma janela de diálogo aparecerá que permite a seleção da imagem fixa (_Fixed image_) e a imagem móvel (_Moving image_). Após selecionar as imagens desejadas, clique no botão _Apply_ para iniciar o registro.
-
-2. Adicione Landmarks (pontos de ancoragem) às imagens clicando em _Add_ na seção _Landmarks_. Arraste os Landmarks conforme desejado para match as mesmas localizações em ambas as imagens. Pode-se usar as várias ferramentas da seção _Visualization_ e a ferramenta window/level localizada na barra de ferramentas para auxiliá-lo nessa tarefa.
-
-3. Após concluir a colocação dos Landmarks, pode-se clicar no botão _Finish registration_. Transformações serão aplicadas à imagem móvel para corresponder à imagem fixa, e o resultado será salvo em uma nova imagem transformada no mesmo diretório da imagem móvel. Pode-se também cancelo todo o processo de registro clicando no botão _Cancel registration_.
-
-## Segmentation
-
-### Manual Segmentation
-
-1. Selecione o nodo (?) de segmentação de saída. O usuário pode criar uma nova segmentação ou editar uma segmentação previamente definida.
-2. Selecione a imagem de entrada a ser segmentada.
-3. Clique em _Add_ para adicionar segmentos.
-4. Selecione um segmento da lista a ser editado.
-5. Selecione uma ferramenta dentre as opção sob a lista de segmentos.
-6. Mais instruções sobre cada ferramenta de segmentação pode ser encontrada após selecionada clicando em _Show details._
-
-### Smart Segmentation
-
-Esse módulo provê métodos avançados para segmentação automática e supervisionada de vários tipos de imagem, tais como seção delgada e tomografia permitindo múltiplas imagens de entrada.
-
-#### __Inputs__
-
-1. __Annotations__: Selecione o nodo de segmentação que contém as anotações feitas na imagem para treinar o método de segmentação escolido.
-2. __Region (SOI)__: Selecione o nodo de segmentação em que o primeiro segmento delimita a região de interesse onde a segmentação será realizada.
-3. __Input image__: Selecione a imagem a ser segmentada. Vários tipos são aceitos, como imagens RGB e tomográficas.
-
-#### __Parameters__
-
-1. __Method__: Selecione o algoritmo para realizará a segmentação.
- 1. __Random Forest__: Florestas aleatórias são um método de aprendizado por agrupamento para classificação que opera construindo múltiplas árvores de decisão em tempo de treinamento. A __entrada__ é uma combinação de:
- * Entrada quantificada (RGB reduzido a um valor de 8 bits)
- * HSV puro
- * Múltiplos kernels gaussianos (tamanho e número de kernels são definidos pelo parâmetro __Radius__)
- * Se selecionado, kernels de __Variância__ são calculados (Ver __Use variation__).
- * If selected, kernels de __Sobel__ são calculados (Ver __Use contours__).
- 2. __Colored K-Means__: Um método de quantificação vetorial que busca particionar __n observações__ em __k clusters__ onde cada observação pertence ao cluster com a média (centro ou centroide) mais próxima. _Colored_ significa que o algoritmo funciona em espaço de cor tridimensional, especialmente HSV.
- * __Seed Initializer__: Algoritmo usado para escolher protótipos de clusters iniciais.
- * __Random__: Escolhe uma semente aleatória a partir das anotações, uma para cada segmento diferente.
- * __Smooth Centroid__: Para cada segmento, combina todas as amostras anotadas para geral uma semente mais geral.
-
-#### __Output__
-
-1. __Output prefix__: Digite um nome para ser usado como prefixo dos resultados.
-
-### Segment Inspector
-
-Para uma discussão mais detalhada sobre o algoritmo watershed, por favor cheque a seguinte [seção](../../Inspector/Watershed/estudos_de_porosidade.md) do manual do GeoSlicer.
-
-Este módulo provê múltiplos métodos para analisar uma imagem segmentada. Particularmente, algoritmos Watershed e Islands permite fragmentar a segmentação em diversas partições, ou diversos segmentos. Normalmente é aplicado a segmentação de espaço de poros para computar as métricas de cada elemento de poro. A entrada é um nodo de segmentação ou volume labelmap, uma região de interesse (definida por um nodo de segmentação) e a imagem/volume mestre. A saída é um labelmap onde cada partição (elemento de poro) está em uma cor diferente, uma tabela com parâmetros globais e uma tabela com as diferentes métricas para cada partição.
-
-#### __Inputs__
-
-1. __Selecionar__ single-shot (segmentação única) ou Batch (múltiplas amostras definidas por múltiplos projetos GeoSlicer).
-2. __Segmentation__: Selecionar um nodo de segmentação ou um labelmap para ser inspecionado.
-3. __Region__: Selecionar um nodo de segmentação para definir uma região de interesse (opcional).
-4. __Image__: Selecionar a imagem/volume mestre ao qual a segmentação é relacionada.
-
-#### __Parameters__
-
-1. __Method__: Selecionar um método a ser aplicado. Com algoritmo island, a segmentação é fragmentada de acordo com conexões diretas. Com watershed, a segmentação é fragmentada de acordo com a transformada de distância e os parâmetros da seção _Advanced_.
-2. __Size Filter__: Filtrar partições espúrias com eixo principal (feret_max) menor que o valor _Size Filter_.
-3. __Smooth factor__: Fator de suavização, que é o desvio padrão do filtro gaussiano aplicado à transformada de distância. Conforme aumenta, menos partições serão criadas. Use valores menores para resultados mais confiáveis.
-4. __Minimum distance__: Distância mínima separando picos em uma região de 2 * min_distance + 1 (i.e. picos são separados por no mínimo min_distance). Para encontrar o número máximo de picos, use min_distance = 0.
-5. __Orientation line__: Selecionar a linha para ser usada para cálculo de ângulo de orientação.
-
-#### __Output__
-
-Digite um nome para ser usado como prefixo dos resultados (labelmap onde cada partição (elemento de poro) está em uma cor diferente, uma tabela com parâmetros globais e uma tabela com as diferentes métricas para cada partição).
-
-#### Propriedades / Métricas:
-
-1. __Label__: Identificador rotular da partição.
-2. __mean__: Valor médio da imagem/volume de entrada dentro da região da partição (poro/grão).
-3. __median__: Valor mediano da imagem/volume de entrada dentro da região da partição (poro/grão).
-4. __stddev__: Desvio padrão da imagem/volume de entrada dentro da região da partição (poro/grão).
-5. __voxelCount__: Número total de pixels/voxels da região da partição (poro/grão).
-6. __area__: Área total da partição (poro/grão). Unidade: mm^2.
-7. __angle__: Ângulo em graus (entre 270 e 90) relacionado à linha de orientação (opcional; se nenhuma linha for selecionada, a orientação de referência é superior horizontal).
-8. __max_feret__: Eixo de caliper de Feret máximo. Unidade: mm.
-9. __min_feret__: Eixo de caliper de Feret mínimo. Unidade: mm.
-10. __mean_feret__: Média entre os calipers mínimo e máximo.
-11. __aspect_ratio__: min_feret / max_feret.
-12. __elongation__: max_feret / min_feret.
-13. __eccentricity__: sqrt(1 - min_feret / max_feret) relacionado à elipse equivalente (0 <= e < 1), igual a 0 para círculos.
-14. __ellipse_perimeter__: Perímetro da elipse equivalente (com eixo dado por caliper de Feret mínimo e máximo). Unidade: mm.
-15. __ellipse_area__: Área da elipse equivalente (com eixo dado por caliper de Feret mínimo e máximo). Unidade: mm^2.
-16. __ellipse_perimeter_over_ellipse_area__: Perímetro da elipse equivalente dividido pela área.
-17. __perimeter__: Perímetro real da partição (poro/grão). Unidade: mm.
-18. __perimeter_over_area__: Perímetro real dividido pela área da partição (poro/grão).
-19. __gamma__: "Redondeza" de uma área calculada como 'gamma = perimeter / (2 * sqrt(PI * area))'.
-20. __pore_size_class__: Símbolo/código/id da classe do poro.
-21. __pore_size_class_label__: Rótulo da classe do poro.
-
-#### Definição das classes de poro:
-
-* __Microporo__: classe 0, max_feret menor que 0.062 mm.
-* __Mesoporo mto pequeno__: classe 1, max_feret entre 0.062 e 0.125 mm.
-* __Mesoporo pequeno__: classe 2, max_feret entre 0.125 e 0.25 mm.
-* __Mesoporo médio__: classe 3, max_feret entre 0.25 e 0.5 mm.
-* __Mesoporo grande__: classe 4, max_feret entre 0.5 e 1 mm.
-* __Mesoporo muito grande__: classe 5, max_feret entre 1 e 4 mm.
-* __Megaporo pequeno__: classe 6, max_feret entre 4 e 32 mm.
-* __Megaporo grande__: classe 7, max_feret maior que 32mm.
-
-### Label Map Editor
-
-Realiza separação e aglutinação manual de objetos rotulados.
-
-#### __Atalhos para ferramentas__
-
-- m: Mesclar dois rótulos
-- a: Dividir rótulo automaticamente usando watershed
-- s: Dividir rótulo com uma linha reta
-- c: Cortar rótulo no ponteiro do mouse
-- z: Desfazer última edição
-- x: Refazer edição desfeita
-- Esc: Cancelar operação
\ No newline at end of file
diff --git a/tools/deploy/GeoSlicerManual/docs/Transforms/transforms.md b/tools/deploy/GeoSlicerManual/docs/Transforms/transforms.md
new file mode 100644
index 0000000..44c9d05
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/Transforms/transforms.md
@@ -0,0 +1,77 @@
+# Transformar Imagem
+
+
+
+
Recorte de Volume
+
Módulo Crop para cortar um volume, conforme descrito nos passos abaixo:
+
+
+
Selecione o volume em Volume to be cropped.
+
+
+
Ajuste a posição e tamanho desejados da ROI nas slice views.
+
+
+
Clique em Crop e aguarde a finalização. O volume cortado aparecerá no mesmo diretório que o volume original.
+
+
+
+
+
+
Video: Recorte de Volume
+
+
+
+
+
+
Ferramentas de imagem
+
Módulo Ferramentas de imagem que permite manipulação de imagens, conforme descrito abaixo:
+
+
+
Selecione a imagem em Input image.
+
+
+
Selecione a ferramenta em Tool e faça as mudanças desejadas.
+
+
+
Clique no botão Apply para confirmar as mudanças. Essas mudanças não são permanentes e podem ser desfeitas clicando no botão Undo; e serão descartadas se o módulo for deixado sem serem salvas ou for clicado o botão Reset (isso reverterá a imagem ao seu último estado salvo). Mudanças podem ser tornadas permanentes clicando no botão Save (isso alterará a imagem e não pode ser desfeito).
+
+
+
+
+
+
Video: Ferramentas de imagem
+
+
+
+
+
+
+
Registro
+
Módulo Register para registrar imagens de seção delgada e QEMSCAN, conforme descrito nos passos abaixo:
+
+
+
Clique no botão Select images to register. Uma janela de diálogo aparecerá que permite a seleção da imagem fixa (Fixed image) e a imagem móvel (Moving image). Após selecionar as imagens desejadas, clique no botão Apply para iniciar o registro.
+
+
+
Adicione Landmarks (pontos de ancoragem) às imagens clicando em Add na seção Landmarks. Arraste os Landmarks conforme desejado para match as mesmas localizações em ambas as imagens. Pode-se usar as várias ferramentas da seção Visualization e a ferramenta window/level localizada na barra de ferramentas para auxiliá-lo nessa tarefa.
+
+
+
Após concluir a colocação dos Landmarks, pode-se clicar no botão Finish registration. Transformações serão aplicadas à imagem móvel para corresponder à imagem fixa, e o resultado será salvo em uma nova imagem transformada no mesmo diretório da imagem móvel. Pode-se também cancelo todo o processo de registro clicando no botão Cancel registration.
+
+
+
+
+
+
Video: Registro
+
+
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/extra.css b/tools/deploy/GeoSlicerManual/docs/assets/extra.css
new file mode 100644
index 0000000..75825c2
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/assets/extra.css
@@ -0,0 +1,256 @@
+/* theme/extra.css */
+[data-md-color-scheme=slate] {
+ --md-hue: 240;
+ --md-default-bg-color: hsl(0deg 0% 17.25%);
+ --md-footer-bg-color: hsl(0, 0%, 13%);
+ --md-typeset-a-color: #dadada !important;
+ --md-accent-fg-color: #dadada !important;
+ --md-default-fg-color--light: #dadada;
+}
+
+[data-md-color-scheme=default] {
+ --md-typeset-a-color: rgba(0, 0, 0, .87) !important;
+ --md-accent-fg-color: rgba(0, 0, 0, .87) !important;
+ --md-default-fg-color--light: rgba(0, 0, 0, .87) !important;
+}
+
+h1 {
+ position: relative;
+ display: inline-block;
+}
+
+h1::after {
+ content: '';
+ position: absolute;
+ bottom: -0.3em;
+ left: 0;
+ width: 20%;
+ height: 0.2em;
+ background-color: rgba(38, 194, 82, 1);
+ border-radius: 0.1em;
+}
+
+h6#entre-em-contato a::after {
+ content: '';
+ position: absolute;
+ bottom: -0.3em;
+ left: 0;
+ width: 20%;
+ height: 0.2em;
+ background-color: rgba(38, 194, 82, 1);
+ border-radius: 0.1em;
+ margin: 0 !important;
+ margin-bottom: 0.2em;
+}
+
+h6#entre-em-contato {
+ position: relative;
+ padding-bottom: 0.1em;
+ display: inline-block;
+ margin: 0% !important;
+ font-size: 1em;
+}
+
+.md-header {
+ background-color: hsl(0, 0%, 13%);
+ ;
+ color: #ffffff !important;
+}
+
+.md-header__button.md-logo {
+ background-color: #dadada;
+ border-radius: 50%;
+ padding: 0.4%;
+ width: 2.7%;
+ height: 3%;
+
+}
+
+.know-more-container {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.know-more-container h3 {
+ margin: 0px;
+}
+
+.know-more-icon {
+ width: 20px;
+ height: auto;
+ cursor: pointer;
+}
+
+.md-search__input {
+ background-color: #dadada;
+ border-radius: 8px;
+}
+
+/* fonts */
+:root {
+ --md-text-font: "proxima-nova";
+}
+
+/* Remove the unwanted icon */
+.md-nav__icon {
+ display: none;
+}
+
+/* Remove the unwanted label for nav drawer */
+.md-nav__title {
+ background-color: white !important;
+}
+
+label[for="__drawer"] {
+ display: none;
+}
+
+/* mkdocs footer delete */
+.md-footer-meta__inner.md-grid {
+ display: none;
+}
+
+/* General styling navigation */
+.md-nav__link {
+ margin-top: auto;
+ padding: 3%;
+ display: block;
+ margin-left: 0;
+ text-align: left;
+ margin-right: 22%;
+}
+
+/* General active link styling */
+.md-nav__item--active .md-nav__link--active {
+ background-color: rgba(38, 195, 82, 0.1);
+ padding: 5px;
+ border-radius: 8px;
+ display: block;
+ margin-left: 0;
+}
+
+/* Basic styling for the active label in nested menus */
+.md-nav__item--nested>label {
+ background-color: transparent;
+ transition: background-color 0.3s;
+ padding: 5px;
+ border-radius: 8px;
+ display: block;
+ margin-left: 0;
+}
+
+.md-nav__item--nested>input[type="checkbox"]:checked+label {
+ background-color: rgba(38, 195, 82, 0.1);
+}
+
+.md-nav__item--nested>input[type="checkbox"]:checked+label+.md-nav {
+ background-color: transparent;
+}
+
+.md-nav__item--nested>input[type="checkbox"]:checked+label+.md-nav .md-nav__item--nested>input[type="checkbox"]:checked+label {
+ background-color: rgba(38, 195, 82, 0.1);
+}
+
+.md-nav__item--nested>input[type="checkbox"]:checked+label+.md-nav .md-nav__item--nested>input[type="checkbox"]:checked+label+.md-nav {
+ background-color: transparent;
+}
+
+.md-typeset__scrollwrap {
+ display: flex;
+ justify-content: center;
+ /* Center horizontally */
+ align-items: center;
+ /* Center vertically (if needed) */
+ overflow-x: auto;
+ touch-action: auto;
+ max-width: 100%;
+}
+
+.md-sidebar.md-sidebar--secondary[data-md-component="sidebar"][data-md-type="toc"] {
+ width: 0;
+ height: 0;
+ overflow: hidden;
+}
+
+
+.content-wrapper {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ width: 100%;
+ position: relative;
+}
+
+.text-content {
+ width: 80%;
+ margin: 0;
+ padding-right: 3%;
+}
+
+.video-wrapper {
+ width: 40%;
+ position: -webkit-sticky;
+ position: sticky;
+ top: 70px;
+ z-index: 1;
+ margin-top: 80px;
+}
+
+.floating-video {
+ width: 100%;
+ height: auto;
+ transform-origin: right top;
+ /* Ensure this is set in the default state */
+ transition: transform 0.3s ease-in-out, transform-origin 0.3s ease-in-out;
+ border-radius: 10px;
+}
+
+.video-wrapper:hover .floating-video {
+ transform: scale(2);
+ transform-origin: right top;
+ /* Scale towards the left */
+ border-radius: 0px;
+}
+
+.video-caption {
+ font-size: 0.8em;
+ margin-top: 10px;
+ color: #555;
+ text-align: center;
+}
+
+@media (max-width: 1220px) {
+ .content-wrapper {
+ flex-direction: column;
+ align-items: stretch;
+ width: 100%;
+ align-items: center;
+ }
+
+ .text-content {
+ width: 100%;
+ }
+
+ .video-wrapper {
+ width: 100%;
+ margin-top: 20px !important;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ /* Center the video and caption */
+ }
+
+ .video-wrapper:hover .floating-video {
+ transform-origin: center;
+ }
+
+ .floating-video {
+ max-width: 50%;
+
+ }
+
+ .md-nav__link {
+ margin-right: 0%;
+ }
+}
\ No newline at end of file
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/icons/saiba_mais.svg b/tools/deploy/GeoSlicerManual/docs/assets/icons/saiba_mais.svg
new file mode 100644
index 0000000..14e1e73
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/assets/icons/saiba_mais.svg
@@ -0,0 +1,3 @@
+
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/account_screen.png b/tools/deploy/GeoSlicerManual/docs/assets/images/account_screen.png
new file mode 100644
index 0000000..14101b2
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/account_screen.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/Image Log/Usabilidade/NMR_1_annotated.png b/tools/deploy/GeoSlicerManual/docs/assets/images/imagelog/NMR_1_annotated.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Image Log/Usabilidade/NMR_1_annotated.png
rename to tools/deploy/GeoSlicerManual/docs/assets/images/imagelog/NMR_1_annotated.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Image Log/Instance Segmenter/area_vs_areaconvexa.png b/tools/deploy/GeoSlicerManual/docs/assets/images/imagelog/area_vs_areaconvexa.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Image Log/Instance Segmenter/area_vs_areaconvexa.png
rename to tools/deploy/GeoSlicerManual/docs/assets/images/imagelog/area_vs_areaconvexa.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Image Log/Usabilidade/curve-1D.png b/tools/deploy/GeoSlicerManual/docs/assets/images/imagelog/curve-1D.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Image Log/Usabilidade/curve-1D.png
rename to tools/deploy/GeoSlicerManual/docs/assets/images/imagelog/curve-1D.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Image Log/Usabilidade/curve-2D-BHI.png b/tools/deploy/GeoSlicerManual/docs/assets/images/imagelog/curve-2D-BHI.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Image Log/Usabilidade/curve-2D-BHI.png
rename to tools/deploy/GeoSlicerManual/docs/assets/images/imagelog/curve-2D-BHI.png
diff --git a/tools/deploy/GeoSlicerManual/docs/Image Log/Usabilidade/curve-2D-labelmap.png b/tools/deploy/GeoSlicerManual/docs/assets/images/imagelog/curve-2D-labelmap.png
similarity index 100%
rename from tools/deploy/GeoSlicerManual/docs/Image Log/Usabilidade/curve-2D-labelmap.png
rename to tools/deploy/GeoSlicerManual/docs/assets/images/imagelog/curve-2D-labelmap.png
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/job_monitor_screen.png b/tools/deploy/GeoSlicerManual/docs/assets/images/job_monitor_screen.png
new file mode 100644
index 0000000..5729d13
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/job_monitor_screen.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/micro_ct/modulos/open_rock_data/interface.png b/tools/deploy/GeoSlicerManual/docs/assets/images/micro_ct/modulos/open_rock_data/interface.png
new file mode 100644
index 0000000..6e73c67
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/micro_ct/modulos/open_rock_data/interface.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/cycles_information.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/cycles_information.png
new file mode 100644
index 0000000..cf476d3
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/cycles_information.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/cycles_input.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/cycles_input.png
new file mode 100644
index 0000000..a253aa1
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/cycles_input.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/cycles_parameters.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/cycles_parameters.png
new file mode 100644
index 0000000..f10d672
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/cycles_parameters.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/extract.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/extract.png
new file mode 100644
index 0000000..1b0147a
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/extract.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/kreleda_crossederror.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/kreleda_crossederror.png
new file mode 100644
index 0000000..0eb91a3
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/kreleda_crossederror.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/kreleda_crossedparameters.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/kreleda_crossedparameters.png
new file mode 100644
index 0000000..f8c793f
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/kreleda_crossedparameters.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/kreleda_curvesdispersion.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/kreleda_curvesdispersion.png
new file mode 100644
index 0000000..973d17c
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/kreleda_curvesdispersion.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/kreleda_filter.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/kreleda_filter.png
new file mode 100644
index 0000000..576f5eb
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/kreleda_filter.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/kreleda_import.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/kreleda_import.png
new file mode 100644
index 0000000..3408e7d
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/kreleda_import.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/kreleda_input.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/kreleda_input.png
new file mode 100644
index 0000000..a17114c
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/kreleda_input.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/kreleda_parameterserrors.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/kreleda_parameterserrors.png
new file mode 100644
index 0000000..866e6f6
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/kreleda_parameterserrors.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/kreleda_parametersresults.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/kreleda_parametersresults.png
new file mode 100644
index 0000000..1b30014
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/kreleda_parametersresults.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/kreleda_second.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/kreleda_second.png
new file mode 100644
index 0000000..c6aa75a
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/kreleda_second.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/kreleda_selfcorrelation.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/kreleda_selfcorrelation.png
new file mode 100644
index 0000000..6f9c8c5
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/kreleda_selfcorrelation.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/labelmap.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/labelmap.png
new file mode 100644
index 0000000..df88636
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/labelmap.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/mercury.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/mercury.png
new file mode 100644
index 0000000..0ea6802
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/mercury.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/molhabilidade.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/molhabilidade.png
new file mode 100644
index 0000000..0026741
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/molhabilidade.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/morrow.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/morrow.png
new file mode 100644
index 0000000..c6957b7
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/morrow.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/one phase.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/one phase.png
new file mode 100644
index 0000000..7c7c8e5
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/one phase.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/one phase_clip.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/one phase_clip.png
new file mode 100644
index 0000000..409abfc
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/one phase_clip.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/one phase_orientation scheme.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/one phase_orientation scheme.png
new file mode 100644
index 0000000..3444060
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/one phase_orientation scheme.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/one phase_solver.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/one phase_solver.png
new file mode 100644
index 0000000..4d3f5d8
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/one phase_solver.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/output.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/output.png
new file mode 100644
index 0000000..cf2e84e
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/output.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/production_parameters.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/production_parameters.png
new file mode 100644
index 0000000..2e86fb7
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/production_parameters.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/production_sensitivity.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/production_sensitivity.png
new file mode 100644
index 0000000..c86765f
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/production_sensitivity.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/production_singlekrel.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/production_singlekrel.png
new file mode 100644
index 0000000..50af31d
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/production_singlekrel.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/rede_multiescala.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/rede_multiescala.png
new file mode 100644
index 0000000..b070746
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/rede_multiescala.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/rede_uniescalar.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/rede_uniescalar.png
new file mode 100644
index 0000000..4da5943
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/rede_uniescalar.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/scalar.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/scalar.png
new file mode 100644
index 0000000..7b7f51d
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/scalar.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/simulation.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/simulation.png
new file mode 100644
index 0000000..b2d240d
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/simulation.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/subscale_fixed.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/subscale_fixed.png
new file mode 100644
index 0000000..3cc849d
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/subscale_fixed.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/subscale_leverett.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/subscale_leverett.png
new file mode 100644
index 0000000..b4b4012
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/subscale_leverett.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/subscale_leverettCurve.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/subscale_leverettCurve.png
new file mode 100644
index 0000000..82bb981
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/subscale_leverettCurve.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/subscale_pressure.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/subscale_pressure.png
new file mode 100644
index 0000000..6320fdc
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/subscale_pressure.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/subscale_radius.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/subscale_radius.png
new file mode 100644
index 0000000..9fa3335
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/subscale_radius.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/two phase.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/two phase.png
new file mode 100644
index 0000000..5d2713a
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/two phase.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/two phase_contact angle.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/two phase_contact angle.png
new file mode 100644
index 0000000..6df7825
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/two phase_contact angle.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/two phase_fluid properties.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/two phase_fluid properties.png
new file mode 100644
index 0000000..af21c2d
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/two phase_fluid properties.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/two phase_input.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/two phase_input.png
new file mode 100644
index 0000000..6f70b4f
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/two phase_input.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/two phase_load.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/two phase_load.png
new file mode 100644
index 0000000..2d2bd63
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/two phase_load.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/two phase_save.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/two phase_save.png
new file mode 100644
index 0000000..bda445a
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/two phase_save.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/two phase_simulation properties.png b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/two phase_simulation properties.png
new file mode 100644
index 0000000..1d2945c
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/pnm/two phase_simulation properties.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/register_screen.png b/tools/deploy/GeoSlicerManual/docs/assets/images/register_screen.png
new file mode 100644
index 0000000..00f309e
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/register_screen.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/analise_resultados/charts.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/analise_resultados/charts.png
new file mode 100644
index 0000000..a75b0a5
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/analise_resultados/charts.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/analise_resultados/customized_tables.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/analise_resultados/customized_tables.png
new file mode 100644
index 0000000..2f444f1
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/analise_resultados/customized_tables.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/exportar_resultados/load_scene_icon.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/exportar_resultados/load_scene_icon.png
new file mode 100644
index 0000000..75e918f
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/exportar_resultados/load_scene_icon.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/exportar_resultados/save_as_icon.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/exportar_resultados/save_as_icon.png
new file mode 100644
index 0000000..ee1eb9e
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/exportar_resultados/save_as_icon.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/exportar_resultados/screenshot_icon.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/exportar_resultados/screenshot_icon.png
new file mode 100644
index 0000000..fbcb1dc
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/exportar_resultados/screenshot_icon.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/auto_registration/interface.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/auto_registration/interface.png
new file mode 100644
index 0000000..7a9fd5c
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/auto_registration/interface.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/crop/interface.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/crop/interface.png
new file mode 100644
index 0000000..6022223
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/crop/interface.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/export/interface.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/export/interface.png
new file mode 100644
index 0000000..51784da
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/export/interface.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/image_tools/contrast.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/image_tools/contrast.png
new file mode 100644
index 0000000..9a5fb7c
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/image_tools/contrast.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/image_tools/interface.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/image_tools/interface.png
new file mode 100644
index 0000000..9359b4d
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/image_tools/interface.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/image_tools/saturation.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/image_tools/saturation.png
new file mode 100644
index 0000000..13417b1
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/image_tools/saturation.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/loader/interface.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/loader/interface.png
new file mode 100644
index 0000000..1e1a4a5
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/loader/interface.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/manual_registration/icon_active.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/manual_registration/icon_active.png
new file mode 100644
index 0000000..0c275ba
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/manual_registration/icon_active.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/manual_registration/icon_add.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/manual_registration/icon_add.png
new file mode 100644
index 0000000..982708d
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/manual_registration/icon_add.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/manual_registration/icon_inactive.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/manual_registration/icon_inactive.png
new file mode 100644
index 0000000..6a2546b
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/manual_registration/icon_inactive.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/manual_registration/icon_trash.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/manual_registration/icon_trash.png
new file mode 100644
index 0000000..4bfa4f1
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/manual_registration/icon_trash.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/manual_registration/interface.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/manual_registration/interface.png
new file mode 100644
index 0000000..4f11262
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/manual_registration/interface.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/manual_registration/intro_interface.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/manual_registration/intro_interface.png
new file mode 100644
index 0000000..4657627
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/manual_registration/intro_interface.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/manual_registration/landmarks.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/manual_registration/landmarks.png
new file mode 100644
index 0000000..c3e9c12
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/manual_registration/landmarks.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/manual_registration/views.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/manual_registration/views.png
new file mode 100644
index 0000000..6a12fec
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/manual_registration/views.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/multiple_image_analysis/MIA-BP.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/multiple_image_analysis/MIA-BP.png
new file mode 100644
index 0000000..eb2bd46
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/multiple_image_analysis/MIA-BP.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/multiple_image_analysis/MIA-HiD.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/multiple_image_analysis/MIA-HiD.png
new file mode 100644
index 0000000..de52ce8
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/multiple_image_analysis/MIA-HiD.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/multiple_image_analysis/MIA-MiD.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/multiple_image_analysis/MIA-MiD.png
new file mode 100644
index 0000000..a0c78e1
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/multiple_image_analysis/MIA-MiD.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/pore_stats/export.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/pore_stats/export.png
new file mode 100644
index 0000000..6b74c58
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/pore_stats/export.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/pore_stats/input.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/pore_stats/input.png
new file mode 100644
index 0000000..be405e0
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/pore_stats/input.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/pore_stats/inspector.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/pore_stats/inspector.png
new file mode 100644
index 0000000..300bc95
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/pore_stats/inspector.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/pore_stats/interface.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/pore_stats/interface.png
new file mode 100644
index 0000000..ae9d8c3
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/pore_stats/interface.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/pore_stats/interface_orig.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/pore_stats/interface_orig.png
new file mode 100644
index 0000000..4ec5524
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/pore_stats/interface_orig.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/pore_stats/max_frags.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/pore_stats/max_frags.png
new file mode 100644
index 0000000..971dcf0
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/pore_stats/max_frags.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/pore_stats/models.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/pore_stats/models.png
new file mode 100644
index 0000000..f9d24fc
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/pore_stats/models.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/pore_stats/output.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/pore_stats/output.png
new file mode 100644
index 0000000..8da85c9
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/pore_stats/output.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/pore_stats/overview.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/pore_stats/overview.png
new file mode 100644
index 0000000..a6156bf
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/pore_stats/overview.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/pore_stats/pixel_size.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/pore_stats/pixel_size.png
new file mode 100644
index 0000000..0452fcf
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/pore_stats/pixel_size.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/pore_stats/pore_cleaning.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/pore_stats/pore_cleaning.png
new file mode 100644
index 0000000..b98ac88
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/pore_stats/pore_cleaning.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/qemscan_loader/interface.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/qemscan_loader/interface.png
new file mode 100644
index 0000000..25fbba4
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/qemscan_loader/interface.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/segment_editor/color_threshold_effect.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/segment_editor/color_threshold_effect.png
new file mode 100644
index 0000000..6ef9d31
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/segment_editor/color_threshold_effect.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/segment_editor/conectivity_effect.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/segment_editor/conectivity_effect.png
new file mode 100644
index 0000000..1aa3826
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/segment_editor/conectivity_effect.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/segment_editor/segment_editor_fig1.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/segment_editor/segment_editor_fig1.png
new file mode 100644
index 0000000..d35c12e
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/segment_editor/segment_editor_fig1.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/segment_editor/segment_editor_fig2.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/segment_editor/segment_editor_fig2.png
new file mode 100644
index 0000000..c54846a
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/segment_editor/segment_editor_fig2.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/segment_inspector/attributes.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/segment_inspector/attributes.png
new file mode 100644
index 0000000..91785d3
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/segment_inspector/attributes.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/segment_inspector/gpu_watershed.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/segment_inspector/gpu_watershed.png
new file mode 100644
index 0000000..b84ab9a
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/segment_inspector/gpu_watershed.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/segment_inspector/inputs_single.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/segment_inspector/inputs_single.png
new file mode 100644
index 0000000..a68d42a
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/segment_inspector/inputs_single.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/segment_inspector/interface.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/segment_inspector/interface.png
new file mode 100644
index 0000000..9318267
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/segment_inspector/interface.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/segment_inspector/separate_objects.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/segment_inspector/separate_objects.png
new file mode 100644
index 0000000..5548e77
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/segment_inspector/separate_objects.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/segment_inspector/transition_analysis.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/segment_inspector/transition_analysis.png
new file mode 100644
index 0000000..ae436e9
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/segment_inspector/transition_analysis.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/segment_inspector/watershed.png b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/segment_inspector/watershed.png
new file mode 100644
index 0000000..3fe3023
Binary files /dev/null and b/tools/deploy/GeoSlicerManual/docs/assets/images/thin_section/modulos/segment_inspector/watershed.png differ
diff --git a/tools/deploy/GeoSlicerManual/docs/assets/javascripts/iframe-worker.js b/tools/deploy/GeoSlicerManual/docs/assets/javascripts/iframe-worker.js
new file mode 100644
index 0000000..5517ce9
--- /dev/null
+++ b/tools/deploy/GeoSlicerManual/docs/assets/javascripts/iframe-worker.js
@@ -0,0 +1 @@
+"use strict"; (() => { function c(s, n) { parent.postMessage(s, n || "*") } function d(...s) { return s.reduce((n, e) => n.then(() => new Promise(r => { let t = document.createElement("script"); t.src = e, t.onload = r, document.body.appendChild(t) })), Promise.resolve()) } var o = class extends EventTarget { constructor(e) { super(); this.url = e; this.m = e => { e.source === this.w && (this.dispatchEvent(new MessageEvent("message", { data: e.data })), this.onmessage && this.onmessage(e)) }; this.e = (e, r, t, i, m) => { if (r === `${this.url}`) { let a = new ErrorEvent("error", { message: e, filename: r, lineno: t, colno: i, error: m }); this.dispatchEvent(a), this.onerror && this.onerror(a) } }; let r = document.createElement("iframe"); r.hidden = !0, document.body.appendChild(this.iframe = r), this.w.document.open(), this.w.document.write(`
{% endif %}
-{% endblock %}
+{% endblock %}
\ No newline at end of file
diff --git a/tools/deploy/Resources/Fonts/Inter_18pt-Bold.ttf b/tools/deploy/Resources/Fonts/Inter_18pt-Bold.ttf
new file mode 100644
index 0000000..cd13f60
Binary files /dev/null and b/tools/deploy/Resources/Fonts/Inter_18pt-Bold.ttf differ
diff --git a/tools/deploy/Resources/Fonts/Inter_18pt-Italic.ttf b/tools/deploy/Resources/Fonts/Inter_18pt-Italic.ttf
new file mode 100644
index 0000000..14d3595
Binary files /dev/null and b/tools/deploy/Resources/Fonts/Inter_18pt-Italic.ttf differ
diff --git a/tools/deploy/Resources/Fonts/Inter_18pt-Regular.ttf b/tools/deploy/Resources/Fonts/Inter_18pt-Regular.ttf
new file mode 100644
index 0000000..ce097c8
Binary files /dev/null and b/tools/deploy/Resources/Fonts/Inter_18pt-Regular.ttf differ
diff --git a/tools/deploy/Resources/Fonts/Inter_18pt-SemiBold.ttf b/tools/deploy/Resources/Fonts/Inter_18pt-SemiBold.ttf
new file mode 100644
index 0000000..053185e
Binary files /dev/null and b/tools/deploy/Resources/Fonts/Inter_18pt-SemiBold.ttf differ
diff --git a/tools/deploy/Resources/Fonts/Inter_18pt-SemiBoldItalic.ttf b/tools/deploy/Resources/Fonts/Inter_18pt-SemiBoldItalic.ttf
new file mode 100644
index 0000000..d9c9896
Binary files /dev/null and b/tools/deploy/Resources/Fonts/Inter_18pt-SemiBoldItalic.ttf differ
diff --git a/tools/deploy/Resources/Fonts/Inter_24pt-Bold.ttf b/tools/deploy/Resources/Fonts/Inter_24pt-Bold.ttf
new file mode 100644
index 0000000..46b3583
Binary files /dev/null and b/tools/deploy/Resources/Fonts/Inter_24pt-Bold.ttf differ
diff --git a/tools/deploy/Resources/Fonts/Inter_24pt-Italic.ttf b/tools/deploy/Resources/Fonts/Inter_24pt-Italic.ttf
new file mode 100644
index 0000000..1048b07
Binary files /dev/null and b/tools/deploy/Resources/Fonts/Inter_24pt-Italic.ttf differ
diff --git a/tools/deploy/Resources/Fonts/Inter_24pt-Regular.ttf b/tools/deploy/Resources/Fonts/Inter_24pt-Regular.ttf
new file mode 100644
index 0000000..6b088a7
Binary files /dev/null and b/tools/deploy/Resources/Fonts/Inter_24pt-Regular.ttf differ
diff --git a/tools/deploy/Resources/Fonts/Inter_24pt-SemiBold.ttf b/tools/deploy/Resources/Fonts/Inter_24pt-SemiBold.ttf
new file mode 100644
index 0000000..ceb8576
Binary files /dev/null and b/tools/deploy/Resources/Fonts/Inter_24pt-SemiBold.ttf differ
diff --git a/tools/deploy/Resources/Fonts/Inter_24pt-SemiBoldItalic.ttf b/tools/deploy/Resources/Fonts/Inter_24pt-SemiBoldItalic.ttf
new file mode 100644
index 0000000..6921df2
Binary files /dev/null and b/tools/deploy/Resources/Fonts/Inter_24pt-SemiBoldItalic.ttf differ
diff --git a/tools/deploy/Resources/Fonts/Inter_28pt-Bold.ttf b/tools/deploy/Resources/Fonts/Inter_28pt-Bold.ttf
new file mode 100644
index 0000000..d17828b
Binary files /dev/null and b/tools/deploy/Resources/Fonts/Inter_28pt-Bold.ttf differ
diff --git a/tools/deploy/Resources/Fonts/Inter_28pt-Italic.ttf b/tools/deploy/Resources/Fonts/Inter_28pt-Italic.ttf
new file mode 100644
index 0000000..c2a143a
Binary files /dev/null and b/tools/deploy/Resources/Fonts/Inter_28pt-Italic.ttf differ
diff --git a/tools/deploy/Resources/Fonts/Inter_28pt-Regular.ttf b/tools/deploy/Resources/Fonts/Inter_28pt-Regular.ttf
new file mode 100644
index 0000000..855b6f4
Binary files /dev/null and b/tools/deploy/Resources/Fonts/Inter_28pt-Regular.ttf differ
diff --git a/tools/deploy/Resources/Fonts/Inter_28pt-SemiBold.ttf b/tools/deploy/Resources/Fonts/Inter_28pt-SemiBold.ttf
new file mode 100644
index 0000000..8b84efc
Binary files /dev/null and b/tools/deploy/Resources/Fonts/Inter_28pt-SemiBold.ttf differ
diff --git a/tools/deploy/Resources/Fonts/Inter_28pt-SemiBoldItalic.ttf b/tools/deploy/Resources/Fonts/Inter_28pt-SemiBoldItalic.ttf
new file mode 100644
index 0000000..2e22c5a
Binary files /dev/null and b/tools/deploy/Resources/Fonts/Inter_28pt-SemiBoldItalic.ttf differ
diff --git a/tools/deploy/Resources/AddIcon.png b/tools/deploy/Resources/Icons/Add.png
similarity index 100%
rename from tools/deploy/Resources/AddIcon.png
rename to tools/deploy/Resources/Icons/Add.png
diff --git a/tools/deploy/Resources/AnnotationDistance.png b/tools/deploy/Resources/Icons/AnnotationDistance.png
similarity index 100%
rename from tools/deploy/Resources/AnnotationDistance.png
rename to tools/deploy/Resources/Icons/AnnotationDistance.png
diff --git a/tools/deploy/Resources/ApplyIcon.png b/tools/deploy/Resources/Icons/Apply.png
similarity index 100%
rename from tools/deploy/Resources/ApplyIcon.png
rename to tools/deploy/Resources/Icons/Apply.png
diff --git a/src/modules/WelcomeGeoSlicer/Resources/BIAEPBrowser.png b/tools/deploy/Resources/Icons/BIAEPBrowser.png
similarity index 100%
rename from src/modules/WelcomeGeoSlicer/Resources/BIAEPBrowser.png
rename to tools/deploy/Resources/Icons/BIAEPBrowser.png
diff --git a/tools/deploy/Resources/CancelIcon.png b/tools/deploy/Resources/Icons/Cancel.png
similarity index 100%
rename from tools/deploy/Resources/CancelIcon.png
rename to tools/deploy/Resources/Icons/Cancel.png
diff --git a/src/modules/WelcomeGeoSlicer/Resources/CoreEnv.png b/tools/deploy/Resources/Icons/CoreEnv.png
similarity index 100%
rename from src/modules/WelcomeGeoSlicer/Resources/CoreEnv.png
rename to tools/deploy/Resources/Icons/CoreEnv.png
diff --git a/tools/deploy/Resources/DeleteIcon.png b/tools/deploy/Resources/Icons/Delete.png
similarity index 100%
rename from tools/deploy/Resources/DeleteIcon.png
rename to tools/deploy/Resources/Icons/Delete.png
diff --git a/tools/deploy/Resources/EditIcon.png b/tools/deploy/Resources/Icons/Edit.png
similarity index 100%
rename from tools/deploy/Resources/EditIcon.png
rename to tools/deploy/Resources/Icons/Edit.png
diff --git a/tools/deploy/Resources/EditAddIcon.png b/tools/deploy/Resources/Icons/EditAdd.png
similarity index 100%
rename from tools/deploy/Resources/EditAddIcon.png
rename to tools/deploy/Resources/Icons/EditAdd.png
diff --git a/tools/deploy/Resources/ClosedEye.png b/tools/deploy/Resources/Icons/EyeClosed.png
similarity index 100%
rename from tools/deploy/Resources/ClosedEye.png
rename to tools/deploy/Resources/Icons/EyeClosed.png
diff --git a/tools/deploy/Resources/OpenEye.png b/tools/deploy/Resources/Icons/EyeOpen.png
similarity index 100%
rename from tools/deploy/Resources/OpenEye.png
rename to tools/deploy/Resources/Icons/EyeOpen.png
diff --git a/tools/deploy/Resources/FitIcon.png b/tools/deploy/Resources/Icons/Fit.png
similarity index 100%
rename from tools/deploy/Resources/FitIcon.png
rename to tools/deploy/Resources/Icons/Fit.png
diff --git a/tools/deploy/Resources/FitRealAspectRatioIcon.png b/tools/deploy/Resources/Icons/FitRealAspectRatio.png
similarity index 100%
rename from tools/deploy/Resources/FitRealAspectRatioIcon.png
rename to tools/deploy/Resources/Icons/FitRealAspectRatio.png
diff --git a/tools/deploy/Resources/GeoSlicer.ico b/tools/deploy/Resources/Icons/GeoSlicer.ico
similarity index 100%
rename from tools/deploy/Resources/GeoSlicer.ico
rename to tools/deploy/Resources/Icons/GeoSlicer.ico
diff --git a/tools/deploy/Resources/GeoSlicer-logo.png b/tools/deploy/Resources/Icons/GeoSlicerLogo.png
similarity index 100%
rename from tools/deploy/Resources/GeoSlicer-logo.png
rename to tools/deploy/Resources/Icons/GeoSlicerLogo.png
diff --git a/tools/deploy/Resources/GreenCheckCircle.png b/tools/deploy/Resources/Icons/GreenCheckCircle.png
similarity index 100%
rename from tools/deploy/Resources/GreenCheckCircle.png
rename to tools/deploy/Resources/Icons/GreenCheckCircle.png
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/About.svg b/tools/deploy/Resources/Icons/IconSet-dark/About.svg
new file mode 100644
index 0000000..96a17a7
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/About.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/Account.svg b/tools/deploy/Resources/Icons/IconSet-dark/Account.svg
new file mode 100644
index 0000000..b24b0e8
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/Account.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/AddData.svg b/tools/deploy/Resources/Icons/IconSet-dark/AddData.svg
new file mode 100644
index 0000000..ba3db50
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/AddData.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/Apps.svg b/tools/deploy/Resources/Icons/IconSet-dark/Apps.svg
new file mode 100644
index 0000000..acb4e35
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/Apps.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/ArrowLeft.svg b/tools/deploy/Resources/Icons/IconSet-dark/ArrowLeft.svg
new file mode 100644
index 0000000..e540bd2
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/ArrowLeft.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/ArrowRight.svg b/tools/deploy/Resources/Icons/IconSet-dark/ArrowRight.svg
new file mode 100644
index 0000000..3b7d9b9
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/ArrowRight.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/Bookmark.svg b/tools/deploy/Resources/Icons/IconSet-dark/Bookmark.svg
new file mode 100644
index 0000000..1cc206e
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/Bookmark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/BookmarkCheck.svg b/tools/deploy/Resources/Icons/IconSet-dark/BookmarkCheck.svg
new file mode 100644
index 0000000..1e72c25
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/BookmarkCheck.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/Bug.svg b/tools/deploy/Resources/Icons/IconSet-dark/Bug.svg
new file mode 100644
index 0000000..c9251ca
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/Bug.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/Charts.svg b/tools/deploy/Resources/Icons/IconSet-dark/Charts.svg
new file mode 100644
index 0000000..f7738bb
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/Charts.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/ChevronRight.svg b/tools/deploy/Resources/Icons/IconSet-dark/ChevronRight.svg
new file mode 100644
index 0000000..3a6a5d0
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/ChevronRight.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/CircleHelp.svg b/tools/deploy/Resources/Icons/IconSet-dark/CircleHelp.svg
new file mode 100644
index 0000000..ad07844
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/CircleHelp.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/Close.svg b/tools/deploy/Resources/Icons/IconSet-dark/Close.svg
new file mode 100644
index 0000000..a712c0d
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/Close.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/CloudJobs.svg b/tools/deploy/Resources/Icons/IconSet-dark/CloudJobs.svg
new file mode 100644
index 0000000..1721f23
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/CloudJobs.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/Console.svg b/tools/deploy/Resources/Icons/IconSet-dark/Console.svg
new file mode 100644
index 0000000..eb97103
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/Console.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/Copy.svg b/tools/deploy/Resources/Icons/IconSet-dark/Copy.svg
new file mode 100644
index 0000000..480e6cc
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/Copy.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/Crop.svg b/tools/deploy/Resources/Icons/IconSet-dark/Crop.svg
new file mode 100644
index 0000000..ee34d04
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/Crop.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/Cut.svg b/tools/deploy/Resources/Icons/IconSet-dark/Cut.svg
new file mode 100644
index 0000000..3f96420
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/Cut.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/Database.svg b/tools/deploy/Resources/Icons/IconSet-dark/Database.svg
new file mode 100644
index 0000000..fc95a03
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/Database.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/Ellipsis.svg b/tools/deploy/Resources/Icons/IconSet-dark/Ellipsis.svg
new file mode 100644
index 0000000..04c166d
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/Ellipsis.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/Eraser.svg b/tools/deploy/Resources/Icons/IconSet-dark/Eraser.svg
new file mode 100644
index 0000000..ab25169
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/Eraser.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/ExportTools.svg b/tools/deploy/Resources/Icons/IconSet-dark/ExportTools.svg
new file mode 100644
index 0000000..1ef1c3f
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/ExportTools.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/Files.svg b/tools/deploy/Resources/Icons/IconSet-dark/Files.svg
new file mode 100644
index 0000000..b6dc50b
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/Files.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/Home.svg b/tools/deploy/Resources/Icons/IconSet-dark/Home.svg
new file mode 100644
index 0000000..01c1ef7
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/Home.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/ImageLogPreProcessing.svg b/tools/deploy/Resources/Icons/IconSet-dark/ImageLogPreProcessing.svg
new file mode 100644
index 0000000..191f860
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/ImageLogPreProcessing.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/ImportTools.svg b/tools/deploy/Resources/Icons/IconSet-dark/ImportTools.svg
new file mode 100644
index 0000000..765dc96
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/ImportTools.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/Layers.svg b/tools/deploy/Resources/Icons/IconSet-dark/Layers.svg
new file mode 100644
index 0000000..2da0caf
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/Layers.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/Load.svg b/tools/deploy/Resources/Icons/IconSet-dark/Load.svg
new file mode 100644
index 0000000..14b2e8c
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/Load.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/Memory.svg b/tools/deploy/Resources/Icons/IconSet-dark/Memory.svg
new file mode 100644
index 0000000..917cbd0
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/Memory.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/MultiTool.svg b/tools/deploy/Resources/Icons/IconSet-dark/MultiTool.svg
new file mode 100644
index 0000000..f71d32a
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/MultiTool.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/Open.svg b/tools/deploy/Resources/Icons/IconSet-dark/Open.svg
new file mode 100644
index 0000000..415a7ca
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/Open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/PaintBrush.svg b/tools/deploy/Resources/Icons/IconSet-dark/PaintBrush.svg
new file mode 100644
index 0000000..5113294
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/PaintBrush.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/PanelLeftClose.svg b/tools/deploy/Resources/Icons/IconSet-dark/PanelLeftClose.svg
new file mode 100644
index 0000000..1d88fce
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/PanelLeftClose.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/PanelLeftOpen.svg b/tools/deploy/Resources/Icons/IconSet-dark/PanelLeftOpen.svg
new file mode 100644
index 0000000..9f5a4d4
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/PanelLeftOpen.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/PanelRightClose.svg b/tools/deploy/Resources/Icons/IconSet-dark/PanelRightClose.svg
new file mode 100644
index 0000000..61edfde
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/PanelRightClose.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/PanelRightOpen.svg b/tools/deploy/Resources/Icons/IconSet-dark/PanelRightOpen.svg
new file mode 100644
index 0000000..1242adf
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/PanelRightOpen.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/Paste.svg b/tools/deploy/Resources/Icons/IconSet-dark/Paste.svg
new file mode 100644
index 0000000..4c047e3
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/Paste.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/Preferences.svg b/tools/deploy/Resources/Icons/IconSet-dark/Preferences.svg
new file mode 100644
index 0000000..93cb9e3
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/Preferences.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/Project.svg b/tools/deploy/Resources/Icons/IconSet-dark/Project.svg
new file mode 100644
index 0000000..bc5899b
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/Project.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/Radar.svg b/tools/deploy/Resources/Icons/IconSet-dark/Radar.svg
new file mode 100644
index 0000000..ca46960
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/Radar.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/Register.svg b/tools/deploy/Resources/Icons/IconSet-dark/Register.svg
new file mode 100644
index 0000000..3b1d6c3
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/Register.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/Save.svg b/tools/deploy/Resources/Icons/IconSet-dark/Save.svg
new file mode 100644
index 0000000..925b28a
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/Save.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/SaveAs.svg b/tools/deploy/Resources/Icons/IconSet-dark/SaveAs.svg
new file mode 100644
index 0000000..b76c31f
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/SaveAs.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/Search.svg b/tools/deploy/Resources/Icons/IconSet-dark/Search.svg
new file mode 100644
index 0000000..9c8da15
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/Search.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/SearchCode.svg b/tools/deploy/Resources/Icons/IconSet-dark/SearchCode.svg
new file mode 100644
index 0000000..34f9b4e
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/SearchCode.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/Settings.svg b/tools/deploy/Resources/Icons/IconSet-dark/Settings.svg
new file mode 100644
index 0000000..e1fc999
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/Settings.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/Spiral.svg b/tools/deploy/Resources/Icons/IconSet-dark/Spiral.svg
new file mode 100644
index 0000000..b12587a
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/Spiral.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/Table.svg b/tools/deploy/Resources/Icons/IconSet-dark/Table.svg
new file mode 100644
index 0000000..9cdadf6
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/Table.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-dark/VolumesPreProcessing.svg b/tools/deploy/Resources/Icons/IconSet-dark/VolumesPreProcessing.svg
new file mode 100644
index 0000000..0b771ca
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-dark/VolumesPreProcessing.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-light/Account.svg b/tools/deploy/Resources/Icons/IconSet-light/Account.svg
new file mode 100644
index 0000000..58b8e04
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-light/Account.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-light/Bookmark.svg b/tools/deploy/Resources/Icons/IconSet-light/Bookmark.svg
new file mode 100644
index 0000000..3a00c5b
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-light/Bookmark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-light/BookmarkCheck.svg b/tools/deploy/Resources/Icons/IconSet-light/BookmarkCheck.svg
new file mode 100644
index 0000000..cdf696e
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-light/BookmarkCheck.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-light/CircleHelp.svg b/tools/deploy/Resources/Icons/IconSet-light/CircleHelp.svg
new file mode 100644
index 0000000..8925f3b
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-light/CircleHelp.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-light/CloudJobs.svg b/tools/deploy/Resources/Icons/IconSet-light/CloudJobs.svg
new file mode 100644
index 0000000..983329d
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-light/CloudJobs.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-light/MultiTool.svg b/tools/deploy/Resources/Icons/IconSet-light/MultiTool.svg
new file mode 100644
index 0000000..171eb5d
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-light/MultiTool.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-light/Register.svg b/tools/deploy/Resources/Icons/IconSet-light/Register.svg
new file mode 100644
index 0000000..73e0c43
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-light/Register.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Carbonate-CT.png b/tools/deploy/Resources/Icons/IconSet-samples/Carbonate-CT.png
similarity index 100%
rename from tools/deploy/Resources/Carbonate-CT.png
rename to tools/deploy/Resources/Icons/IconSet-samples/Carbonate-CT.png
diff --git a/tools/deploy/Resources/Carbonate-microCT.png b/tools/deploy/Resources/Icons/IconSet-samples/Carbonate-mCT.png
similarity index 100%
rename from tools/deploy/Resources/Carbonate-microCT.png
rename to tools/deploy/Resources/Icons/IconSet-samples/Carbonate-mCT.png
diff --git a/tools/deploy/Resources/Sandstone-CT.png b/tools/deploy/Resources/Icons/IconSet-samples/Sandstone-CT.png
similarity index 100%
rename from tools/deploy/Resources/Sandstone-CT.png
rename to tools/deploy/Resources/Icons/IconSet-samples/Sandstone-CT.png
diff --git a/tools/deploy/Resources/Sandstone-microCT.png b/tools/deploy/Resources/Icons/IconSet-samples/Sandstone-mCT.png
similarity index 100%
rename from tools/deploy/Resources/Sandstone-microCT.png
rename to tools/deploy/Resources/Icons/IconSet-samples/Sandstone-mCT.png
diff --git a/tools/deploy/Resources/grains_menor.png b/tools/deploy/Resources/Icons/IconSet-samples/small_Grains.png
similarity index 100%
rename from tools/deploy/Resources/grains_menor.png
rename to tools/deploy/Resources/Icons/IconSet-samples/small_Grains.png
diff --git a/tools/deploy/Resources/Pores_menor.png b/tools/deploy/Resources/Icons/IconSet-samples/small_Pores.png
similarity index 100%
rename from tools/deploy/Resources/Pores_menor.png
rename to tools/deploy/Resources/Icons/IconSet-samples/small_Pores.png
diff --git a/tools/deploy/Resources/MicroTom_menor.png b/tools/deploy/Resources/Icons/IconSet-samples/small_mCT.png
similarity index 100%
rename from tools/deploy/Resources/MicroTom_menor.png
rename to tools/deploy/Resources/Icons/IconSet-samples/small_mCT.png
diff --git a/tools/deploy/Resources/Icons/IconSet-svg/BigImage.svg b/tools/deploy/Resources/Icons/IconSet-svg/BigImage.svg
new file mode 100644
index 0000000..29ee631
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-svg/BigImage.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-svg/Multi-Scale.svg b/tools/deploy/Resources/Icons/IconSet-svg/Multi-Scale.svg
new file mode 100644
index 0000000..b54825a
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-svg/Multi-Scale.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-svg/Pore Network.svg b/tools/deploy/Resources/Icons/IconSet-svg/Pore Network.svg
new file mode 100644
index 0000000..227dc95
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-svg/Pore Network.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-svg/Register.svg b/tools/deploy/Resources/Icons/IconSet-svg/Register.svg
new file mode 100644
index 0000000..98c1389
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-svg/Register.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-widgets/checkbox_checked_disabled.svg b/tools/deploy/Resources/Icons/IconSet-widgets/checkbox_checked_disabled.svg
new file mode 100644
index 0000000..286f0d6
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-widgets/checkbox_checked_disabled.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tools/deploy/Resources/Icons/IconSet-widgets/checkbox_checked_enabled.svg b/tools/deploy/Resources/Icons/IconSet-widgets/checkbox_checked_enabled.svg
new file mode 100644
index 0000000..ebc2bbe
--- /dev/null
+++ b/tools/deploy/Resources/Icons/IconSet-widgets/checkbox_checked_enabled.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/modules/WelcomeGeoSlicer/Resources/ImageLogEnv.png b/tools/deploy/Resources/Icons/ImageLog.png
similarity index 100%
rename from src/modules/WelcomeGeoSlicer/Resources/ImageLogEnv.png
rename to tools/deploy/Resources/Icons/ImageLog.png
diff --git a/tools/deploy/Resources/LoadIcon.png b/tools/deploy/Resources/Icons/Load.png
similarity index 100%
rename from tools/deploy/Resources/LoadIcon.png
rename to tools/deploy/Resources/Icons/Load.png
diff --git a/src/modules/WelcomeGeoSlicer/Resources/MicroCTEnv.png b/tools/deploy/Resources/Icons/MicroCT.png
similarity index 100%
rename from src/modules/WelcomeGeoSlicer/Resources/MicroCTEnv.png
rename to tools/deploy/Resources/Icons/MicroCT.png
diff --git a/tools/deploy/Resources/Icons/MicroCT3D.png b/tools/deploy/Resources/Icons/MicroCT3D.png
new file mode 100644
index 0000000..19dc331
Binary files /dev/null and b/tools/deploy/Resources/Icons/MicroCT3D.png differ
diff --git a/tools/deploy/Resources/Icons/MultiscaleIcon.png b/tools/deploy/Resources/Icons/MultiscaleIcon.png
new file mode 100644
index 0000000..fab9f32
Binary files /dev/null and b/tools/deploy/Resources/Icons/MultiscaleIcon.png differ
diff --git a/src/modules/NetCDF/Resources/Icons/NetCDF.png b/tools/deploy/Resources/Icons/NetCDF.png
similarity index 100%
rename from src/modules/NetCDF/Resources/Icons/NetCDF.png
rename to tools/deploy/Resources/Icons/NetCDF.png
diff --git a/src/modules/OpenRockData/Resources/Icons/OpenRockData.png b/tools/deploy/Resources/Icons/OpenRockData.png
similarity index 100%
rename from src/modules/OpenRockData/Resources/Icons/OpenRockData.png
rename to tools/deploy/Resources/Icons/OpenRockData.png
diff --git a/tools/deploy/Resources/ProjectIcon.ico b/tools/deploy/Resources/Icons/Project.ico
similarity index 100%
rename from tools/deploy/Resources/ProjectIcon.ico
rename to tools/deploy/Resources/Icons/Project.ico
diff --git a/tools/deploy/Resources/Icons/ProjectIcon.ico b/tools/deploy/Resources/Icons/ProjectIcon.ico
new file mode 100644
index 0000000..022e256
Binary files /dev/null and b/tools/deploy/Resources/Icons/ProjectIcon.ico differ
diff --git a/tools/deploy/Resources/PushPinIn.png b/tools/deploy/Resources/Icons/PushPinIn.png
similarity index 100%
rename from tools/deploy/Resources/PushPinIn.png
rename to tools/deploy/Resources/Icons/PushPinIn.png
diff --git a/tools/deploy/Resources/PushPinOut.png b/tools/deploy/Resources/Icons/PushPinOut.png
similarity index 100%
rename from tools/deploy/Resources/PushPinOut.png
rename to tools/deploy/Resources/Icons/PushPinOut.png
diff --git a/tools/deploy/Resources/RedBangCircle.png b/tools/deploy/Resources/Icons/RedBangCircle.png
similarity index 100%
rename from tools/deploy/Resources/RedBangCircle.png
rename to tools/deploy/Resources/Icons/RedBangCircle.png
diff --git a/tools/deploy/Resources/RedoIcon.png b/tools/deploy/Resources/Icons/Redo.png
similarity index 100%
rename from tools/deploy/Resources/RedoIcon.png
rename to tools/deploy/Resources/Icons/Redo.png
diff --git a/tools/deploy/Resources/ResetIcon.png b/tools/deploy/Resources/Icons/Reset.png
similarity index 100%
rename from tools/deploy/Resources/ResetIcon.png
rename to tools/deploy/Resources/Icons/Reset.png
diff --git a/tools/deploy/Resources/RunIcon.png b/tools/deploy/Resources/Icons/Run.png
similarity index 100%
rename from tools/deploy/Resources/RunIcon.png
rename to tools/deploy/Resources/Icons/Run.png
diff --git a/tools/deploy/Resources/SaveIcon.png b/tools/deploy/Resources/Icons/Save.png
similarity index 100%
rename from tools/deploy/Resources/SaveIcon.png
rename to tools/deploy/Resources/Icons/Save.png
diff --git a/tools/deploy/Resources/SaveAsIcon.png b/tools/deploy/Resources/Icons/SaveAs.png
similarity index 100%
rename from tools/deploy/Resources/SaveAsIcon.png
rename to tools/deploy/Resources/Icons/SaveAs.png
diff --git a/tools/deploy/Resources/ScreenshotIcon.png b/tools/deploy/Resources/Icons/ScreenShot.png
similarity index 100%
rename from tools/deploy/Resources/ScreenshotIcon.png
rename to tools/deploy/Resources/Icons/ScreenShot.png
diff --git a/tools/deploy/Resources/StopIcon.png b/tools/deploy/Resources/Icons/Stop.png
similarity index 100%
rename from tools/deploy/Resources/StopIcon.png
rename to tools/deploy/Resources/Icons/Stop.png
diff --git a/src/modules/WelcomeGeoSlicer/Resources/ThinSectionEnv.png b/tools/deploy/Resources/Icons/ThinSection.png
similarity index 100%
rename from src/modules/WelcomeGeoSlicer/Resources/ThinSectionEnv.png
rename to tools/deploy/Resources/Icons/ThinSection.png
diff --git a/tools/deploy/Resources/Icons/ToggleMenu.png b/tools/deploy/Resources/Icons/ToggleMenu.png
new file mode 100644
index 0000000..bcb8705
Binary files /dev/null and b/tools/deploy/Resources/Icons/ToggleMenu.png differ
diff --git a/tools/deploy/Resources/UndoIcon.png b/tools/deploy/Resources/Icons/Undo.png
similarity index 100%
rename from tools/deploy/Resources/UndoIcon.png
rename to tools/deploy/Resources/Icons/Undo.png
diff --git a/tools/deploy/Resources/Styles/StyleSheet-dark.qss b/tools/deploy/Resources/Styles/StyleSheet-dark.qss
new file mode 100644
index 0000000..0ffce7a
--- /dev/null
+++ b/tools/deploy/Resources/Styles/StyleSheet-dark.qss
@@ -0,0 +1,145 @@
+QPushButton {
+ background-color: #535353; /* Button color */
+ color: #ffffff; /* Button text color for readability */
+ padding: 4px 8px;
+ border-radius: 4px;
+}
+
+.actionButtonBackground {background-color: #26C252;}
+
+
+QPushButton:hover {
+ background-color: #37403A; /* Slightly lighter green on hover */
+}
+
+QPushButton:pressed {
+ background-color: #1e8e41; /* Darker shade on button press */
+}
+
+QLineEdit, QTextEdit {
+ background-color: #535353;
+ border: 1px solid #666666;
+ border-radius: 4px;
+ padding: 4px;
+}
+
+QTextEdit#pythonConsole {
+ background-color: #333333; /* Console background color */
+ border-radius: 0px;
+}
+
+
+QListView, QTableView {
+ background-color: #333333; /* Match general background */
+ alternate-background-color: #3b3b3b; /* Subtle alternating row color */
+ color: #f0f0f0; /* Text color */
+ selection-background-color: #37403A; /* Selection color */
+ selection-color: #ffffff; /* Text color when selected */
+}
+
+QScrollBar:vertical, QScrollBar:horizontal {
+ background: #535353; /* Scrollbar background */
+ width: 10px;
+}
+
+QScrollBar::handle:vertical, QScrollBar::handle:horizontal {
+ background: #26C252; /* Scrollbar handle */
+ border-radius: 4px;
+}
+
+/* -~- Checkboxes -~- */
+QCheckBox, QRadioButton {
+ color: #f0f0f0; /* Text color */
+}
+
+QWidget::indicator {
+ border-radius: 3px;
+}
+
+QCheckBox::indicator, QRadioButton::indicator, QGroupBox::indicator {
+ width: 16px;
+ height: 16px;
+ border-radius: 4px;
+ margin-top: 0.07em; /* Make checkbox look centered vertically in relation to text */
+}
+
+/* Hack: QGroupBox has weird border behavior */
+QGroupBox::indicator {
+ width: 20px;
+ height: 20px;
+}
+
+QRadioButton::indicator {
+ border-radius: 10px;
+}
+
+QWidget::indicator:unchecked {
+ background-color: none;
+ border: 2px solid #bbb;
+}
+
+QWidget::indicator:checked:enabled {
+ background-color: #26C252;
+ border: 2px solid #26C252;
+ image: url($iconPath/checkbox_checked_enabled.svg);
+}
+
+/* QListWidget and QTreeWidget are excluded here because hovering the text counts as hovering them,
+ * but clicking the text does not toggle the checkbox.
+ */
+QCheckBox::indicator:unchecked:hover, QRadioButton::indicator:unchecked:hover, QGroupBox::indicator:unchecked:hover {
+ border-color: #26C252;
+}
+
+/* Don't style hovered checked radio button because clicking it won't uncheck it */
+QCheckBox::indicator:checked:hover, QGroupBox::indicator:checked:hover {
+ border-color: #46E262;
+}
+
+QWidget::indicator:unchecked:disabled {
+ background-color: none;
+ border: 2px solid #535353;
+}
+
+QWidget::indicator:checked:disabled {
+ background-color: #535353;
+ border: 2px solid #535353;
+ image: url($iconPath/checkbox_checked_disabled.svg);
+}
+/* -~- End Checkboxes -~- */
+
+QSlider::groove:horizontal {
+ height: 4px; /* the groove expands to the size of the slider by default. by giving it a height, it has a fixed size */
+ background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #B1B1B1, stop:1 #c4c4c4);
+ margin: 2px 0;
+}
+
+QSlider::handle:horizontal {
+ background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #535353, stop:1 #8f8f8f);
+ border: 1px solid #5c5c5c;
+ width: 18px;
+ margin: -2px 0; /* handle is placed by default on the contents rect of the groove. Expand outside the groove */
+ border-radius: 3px;
+}
+
+QProgressBar {
+ background-color: #535353; /* Background of the progress bar */
+ border-radius: 4px;
+ text-align: center; /* Center text over progress bar */
+}
+
+QProgressBar::chunk {
+ background-color: #26C252; /* Grey progress color */
+ border-radius: 4px;
+}
+
+/* Thin ProgressBar */
+QProgressBar[style="thin"] {
+ height: 8px; /* Thin progress bar */
+ font-size: 7px;
+}
+
+QProgressBar[style="thin"]::chunk {
+ background-color: #26C252; /* Grey progress color */
+ border-radius: 4px;
+}
\ No newline at end of file
diff --git a/tools/deploy/Resources/Accounts.png b/tools/deploy/ResourcesZZ/Accounts.png
similarity index 100%
rename from tools/deploy/Resources/Accounts.png
rename to tools/deploy/ResourcesZZ/Accounts.png
diff --git a/tools/deploy/ResourcesZZ/AddIcon.png b/tools/deploy/ResourcesZZ/AddIcon.png
new file mode 100644
index 0000000..c98060c
Binary files /dev/null and b/tools/deploy/ResourcesZZ/AddIcon.png differ
diff --git a/tools/deploy/ResourcesZZ/AnnotationDistance.png b/tools/deploy/ResourcesZZ/AnnotationDistance.png
new file mode 100644
index 0000000..5488b2c
Binary files /dev/null and b/tools/deploy/ResourcesZZ/AnnotationDistance.png differ
diff --git a/tools/deploy/ResourcesZZ/ApplyIcon.png b/tools/deploy/ResourcesZZ/ApplyIcon.png
new file mode 100644
index 0000000..21ccc75
Binary files /dev/null and b/tools/deploy/ResourcesZZ/ApplyIcon.png differ
diff --git a/tools/deploy/Resources/BugReport.png b/tools/deploy/ResourcesZZ/BugReport.png
similarity index 100%
rename from tools/deploy/Resources/BugReport.png
rename to tools/deploy/ResourcesZZ/BugReport.png
diff --git a/tools/deploy/ResourcesZZ/CancelIcon.png b/tools/deploy/ResourcesZZ/CancelIcon.png
new file mode 100644
index 0000000..9647611
Binary files /dev/null and b/tools/deploy/ResourcesZZ/CancelIcon.png differ
diff --git a/tools/deploy/ResourcesZZ/Carbonate-CT.png b/tools/deploy/ResourcesZZ/Carbonate-CT.png
new file mode 100644
index 0000000..f1e01d0
Binary files /dev/null and b/tools/deploy/ResourcesZZ/Carbonate-CT.png differ
diff --git a/tools/deploy/ResourcesZZ/Carbonate-microCT.png b/tools/deploy/ResourcesZZ/Carbonate-microCT.png
new file mode 100644
index 0000000..83b131c
Binary files /dev/null and b/tools/deploy/ResourcesZZ/Carbonate-microCT.png differ
diff --git a/tools/deploy/Resources/CloneIcon.png b/tools/deploy/ResourcesZZ/CloneIcon.png
similarity index 100%
rename from tools/deploy/Resources/CloneIcon.png
rename to tools/deploy/ResourcesZZ/CloneIcon.png
diff --git a/tools/deploy/ResourcesZZ/ClosedEye.png b/tools/deploy/ResourcesZZ/ClosedEye.png
new file mode 100644
index 0000000..4902d4a
Binary files /dev/null and b/tools/deploy/ResourcesZZ/ClosedEye.png differ
diff --git a/tools/deploy/ResourcesZZ/DeleteIcon.png b/tools/deploy/ResourcesZZ/DeleteIcon.png
new file mode 100644
index 0000000..0201412
Binary files /dev/null and b/tools/deploy/ResourcesZZ/DeleteIcon.png differ
diff --git a/tools/deploy/ResourcesZZ/EditAddIcon.png b/tools/deploy/ResourcesZZ/EditAddIcon.png
new file mode 100644
index 0000000..4bd07db
Binary files /dev/null and b/tools/deploy/ResourcesZZ/EditAddIcon.png differ
diff --git a/tools/deploy/ResourcesZZ/EditIcon.png b/tools/deploy/ResourcesZZ/EditIcon.png
new file mode 100644
index 0000000..d907c43
Binary files /dev/null and b/tools/deploy/ResourcesZZ/EditIcon.png differ
diff --git a/tools/deploy/Resources/EmptyCircleIcon.png b/tools/deploy/ResourcesZZ/EmptyCircleIcon.png
similarity index 100%
rename from tools/deploy/Resources/EmptyCircleIcon.png
rename to tools/deploy/ResourcesZZ/EmptyCircleIcon.png
diff --git a/tools/deploy/Resources/Explorer.png b/tools/deploy/ResourcesZZ/Explorer.png
similarity index 100%
rename from tools/deploy/Resources/Explorer.png
rename to tools/deploy/ResourcesZZ/Explorer.png
diff --git a/tools/deploy/Resources/FilledCircleIcon.png b/tools/deploy/ResourcesZZ/FilledCircleIcon.png
similarity index 100%
rename from tools/deploy/Resources/FilledCircleIcon.png
rename to tools/deploy/ResourcesZZ/FilledCircleIcon.png
diff --git a/tools/deploy/ResourcesZZ/FitIcon.png b/tools/deploy/ResourcesZZ/FitIcon.png
new file mode 100644
index 0000000..7634bef
Binary files /dev/null and b/tools/deploy/ResourcesZZ/FitIcon.png differ
diff --git a/tools/deploy/ResourcesZZ/FitRealAspectRatioIcon.png b/tools/deploy/ResourcesZZ/FitRealAspectRatioIcon.png
new file mode 100644
index 0000000..b98f0de
Binary files /dev/null and b/tools/deploy/ResourcesZZ/FitRealAspectRatioIcon.png differ
diff --git a/tools/deploy/Resources/GeoSlicer-ProgressBar.ico b/tools/deploy/ResourcesZZ/GeoSlicer-ProgressBar.ico
similarity index 100%
rename from tools/deploy/Resources/GeoSlicer-ProgressBar.ico
rename to tools/deploy/ResourcesZZ/GeoSlicer-ProgressBar.ico
diff --git a/tools/deploy/ResourcesZZ/GeoSlicer-logo.png b/tools/deploy/ResourcesZZ/GeoSlicer-logo.png
new file mode 100644
index 0000000..931a9cd
Binary files /dev/null and b/tools/deploy/ResourcesZZ/GeoSlicer-logo.png differ
diff --git a/tools/deploy/ResourcesZZ/GeoSlicer.ico b/tools/deploy/ResourcesZZ/GeoSlicer.ico
new file mode 100644
index 0000000..5cff7b1
Binary files /dev/null and b/tools/deploy/ResourcesZZ/GeoSlicer.ico differ
diff --git a/tools/deploy/Resources/Green3dCustomLayoutIcon.png b/tools/deploy/ResourcesZZ/Green3dCustomLayoutIcon.png
similarity index 100%
rename from tools/deploy/Resources/Green3dCustomLayoutIcon.png
rename to tools/deploy/ResourcesZZ/Green3dCustomLayoutIcon.png
diff --git a/tools/deploy/ResourcesZZ/GreenCheckCircle.png b/tools/deploy/ResourcesZZ/GreenCheckCircle.png
new file mode 100644
index 0000000..449bc5d
Binary files /dev/null and b/tools/deploy/ResourcesZZ/GreenCheckCircle.png differ
diff --git a/tools/deploy/Resources/HistNoColor.png b/tools/deploy/ResourcesZZ/HistNoColor.png
similarity index 100%
rename from tools/deploy/Resources/HistNoColor.png
rename to tools/deploy/ResourcesZZ/HistNoColor.png
diff --git a/tools/deploy/ResourcesZZ/Icons/BIAEPBrowser.png b/tools/deploy/ResourcesZZ/Icons/BIAEPBrowser.png
new file mode 100644
index 0000000..ed2f20d
Binary files /dev/null and b/tools/deploy/ResourcesZZ/Icons/BIAEPBrowser.png differ
diff --git a/tools/deploy/ResourcesZZ/Icons/MicroCTEnv.png b/tools/deploy/ResourcesZZ/Icons/MicroCTEnv.png
new file mode 100644
index 0000000..380a785
Binary files /dev/null and b/tools/deploy/ResourcesZZ/Icons/MicroCTEnv.png differ
diff --git a/src/modules/WelcomeGeoSlicer/Resources/NetCDF.png b/tools/deploy/ResourcesZZ/Icons/NetCDF.png
similarity index 100%
rename from src/modules/WelcomeGeoSlicer/Resources/NetCDF.png
rename to tools/deploy/ResourcesZZ/Icons/NetCDF.png
diff --git a/tools/deploy/Resources/JobMonitor.png b/tools/deploy/ResourcesZZ/JobMonitor.png
similarity index 100%
rename from tools/deploy/Resources/JobMonitor.png
rename to tools/deploy/ResourcesZZ/JobMonitor.png
diff --git a/tools/deploy/Resources/LTrace-logo.png b/tools/deploy/ResourcesZZ/LTrace-logo.png
similarity index 100%
rename from tools/deploy/Resources/LTrace-logo.png
rename to tools/deploy/ResourcesZZ/LTrace-logo.png
diff --git a/tools/deploy/Resources/LTraceIcon.png b/tools/deploy/ResourcesZZ/LTraceIcon.png
similarity index 100%
rename from tools/deploy/Resources/LTraceIcon.png
rename to tools/deploy/ResourcesZZ/LTraceIcon.png
diff --git a/tools/deploy/ResourcesZZ/LoadIcon.png b/tools/deploy/ResourcesZZ/LoadIcon.png
new file mode 100644
index 0000000..cd25ad4
Binary files /dev/null and b/tools/deploy/ResourcesZZ/LoadIcon.png differ
diff --git a/tools/deploy/Resources/LoadSceneIcon.png b/tools/deploy/ResourcesZZ/LoadSceneIcon.png
similarity index 100%
rename from tools/deploy/Resources/LoadSceneIcon.png
rename to tools/deploy/ResourcesZZ/LoadSceneIcon.png
diff --git a/tools/deploy/ResourcesZZ/MicroTom_menor.png b/tools/deploy/ResourcesZZ/MicroTom_menor.png
new file mode 100644
index 0000000..19dc331
Binary files /dev/null and b/tools/deploy/ResourcesZZ/MicroTom_menor.png differ
diff --git a/tools/deploy/Resources/MoveDownIcon.png b/tools/deploy/ResourcesZZ/MoveDownIcon.png
similarity index 100%
rename from tools/deploy/Resources/MoveDownIcon.png
rename to tools/deploy/ResourcesZZ/MoveDownIcon.png
diff --git a/tools/deploy/Resources/MoveUpIcon.png b/tools/deploy/ResourcesZZ/MoveUpIcon.png
similarity index 100%
rename from tools/deploy/Resources/MoveUpIcon.png
rename to tools/deploy/ResourcesZZ/MoveUpIcon.png
diff --git a/tools/deploy/ResourcesZZ/OpenEye.png b/tools/deploy/ResourcesZZ/OpenEye.png
new file mode 100644
index 0000000..186705e
Binary files /dev/null and b/tools/deploy/ResourcesZZ/OpenEye.png differ
diff --git a/tools/deploy/ResourcesZZ/Pores_menor.png b/tools/deploy/ResourcesZZ/Pores_menor.png
new file mode 100644
index 0000000..83f822c
Binary files /dev/null and b/tools/deploy/ResourcesZZ/Pores_menor.png differ
diff --git a/tools/deploy/ResourcesZZ/ProjectIcon.ico b/tools/deploy/ResourcesZZ/ProjectIcon.ico
new file mode 100644
index 0000000..022e256
Binary files /dev/null and b/tools/deploy/ResourcesZZ/ProjectIcon.ico differ
diff --git a/tools/deploy/ResourcesZZ/PushPinIn.png b/tools/deploy/ResourcesZZ/PushPinIn.png
new file mode 100644
index 0000000..6fa52f1
Binary files /dev/null and b/tools/deploy/ResourcesZZ/PushPinIn.png differ
diff --git a/tools/deploy/ResourcesZZ/PushPinOut.png b/tools/deploy/ResourcesZZ/PushPinOut.png
new file mode 100644
index 0000000..72dc488
Binary files /dev/null and b/tools/deploy/ResourcesZZ/PushPinOut.png differ
diff --git a/tools/deploy/ResourcesZZ/QEMSCAN/LookupColorTables/Default mineral colors.csv b/tools/deploy/ResourcesZZ/QEMSCAN/LookupColorTables/Default mineral colors.csv
new file mode 100644
index 0000000..714629a
--- /dev/null
+++ b/tools/deploy/ResourcesZZ/QEMSCAN/LookupColorTables/Default mineral colors.csv
@@ -0,0 +1,78 @@
+Mineral;R;G;B
+Al-Micas+Ilita+Al-Esmectitas;0;192;0
+Al-Esmectitas;51;102;0
+Anfib�lio;184;222;101
+Ankerita;255;192;128
+Anortocl�sio;220;19;59
+Apatita;255;128;128
+Albita;244;164;96
+Badele�ta;200;72;221
+Barita;196;189;151
+Calcita (0% a 1%MgO);0;255;255
+Calcita (1% a 4%MgO);32;178;170
+Calcita (4% a 7%MgO);0;90;90
+Calcita (7% a 10%MgO);0;50;50
+Calcopirita;169;133;181
+Caulinita;192;64;0
+Celestita;135;206;235
+Clorita+Clorita-Esmectita;0;255;0
+Criolita;157;153;81
+Dawsonita;118;147;60
+Dolomita;0;112;192
+Dolomita (17% a 20%MgO);0;0;128
+Dolomita (20% a 22%MgO);128;128;255
+Escapolita;192;192;255
+Epidoto;188;143;143
+Esfalerita;189;183;106
+Estroncianita;192;192;255
+Fase de Al, Si, Ca, Fe;106;142;34
+Fase de F, Al, Si, Ca;149;71;212
+Fase de F, Mg, Si, Al;0;0;255
+Fase de Mg, Al, Si, Fe;46;78;78
+Fase de Mg, Si, Ca, Fe;93;41;233
+Fase de Na, Mg, Al, Si;185;133;24
+Fase de Na, Mg, Si, Ca, Fe;14;163;240
+Fase de S, Ti, Fe;255;215;0
+Fase de Si, Ca;255;98;70
+Fase de Si, Ca, Ti, Fe;46;46;142
+Fase de Si, Fe;210;200;140
+Fe-Micas;152;251;152
+Ferrosilita;128;64;64
+Fluorita;110;135;178
+Gipsita/Anidrita;255;0;0
+Goyazita;143;59;211
+Halita;128;128;128
+Ilmenita;64;64;64
+Jarosita;210;180;140
+K-Feldspato;206;107;10
+Magnesita;82;165;178
+Material Rico em Carbono (MC);255;0;255
+Mg-Argilominerais;218;165;32
+Mg-Micas+Mg-Esmectitas;128;0;0
+Mineral Rico em Al;30;56;68
+Morinita;255;192;192
+Natrojarosita;255;222;173
+Nefelina;0;128;0
+Noseana;192;192;0
+Olivina;96;73;122
+Outros;255;255;255
+�xido de Alum�nio;192;192;192
+�xido de Ferro;111;128;144
+�xido de Ferro e Tit�nio;105;105;105
+�xido de Tit�nio;192;255;255
+Paragonita;240;230;140
+Pirita;151;71;6
+Pirox�nio;204;153;255
+Plagiocl�sio/Ze�lita;214;184;54
+Poros;0;0;0
+Prosopita;103;105;178
+Quartzo;255;255;0
+Siderita;255;128;0
+Silvita;191;191;191
+Sodalita;101;66;162
+Svanbergita;192;192;255
+Titanita;143;188;139
+Tunisita;97;81;174
+Zirc�o;255;0;153
+Zoisita;255;192;203
+Sr-Barita;128;0;128
diff --git a/tools/deploy/Resources/Red3dCustomLayoutIcon.png.png b/tools/deploy/ResourcesZZ/Red3dCustomLayoutIcon.png.png
similarity index 100%
rename from tools/deploy/Resources/Red3dCustomLayoutIcon.png.png
rename to tools/deploy/ResourcesZZ/Red3dCustomLayoutIcon.png.png
diff --git a/tools/deploy/ResourcesZZ/RedBangCircle.png b/tools/deploy/ResourcesZZ/RedBangCircle.png
new file mode 100644
index 0000000..07fcd2c
Binary files /dev/null and b/tools/deploy/ResourcesZZ/RedBangCircle.png differ
diff --git a/tools/deploy/ResourcesZZ/RedoIcon.png b/tools/deploy/ResourcesZZ/RedoIcon.png
new file mode 100644
index 0000000..5e7e81b
Binary files /dev/null and b/tools/deploy/ResourcesZZ/RedoIcon.png differ
diff --git a/tools/deploy/ResourcesZZ/ResetIcon.png b/tools/deploy/ResourcesZZ/ResetIcon.png
new file mode 100644
index 0000000..e35d8c7
Binary files /dev/null and b/tools/deploy/ResourcesZZ/ResetIcon.png differ
diff --git a/tools/deploy/ResourcesZZ/RunIcon.png b/tools/deploy/ResourcesZZ/RunIcon.png
new file mode 100644
index 0000000..e3987d3
Binary files /dev/null and b/tools/deploy/ResourcesZZ/RunIcon.png differ
diff --git a/tools/deploy/ResourcesZZ/Sandstone-CT.png b/tools/deploy/ResourcesZZ/Sandstone-CT.png
new file mode 100644
index 0000000..0328a0e
Binary files /dev/null and b/tools/deploy/ResourcesZZ/Sandstone-CT.png differ
diff --git a/tools/deploy/ResourcesZZ/Sandstone-microCT.png b/tools/deploy/ResourcesZZ/Sandstone-microCT.png
new file mode 100644
index 0000000..e417a08
Binary files /dev/null and b/tools/deploy/ResourcesZZ/Sandstone-microCT.png differ
diff --git a/tools/deploy/ResourcesZZ/SaveAsIcon.png b/tools/deploy/ResourcesZZ/SaveAsIcon.png
new file mode 100644
index 0000000..8475e98
Binary files /dev/null and b/tools/deploy/ResourcesZZ/SaveAsIcon.png differ
diff --git a/tools/deploy/ResourcesZZ/SaveIcon.png b/tools/deploy/ResourcesZZ/SaveIcon.png
new file mode 100644
index 0000000..db02a20
Binary files /dev/null and b/tools/deploy/ResourcesZZ/SaveIcon.png differ
diff --git a/tools/deploy/ResourcesZZ/ScreenshotIcon.png b/tools/deploy/ResourcesZZ/ScreenshotIcon.png
new file mode 100644
index 0000000..ccb1181
Binary files /dev/null and b/tools/deploy/ResourcesZZ/ScreenshotIcon.png differ
diff --git a/tools/deploy/ResourcesZZ/SideBySideImageIcon.png b/tools/deploy/ResourcesZZ/SideBySideImageIcon.png
new file mode 100644
index 0000000..920374e
Binary files /dev/null and b/tools/deploy/ResourcesZZ/SideBySideImageIcon.png differ
diff --git a/tools/deploy/ResourcesZZ/SideBySideSegmentationIcon.png b/tools/deploy/ResourcesZZ/SideBySideSegmentationIcon.png
new file mode 100644
index 0000000..a657451
Binary files /dev/null and b/tools/deploy/ResourcesZZ/SideBySideSegmentationIcon.png differ
diff --git a/tools/deploy/Resources/SlicerInvisible.png b/tools/deploy/ResourcesZZ/SlicerInvisible.png
similarity index 100%
rename from tools/deploy/Resources/SlicerInvisible.png
rename to tools/deploy/ResourcesZZ/SlicerInvisible.png
diff --git a/tools/deploy/Resources/SlicerVisible.png b/tools/deploy/ResourcesZZ/SlicerVisible.png
similarity index 100%
rename from tools/deploy/Resources/SlicerVisible.png
rename to tools/deploy/ResourcesZZ/SlicerVisible.png
diff --git a/tools/deploy/ResourcesZZ/StopIcon.png b/tools/deploy/ResourcesZZ/StopIcon.png
new file mode 100644
index 0000000..aa1a9eb
Binary files /dev/null and b/tools/deploy/ResourcesZZ/StopIcon.png differ
diff --git a/tools/deploy/ResourcesZZ/UndoIcon.png b/tools/deploy/ResourcesZZ/UndoIcon.png
new file mode 100644
index 0000000..003f117
Binary files /dev/null and b/tools/deploy/ResourcesZZ/UndoIcon.png differ
diff --git a/tools/deploy/Resources/Yellow3dCustomLayoutIcon.png b/tools/deploy/ResourcesZZ/Yellow3dCustomLayoutIcon.png
similarity index 100%
rename from tools/deploy/Resources/Yellow3dCustomLayoutIcon.png
rename to tools/deploy/ResourcesZZ/Yellow3dCustomLayoutIcon.png
diff --git a/tools/deploy/ResourcesZZ/grains_menor.png b/tools/deploy/ResourcesZZ/grains_menor.png
new file mode 100644
index 0000000..09685f8
Binary files /dev/null and b/tools/deploy/ResourcesZZ/grains_menor.png differ
diff --git a/tools/deploy/deploy_slicer.py b/tools/deploy/deploy_slicer.py
index 76b9957..eab101a 100644
--- a/tools/deploy/deploy_slicer.py
+++ b/tools/deploy/deploy_slicer.py
@@ -64,7 +64,12 @@
module_name = line.split("=")[0]
import_name = module_name
- if import_name != "mkdocs-localsearch" and import_name != "mkdocs-material":
+ if (
+ import_name != "mkdocs-localsearch"
+ and import_name != "mkdocs-material"
+ and import_name != "mkdocs-mermaid2-plugin"
+ and import_name != "mkdocs-include-markdown-plugin"
+ ):
try:
logger.info(f"Importing python module: {import_name}")
globals()[import_name] = importlib.__import__(import_name)
@@ -100,7 +105,6 @@ def generate_slicer_package(
output_dir,
version,
fast_and_dirty,
- with_porespy=args.with_porespy,
development=False,
)
@@ -112,7 +116,6 @@ def deploy_development_environment(
output_dir,
fast_and_dirty,
keep_name,
- with_porespy,
args,
):
generic_deploy(
@@ -124,7 +127,6 @@ def deploy_development_environment(
None,
fast_and_dirty,
development=True,
- with_porespy=with_porespy,
keep_name=keep_name,
)
@@ -135,31 +137,11 @@ def generic_deploy(
modules_package_folder,
slicerltrace_repo_folder,
output_dir,
- version,
+ version_string: str,
fast_and_dirty,
development,
- with_porespy,
keep_name=False,
):
- public_version = False
- if version is not None:
- parts = version.split(".")
- if len(parts) > 3:
- raise RuntimeError("Invalid version: {}".format(version))
-
- parsed_version = [0, 0, 0]
- for i in range(len(parts)):
- v = parts[i]
- if "RC" in v:
- v = v.replace("RC", "")
- if "-public" in v:
- public_version = True
- v = v.replace("-public", "")
- assert int(v) >= 0
- parsed_version[i] = str(parts[i])
-
- version = tuple(parsed_version)
-
logger.info("Extracting")
slicer_dir = extract_archive(slicer_archive, output_dir)
@@ -169,9 +151,7 @@ def generic_deploy(
repo.git.submodule("update", "--init", "--recursive")
# getting the 3D Slicer version
- lib_dir = slicer_dir / "lib"
- lib_dir_subdirs = [f.name for f in lib_dir.iterdir() if f.is_dir()]
- slicer_version = [s[len(APP_NAME) + 1 :] for s in lib_dir_subdirs if APP_NAME + "-" in s][0]
+ slicer_version = get_slicer_version(slicer_dir)
logger.info("Slicer version " + str(slicer_version))
if not fast_and_dirty:
@@ -190,7 +170,7 @@ def generic_deploy(
for path in find_submodules_setup_directory(SUBMODULES_PACKAGE_FOLDER):
submodule_name = path.name
if (
- not with_porespy
+ args.without_porespy
and submodule_name
in [
"porespy",
@@ -231,7 +211,7 @@ def generic_deploy(
os.chdir(wd)
logger.info("Installing customizer")
- install_customizer(slicer_dir, modules_to_add, find_extensions(slicer_dir), version, development)
+ install_customizer(slicer_dir, modules_to_add, find_extensions(slicer_dir), version_string, development)
logger.info("Copying assets")
copy_extra_files(slicer_dir, slicer_version, slicerltrace_repo_folder, fast_and_dirty, development)
@@ -246,22 +226,23 @@ def generic_deploy(
apply_patches(slicer_dir, slicer_version)
if not development:
- major, minor, revision = version
- if revision:
- version_string = "{}.{}.{}".format(major, minor, revision)
- else:
- version_string = "{}.{}".format(major, minor)
-
version_name = "GeoSlicer-{}".format(version_string)
archive_folder_name = slicer_dir.with_name(version_name)
if slicer_dir.name != version_name:
shutil.move(slicer_dir, archive_folder_name)
- if not fast_and_dirty:
+ if not fast_and_dirty and not args.disable_archiving:
logger.info("Archiving")
make_archive(args, archive_folder_name, slicer_archive.with_name(version_name))
+def get_slicer_version(slicer_dir: Path) -> str:
+ lib_dir = slicer_dir / "lib"
+ lib_dir_subdirs = [f.name for f in lib_dir.iterdir() if f.is_dir()]
+ slicer_version = [s[len(APP_NAME) + 1 :] for s in lib_dir_subdirs if APP_NAME + "-" in s][0]
+ return slicer_version
+
+
def remove_unwanted_files(slicer_dir, slicer_version):
with open(DEPLOY_CONFIG) as f:
config = json.JSONDecoder().decode(f.read())
@@ -345,7 +326,7 @@ def install_pip_dependencies(slicer_dir, lib_folder, development=False):
pip_call.append("--editable")
pip_call.append(str(lib_folder))
- subprocess.run([str(slicer_python), "-m", "pip", "install", "--upgrade", "pip==22.3", "setuptools==59.8.0"])
+ subprocess.run([str(slicer_python), "-m", "pip", "install", "--upgrade", "pip==22.3", "setuptools==60.2.0"])
runResult = subprocess.run(pip_call)
runResult.check_returncode()
@@ -377,55 +358,30 @@ def find_extensions(slicer_dir):
def install_customizer(slicer_dir, modules, extensions, version, dev_environment):
- def format_string_list_to_write(l):
- strings = [repr("{}".format(s)) for s in l]
- return "[\n {}\n ]".format(",\n ".join(strings))
-
- paths = modules + extensions
- if dev_environment:
- formatted_paths = format_string_list_to_write(paths)
- else:
- formatted_paths = format_string_list_to_write((p.relative_to(slicer_dir) for p in paths))
- paths = (p.relative_to(slicer_dir) for p in paths)
-
- ltrace_modules_whitelist = []
- for module in modules:
- if module.name.lower().endswith("cli"):
- continue
- module_file = module / (module.name + ".py")
- assert module_file.is_file()
- with open(module_file, encoding="utf-8") as m:
- content = m.read()
- if f"class {module.name}" in content:
- ltrace_modules_whitelist.append(module.name)
- else:
- logger.info(
- f"Warning: Class {module.name} not found in {module_file.name}. It will not be visible in the menu."
- )
-
- with open(THIS_FOLDER / "Customizer.py") as f:
- customizer_source = f.read()
+ src_path = THIS_FOLDER / "slicerrc.py"
+ dest_path = slicer_dir / ".slicerrc.py"
- formatted_whitelist = format_string_list_to_write(ltrace_modules_whitelist)
+ shutil.copy(src_path, dest_path)
repo = git.Repo(path=SLICERLTRACE_REPO_FOLDER, search_parent_directories=True)
- with open(get_plugins_dir(slicer_dir) / "qt-scripted-modules" / "Customizer.py", "w") as f:
- f.write(customizer_source)
+ if dev_environment:
+ module_dir = modules[0].parent
+ else:
+ module_dir = modules[0].parent.relative_to(slicer_dir)
json_output = {
- "name": "WelcomeGeoSlicer",
+ "name": "GeoSlicer",
"itk_module": None,
- "CUSTOM_REL_PATHS": [str(p) for p in paths],
- "VISIBLE_LTRACE_PLUGINS": ltrace_modules_whitelist,
"GEOSLICER_VERSION": version,
"GEOSLICER_HASH": repr(repo.head.object.hexsha),
"GEOSLICER_HASH_DIRTY": repr(repo.is_dirty()),
"GEOSLICER_BUILD_TIME": str(datetime.datetime.now()),
"GEOSLICER_DEV_ENVIRONMENT": repr(dev_environment),
+ "GEOSLICER_MODULES": str(module_dir),
}
- json_path = get_plugins_dir(slicer_dir) / "qt-scripted-modules" / "Resources" / "json" / "WelcomeGeoSlicer.json"
+ json_path = get_plugins_dir(slicer_dir) / "qt-scripted-modules" / "Resources" / "json" / "GeoSlicer.json"
with open(json_path, "w") as json_file:
json.dump(json_output, json_file, indent=4)
@@ -611,6 +567,7 @@ def make_archive(args, source_dir: Path, target_file_without_extension: Path) ->
result = Path(result)
target = target_file_without_extension.with_name(result.name)
shutil.move(result, target)
+ logging.info(f"Compressed file created: {target.as_posix()}")
logger.info(f"Compressed file created: {target.as_posix()}")
return target
@@ -831,6 +788,39 @@ def check_init_files() -> None:
raise RuntimeError("The following ltrace lib modules does not contain a __init__.py file: " + str(no_init_list))
+def get_version_string(version: str) -> str:
+ if version is None:
+ return None
+
+ try:
+ if version is not None:
+ parts = version.split(".")
+ if len(parts) > 3:
+ raise
+
+ parsed_version = ["0", "0", "0"]
+ for i in range(len(parts)):
+ v = parts[i]
+ if "RC" in v:
+ v = v.replace("RC", "")
+ if "-public" in v:
+ v = v.replace("-public", "")
+ assert int(v) >= 0
+ parsed_version[i] = str(parts[i])
+
+ version = tuple(parsed_version)
+ version = ".".join(version)
+
+ except Exception as error:
+ message = f"Invalid version input: {version}."
+ if str(error):
+ message += f"\nError: {error}"
+
+ raise RuntimeError(message)
+
+ return version
+
+
def run(args):
if not args.public_commit_only:
if args.archive:
@@ -847,6 +837,7 @@ def run(args):
if args.geoslicer_version and args.dev:
raise RuntimeError("Can't deploy the development version together with production version")
+ geoslicer_version = get_version_string(args.geoslicer_version)
if args.no_public_commit and args.public_commit_only:
raise RuntimeError("Can't avoid public commit if you want only to make the public commit.")
@@ -862,7 +853,6 @@ def run(args):
output_dir,
fast_and_dirty=args.fast_and_dirty,
keep_name=args.keep_name,
- with_porespy=args.with_porespy,
args=args,
)
except Exception as error:
@@ -899,7 +889,7 @@ def run(args):
MODULES_PACKAGE_FOLDER,
SLICERLTRACE_REPO_FOLDER,
output_dir,
- args.geoslicer_version,
+ geoslicer_version,
fast_and_dirty=args.fast_and_dirty,
args=args,
)
@@ -978,9 +968,9 @@ def run(args):
default=False,
)
parser.add_argument(
- "--with-porespy",
+ "--without-porespy",
action="store_true",
- help="Install porespy as editable local submodules for development",
+ help="Avoid porespy submodule installation to use the installed version from the base file.",
default=False,
)
parser.add_argument(
@@ -995,4 +985,10 @@ def run(args):
help="Keeps tests when generating standalone build.",
default=False,
)
+ parser.add_argument(
+ "--disable-archiving",
+ action="store_true",
+ help="Avoid the archiving step when generating a release build.",
+ default=False,
+ )
run(parser.parse_args())
diff --git a/tools/deploy/requirements.txt b/tools/deploy/requirements.txt
index 64fe29d..edc30cf 100644
--- a/tools/deploy/requirements.txt
+++ b/tools/deploy/requirements.txt
@@ -3,7 +3,10 @@
patch==1.16
GitPython==3.1.27 # git
vswhere==1.3.0
-packaging==21.3
-mkdocs==1.3.0
-mkdocs-localsearch==0.9.1
-mkdocs-material==8.2.16
+packaging==23.2
+mkdocs==1.6.0
+mkdocs-localsearch==0.9.2
+mkdocs-material==9.5.33
+markupsafe==2.0.1
+mkdocs-mermaid2-plugin==1.1.1
+mkdocs-include-markdown-plugin==6.2.2
\ No newline at end of file
diff --git a/tools/deploy/slicer_deploy_config.json b/tools/deploy/slicer_deploy_config.json
index 2dc518d..f884a6d 100644
--- a/tools/deploy/slicer_deploy_config.json
+++ b/tools/deploy/slicer_deploy_config.json
@@ -57,11 +57,11 @@
["tools/deploy/Assets/presets.xml", "share/$slicer_dir/qt-loadable-modules/VolumeRendering"],
["tools/deploy/Assets/GenericAnatomyColors.txt", "share/$slicer_dir/ColorFiles"],
["tools/deploy/Assets/Terminologies/*", "share/$slicer_dir/qt-loadable-modules/Terminologies"],
- ["tools/deploy/Resources/*", "lib/$slicer_dir/qt-scripted-modules/Resources"],
["linux", "tools/deploy/Assets/GeoSlicerLauncherSettingsLinux.ini", "bin/GeoSlicerLauncherSettings.ini"],
["windows", "tools/deploy/Assets/GeoSlicerLauncherSettingsWindows.ini", "bin/GeoSlicerLauncherSettings.ini"],
["linux", "tools/deploy/Assets/Tesseract-OCR.tar.xz", "bin"],
- ["windows", "tools/deploy/Assets/Tesseract-OCR.zip", "bin"]
+ ["windows", "tools/deploy/Assets/Tesseract-OCR.zip", "bin"],
+ ["tools/deploy/Resources/*", "LTrace/Resources"]
],
"OpenSourceRepoFiles" : [
@@ -69,16 +69,12 @@
],
"ClosedSourceFiles" : [
- "src/modules/ImageLogEnv/EccentricityCLI/EccentricityCLI.py",
- "src/modules/ImageLogEnv/EccentricityCLI/EccentricityCLI.xml",
- "src/modules/ImageLogEnv/ImageLogsLib/Eccentricity.md",
- "src/modules/ImageLogEnv/ImageLogsLib/Eccentricity.py",
+ "src/modules/Eccentricity",
"src/modules/PetroPUCImageLogSegmenterCLI/models/",
"src/modules/PetroPUCImageLogSegmenterCLI/source/",
"src/modules/PetroPUCImageLogSegmenterCLI/results/",
"src/modules/RemoteService/Resources/sample-configs.json",
"src/modules/WidgetIdentification/",
- "src/modules/CBPF_PAD_REMOVAL.py",
"src/modules/Notebooks/data",
"src/submodules/biaep/",
"src/modules/PNMReport/ReportLib/StreamlitServer.py",
@@ -105,44 +101,6 @@
"src/modules/ExpandSegmentsBigImage",
"src/modules/PolynomialShadingCorrectionBigImage",
"src/modules/BoundaryRemovalBigImage",
- "src/modules/ImageLogInstanceSegmenter/Test/",
- "src/modules/PoreNetworkProduction/Test/",
- "src/modules/MultiScale/Test/",
- "src/modules/ThinSectionLoader/Test/",
- "src/modules/CustomResampleScalarVolume/Test/",
- "src/modules/ImageLogEnv/Test/",
- "src/modules/PoreNetworkKrelEda/Test/",
- "src/modules/MicroCTExport/Test/",
- "src/modules/WidgetIdentification/Test/",
- "src/modules/SpiralFilter/Test/",
- "src/modules/PoreNetworkVisualization/Test/",
- "src/modules/ThinSectionInstanceSegmenter/Test/",
- "src/modules/SegmentationModelling/Test/",
- "src/modules/PoreNetworkExtractor/Test/",
- "src/modules/PoreNetworkMicp/Test/",
- "src/modules/CustomizedGradientAnisotropicDiffusion/Test/",
- "src/modules/VolumeCalculator/Test/",
- "src/modules/ImageLogCropVolume/Test/",
- "src/modules/DLISImport/Test/",
- "src/modules/PolynomialShadingCorrection/Test/",
- "src/modules/MultipleImageAnalysis/Test/",
- "src/modules/ImageLogExport/Test/",
- "src/modules/MicroCTLoader/Test/",
- "src/modules/ExpandSegmentsBigImage/Test/",
- "src/modules/SegmentationKmeans/Test/",
- "src/modules/ImageLogData/Test/",
- "src/modules/ImageLogUnwrapImport/Test/",
- "src/modules/BoundaryRemovalBigImage/Test/",
- "src/modules/BigImage/Test/",
- "src/modules/CustomizedSegmentEditor/Test/",
- "src/modules/PoreNetworkSimulation/Test/",
- "src/modules/ResampleVectorVolume/Test/",
- "src/modules/CustomizedData/Test/",
- "src/modules/NetCDF/Test/",
- "src/modules/PoreNetworkCompare/Test/",
- "src/modules/SegmentInspector/Test/",
- "src/modules/Segmenter/Test/",
- "src/modules/CustomizedCropVolume/Test/",
"src/ltrace/ltrace/slicer/tracking",
"src/ltrace/ltrace/remote/hosts/biaep/"
],
diff --git a/tools/deploy/slicerrc.py b/tools/deploy/slicerrc.py
new file mode 100644
index 0000000..a922806
--- /dev/null
+++ b/tools/deploy/slicerrc.py
@@ -0,0 +1,1425 @@
+import logging
+import os
+import pickle
+import shutil
+from string import Template
+from functools import partial
+from pathlib import Path
+from types import MethodType
+
+import ctk
+import qt
+import slicer
+import slicer.util
+
+from ltrace.slicer.about.about_dialog import AboutDialog
+from ltrace.slicer.app import getApplicationVersion, updateWindowTitle, getJsonData
+from ltrace.slicer.app.custom_3dview import customize_3d_view as customize3DView
+from ltrace.slicer.app.custom_colormaps import customize_color_maps as customizeColorMaps
+from ltrace.slicer.app.onboard import showDataLoaders, LOADERS, loadEnvironment
+from ltrace.slicer.application_observables import ApplicationObservables
+from ltrace.slicer.custom_export_to_file import customizeExportToFile
+from ltrace.slicer.module_utils import loadModules, fetchModulesFrom
+from ltrace.slicer.widget.global_progress_bar import GlobalProgressBar
+from ltrace.slicer.widget.memory_usage import MemoryUsageWidget
+from ltrace.slicer.widget.module_header import ModuleHeader
+from ltrace.slicer_utils import slicer_is_in_developer_mode, getResourcePath
+from ltrace.slicer import helpers
+from ltrace.utils.ProgressBarProc import ProgressBarProc
+from ltrace.utils.custom_event_filter import CustomEventFilter
+
+# This line solve some problems with Geoslicer Restart when mmengine in installed
+# because of a dependence on opencv, instead of opencv-headless, currently used in
+# geoslicer. The problem and some solutions are described in this
+# post: https://forum.qt.io/post/617768
+os.environ.pop("QT_QPA_PLATFORM_PLUGIN_PATH", None)
+os.environ["TESSDATA_PREFIX"] = Path(f"{slicer.app.slicerHome}/bin/Tesseract-OCR/tessdata/").as_posix()
+
+RUN_MODE = os.environ.get("GEOSLICER_RUN_MODE", "development")
+APP_NAME = slicer.app.applicationName
+APP_HOME = Path(slicer.app.slicerHome)
+ICON_DIR = APP_HOME / "LTrace" / "Resources" / "Icons"
+APP_TOOLBARS = {toolbar.name: toolbar for toolbar in slicer.util.mainWindow().findChildren("QToolBar")}
+GEOSLICER_MODULES_DIR = Path(getJsonData()["GEOSLICER_MODULES"])
+
+toBool = slicer.util.toBool
+
+
+def getAppContext():
+ return slicer.modules.AppContextInstance
+
+
+# {
+# "MainToolBar": QToolBar(0x1A0040D6EC0, name="MainToolBar"),
+# "ModuleSelectorToolBar": qSlicerModuleSelectorToolBar(0x1A0040DAB00, name="ModuleSelectorToolBar"),
+# "ModuleToolBar": QToolBar(0x1A0040D7000, name="ModuleToolBar"),
+# "ViewToolBar": QToolBar(0x1A0040D6A00, name="ViewToolBar"),
+# "MouseModeToolBar": qSlicerMouseModeToolBar(0x1A0040D6BC0, name="MouseModeToolBar"),
+# "CaptureToolBar": qMRMLCaptureToolBar(0x1A00415A4B0, name="CaptureToolBar"),
+# "ViewersToolBar": qSlicerViewersToolBar(0x1A0040D82C0, name="ViewersToolBar"),
+# "DialogToolBar": QToolBar(0x1A0040D93C0, name="DialogToolBar"),
+# "MarkupsToolBar": qMRMLMarkupsToolBar(0x1A000D6F3F0, name="MarkupsToolBar"),
+# "SequenceBrowserToolBar": qMRMLSequenceBrowserToolBar(0x1A003DB7870, name="SequenceBrowserToolBar"),
+# "": QToolBar(0x1A01F1DA6F0),
+# }
+
+# __indicator = ModuleIndicator(APP_TOOLBARS["ModuleSelectorToolBar"])
+
+
+def trySelectModule(clicked, moduleName=None):
+ try:
+ if not moduleName:
+ return
+
+ slicer.util.selectModule(moduleName)
+ except Exception as e:
+ logging.error(f"Error selecting module {moduleName}: {e}")
+
+
+def toggleMenuBar():
+ # Toggle the visibility of the menu bar
+ menubar = slicer.util.mainWindow().menuBar()
+ menubar.setVisible(not menubar.isVisible())
+
+
+def ltraceBugReport():
+ ltraceBugReportDialog = qt.QDialog(slicer.util.mainWindow())
+ ltraceBugReportDialog.setWindowTitle("Generate a bug report")
+ ltraceBugReportDialog.setMinimumSize(600, 400)
+ layout = qt.QFormLayout(ltraceBugReportDialog)
+ layout.setLabelAlignment(qt.Qt.AlignRight)
+
+ layout.addRow("Please describe the problem in the area bellow:", None)
+ errorDescriptionArea = qt.QPlainTextEdit()
+ layout.addRow(errorDescriptionArea)
+ layout.addRow(" ", None)
+
+ ltraceBugReportDirectoryButton = ctk.ctkDirectoryButton()
+ ltraceBugReportDirectoryButton.caption = "Select a directory to save the report"
+ layout.addRow("Report destination directory:", None)
+ layout.addRow(ltraceBugReportDirectoryButton)
+ layout.addRow(" ", None)
+
+ buttonsLayout = qt.QHBoxLayout()
+ generateButton = qt.QPushButton("Generate report")
+ generateButton.setFixedHeight(40)
+ buttonsLayout.addWidget(generateButton)
+ cancelButton = qt.QPushButton("Cancel")
+ cancelButton.setFixedHeight(40)
+ buttonsLayout.addWidget(cancelButton)
+ layout.addRow(buttonsLayout)
+
+ def ltraceBugReportGenerate():
+ reportPath = Path(ltraceBugReportDirectoryButton.directory).absolute() / "GeoSlicerBugReport"
+ reportPath.mkdir(parents=True, exist_ok=True)
+
+ geoslicerLogFiles = list(slicer.app.recentLogFiles())
+ trackingManager = getAppContext().getTracker()
+ trackingLogFiles = trackingManager.getRecentLogs() if trackingManager else []
+
+ for file in geoslicerLogFiles + trackingLogFiles:
+ try:
+ shutil.copy2(file, str(reportPath))
+ except FileNotFoundError:
+ pass
+
+ Path(reportPath / "bug_description.txt").write_text(errorDescriptionArea.toPlainText())
+ shutil.make_archive(reportPath, "zip", reportPath)
+
+ try:
+ shutil.rmtree(str(reportPath))
+ except OSError as e:
+ # If for some reason can't delete the directory
+ pass
+
+ errorDescriptionArea.setPlainText("")
+ ltraceBugReportDialog.close()
+
+ generateButton.clicked.connect(ltraceBugReportGenerate)
+ cancelButton.connect("clicked()", ltraceBugReportDialog.close)
+
+ ltraceBugReportDialog.exec_()
+
+
+class ExpandToolbarActionNames:
+ def __init__(self):
+ self.__closeIcon = qt.QIcon((ICON_DIR / "IconSet-dark" / "PanelLeftClose.svg").as_posix())
+ self.__openIcon = qt.QIcon((ICON_DIR / "IconSet-dark" / "PanelLeftOpen.svg").as_posix())
+
+ def __call__(self, *args, **kwargs):
+ modulebar = APP_TOOLBARS["ModuleToolBar"]
+ actions = modulebar.actions()
+ widget = None
+ for action in actions:
+ widget = modulebar.widgetForAction(action)
+ item = widget if isinstance(widget, qt.QToolButton) else action
+ if hasattr(item, "toggleName"):
+ item.toggleName()
+
+ if not widget:
+ logging.warning("No action found")
+ slicer.modules.AppContextInstance.modules.showDataLoaders(APP_TOOLBARS["ModuleToolBar"])
+ return
+
+ expander = APP_TOOLBARS["MainToolBar"].actions()[0]
+ if widget.toolButtonStyle == qt.Qt.ToolButtonTextBesideIcon:
+ expander.setIcon(self.__closeIcon)
+ expander.setToolTip("Collapse Menu")
+ slicer.app.userSettings().setValue(f"{slicer.app.applicationName}/LeftDrawerVisible", True)
+ else:
+ expander.setIcon(self.__openIcon)
+ expander.setToolTip("Expand Menu")
+ slicer.app.userSettings().setValue(f"{slicer.app.applicationName}/LeftDrawerVisible", False)
+
+
+def ui_InformUserAboutRestart(text):
+ msg = qt.QMessageBox(slicer.util.mainWindow())
+ msg.setText(f"{text} Geoslicer will restart itself in 10 seconds.")
+ msg.setWindowTitle("Configuration finished")
+ restartTimer = qt.QTimer(msg)
+ restartTimer.singleShot(10000, msg.accept)
+ msg.exec_()
+
+
+def ui_Spacer(layoutType):
+ spacer = qt.QWidget()
+ spacer.setSizePolicy(qt.QSizePolicy.MinimumExpanding, qt.QSizePolicy.Expanding)
+ layout = layoutType(spacer)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.addStretch(1)
+ return spacer
+
+
+def installFonts():
+ fontId = qt.QFontDatabase().addApplicationFont(
+ APP_HOME / "LTrace" / "Resources" / "Fonts" / "Inter_18pt-SemiBold.ttf"
+ )
+ _fontstr = qt.QFontDatabase().applicationFontFamilies(fontId)[0]
+ _font = qt.QFont(_fontstr, 9)
+ slicer.app.setFont(_font)
+ slicer.app.pythonConsole().setFont(_font)
+ # userSettings.setValue("General/font", _font)
+ # userSettings.setValue("Python/Font", _font)
+
+
+def setDataProbeCollapsibleButton():
+ dataProbeWidget = slicer.util.mainWindow().findChild(ctk.ctkCollapsibleButton, "DataProbeCollapsibleWidget")
+ dataProbeWidget.collapsed = True
+ dataProbeWidget.minimumWidth = 450
+
+
+def setCustomDataProbeInfo():
+ infoWidget = slicer.modules.DataProbeInstance.infoWidget
+
+ def customized_describe_pixel(self, ijk, slicerLayerLogic):
+ """
+ Does not show pixel name if it is either 'invalid' or '(none)'. In
+ such cases, show rest of the description (i.e., pixel value).
+ """
+ volumeNode = slicerLayerLogic.GetVolumeNode()
+ if not volumeNode:
+ return ""
+ description = self.getPixelString(volumeNode, ijk)
+ if description.startswith("invalid "):
+ description = description[8:]
+ elif description.startswith("(none) "):
+ description = description[7:]
+ return f"{description}"
+
+ infoWidget.generateIJKPixelValueDescription = MethodType(customized_describe_pixel, infoWidget)
+
+
+# enable/disable volume interpolation
+def setVolumeInterpolation(v):
+ def setInterpolationAll(v):
+ for node in slicer.util.getNodes("*").values():
+ if node.IsA("vtkMRMLScalarVolumeDisplayNode"):
+ node.SetInterpolate(v)
+
+ def interpolator(caller, event):
+ setInterpolationAll(v)
+
+ # set value for all current nodes:
+ setInterpolationAll(v)
+ # observe new volumes
+ slicer.mrmlScene.AddObserver(slicer.mrmlScene.NodeAddedEvent, interpolator)
+
+
+def setModuleSelectorToolBar():
+ toolbar = slicer.util.mainWindow().moduleSelector()
+ actions = toolbar.actions()
+ actions[0].setVisible(False)
+ actions[1].setVisible(False)
+ actions[2].setVisible(False)
+ actions[3].setVisible(False)
+
+ try:
+ spacer = ui_Spacer(layoutType=qt.QHBoxLayout)
+ toolbar.addWidget(spacer)
+
+ expandRightFunc = getAppContext().rightDrawer
+ action = qt.QAction(expandRightFunc.closeIcon, "", toolbar)
+ action.setToolTip("Expand Data")
+ expandRightFunc.setAction(action)
+ action.triggered.connect(expandRightFunc)
+ toolbar.addAction(action)
+
+ openDrawer = toBool(slicer.app.userSettings().value(f"{APP_NAME}/RighDrawerVisible", True))
+
+ if openDrawer:
+ expandRightFunc.show()
+ else:
+ expandRightFunc.hide()
+
+ toolbar.setVisible(True)
+ except:
+ import traceback
+
+ traceback.print_exc()
+
+ addEnvSelectorMenu()
+
+
+def addEnvSelectorMenu():
+ envButton = qt.QToolButton(APP_TOOLBARS["ModuleSelectorToolBar"])
+ envButton.setText("Choose a environment")
+ envButton.setToolTip("Change current envinronment")
+ envButton.objectName = "environment Selector Menu"
+
+ menu = qt.QMenu(envButton)
+
+ for env, info in LOADERS.items():
+ icon = qt.QIcon(info.icon)
+
+ actione = qt.QAction(icon, env, APP_TOOLBARS["ModuleSelectorToolBar"])
+ actione.triggered.connect(lambda _, info=info: loadEnvironment(APP_TOOLBARS["ModuleToolBar"], info))
+
+ menu.addAction(actione)
+
+ envButton.setMenu(menu)
+ envButton.setPopupMode(qt.QToolButton.MenuButtonPopup)
+ envButton.clicked.connect(lambda: envButton.showMenu())
+
+ toolButtonAction = qt.QWidgetAction(APP_TOOLBARS["ModuleSelectorToolBar"])
+ toolButtonAction.setDefaultWidget(envButton)
+
+ beforeAction = APP_TOOLBARS["ModuleSelectorToolBar"].actions()[6]
+
+ APP_TOOLBARS["ModuleSelectorToolBar"].insertAction(beforeAction, toolButtonAction)
+
+
+def setModulesToolBar():
+ toolbarArea = qt.Qt.LeftToolBarArea
+
+ APP_TOOLBARS["ModuleToolBar"].clear()
+
+ setToolbars(
+ [
+ APP_TOOLBARS["ModuleToolBar"],
+ ],
+ toolbarArea,
+ qt.Qt.Vertical,
+ )
+
+
+def setMainToolBar():
+ APP_TOOLBARS["MainToolBar"].clear()
+
+ handler = ExpandToolbarActionNames()
+
+ APP_TOOLBARS["MainToolBar"].addAction(
+ qt.QIcon((ICON_DIR / "IconSet-dark" / "PanelLeftOpen.svg").as_posix()), "Expand Menu", handler
+ )
+
+ APP_TOOLBARS["MainToolBar"].setVisible(True)
+
+
+def setDialogToolBar():
+ toolbarArea = qt.Qt.RightToolBarArea
+ verticalToolBars = [
+ "DialogToolBar",
+ ]
+
+ dialogToolBar = APP_TOOLBARS["DialogToolBar"]
+
+ dialogToolBar.addAction(
+ qt.QIcon((ICON_DIR / "IconSet-dark" / "Bug.svg").as_posix()),
+ "Bug Report",
+ ltraceBugReport,
+ )
+
+ dialogToolBar.addAction(
+ qt.QIcon((ICON_DIR / "IconSet-dark" / "CloudJobs.svg").as_posix()),
+ "Cluster Jobs",
+ lambda clicked: getAppContext().rightDrawer.show(1),
+ )
+
+ dialogToolBar.addAction(
+ qt.QIcon((ICON_DIR / "IconSet-dark" / "Apps.svg").as_posix()),
+ "Data Sources",
+ lambda: slicer.modules.AppContextInstance.modules.showDataLoaders(APP_TOOLBARS["ModuleToolBar"]),
+ )
+
+ dialogToolBar.addAction(
+ qt.QIcon((ICON_DIR / "IconSet-dark" / "Account.svg").as_posix()),
+ "Accounts",
+ partial(slicer.modules.RemoteServiceInstance.cli.initiateConnectionDialog, keepDialogOpen=True),
+ )
+
+ # dialogToolBar.addAction(
+ # qt.QIcon((ICON_DIR / "IconSet-dark" / "Settings.svg").as_posix()),
+ # "Settings",
+ # lambda: None,
+ # )
+
+ setToolbars([APP_TOOLBARS[tb] for tb in verticalToolBars], toolbarArea, qt.Qt.Vertical)
+
+ firstAction = APP_TOOLBARS["DialogToolBar"].actions()[0]
+ firstAction.setIcon(helpers.svgToQIcon(ICON_DIR / "IconSet-dark" / "Console.svg"))
+ spacer = ui_Spacer(layoutType=qt.QVBoxLayout)
+ spacer.setSizePolicy(qt.QSizePolicy.Preferred, qt.QSizePolicy.Expanding)
+ APP_TOOLBARS["DialogToolBar"].insertWidget(firstAction, spacer)
+
+
+def setMarkupToolBar():
+ toolbar = APP_TOOLBARS["MarkupsToolBar"]
+ spacer = ui_Spacer(layoutType=qt.QHBoxLayout)
+ firstAction = toolbar.actions()[0]
+ toolbar.insertWidget(firstAction, spacer)
+
+
+def setRightToolBar(visible):
+ """
+ Set docked toolbar on the right side of the main window
+ """
+
+ toolbarArea = qt.Qt.RightToolBarArea
+
+ verticalToolBars = [
+ "ViewToolBar",
+ "ViewersToolBar",
+ "MouseModeToolBar",
+ "CaptureToolBar",
+ ]
+
+ setToolbars([APP_TOOLBARS[tb] for tb in verticalToolBars], toolbarArea, qt.Qt.Vertical)
+
+
+def setToolbars(toolbars, area, orientation, iconSize: qt.QSize = None):
+ for toolbar in toolbars:
+ toolbar.setVisible(True)
+ toolbar.setOrientation(orientation)
+ if iconSize:
+ toolbar.setIconSize(iconSize)
+ slicer.util.mainWindow().addToolBar(area, toolbar)
+
+
+def setCustomCaptureToolBar():
+ from ltrace.screenshot.Screenshot import ScreenshotWidget
+
+ # Add customized screenshot dialog
+ captureToolBar = APP_TOOLBARS["CaptureToolBar"]
+
+ for action in captureToolBar.actions():
+ captureToolBar.removeAction(action)
+
+ screenshotAction = captureToolBar.addAction(
+ qt.QIcon((ICON_DIR / "Screenshot.png").as_posix()),
+ "",
+ lambda: ScreenshotWidget().exec(),
+ )
+ screenshotAction.setToolTip("Capture a screenshot")
+
+
+def setModulePanelVisible(visible):
+ modulePanelDockWidget = slicer.util.mainWindow().findChildren("QDockWidget", "PanelDockWidget")[0]
+ modulePanelDockWidget.setVisible(visible)
+
+
+def setModulePanel():
+ modulePanelDockWidget = slicer.util.mainWindow().findChildren("QDockWidget", "PanelDockWidget")[0]
+ modulePanelDockWidget.setFeatures(qt.QDockWidget.NoDockWidgetFeatures)
+
+ moduleHeader = ModuleHeader(modulePanelDockWidget)
+
+ def handle(moduleName):
+ try:
+ module = getattr(slicer.modules, moduleName.lower())
+ moduleHeader.update(module.title, module.helpText)
+ except Exception as e:
+ pass
+
+ modulePanelDockWidget.setTitleBarWidget(moduleHeader)
+
+ slicer.util.moduleSelector().connect("moduleSelected(QString)", handle)
+
+
+def updateFileMenu():
+ fileMenu = slicer.util.mainWindow().findChild("QMenu", "FileMenu")
+ actions = {action.text: action for action in fileMenu.actions()}
+ createIcon = lambda fileName: qt.QIcon((getResourcePath("Icons") / "IconSet-dark" / fileName).as_posix())
+
+ loadSceneAction = qt.QAction(createIcon("Load.svg"), "Load Scene", fileMenu)
+ loadSceneAction.setToolTip("Load project/scene .mrml file")
+ loadSceneAction.triggered.connect(getAppContext().projectEventsLogic.loadScene)
+
+ # Save scene
+ saveDataAction = actions["Save Data"]
+ saveDataAction.setIcon(createIcon("Save.svg"))
+ saveDataAction.triggered.disconnect()
+ saveDataAction.triggered.connect(getAppContext().projectEventsLogic.saveScene)
+ saveDataAction.setToolTip("Save the current and modified project/scene .mrml file")
+
+ saveSceneAsAction = qt.QAction(createIcon("SaveAs.svg"), "Save Scene As", fileMenu)
+ saveSceneAsAction.setShortcut(qt.QKeySequence("Ctrl+Shift+S"))
+ saveSceneAsAction.triggered.connect(getAppContext().projectEventsLogic.saveSceneAs)
+ saveSceneAsAction.setToolTip("Save the current and modified project/scene .mrml file into a new directory")
+
+ closeSceneAction = actions["Close Scene"]
+ closeSceneAction.setIcon(createIcon("Close.svg"))
+ closeSceneAction.triggered.disconnect() # Disconnect previous callback
+ closeSceneAction.triggered.connect(getAppContext().projectEventsLogic.onCloseScene)
+ closeSceneAction.setToolTip("Clear the current project/scene")
+
+ order = [
+ actions["&Add Data"],
+ actions["Recent"],
+ "Separator",
+ loadSceneAction,
+ saveDataAction,
+ saveSceneAsAction,
+ "Separator",
+ closeSceneAction,
+ "Separator",
+ actions["E&xit"],
+ ]
+
+ [fileMenu.removeAction(action) for action in fileMenu.actions()]
+
+ for action in order:
+ if action == "Separator":
+ fileMenu.addSeparator()
+ continue
+
+ fileMenu.addAction(action)
+
+ actions["Recent"].visible = slicer_is_in_developer_mode()
+ actions["Save Data"].setText("Save Scene")
+ actions["&Add Data"].setIcon(createIcon("AddData.svg"))
+
+
+def updateEditMenu():
+ editMenu = slicer.util.mainWindow().findChild("QMenu", "EditMenu")
+ actions = {action.text: action for action in editMenu.actions()}
+ createIcon = lambda fileName: qt.QIcon((getResourcePath("Icons") / "IconSet-dark" / fileName).as_posix())
+
+ actions["Cut"].setIcon(createIcon("Cut.svg"))
+ actions["Copy"].setIcon(createIcon("Copy.svg"))
+ actions["Paste"].setIcon(createIcon("Paste.svg"))
+ actions["Application Settings"].setIcon(createIcon("Preferences.svg"))
+
+
+def updateViewMenu():
+ viewMenu = slicer.util.mainWindow().findChild("QMenu", "ViewMenu")
+ actions = {action.text: action for action in viewMenu.actions()}
+ createIcon = lambda fileName: helpers.svgToQIcon(getResourcePath("Icons") / "IconSet-dark" / fileName)
+
+ fuzzySearchAction = qt.QAction(createIcon("SearchCode.svg"), "Module Search", viewMenu)
+ fuzzySearchAction.triggered.connect(showSearchPopup)
+
+ order = [
+ actions["Layout"],
+ actions["&Toolbars"],
+ actions["&Appearance"],
+ "Separator",
+ fuzzySearchAction,
+ "Separator",
+ actions["Module Finder"],
+ actions["&Python Console"],
+ actions["&Error Log"],
+ actions["Home"],
+ ]
+
+ [viewMenu.removeAction(action) for action in viewMenu.actions()]
+
+ for action in order:
+ if action == "Separator":
+ viewMenu.addSeparator()
+ continue
+
+ viewMenu.addAction(action)
+
+ actions["Module Finder"].setIcon(createIcon("Search.svg"))
+ actions["Module Finder"].setText("Legacy Module Finder")
+ actions["Home"].setIcon(createIcon("Home.svg"))
+ actions["&Python Console"].setIcon(createIcon("Console.svg"))
+
+
+def updateHelpMenu():
+ helpMenu = slicer.util.mainWindow().findChild("QMenu", "HelpMenu")
+ actions = {action.text: action for action in helpMenu.actions()}
+ createIcon = lambda fileName: qt.QIcon((getResourcePath("Icons") / "IconSet-dark" / fileName).as_posix())
+
+ # Bug report
+ bugReportAction = qt.QAction(createIcon("Bug.svg"), "Bug Report", helpMenu)
+ bugReportAction.triggered.connect(ltraceBugReport)
+
+ # About
+ def __onAboutGeoSlicerClicked():
+ AboutDialog(parent=slicer.util.mainWindow()).show()
+
+ aboutAction = qt.QAction(createIcon("About.svg"), "About GeoSlicer", helpMenu)
+ aboutAction.triggered.connect(__onAboutGeoSlicerClicked)
+
+ order = [
+ actions["&Keyboard Shortcuts"],
+ bugReportAction,
+ "Separator",
+ aboutAction,
+ ]
+
+ [helpMenu.removeAction(action) for action in helpMenu.actions()]
+
+ for action in order:
+ if action == "Separator":
+ helpMenu.addSeparator()
+ continue
+
+ helpMenu.addAction(action)
+
+
+def setMenu():
+ updateFileMenu()
+ updateEditMenu()
+ updateViewMenu()
+ updateHelpMenu()
+
+
+def setDefaultSegmentationTerminology(userSettings):
+ """
+ author: Rafael Arenhart
+ modified by: Gabriel Muller
+ commit 6bed3b0767556af4087da48d8935f550b72e4cf2
+ * PL-1385 Fix selecting "Pores" terminology
+ """
+ terminology = [
+ ("Segmentation category and type - DICOM master list",),
+ ("SCT", "85756011", "Other"),
+ ("SCT", "5000", "Default Terminology"),
+ ("", "", ""),
+ ("Anatomic codes - DICOM master list",),
+ ("", "", ""),
+ ("", "", ""),
+ ]
+
+ terminology_string = "~".join(["^".join(i) for i in terminology]) + "|"
+ userSettings.setValue("Segmentations/DefaultTerminologyEntry", terminology_string)
+
+
+def setExtentionManagerOff():
+ slicer.app.revisionUserSettings().setValue("Extensions/ManagerEnabled", False)
+ window = slicer.util.mainWindow()
+ window.findChild(qt.QMenu, "ViewMenu").actions()[5].visible = False
+ # window.findChild(qt.QToolBar, "DialogToolBar").actions()[1].visible = False
+
+
+def setStatusBar():
+ statusBar = slicer.util.mainWindow().statusBar()
+ statusBar.setVisible(True)
+
+ statusBar.findChild(qt.QToolButton).setVisible(False)
+ statusBar.setFixedHeight(20)
+
+ progressBar = GlobalProgressBar.instance()
+ progressBar.setObjectName("GlobalProgressBar")
+
+ statusBar.addPermanentWidget(progressBar)
+
+ ctx = getAppContext()
+ ctx.memoryUsageWidget = MemoryUsageWidget()
+ statusBar.addPermanentWidget(ctx.memoryUsageWidget)
+ ctx.memoryUsageWidget.start()
+
+
+def setColorNodeName():
+ colorNode = slicer.util.getNode("GenericAnatomyColors")
+ colorNode.SetName("GenericColors")
+
+
+def setOrientationNames():
+ """
+ created by: Giulio Simão
+ commit 14c032131ee0affb7ae2aa806250bae8d59bb575
+ * PL-1387 Swapped strings, created function to change orientation names
+ """
+ sliceNodes = slicer.util.getNodesByClass("vtkMRMLSliceNode")
+ sliceNodes.append(slicer.mrmlScene.GetDefaultNodeByClass("vtkMRMLSliceNode"))
+ for sliceNode in sliceNodes:
+ axialSliceToRas = sliceNode.GetSliceOrientationPreset("Axial")
+ sliceNode.RemoveSliceOrientationPreset("Axial")
+ sliceNode.AddSliceOrientationPreset("XY", axialSliceToRas)
+
+ sagittalSliceToRas = sliceNode.GetSliceOrientationPreset("Sagittal")
+ sliceNode.RemoveSliceOrientationPreset("Sagittal")
+ sliceNode.AddSliceOrientationPreset("YZ", sagittalSliceToRas)
+
+ coronalSliceToRas = sliceNode.GetSliceOrientationPreset("Coronal")
+ sliceNode.RemoveSliceOrientationPreset("Coronal")
+ sliceNode.AddSliceOrientationPreset("XZ", coronalSliceToRas)
+
+
+def showHideAllVolumes(self, button):
+ from Multicore import MulticoreLogic
+
+ buttonText = button.text
+ buttonToolTip = button.toolTip
+ if "Show" in buttonText:
+ button.setText(buttonText.replace("Show", "Hide"))
+ button.setToolTip(buttonToolTip.replace("Show", "Hide"))
+ show = True
+ else:
+ button.setText(buttonText.replace("Hide", "Show"))
+ button.setToolTip(buttonToolTip.replace("Hide", "Show"))
+ show = False
+
+ viewNode = slicer.app.layoutManager().threeDWidget(0).mrmlViewNode()
+ volumeRenderingLogic = slicer.modules.volumerendering.logic()
+ multicoreLogic = MulticoreLogic()
+ volumes = multicoreLogic.getVolumes()
+ for volume in volumes:
+ renderingNode = volumeRenderingLogic.GetVolumeRenderingDisplayNodeForViewNode(volume, viewNode)
+ if renderingNode is None:
+ renderingNode = volumeRenderingLogic.CreateDefaultVolumeRenderingNodes(volume)
+ renderingNode.SetVisibility(show)
+
+
+def linkAllVolumesRenderingDisplayProperties(volume_rendering_module):
+ from Multicore import MulticoreLogic
+
+ volumeNodeComboBox = volume_rendering_module.findChild(qt.QObject, "VolumeNodeComboBox")
+ currentNode = volumeNodeComboBox.currentNode()
+ volumeRenderingLogic = slicer.modules.volumerendering.logic()
+ currentDisplayNode = volumeRenderingLogic.CreateDefaultVolumeRenderingNodes(currentNode)
+ volumeRenderingLogic = slicer.modules.volumerendering.logic()
+ multicoreLogic = MulticoreLogic()
+ volumes = multicoreLogic.getVolumes()
+ for volume in volumes:
+ displayNode = volumeRenderingLogic.CreateDefaultVolumeRenderingNodes(volume)
+ displayNode.SetAndObserveVolumePropertyNodeID(currentDisplayNode.GetVolumePropertyNodeID())
+
+
+def onPresetComboBoxClicked(object, event):
+ if type(event) == qt.QMouseEvent:
+ if event.type() == qt.QEvent.MouseButtonPress:
+ if event.button() == qt.Qt.LeftButton:
+ volumeRenderingModule = slicer.modules.volumerendering.widgetRepresentation()
+ synchronizeScalarDisplayNodeButton = volumeRenderingModule.findChild(
+ ctk.ctkCheckablePushButton, "SynchronizeScalarDisplayNodeButton"
+ )
+ if synchronizeScalarDisplayNodeButton.checked:
+ # Expanding advanced collapsible button
+ advancedCollapsibleButton = volumeRenderingModule.findChild(
+ ctk.ctkCollapsibleButton, "AdvancedCollapsibleButton"
+ )
+ advancedCollapsibleButton.collapsed = False
+
+ # Setting current tab to "Volume properties"
+ tabBar = volumeRenderingModule.findChild(qt.QTabBar, "qt_tabwidget_tabbar")
+ tabBar.setCurrentIndex(1)
+
+ # Unchecking Synchronize with Volumes module button
+ synchronizeScalarDisplayNodeButton.setChecked(False)
+
+ # Setting the style of the button to alert the user about the checked state change
+ synchronizeScalarDisplayNodeButton = volumeRenderingModule.findChild(
+ ctk.ctkCheckablePushButton, "SynchronizeScalarDisplayNodeButton"
+ )
+ synchronizeScalarDisplayNodeButton.setStyleSheet("color: red;")
+
+ # Resetting the style after a short time
+ qt.QTimer.singleShot(3000, lambda: synchronizeScalarDisplayNodeButton.setStyleSheet(None))
+
+
+def setVolumeRenderingModule():
+ volumeRenderingModule = slicer.modules.volumerendering.widgetRepresentation()
+ qSlicerIconComboBox = volumeRenderingModule.findChild(qt.QObject, "PresetComboBox").children()[2].children()[-1]
+ qSlicerIconComboBox.setItemText(0, "CT Carbonate")
+ qSlicerIconComboBox.setItemIcon(0, qt.QIcon(ICON_DIR / "IconSet-samples" / "Carbonate-CT.png"))
+ qSlicerIconComboBox.setItemText(1, "CT Sandstone")
+ qSlicerIconComboBox.setItemIcon(1, qt.QIcon(ICON_DIR / "IconSet-samples" / "Sandstone-CT.png"))
+ qSlicerIconComboBox.setItemText(2, "Grains")
+ qSlicerIconComboBox.setItemIcon(2, qt.QIcon(ICON_DIR / "IconSet-samples" / "small_Grains.png"))
+ qSlicerIconComboBox.setItemText(3, "Pores")
+ qSlicerIconComboBox.setItemIcon(3, qt.QIcon(ICON_DIR / "IconSet-samples" / "small_Pores.png"))
+ qSlicerIconComboBox.setItemText(4, "microCT")
+ qSlicerIconComboBox.setItemIcon(4, qt.QIcon(ICON_DIR / "IconSet-samples" / "small_mCT.png"))
+
+ # Link all volumes display properties
+ layout = volumeRenderingModule.findChild(qt.QObject, "DisplayCollapsibleButton").children()[0]
+ button = qt.QPushButton("Link all volumes")
+ button.setToolTip("Link all volumes rendering display properties")
+ button.setFixedHeight(40)
+ button.clicked.disconnect()
+ button.clicked.connect(lambda: linkAllVolumesRenderingDisplayProperties(volumeRenderingModule))
+ layout.addWidget(button)
+
+ # Show/hide all volumes
+ layout = volumeRenderingModule.findChild(qt.QObject, "DisplayCollapsibleButton").children()[0]
+ button = qt.QPushButton("Show all volumes")
+ button.setToolTip("Show all volumes on 3D scene")
+ button.setFixedHeight(40)
+ button.clicked.disconnect()
+ button.clicked.connect(lambda: showHideAllVolumes(button))
+ layout.addWidget(button)
+
+ # Blocks commas from the X spin boxes
+ advancedPropertyWidget = (
+ volumeRenderingModule.findChild(ctk.ctkCollapsibleButton, "AdvancedCollapsibleButton")
+ .findChild(qt.QTabWidget, "AdvancedTabWidget")
+ .findChild(qt.QStackedWidget, "qt_tabwidget_stackedwidget")
+ .findChild(qt.QWidget, "VolumePropertyTab")
+ .findChild(slicer.qMRMLVolumePropertyNodeWidget, "VolumePropertyNodeWidget")
+ .findChild(ctk.ctkVTKVolumePropertyWidget, "VolumePropertyWidget")
+ )
+ scalarOpacityXSpinBox = (
+ advancedPropertyWidget.findChild(ctk.ctkCollapsibleGroupBox, "ScalarOpacityGroupBox")
+ .findChild(ctk.ctkVTKScalarsToColorsWidget, "ScalarOpacityWidget")
+ .findChild(qt.QDoubleSpinBox, "XSpinBox")
+ )
+ scalarColorXSpinBox = (
+ advancedPropertyWidget.findChild(ctk.ctkCollapsibleGroupBox, "ScalarColorGroupBox")
+ .findChild(ctk.ctkVTKScalarsToColorsWidget, "ScalarColorWidget")
+ .findChild(qt.QDoubleSpinBox, "XSpinBox")
+ )
+ gradientXSpinBox = (
+ advancedPropertyWidget.findChild(ctk.ctkCollapsibleGroupBox, "GradientGroupBox")
+ .findChild(ctk.ctkVTKScalarsToColorsWidget, "GradientWidget")
+ .findChild(qt.QDoubleSpinBox, "XSpinBox")
+ )
+ locale = qt.QLocale()
+ locale.setNumberOptions(qt.QLocale.RejectGroupSeparator)
+ opacityValidator = qt.QDoubleValidator(scalarOpacityXSpinBox.findChild(qt.QLineEdit))
+ opacityValidator.setLocale(locale)
+ scalarOpacityXSpinBox.findChild(qt.QLineEdit).setValidator(opacityValidator)
+ colorValidator = qt.QDoubleValidator(scalarColorXSpinBox.findChild(qt.QLineEdit))
+ scalarColorXSpinBox.findChild(qt.QLineEdit).setValidator(colorValidator)
+ gradientValidator = qt.QDoubleValidator(gradientXSpinBox.findChild(qt.QLineEdit))
+ gradientXSpinBox.findChild(qt.QLineEdit).setValidator(gradientValidator)
+
+ # Detect clicking over the preset combo box to check for Synchronize with volumes button state, and act over
+ presetComboBox = volumeRenderingModule.findChild(qt.QObject, "PresetComboBox")
+ qSlicerIconComboBox = presetComboBox.children()[2].children()[-1]
+ CustomEventFilter(onPresetComboBoxClicked, qSlicerIconComboBox).install() # TODO generalize
+
+
+def setPyQtGraph():
+ import pyqtgraph as pg
+
+ pg.setConfigOption("background", "w")
+ pg.setConfigOption("foreground", "k")
+ pg.setConfigOptions(antialias=True)
+
+ def warning_wrap(func):
+ def wrapped(*args, **kwargs):
+ logging.warning(
+ "WARNING: setting up pyqtgraph configurations globally may affect other modules.\n"
+ "Customizer.setup_pyqtgraph_config() configures pyqtgraph during initialization."
+ )
+ return func(*args, **kwargs)
+
+ return wrapped
+
+ pg.setConfigOption = warning_wrap(pg.setConfigOption)
+ pg.setConfigOptions = warning_wrap(pg.setConfigOptions)
+
+
+# TODO relocate
+def on_html_link_clicked(self, url):
+ if not url.scheme():
+ qt.QDesktopServices.openUrl(qt.QUrl("file:///" + slicer.app.applicationDirPath() + "/../" + str(url)))
+ else:
+ qt.QDesktopServices.openUrl(url)
+
+
+def setHelpModule():
+ main_window = slicer.util.mainWindow()
+ module_panel = main_window.findChild(slicer.qSlicerModulePanel, "ModulePanel")
+ help_label = module_panel.findChild(ctk.ctkFittedTextBrowser, "HelpLabel")
+ help_label.setOpenLinks(False)
+ help_label.anchorClicked.connect(on_html_link_clicked)
+
+
+def loadEffects(modules):
+ effects = [
+ "BoundaryRemovalEffect",
+ "ColorThresholdEffect",
+ "ConnectivityEffect",
+ "CustomizedSmoothingEffect",
+ "DepthRangeSegmenterEffect",
+ "ExpandSegmentsEffect",
+ "MaskVolumeEffect",
+ "MultiThresholdEffect",
+ "SampleSegmentationEffect",
+ "SmartForegroundEffect",
+ ]
+
+ loadModules([modules[effect] for effect in effects if effect in modules], permanent=True, favorite=False)
+
+
+def registerEffects():
+ """
+ added by: Gabriel Muller
+ commit 7930e1b6f6c8d8b1fc473670303bd10733c98979
+ PL-1761 Register effects before seg editor configuration
+ """
+
+ slicer.modules.BoundaryRemovalEffectInstance.registerEditorEffect()
+ slicer.modules.ColorThresholdEffectInstance.registerEditorEffect()
+ slicer.modules.ConnectivityEffectInstance.registerEditorEffect()
+ slicer.modules.CustomizedSmoothingEffectInstance.registerEditorEffect()
+ slicer.modules.DepthRangeSegmenterEffectInstance.registerEditorEffect()
+ slicer.modules.ExpandSegmentsEffectInstance.registerEditorEffect()
+ slicer.modules.MaskVolumeEffectInstance.registerEditorEffect()
+ slicer.modules.MultiThresholdEffectInstance.registerEditorEffect()
+ slicer.modules.SampleSegmentationEffectInstance.registerEditorEffect()
+ slicer.modules.SmartForegroundEffectInstance.registerEditorEffect()
+
+
+def loadFoundations(modules):
+
+ corePlugins = [
+ "AppContext",
+ "SideBySideLayoutView",
+ "CustomizedData",
+ "Export",
+ "JobMonitor",
+ "RemoteService",
+ # "BIAEPBrowser",
+ # "OpenRockData",
+ "NetCDFLoader",
+ "NetCDFExport",
+ "NetCDF",
+ "VolumeCalculator",
+ "CustomizedTables",
+ "TableFilter",
+ "Charts",
+ "CustomizedSegmentEditor",
+ "SegmentationEnv",
+ ]
+
+ if slicer_is_in_developer_mode():
+ corePlugins.append("TestsModule")
+
+ coreModules = [modules[m] for m in corePlugins]
+
+ loadModules(coreModules, permanent=True, favorite=False)
+
+ cliModules = [modules[m] for m in modules if m.endswith("CLI")]
+
+ loadModules(cliModules, permanent=True, favorite=False)
+
+
+def tryPetrobrasPlugins():
+ try:
+ if not slicer_is_in_developer_mode():
+ from ltrace.slicer.helpers import install_git_module
+
+ install_git_module("https://git.ep.petrobras.com.br/DRP/geoslicer_plugins.git")
+ except Exception as e:
+ logging.warning("Petrobras GeoSlicer plugins not installed. Cause: " + str(e))
+
+
+def setGPUStatus():
+ if os.name == "nt":
+ try:
+ from win32.win32api import GetFileVersionInfo, LOWORD, HIWORD
+
+ def get_version_number(filename):
+ info = GetFileVersionInfo(filename, "\\")
+ ms = info["FileVersionMS"]
+ ls = info["FileVersionLS"]
+ return HIWORD(ms), LOWORD(ms), HIWORD(ls), LOWORD(ls)
+
+ version = get_version_number(r"C:\Windows\System32\nvcuda.dll")
+ if version[2] == 14 and version[3] < 5239 or version[2] < 14:
+ raise Exception(
+ "Driver version not supported.\n Your system has version "
+ + str(version)
+ + ", but tensorflow needs version >= x.x.14.5239 (452.39)"
+ )
+ except Exception as e:
+ qt.QSettings().setValue("TensorFlow/GPUEnabled", str(False))
+ logging.warning(
+ "Tensorflow GPU support disabled, please check your driver and make sure your system has a recent NVDIA GPU.\n"
+ + str(e)
+ )
+ return
+ qt.QSettings().setValue("TensorFlow/GPUEnabled", str(True))
+
+
+# # TODO usar isso na hora da busca (os modulos)
+# def setAllowedModules():
+# slicer_basic_modules = [
+# "Annotations",
+# "Data",
+# "Markups",
+# "Models",
+# "SceneViews",
+# "SegmentEditor",
+# "Segmentations",
+# "SubjectHierarchy",
+# "ViewControllers",
+# "VolumeRendering",
+# "Volumes",
+# ]
+#
+# slicer_module_whitelist = [
+# "Tables",
+# "CropVolume",
+# "SegmentMesher",
+# "RawImageGuess",
+# "LandmarkRegistration",
+# "GradientAnisotropicDiffusion",
+# "CurvatureAnisotropicDiffusion",
+# "GaussianBlurImageFilter",
+# "MedianImageFilter",
+# "VectorToScalarVolume",
+# "ScreenCapture",
+# "SimpleFilters",
+# "SegmentStatistics",
+# "MONAILabel",
+# "MONAILabelReviewer",
+# ]
+#
+# ltrace_module_whitelist = VISIBLE_LTRACE_PLUGINS
+# ltrace_module_whitelist.extend(slicer_basic_modules)
+# ltrace_module_whitelist.extend(slicer_module_whitelist)
+#
+# category_use_count = defaultdict(lambda: 0)
+# module_selector = slicer.util.mainWindow().moduleSelector().modulesMenu()
+#
+# for module_name in dir(slicer.moduleNames):
+# if module_name.startswith("_"):
+# # python attribute
+# continue
+#
+# module = getattr(slicer.modules, module_name.lower(), None)
+# if module is None:
+# # Module is disabled
+# continue
+#
+# if module_name not in slicer_basic_modules:
+# # ignore basic modules to avoid overcrowding menu
+# for category in module.categories:
+# category_use_count[category] += 1
+# # parent category must be counted also
+# category_split = category.split(".")
+# if len(category_split) > 1:
+# category_use_count[category_split[0]] += 1
+#
+# if module.name not in ltrace_module_whitelist:
+# module_selector.removeModule(module)
+# for category in module.categories:
+# category_use_count[category] -= 1
+# # parent category must be counted also
+# category_split = category.split(".")
+# if len(category_split) > 1:
+# category_use_count[category_split[0]] -= 1
+#
+# for category, count in category_use_count.items():
+# if count == 0:
+# module_selector.removeCategory(category)
+
+
+def extraChangesOnMenu():
+ # disable Ruler and change name of Line to Ruler
+ sn = slicer.mrmlScene.GetNodeByID("vtkMRMLSelectionNodeSingleton")
+ sn.RemovePlaceNodeClassNameFromList("vtkMRMLAnnotationRulerNode")
+ cname = "vtkMRMLMarkupsLineNode"
+ resource = ":/Icons/AnnotationDistanceWithArrow.png"
+ iconName = "Ruler"
+ sn.AddNewPlaceNodeClassNameToList(cname, resource, iconName)
+
+
+def onImageLogViewSelected(self):
+ widget = slicer.util.getModuleWidget("ImageLogEnv")
+ if widget is None:
+ logging.critical("ImageLogData module was not found!")
+ return
+
+ widget.imageLogDataWidget.self().logic.changeToLayout()
+
+
+def setLayoutViews():
+
+ slicer.modules.SideBySideLayoutViewInstance.sideBySideImageLayout()
+ slicer.modules.SideBySideLayoutViewInstance.sideBySideSegmentationLayout()
+
+ toolbar = APP_TOOLBARS["ViewToolBar"]
+ layoutAction = toolbar.actions()[0]
+ layoutButton = toolbar.widgetForAction(layoutAction)
+ layoutMenu = layoutButton.menu()
+
+ moved_itens_indexes = [1, 2, 4, 5, 6, 7, 8, 10, 11, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28]
+ moved_itens = []
+ for i in moved_itens_indexes:
+ moved_itens.append(layoutMenu.actions()[i])
+
+ layoutMenu.insertSeparator(layoutMenu.actions()[11])
+ layoutMenu.insertSeparator(layoutMenu.actions()[16])
+ layoutMenu.addSeparator()
+
+ advancedMenu = qt.QMenu("More", slicer.util.mainWindow())
+
+ for i in moved_itens:
+ advancedMenu.addAction(i)
+ layoutMenu.removeAction(i)
+
+ layoutMenu.addMenu(advancedMenu)
+ layoutMenu.setStyleSheet("QMenu::separator { height: 1px; background: gray; }")
+
+ # # Slicer bug. It always start on this item, which is not selectable (it is a submenu)
+ # if layoutAction.text == "Three over three Quantitative":
+ # default_action = layoutMenu.actions()[0]
+ # default_action.trigger()
+
+ # layoutButton.setDefaultAction(layoutAction)
+
+ # 0"Conventional"
+ # 1"Conventional Widescreen"
+ # 2"Conventional Plot"
+ # 3"Four-Up"
+ # 4"Four-Up Table"
+ # 5"Four-Up Plot"
+ # 6"Four-Up Quantitative"
+ # 7"Dual 3D"
+ # 8"Triple 3D"
+ # 9"3D only"
+ # 10"3D Table"
+ # 11"Plot only"
+ # 12"Red slice only"
+ # 13"Yellow slice only"
+ # 14"Green slice only"
+ # 15"Tabbed 3D"
+ # 16"Tabbed slice"
+ # 17"Compare"
+ # 18"Compare Widescreen"
+ # 19"Compare Grid"
+ # 20"Three over three"
+ # 21"Three over three Plot"
+ # 22"Four over four"
+ # 23"Two over two"
+ # 24"Side by side"
+ # 25"Four by three slice"
+ # 26"Four by two slice"
+ # 27"Three by three slice"
+ # 28"Red slice and 3D"
+ # 29"Yellow slice and 3D"
+ # 30"Green slice and 3D"
+ # 31"Side by side"
+ # 32"Side by side segmentation"
+
+ # Add ImageLog view as option
+ # imageLogLayoutViewAction = qt.QAction(qt.QIcon(self.IMAGELOG_ICON_PATH), "ImageLog View", toolbar)
+ # imageLogLayoutViewAction.triggered.connect(onImageLogViewSelected)
+ #
+ # after3dOnlyAction = layoutMenu.actions()[3]
+ # layoutMenu.insertAction(after3dOnlyAction, imageLogLayoutViewAction)
+
+ # layoutMenu.triggered.connect(lambda action: self.__layout_menu.setActiveAction(action))
+
+
+def setPaths():
+ revision_settings = slicer.app.revisionUserSettings()
+
+ revision_settings.endArray()
+ Path(slicer.app.toSlicerHomeAbsolutePath("LTrace/saved_scenes")).mkdir(parents=True, exist_ok=True)
+ slicer.app.defaultScenePath = slicer.app.toSlicerHomeAbsolutePath("LTrace/saved_scenes")
+ Path(slicer.app.toSlicerHomeAbsolutePath("LTrace/temp")).mkdir(parents=True, exist_ok=True)
+ slicer.app.temporaryPath = slicer.app.toSlicerHomeAbsolutePath("LTrace/temp")
+
+
+def expandSceneFolder():
+ folder_tree = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
+ scene_id = folder_tree.GetSceneItemID()
+ dataWidget = slicer.modules.data.widgetRepresentation()
+ shTreeView = slicer.util.findChild(dataWidget, name="SubjectHierarchyTreeView")
+ shTreeView.expandItem(scene_id)
+
+
+def removeDataStore():
+ slicer.util.mainWindow().moduleSelector().modulesMenu().removeModule("DataStore")
+
+
+def showSearchPopup():
+ slicer.modules.AppContextInstance.fuzzySearch.exec_()
+
+
+def setupShortcuts():
+
+ mainWindow = getAppContext().mainWindow
+ # # find buttons
+ # widget = slicer.modules.SegmentEditorWidget.editor.findChild("QWidget", "EffectsGroupBox")
+ # def addEffectShortcut(name, keysequence ):
+ # but = widget.findChild( 'QToolButton', name )
+ # shortcut = qt.QShortcut( mainWindow() ) # ^TODO: use SegmentEditorWidget for focused shortcut, does thtat work?
+ # shortcut.setKey( keysequence )
+ # shortcut.connect('activated()', lambda: but.click())
+ # addEffectShortcut("NULL", qt.QKeySequence( 'Ctrl+Q'))
+ # addEffectShortcut("Erase", qt.QKeySequence('Shift+F4'))
+ # addEffectShortcut("Paint", qt.QKeySequence( 'Shift+F5'))
+ # addEffectShortcut("Scissors", qt.QKeySequence( 'Shift+F6'))
+ # add shortcut for brush size
+ qt.QShortcut(qt.QKeySequence("Ctrl+Shift+o"), mainWindow).connect(
+ "activated()", lambda: showDataLoaders(APP_TOOLBARS["ModuleToolBar"])
+ )
+
+ mtoolbar = mainWindow.findChild(slicer.qSlicerModuleSelectorToolBar)
+
+ for toolbutton in mtoolbar.findChildren(qt.QToolButton):
+ for a in toolbutton.actions():
+ if a.objectName == "ViewFindModuleAction":
+ a.setShortcut(qt.QKeySequence("Ctrl+Shift+F"))
+
+ def showSearchPopup():
+ slicer.modules.AppContextInstance.fuzzySearch.exec_()
+
+ qt.QShortcut(qt.QKeySequence("Ctrl+F"), mainWindow).connect("activated()", showSearchPopup)
+
+ def resetMouseMode():
+ """Reset the mode to 'View' in MouseModeToolBar."""
+ mouseModeToolBar = APP_TOOLBARS.get("MouseModeToolBar")
+ if not mouseModeToolBar:
+ return
+
+ for action in mouseModeToolBar.actions():
+ if action.text == "View":
+ action.trigger()
+ else:
+ action.setChecked(False)
+
+ qt.QShortcut(qt.QKeySequence("Escape"), mainWindow).connect("activated()", resetMouseMode)
+
+
+def updateModuleSelectorIcons() -> None:
+ createIcon = lambda fileName: qt.QIcon((getResourcePath("Icons") / "IconSet-dark" / fileName).as_posix())
+ for child in slicer.util.moduleSelector().children():
+ if not hasattr(child, "text"):
+ continue
+
+ if child.text == "Next":
+ child.setIcon(createIcon("ArrowRight.svg"))
+ elif child.text == "Previous":
+ child.setIcon(createIcon("ArrowLeft.svg"))
+
+
+def disableThemeSelectorInSettings():
+ styleBox = slicer.app.settingsDialog().findChild(qt.QComboBox, "StyleComboBox")
+ styleBox.enabled = False
+ styleBox.setToolTip("Light Mode support coming soon")
+
+
+def configure(rebuild_index=False):
+ mainWindow = getAppContext().mainWindow
+ mainWindow.showMaximized()
+
+ setModulePanelVisible(False)
+ slicer.app.setRenderPaused(True)
+
+ if rebuild_index:
+ modules = createIndex()
+ else:
+ strData = slicer.app.revisionUserSettings().value(f"{APP_NAME}/LTraceModules", "")
+ modules = pickle.loads(strData.encode()) if strData else {}
+
+ registerEffects()
+
+ # Keep this ALWAYS after the loadModules call above
+ appContext = getAppContext()
+ appContext.modules.initCache(modules)
+
+ # Hide Data Store module
+ removeDataStore()
+
+ setPyQtGraph()
+ # setGPUStatus()
+ setColorNodeName()
+ setOrientationNames()
+ setVolumeInterpolation(False)
+
+ slicer.util.setModuleHelpSectionVisible(False)
+ slicer.util.setDataProbeVisible(True)
+ slicer.util.setModulePanelTitleVisible(True)
+ slicer.util.setModuleHelpSectionVisible(False)
+
+ setHelpModule()
+ setStatusBar()
+ setMenu()
+ setModuleSelectorToolBar()
+ setModulesToolBar()
+ setMainToolBar()
+ setMarkupToolBar()
+ setExtentionManagerOff()
+ setCustomCaptureToolBar()
+ setRightToolBar(True)
+ setDialogToolBar()
+ setDataProbeCollapsibleButton()
+ setCustomDataProbeInfo()
+ updateWindowTitle(getApplicationVersion())
+
+ # TODO move to function (lock moveble)
+ for toolbar in APP_TOOLBARS.values():
+ toolbar.setMovable(False)
+
+ customizeExportToFile()
+ setVolumeRenderingModule()
+ setLayoutViews()
+ extraChangesOnMenu()
+
+ slicer.modules.AppContextInstance.setupObservers()
+
+ customizeColorMaps()
+ customize3DView()
+
+ setModulePanel()
+ updateModuleSelectorIcons()
+ installFonts()
+
+ disableThemeSelectorInSettings()
+
+ setupShortcuts()
+ with open(getResourcePath("Styles") / "StyleSheet-dark.qss", "r") as style:
+ stylesheet = style.read()
+ stylesheet = Template(stylesheet).substitute(iconPath=getResourcePath("Icons/IconSet-widgets").as_posix())
+ slicer.app.styleSheet = stylesheet
+
+ # set default module: Data
+ slicer.util.selectModule("CustomizedData")
+
+ slicer.app.setRenderPaused(False)
+
+ setModulePanelVisible(True)
+
+ mainWindow.addDockWidget(qt.Qt.RightDockWidgetArea, getAppContext().rightDrawer.widget())
+
+ def _showDataLoaders():
+ showDataLoaders(APP_TOOLBARS["ModuleToolBar"])
+ ApplicationObservables().applicationLoadFinished.disconnect(_showDataLoaders)
+
+ ApplicationObservables().applicationLoadFinished.connect(_showDataLoaders)
+
+ expandSceneFolder()
+
+
+def createIndex():
+ with ProgressBarProc() as pb:
+ pb.setMessage("Indexing installed modules...")
+ pb.setProgress(0)
+
+ ltracePlugins = fetchModulesFrom(path=GEOSLICER_MODULES_DIR)
+
+ pb.setProgress(10)
+ pb.setMessage("Indexing installed commands...")
+
+ cliDir = GEOSLICER_MODULES_DIR.parent / "cli-modules"
+ if not cliDir.exists():
+ cliDir = GEOSLICER_MODULES_DIR
+
+ ltracePlugins.update(fetchModulesFrom(path=cliDir, depth=2))
+
+ pb.setProgress(20)
+ pb.setMessage("Saving indexing...")
+
+ # TODO how to control that externally
+ petroPlugins = fetchModulesFrom(path="https://git.ep.petrobras.com.br/DRP/geoslicer_plugins.git", depth=2)
+
+ allPlugins = {**ltracePlugins, **petroPlugins}
+
+ slicer.app.revisionUserSettings().setValue(f"{APP_NAME}/LTraceModules", pickle.dumps(allPlugins, 0).decode())
+
+ pb.setProgress(30)
+ pb.setMessage("Registering indexed effects...")
+
+ loadEffects(allPlugins)
+
+ pb.setProgress(70)
+ pb.setMessage("Registering indexed base modules...")
+
+ loadFoundations(allPlugins)
+
+ pb.setProgress(100)
+
+ return allPlugins
+
+
+def bootstrapped(userSettings):
+ revision = slicer.app.revisionUserSettings()
+ booted = toBool(revision.value(f"{APP_NAME}/Booted", False))
+ populated = len(revision.value(f"{APP_NAME}/LTraceModules", "")) > 0
+ conflicted = len(userSettings.value("Modules/FavoriteModules", [])) > 0
+ themeIsDark = userSettings.value("Styles/Style", "") == "Dark Slicer"
+
+ return not conflicted and booted and populated and themeIsDark
+
+
+def bootstrap(userSettings):
+ try:
+ slicer.app.userSettings().setValue("Styles/Style", "Dark Slicer")
+
+ setModulePanelVisible(False)
+ slicer.app.setRenderPaused(True)
+
+ if len(userSettings.value("Modules/FavoriteModules", [])) > 0:
+ msg = (
+ "This GeoSlicer version is not compatible with the previous one. We already fixed the configuration for you"
+ " but we need to restart GeoSlicer for the changes to take effect."
+ )
+ else:
+ msg = "Congratulations! GeoSlicer has been configured. We just need to restart GeoSlicer to complete the installation."
+
+ setPaths()
+
+ # userSettings.setValue("Developer/DeveloperMode", "true")
+ userSettings.setValue("Modules/HomeModule", "Data")
+ userSettings.setValue("Python/ConsoleLogLevel", "None")
+ userSettings.setValue(
+ "VolumeRendering/RenderingMethod",
+ "vtkMRMLGPURayCastVolumeRenderingDisplayNode",
+ )
+
+ userSettings.setValue("Modules/FavoriteModules", [])
+ setDefaultSegmentationTerminology(userSettings)
+
+ createIndex()
+
+ tryPetrobrasPlugins()
+
+ slicer.app.revisionUserSettings().setValue(f"{APP_NAME}/Booted", True)
+
+ userSettings.setValue("AppVersion", getApplicationVersion())
+
+ ui_InformUserAboutRestart(msg) # blocking dialog
+ slicer.util.restart()
+
+ except Exception as e:
+ import traceback
+
+ logging.error(f"Failed to bootstrap GeoSlicer:\n{traceback.format_exc()}")
+
+
+def init():
+ os.chdir(slicer.app.slicerHome)
+ userSettings = slicer.app.userSettings()
+
+ if not bootstrapped(userSettings):
+ bootstrap(userSettings)
+ else:
+ previousAppVersion = userSettings.value(f"{APP_NAME}/Version", "")
+ mustRebuildIndex = slicer_is_in_developer_mode() or (previousAppVersion != getApplicationVersion())
+ configure(rebuild_index=mustRebuildIndex)
+
+ if previousAppVersion != getApplicationVersion():
+ userSettings.setValue(f"{APP_NAME}/Version", getApplicationVersion())
+
+
+init()
diff --git a/tools/docker/windows.Dockerfile b/tools/docker/windows.Dockerfile
index 2af48e8..ea45f7e 100644
--- a/tools/docker/windows.Dockerfile
+++ b/tools/docker/windows.Dockerfile
@@ -26,6 +26,7 @@ RUN python -m pip install -r c:/slicerltrace/src/modules/MicrotomRemote/Libs/mic
COPY ./src/submodules/ c:/slicerltrace/src/submodules/
RUN python -m pip install -e c:/slicerltrace/src/submodules/porespy
RUN python -m pip install -e c:/slicerltrace/src/submodules/biaep
+RUN python -m pip install -e c:/slicerltrace/src/submodules/py_pore_flow
COPY ./tests/ c:/slicerltrace/tests/
diff --git a/tools/install_packages.sh b/tools/install_packages.sh
index ac5008f..8879713 100644
--- a/tools/install_packages.sh
+++ b/tools/install_packages.sh
@@ -8,12 +8,18 @@ python -m pip install -e "$repository_path"/src/ltrace
# Install Geoslicer modules package
python -m pip install -e "$repository_path"/src/modules
+# Install Microtom required libraries
+python -m pip install -r "$repository_path"/src/modules/MicrotomRemote/Libs/microtom/requirements.txt
+
# Install porespy package
python -m pip install -e "$repository_path"/src/submodules/porespy
# Install biaep package
python -m pip install -e "$repository_path"/src/submodules/biaep
+# Install py_pore_flow package
+python -m pip install -e "$repository_path"/src/submodules/py_pore_flow
+
# Install tools package
python -m pip install -e "$repository_path"/tools
diff --git a/tools/jenkins/release.Jenkinsfile b/tools/jenkins/release.Jenkinsfile
index 39f6aba..df7a4d6 100644
--- a/tools/jenkins/release.Jenkinsfile
+++ b/tools/jenkins/release.Jenkinsfile
@@ -8,6 +8,7 @@ pipeline {
booleanParam(name: 'SFX', defaultValue: false, description: 'Create Self-Extracting File instead of the compressed file.')
booleanParam(name: 'PUBLIC_VERSION', defaultValue: false, description: 'Generate the application\'s public version.')
booleanParam(name: 'NO_PUBLIC_COMMIT', defaultValue: true, description: 'Avoid commiting to the opensource code repository. (only valid when enabling the public version generation parameter)')
+ booleanParam(name: 'MESA3D_DRIVER', defaultValue: false, description: 'Install the Mesa3D Driver (no-GPU support).')
choice(name: 'PLATFORM', choices: ['Linux', 'Windows'], description: 'Select the desired Operational System to generate the application.')
string(name: 'BASE', defaultValue: 'latest', description: 'Filename of the base archive or version number. "latest" will download the latest version from the OCI bucket.')
}
@@ -35,7 +36,7 @@ pipeline {
label "${PLATFORM}".toLowerCase()
}
options {
- timeout(time: 10, unit: "MINUTES")
+ timeout(time: 20, unit: "MINUTES")
skipDefaultCheckout()
}
steps {
@@ -85,16 +86,17 @@ pipeline {
steps {
script {
def git_tag = util.get_git_tag()
- def no_test_flag = ("${params.EXPORT}" == "false") ? "--no-export" : ""
- def no_export_flag = ("${params.TEST}" == "false") ? "--no-test" : ""
+ def no_export_flag = ("${params.EXPORT}" == "false") ? "--no-export --disable-archiving" : ""
+ def no_test_flag = ("${params.TEST}" == "false") ? "--no-test" : ""
def sfx_flag = ("${params.SFX}" == "true") ? "--sfx" : ""
def public_flag = ("${params.PUBLIC_VERSION}" == "true") ? "--generate-public-version" : ""
def no_public_commit_flag = ("${params.NO_PUBLIC_COMMIT}" == "true") ? "--no-public-commit" : ""
+ def no_gpu = ("${params.MESA3D_DRIVER}" == "true") ? "--no-gpu" : ""
def base = "${params.BASE}"
if (base.trim().isEmpty()) {
error("Base is empty. Please, set the base parameter.")
}
- def arguments = "--version ${git_tag} --no-gpu ${no_test_flag} ${no_export_flag} ${sfx_flag} ${public_flag} ${no_public_commit_flag} --base ${base} --production".trim()
+ def arguments = "--version ${git_tag} ${no_gpu} ${no_test_flag} ${no_export_flag} ${sfx_flag} ${public_flag} ${no_public_commit_flag} --base ${base} --production".trim()
util.download_and_deploy(arguments, false)
}
}
@@ -142,16 +144,17 @@ pipeline {
steps {
script {
def git_tag = util.get_git_tag()
- def no_test_flag = ("${params.EXPORT}" == "false") ? "--no-export" : ""
- def no_export_flag = ("${params.TEST}" == "false") ? "--no-test" : ""
+ def no_export_flag = ("${params.EXPORT}" == "false") ? "--no-export --disable-archiving" : ""
+ def no_test_flag = ("${params.TEST}" == "false") ? "--no-test" : ""
def sfx_flag = ("${params.SFX}" == "true") ? "--sfx" : ""
def public_flag = ("${params.PUBLIC_VERSION}" == "true") ? "--generate-public-version" : ""
def no_public_commit_flag = ("${params.NO_PUBLIC_COMMIT}" == "true") ? "--no-public-commit" : ""
+ def no_gpu = ("${params.MESA3D_DRIVER}" == "true") ? "--no-gpu" : ""
def base = "${params.BASE}"
if (base.trim().isEmpty()) {
error("Base is empty. Please, set the base parameter.")
}
- def arguments = "--version ${git_tag} --no-gpu ${no_test_flag} ${no_export_flag} ${sfx_flag} ${public_flag} ${no_public_commit_flag} --base ${base} --production".trim()
+ def arguments = "--version ${git_tag} ${no_gpu} ${no_test_flag} ${no_export_flag} ${sfx_flag} ${public_flag} ${no_public_commit_flag} --base ${base} --production".trim()
util.download_and_deploy(arguments, true)
}
}
diff --git a/tools/jenkins/testing.Jenkinsfile b/tools/jenkins/testing.Jenkinsfile
index e22f200..1cdeda7 100644
--- a/tools/jenkins/testing.Jenkinsfile
+++ b/tools/jenkins/testing.Jenkinsfile
@@ -9,6 +9,7 @@ pipeline {
booleanParam(name: 'EXPORT', defaultValue: false, description: 'Allow to export the generated application to the OCI bucket')
booleanParam(name: 'SFX', defaultValue: false, description: 'Create Self-Extracting File instead of the compressed file')
booleanParam(name: 'PUBLIC_VERSION', defaultValue: false, description: 'Generate the application\'s public version. The commit to the opensource code will be ignored.')
+ booleanParam(name: 'MESA3D_DRIVER', defaultValue: true, description: 'Install the Mesa3D Driver (no-GPU support).')
choice(name: 'PLATFORM', choices: ['Linux', 'Windows'], description: 'Select the desired Operational System to generate the application.')
string(name: 'BASE', defaultValue: 'latest', description: 'Filename of the base archive or version number. "latest" will download the latest version from the OCI bucket.')
choice(name: 'MODE', choices: ['Production', 'Development'], description: 'Select the desired application\'s mode to export')
@@ -129,17 +130,18 @@ pipeline {
steps {
script {
Random rnd = new Random()
- def version = "v${rnd.nextInt(9)}.${rnd.nextInt(9)}${rnd.nextInt(9)}"
- def no_test_flag = ("${params.EXPORT}" == "false") ? "--no-export" : ""
- def no_export_flag = ("${params.INTEGRATION_TESTS}" == "false") ? "--no-test" : ""
+ def version = "v${rnd.nextInt(9)}.${rnd.nextInt(9)}.${rnd.nextInt(9)}"
+ def no_export_flag = ("${params.EXPORT}" == "false") ? "--no-export --disable-archiving" : ""
+ def no_test_flag = ("${params.INTEGRATION_TESTS}" == "false") ? "--no-test" : ""
def sfx_flag = ("${params.SFX}" == "true") ? "--sfx" : ""
def prod_flag = ("${params.MODE}" == "Production") ? "--production" : ""
def public_flag = ("${params.PUBLIC_VERSION}" == "true") ? "--generate-public-version --no-public-commit" : ""
+ def no_gpu = ("${params.MESA3D_DRIVER}" == "true") ? "--no-gpu" : ""
def base = "${params.BASE}"
if (base.trim().isEmpty()) {
error("Base is empty. Please, set the base parameter.")
}
- def arguments = "--version ${version} --no-gpu ${no_test_flag} --base ${base} ${no_export_flag} ${sfx_flag} ${prod_flag} ${public_flag}"
+ def arguments = "--version ${version} ${no_gpu} ${no_test_flag} --base ${base} ${no_export_flag} ${sfx_flag} ${prod_flag} ${public_flag}"
util.download_and_deploy(arguments, false)
if (params.EXPORT == true) {
def file_name = util.get_exported_application_name(params.MODE, version)
@@ -254,16 +256,17 @@ pipeline {
script {
Random rnd = new Random()
def version = "v${rnd.nextInt(9)}.${rnd.nextInt(9)}${rnd.nextInt(9)}"
- def no_test_flag = ("${params.EXPORT}" == "false") ? "--no-export" : ""
- def no_export_flag = ("${params.INTEGRATION_TESTS}" == "false") ? "--no-test" : ""
+ def no_export_flag = ("${params.EXPORT}" == "false") ? "--no-export --disable-archiving" : ""
+ def no_test_flag = ("${params.INTEGRATION_TESTS}" == "false") ? "--no-test" : ""
def sfx_flag = ("${params.SFX}" == "true") ? "--sfx" : ""
def prod_flag = ("${params.MODE}" == "Production") ? "--production" : ""
def public_flag = ("${params.PUBLIC_VERSION}" == "true") ? "--generate-public-version --no-public-commit" : ""
+ def no_gpu = ("${params.MESA3D_DRIVER}" == "true") ? "--no-gpu" : ""
def base = "${params.BASE}"
if (base.trim().isEmpty()) {
error("Base is empty. Please, set the base parameter.")
}
- def arguments = "--version ${version} --no-gpu ${no_test_flag} --base ${base} ${no_export_flag} ${sfx_flag} ${prod_flag} ${public_flag}"
+ def arguments = "--version ${version} ${no_gpu} ${no_test_flag} --base ${base} ${no_export_flag} ${sfx_flag} ${prod_flag} ${public_flag}"
util.download_and_deploy(arguments, true)
if (params.EXPORT == true) {
def file_name = util.get_exported_application_name(params.MODE, version)
diff --git a/tools/new_module.py b/tools/new_module.py
index 79c115d..9fcf9db 100644
--- a/tools/new_module.py
+++ b/tools/new_module.py
@@ -40,7 +40,7 @@ def get_modules_directory_path():
help="The module's title. Example: 'New Module'. Default to the same as the name input string.",
)
parser.add_argument(
- "-c", "--category", type=str, default="LTrace Tools", help="The module's category. Default to 'LTrace Tools'."
+ "-c", "--category", type=str, default="Tools", help="The module's category. Default to 'LTrace Tools'."
)
parser.add_argument("--cli", action="store_true", help="Add CLI related files", default=False)
diff --git a/tools/pipeline/check_dependencies_licenses.sh b/tools/pipeline/check_dependencies_licenses.sh
index 7da13a7..47529c5 100644
--- a/tools/pipeline/check_dependencies_licenses.sh
+++ b/tools/pipeline/check_dependencies_licenses.sh
@@ -1,5 +1,6 @@
#!/bin/bash
+excluded_modules="PNM_Report other"
repository_path=$( cd "$(dirname "$0")" ; pwd -P )\\..\\..
requirement_files=$(find ./src -iname "requirements.txt" -type f)
@@ -10,6 +11,9 @@ echo -e "\
for requirement_file in $requirement_files; do
module_name=$(basename $(dirname ${requirement_file}))
+ if [[ " ${excluded_modules} " =~ " ${module_name} " ]]; then
+ continue
+ fi
echo "Checking licenses for '${module_name}' dependencies"
echo -e "%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n"
python -m liccheck -r "${requirement_file}"
diff --git a/tools/pipeline/generate_test_deploy.py b/tools/pipeline/generate_test_deploy.py
index 0eade49..4d1ec8e 100644
--- a/tools/pipeline/generate_test_deploy.py
+++ b/tools/pipeline/generate_test_deploy.py
@@ -185,7 +185,6 @@ def generate_application(
"python",
deploy_script_path,
geoslicer_base_zip_file_path,
- "--with-porespy",
]
if production:
@@ -204,6 +203,9 @@ def generate_application(
if args.no_public_commit:
command.append("--no-public-commit")
+ if args.disable_archiving:
+ command.append("--disable-archiving")
+
logger.info("Running GeoSlicer deploy script...")
run_subprocess(command)
@@ -613,6 +615,7 @@ def make_generated_application_archive(args: argparse.Namespace, tag: str) -> Pa
parser.add_argument(
"--base",
help="Filename of base archive in the bucket's release base directory",
+ default="latest",
)
parser.add_argument(
"--production", action="store_true", help="Generate application in production mode to export.", default=False
@@ -635,7 +638,12 @@ def make_generated_application_archive(args: argparse.Namespace, tag: str) -> Pa
help="Avoid commiting to the opensource code repository.",
default=True,
)
-
+ parser.add_argument(
+ "--disable-archiving",
+ action="store_true",
+ help="Avoid the archiving step when generating a release build.",
+ default=False,
+ )
p_args = parser.parse_args()
if not p_args.version: