diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..abf652b
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,21 @@
+---
+name: Feature request
+about: Suggest new features
+title: ''
+labels: feature
+assignees: jgray-19
+
+---
+
+**Feature requested**
+What is DECTSim missing, and why would it be useful.
+
+**Area**
+Provided a short indication of where this feature would sit e.g. GUI-image display, simutlation-scatter_correction
+
+**Suggested behaviour**
+Detail of the proposed feature.
+
+**Additional context**
+Add any other context about the problem here.
+
diff --git a/.gitignore b/.gitignore
index d84b6eb..d713cb0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,14 @@
*.slx.r*
*.mdl.r*
+#Example binaries
+gui/PhantomExample*.mat
+gui/PhantomExample*.png
+gui/SourceExample*.mat
+
+# MATLAB Compiler output
+build/
+
# Derived content-obscured files
*.p
diff --git a/APPLICATION_LICENSE.txt b/APPLICATION_LICENSE.txt
new file mode 100644
index 0000000..0eb6eed
--- /dev/null
+++ b/APPLICATION_LICENSE.txt
@@ -0,0 +1,34 @@
+DECTSim Application License
+
+The DECTSim source code is separately available under the BSD 3-Clause License in the accompanying LICENSE file.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+MATHWORKS COMPONENTS
+
+DECTSim includes MATLAB Runtime and other components owned by The MathWorks, Inc. or its licensors. You may use those components only as part of DECTSim and only to run DECTSim. You may not:
+
+- use MATLAB Runtime separately from DECTSim
+- use it with another application
+- provide it as a service
+- redistribute it separately
+- claim any right to use or deploy MATLAB or any other MathWorks product.
+
+Use of MATLAB Runtime is also subject to the MATLAB Runtime License, which is incorporated into this licence by reference. It is available at: [MATLAB Runtime installation directory]\R2026a\matlabruntime_license_agreement.pdf and agreement to this is required during DECTSim install.
+
+You must not remove or alter any copyright, trademark, logo, proprietary-rights, disclaimer, or warning notice contained in DECTSim or its MathWorks components.
+
+DECTSim and its MathWorks components are provided “as is,” without warranties of any kind. No warranty is provided for MATLAB Runtime or any other MathWorks program. MathWorks, Inc. and its licensors are excluded from all liability for damages, remedies, claims, losses, or expenses arising from the use of DECTSim or any MathWorks component.
+
+
+
+
diff --git a/NOTICE.txt b/NOTICE.txt
new file mode 100644
index 0000000..ddbddd7
--- /dev/null
+++ b/NOTICE.txt
@@ -0,0 +1,19 @@
+DECTSim Notices
+===============
+
+DECTSim
+Copyright (c) 2023, Joshua Gray and Sofia Pearson.
+
+DECTSim source code is distributed under the BSD 3-Clause License.
+See the accompanying LICENSE file for the complete terms.
+
+MATLAB®. © 1984 - 2026 The MathWorks, Inc.
+
+The application was created using MATLAB Compiler and requires
+MATLAB Runtime.
+
+MATLAB and MATLAB Runtime are products of The MathWorks, Inc.
+
+Use of DECTSim and the MATLAB Runtime components is subject to
+the accompanying APPLICATION_LICENSE.txt and the applicable
+MATLAB Runtime License.
\ No newline at end of file
diff --git a/build_windows.m b/build_windows.m
new file mode 100644
index 0000000..74f2963
--- /dev/null
+++ b/build_windows.m
@@ -0,0 +1,126 @@
+function build_windows()
+%BUILD_WINDOWS Build and package DECTSim as a Windows standalone application.
+%
+% Requirements:
+% - Windows
+% - MATLAB R2026a or newer
+% - MATLAB Compiler
+% - Image Processing Toolbox
+
+ rootDir = fileparts(mfilename("fullpath"));
+ guiDir = fullfile(rootDir, "gui");
+ srcDir = fullfile(rootDir, "src");
+ buildRoot = fullfile(rootDir, "build");
+ applicationOutput = fullfile(buildRoot, "application");
+ installerOutput = fullfile(buildRoot, "installer");
+
+ assert(ispc, ...
+ "DECTSim:UnsupportedPlatform", ...
+ "A Windows executable must be built on Windows.");
+
+ assert(~isempty(which("compiler.build.standaloneWindowsApplication")), ...
+ "DECTSim:MissingCompiler", ...
+ ["MATLAB Compiler is not available. " ...
+ "Install and license MATLAB Compiler before building."]);
+
+ assert(~isempty(which("fan2para")) && ~isempty(which("iradon")), ...
+ "DECTSim:MissingImageProcessingToolbox", ...
+ ["Image Processing Toolbox is not available. " ...
+ "DECTSim requires it for reconstruction."]);
+
+ assert(isfile(fullfile(guiDir, "gui.m")), ...
+ "DECTSim:MissingGUI", ...
+ "Could not find gui/gui.m.");
+
+ assert(isfolder(srcDir), ...
+ "DECTSim:MissingSource", ...
+ "Could not find the src directory.");
+
+ % Make all DECTSim classes and functions visible during dependency analysis.
+ originalPath = path;
+ pathCleanup = onCleanup(@() path(originalPath));
+
+ addpath(guiDir);
+ addpath(genpath(srcDir));
+
+ requiredDataNames = [
+ "PhantomExample1.mat"
+ "PhantomExample1.png"
+ "PhantomExample2.mat"
+ "PhantomExample1.png"
+ "PhantomExample3.mat"
+ "PhantomExample1.png"
+ "PhantomExample4.mat"
+ "PhantomExample1.png"
+ "SourceExample40kvp.mat"
+ "SourceExample80kvp.mat"
+ ];
+
+ requiredDataFiles = fullfile(guiDir, requiredDataNames);
+
+ % Generate the bundled example objects when they do not yet exist.
+ if any(~isfile(requiredDataFiles))
+ fprintf("Generating missing DECTSim example data...\n");
+ run(fullfile(guiDir, "ExampleObjects.m"));
+ end
+
+ missingData = requiredDataFiles(~isfile(requiredDataFiles));
+
+ if ~isempty(missingData)
+ error( ...
+ "DECTSim:MissingExampleData", ...
+ "The following example files were not generated:\n%s", ...
+ strjoin(missingData, newline));
+ end
+
+ % Start each build with clean output directories.
+ if isfolder(buildRoot)
+ rmdir(buildRoot, "s");
+ end
+
+ mkdir(applicationOutput);
+ mkdir(installerOutput);
+
+ additionalApplicationFiles = [
+ fullfile(guiDir, "graphics")
+ fullfile(guiDir, "40kvp.spk")
+ fullfile(guiDir, "80kvp.spk")
+ requiredDataFiles(:)
+ srcDir
+ ];
+
+ fprintf("Building the standalone Windows application...\n");
+
+ buildResults = compiler.build.standaloneWindowsApplication( ...
+ fullfile(guiDir, "gui.m"), ...
+ "ExecutableName", "DECTSim", ...
+ "ExecutableVersion", "1.0.0.0", ...
+ "OutputDir", applicationOutput, ...
+ "AdditionalFiles", additionalApplicationFiles, ...
+ "AutoDetectDataFiles", "on", ...
+ "Verbose", "on");
+
+ fprintf("Creating the Windows installer...\n");
+
+ compiler.package.installer( ...
+ buildResults, ...
+ "ApplicationName", "DECTSim", ...
+ "InstallerName", "DECTSimInstaller", ...
+ "Version", "1.0.0", ...
+ "Summary", ...
+ "Dual-energy computed tomography simulation application.", ...
+ "OutputDir", installerOutput, ...
+ "RuntimeDelivery", "web", ...
+ "AdditionalFiles", [...
+ fullfile(rootDir, "LICENSE")
+ fullfile(rootDir, "NOTICE.txt")
+ fullfile(rootDir, "APPLICATION_LICENSE.txt")
+ ],"Verbose", "on");
+
+ fprintf("\nDECTSim build completed successfully.\n");
+ fprintf("Application output:\n %s\n", applicationOutput);
+ fprintf("Installer output:\n %s\n", installerOutput);
+
+ % Keep the onCleanup object alive until the function completes.
+ clear pathCleanup
+end
\ No newline at end of file
diff --git a/docs/source/dev_guide/save_phantom_preview.rst b/docs/source/dev_guide/save_phantom_preview.rst
new file mode 100644
index 0000000..581312c
--- /dev/null
+++ b/docs/source/dev_guide/save_phantom_preview.rst
@@ -0,0 +1,74 @@
+Save Phantom Preview
+=============
+
+This allows a preview image to be generated when you create a custom phantom. The preview image can then be displayed on the GUI when your phantom is selected.
+
+save_phantom_preview
+------------
+
+Purpose
+~~~~~~~
+
+The ``save_phantom_preview`` function generates a transparent PNG preview of a voxel phantom.
+
+The function selects the central slice in the z direction and uses :func:`voxel_array.get_object_idxs` to determine which voxel object occupies each position in the slice. The resulting object indices are converted to grayscale values.
+
+Voxels whose object index is equal to :attr:`voxel_array.nobj` represent the world material and are made fully transparent. All voxels belonging to objects within the phantom are made fully opaque.
+
+Arguments
+~~~~~~~~~
+
+.. attribute:: phantom
+ (:class:`voxel_array`) The voxel array from which the preview image is generated.
+
+.. attribute:: output_file
+ (:class:`string`) The path at which the preview image is saved. The output image is written in PNG format.
+
+Returns
+
+.. attribute:: preview
+ (:class:`double`) A two-dimensional grayscale image containing the central axial slice of the phantom. The values are normalised to the range ``[0, 1]``.
+
+.. attribute:: alpha
+ (:class:`double`) A two-dimensional alpha channel with the same dimensions as ``preview``. A value of ``0`` represents a fully transparent background pixel, while a value of ``1`` represents a fully opaque phantom pixel.
+
+Functions
+~~~~~~~~~
+
+.. function:: save_phantom_preview(phantom, output_file)
+
+ Generates and saves a transparent preview image from the central axial slice of a voxel phantom.
+
+ The number of voxels in each dimension is calculated from :attr:`voxel_array.num_planes`. Because ``num_planes`` contains the voxel boundary planes, the number of voxel cells is given by ``num_planes - 1``.
+
+ The function constructs the voxel indices for the central x-y slice and passes them to :func:`voxel_array.get_object_idxs`. This returns the index of the voxel object occupying each position.
+
+ The object indices are reshaped into a two-dimensional image and normalised to grayscale. Positions containing the world material, identified by :attr:`voxel_array.nobj`, are assigned an alpha value of ``0`` and are therefore transparent in the saved PNG.
+
+ :param phantom: The voxel array from which the preview is generated.
+ :type phantom: :class:`voxel_array`
+ :param output_file: The path at which the PNG preview is saved.
+ :type output_file: :class:`string`
+
+ :returns: A grayscale preview image and its corresponding alpha channel.
+ :rtype: tuple(:class:`double`, :class:`double`)
+
+
+Properties
+~~~~~~~~~~
+All of these properties are immutable, therefore they cannot be changed after the object is created.
+
+.. attribute:: is_in_object
+
+ (:class:`function`) A function that takes a set of points ``(x, y, z)`` and returns a list of logical values indicating if the point is inside the object or not.
+
+.. attribute:: material
+
+ (:class:`material_attenuation`) The material of the object.
+
+.. attribute:: get_mu
+ :noindex:
+
+ (:class:`function`) A function that takes an energy and returns the linear attenuation coefficient of the material at that energy.
+
+
diff --git a/docs/source/index.rst b/docs/source/index.rst
index e4460e9..5b52980 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -11,6 +11,7 @@ Welcome to DECTSim's documentation!
:caption: User Guide:
:glob:
+ user_guide/first_run.md
user_guide/gui.md
user_guide/first_sim.md
@@ -25,6 +26,7 @@ Welcome to DECTSim's documentation!
dev_guide/detector.rst
dev_guide/materials.rst
dev_guide/voxel_shapes.rst
+ dev_guide/save_phantom_preview.rst
dev_guide/voxel_array.rst
dev_guide/ray_tracing.rst
dev_guide/sensors.rst
diff --git a/docs/source/user_guide/first_run.md b/docs/source/user_guide/first_run.md
new file mode 100644
index 0000000..cd8f001
--- /dev/null
+++ b/docs/source/user_guide/first_run.md
@@ -0,0 +1,204 @@
+# DECTSim First-Time Setup
+
+DECTSim can be run in three ways:
+
+1. As an installed Windows application.
+2. From the source code in MATLAB Desktop.
+3. From the source code in MATLAB Online.
+
+---
+
+## 1. Windows application
+
+The Windows application does not require a MATLAB licence. It cannot be used to modify or debug the DECTSim source code, run exported MATLAB scripts, or add new MATLAB functions and classes. Users requiring these capabilities should run the source-code version in MATLAB Desktop or MATLAB Online.
+
+### Install
+
+
+1. Open the DECTSim **Releases** page on GitHub.
+2. Open the latest release.
+3. Under **Assets**, download:
+
+ `DECTSim-Windows-v1.0.0.zip`
+
+4. Do not download the file labelled **Source code**, as that contains
+ the MATLAB source rather than the Windows application.
+5. Extract the downloaded ZIP file.
+
+6. Run:
+
+ ```text
+ DECTSimInstaller.exe
+ ```
+
+7. Follow the installer instructions.
+
+8. Allow the installer to install MATLAB Runtime when prompted.
+
+### Run
+
+After installation, open **DECTSim** from the Windows Start menu or desktop shortcut.
+
+When the application opens, leave the settings at their defaults and click:
+
+```text
+Run
+```
+
+The simulation results will appear in the Results panel.
+
+You do not need to run the installer again after DECTSim has been installed.
+
+---
+
+## 2. MATLAB Desktop
+
+The source-code version requires MATLAB and Image Processing Toolbox.
+
+### Download the source code
+
+Clone or download the DECTSim repository.
+
+The main repository folder should contain:
+
+```text
+DECTSim.prj
+gui/
+src/
+```
+
+### Open the project
+
+1. Start MATLAB.
+2. Open the local DECTSim folder.
+3. Double-click:
+
+ ```text
+ DECTSim.prj
+ ```
+
+Alternatively, run:
+
+```matlab
+project = openProject("C:\path\to\DECTSim\DECTSim.prj");
+```
+
+Replace the example path with the location of your DECTSim folder.
+
+### Create the example files
+
+The following files are required by the application:
+
+```text
+gui/PhantomExample1.mat
+gui/PhantomExample2.mat
+gui/PhantomExample3.mat
+gui/PhantomExample4.mat
+gui/SourceExample40kvp.mat
+gui/SourceExample80kvp.mat
+```
+
+When these files are missing, run:
+
+```matlab
+root = string(project.RootFolder);
+
+addpath(fullfile(root, "gui"));
+addpath(genpath(fullfile(root, "src")));
+
+run(fullfile(root, "gui", "ExampleObjects.m"));
+```
+
+This normally only needs to be done once after downloading the repository.
+
+### Run DECTSim
+
+In the MATLAB Command Window, run:
+
+```matlab
+app = gui;
+```
+
+When the application opens, leave the settings at their defaults and click:
+
+```text
+Run
+```
+
+For later sessions:
+
+1. Open `DECTSim.prj`.
+2. Run `app = gui`.
+
+---
+
+## 3. MATLAB Online
+
+The source-code version requires access to MATLAB Online and Image Processing Toolbox.
+
+The compiled Windows application cannot be run in MATLAB Online.
+
+### Clone the repository
+
+1. Open MATLAB Online.
+
+2. Select **Home > New > Git Clone**.
+
+3. Enter the repository URL:
+
+ ```text
+ https://github.com/lborophysics/DECTSim.git
+ ```
+
+4. Select **Clone**.
+
+Alternatively, run:
+
+```matlab
+gitclone("https://github.com/lborophysics/DECTSim.git", "DECTSim");
+cd("DECTSim");
+```
+
+### Open the project
+
+Double-click:
+
+```text
+DECTSim.prj
+```
+
+Alternatively, run:
+
+```matlab
+project = openProject("DECTSim.prj");
+```
+
+### Create the example files
+
+Run:
+
+```matlab
+root = string(project.RootFolder);
+
+addpath(fullfile(root, "gui"));
+addpath(genpath(fullfile(root, "src")));
+
+run(fullfile(root, "gui", "ExampleObjects.m"));
+```
+
+This normally only needs to be done once.
+
+### Run DECTSim
+
+Run:
+
+```matlab
+app = gui;
+```
+
+When the application opens, leave the settings at their defaults and click:
+
+```text
+Run
+```
+
diff --git a/docs/source/user_guide/gui.md b/docs/source/user_guide/gui.md
index 353f9d5..a0a759d 100644
--- a/docs/source/user_guide/gui.md
+++ b/docs/source/user_guide/gui.md
@@ -33,7 +33,7 @@ The phantom is the object being imaged. It is the representation of the 3D objec
The voxel size is the size of each cube in the phantom, therefore, the smaller the voxel size, the more accurate the simulation will be, but the longer it will take to run the simulation.
-It is possible to load a custom phantom, this must be a '.mat' file containing an object called `phantom`, using the objects within the DECTSim backend. In the future, you should be able to create this new phantom within the GUI.
+It is possible to load a custom phantom, this must be a '.mat' file containing an object called `phantom`, using the objects within the DECTSim backend. If you also create a preview image, as in the examples, this will display the central slice of your phantom in the GUI. In the future, you should be able to create this new phantom within the GUI.
### Phantom Examples
diff --git a/gui/ExampleObjects.m b/gui/ExampleObjects.m
index 435a48e..affed97 100644
--- a/gui/ExampleObjects.m
+++ b/gui/ExampleObjects.m
@@ -1,3 +1,5 @@
+guiDir = fileparts(mfilename("fullpath"));
+
% Voxel array constants
phantom_scale = 30*units.cm; % In the x-y plane
voxel_size = 0.5 * units.mm;
@@ -51,7 +53,9 @@
voxel_size, a_shepp_logan);
% Save the phantom
-save gui/PhantomExample1.mat phantom
+save(fullfile(guiDir, "PhantomExample1.mat"), "phantom");
+
+save_phantom_preview(phantom, fullfile(guiDir, "PhantomExample1.png"));
% Example 2!
@@ -77,7 +81,8 @@
phantom = voxel_array(vox_arr_center, [zeros(2, 1)+phantom_radius*2; phantom_width], ...
voxel_size, {water_cylinder, bone_cylinder, fat_cylinder, blood_cylinder, muscle_cylinder});
-save gui/PhantomExample2.mat phantom
+save(fullfile(guiDir, "PhantomExample2.mat"), "phantom");
+save_phantom_preview(phantom, fullfile(guiDir, "PhantomExample2.png"));
% Example 3!
% Voxel array constants
@@ -102,7 +107,8 @@
phantom = voxel_array(vox_arr_center, [zeros(2, 1)+phantom_radius*2; phantom_width], ...
voxel_size, {water_cylinder, bone_cylinder, fat_cylinder, ti_cylinder, muscle_cylinder});
-save gui/PhantomExample3.mat phantom
+save(fullfile(guiDir, "PhantomExample3.mat"), "phantom");
+save_phantom_preview(phantom, fullfile(guiDir, "PhantomExample3.png"));
% Example 4!
% Voxel array constants
@@ -120,16 +126,17 @@
phantom = voxel_array(vox_arr_center, [zeros(2, 1)+phantom_radius*2; phantom_width], ...
voxel_size, {water_cylinder, bone_cylinder, fat_cylinder, ti_cylinder});
-save gui/PhantomExample4.mat phantom
+save(fullfile(guiDir, "PhantomExample4.mat"), "phantom");
+save_phantom_preview(phantom, fullfile(guiDir, "PhantomExample4.png"));
-source_40kvp = source_fromfile('40kvp.spk');
-source_80kvp = source_fromfile('80kvp.spk');
+source_40kvp = source_fromfile(fullfile(guiDir, "40kvp.spk"));
+source_80kvp = source_fromfile(fullfile(guiDir, "80kvp.spk"));
source = source_40kvp;
-save gui/SourceExample40kvp.mat source
+save(fullfile(guiDir, "SourceExample40kvp.mat"), "source");
source = source_80kvp;
-save gui/SourceExample80kvp.mat source
+save(fullfile(guiDir, "SourceExample80kvp.mat"), "source");
function new_struct = create_struct(center, a, b, c, phi, mag)
new_struct.center = center;
diff --git a/gui/gui.m b/gui/gui.m
index 315d748..cfaac3c 100644
--- a/gui/gui.m
+++ b/gui/gui.m
@@ -13,6 +13,7 @@
ResetMenu matlab.ui.container.Menu
HelpMenu matlab.ui.container.Menu
DocumentationMenu matlab.ui.container.Menu
+ AboutMenu matlab.ui.container.Menu
TabGroup matlab.ui.container.TabGroup
RunTab matlab.ui.container.Tab
ImagePanel matlab.ui.container.Panel
@@ -126,6 +127,69 @@
% Callbacks that handle component events
methods (Access = private)
+% Update the phantom preview image
+% Update the phantom preview image
+function UpdatePhantomPreview(app)
+ pathToMLAPP = fileparts(mfilename("fullpath"));
+
+ % Default image used when no PNG preview is available.
+ defaultPreview = fullfile( ...
+ pathToMLAPP, ...
+ "graphics", ...
+ "SheppLogan_Phantom.svg");
+
+ previewPath = defaultPreview;
+
+ phantomIndex = app.PhantomListBox.ValueIndex;
+
+ if isempty(phantomIndex) || ...
+ phantomIndex > numel(app.phantom_files)
+ app.PhantomImage.ImageSource = previewPath;
+ return;
+ end
+
+ phantomFile = string( ...
+ app.phantom_files{phantomIndex});
+
+ % Built-in phantoms are stored as filenames. Custom phantoms are
+ % stored as absolute paths.
+ [phantomFolder, ~, ~] = fileparts(phantomFile);
+
+ if phantomFolder == ""
+ phantomFile = fullfile( ...
+ pathToMLAPP, ...
+ phantomFile);
+ end
+
+ [phantomFolder, phantomName, ~] = ...
+ fileparts(phantomFile);
+
+ % First look beside the MAT-file.
+ adjacentPreview = fullfile( ...
+ phantomFolder, ...
+ phantomName + ".png");
+
+ % Then look in the GUI graphics directory.
+ graphicsPreview = fullfile( ...
+ pathToMLAPP, ...
+ "graphics", ...
+ phantomName + ".png");
+
+ if isfile(adjacentPreview)
+ previewPath = adjacentPreview;
+ elseif isfile(graphicsPreview)
+ previewPath = graphicsPreview;
+ end
+
+ app.PhantomImage.ImageSource = previewPath;
+end
+
+
+% Value changed function: PhantomListBox
+function PhantomListBoxValueChanged(app, ~)
+ app.UpdatePhantomPreview();
+end
+
% Image clicked function: RaysImage, SourceImage
function ToggleSourcePanelVisibility(app, ~)
if strcmp(app.SourcePanel.Visible, 'off')
@@ -287,7 +351,7 @@ function RunButtonPushed(app, event)
interpolation = app.InterpolationDropDown.Value;
% Source 1
- sinogram = squeeze(compute_sinogram(source1, phantom, d, scatter_type, scatter_factor));
+ sinogram = squeeze(compute_sinogram(source1, phantom, d, scatter_type, scatter_factor));
if do_fan2para
rotation_angle = scan_angles(1); % Assumes even spacing
[P,~,paraRotAngles] = fan2para(sinogram, (dist_to_detector/2)/pixel_dims(1), ...
@@ -437,42 +501,86 @@ function DetectorShapeDropDownValueChanged(app, ~)
end
% Callback function: LoadPhantomMenu, PhantomLoadButton
- function PhantomLoadButtonPushed(app, ~)
- [file,path] = uigetfile('*.mat','Load Saved Phantom File');
- if ischar(file)
- [~, name, ~] = fileparts(file);
- app.PhantomListBox.Items{end + 1} = name;
- try
- app.PhantomListBox.ItemsData{end + 1} = ...
- load(fullfile(path, file), 'phantom').phantom;
- app.phantom_files{end+1} = fullfile(path, file);
- catch ME
- % If there is an error loading the phantom - let the user know
- uialert(app.UIFigure, ME.message, 'Invalid Phantom File');
+ function PhantomLoadButtonPushed(app, event)
+
+ [phantomFile, phantomPath] = uigetfile( ...
+ "*.mat", ...
+ "Load Phantom");
+
+ if isequal(phantomFile, 0)
+ return;
+ end
+
+ fullPhantomPath = fullfile(phantomPath, phantomFile);
+ [~, phantomName] = fileparts(phantomFile);
+
+ try
+ loadedData = load(fullPhantomPath, "phantom");
+
+ if ~isfield(loadedData, "phantom")
+ error("The selected MAT-file does not contain a variable named 'phantom'.");
end
+
+ loadedPhantom = loadedData.phantom;
+ catch ME
+ uialert( ...
+ app.UIFigure, ...
+ sprintf("The phantom could not be loaded:\n\n%s", ME.message), ...
+ "Load Phantom Error");
+ return;
end
- end
+
+ app.PhantomListBox.Items{end + 1} = phantomName;
+ app.PhantomListBox.ItemsData{end + 1} = loadedPhantom;
+ app.phantom_files{end + 1} = fullPhantomPath;
+
+ % Select the newly loaded phantom.
+ app.PhantomListBox.Value = loadedPhantom;
+ app.UpdatePhantomPreview();
+
+ end
% Callback function: LoadSourceMenu, SourceLoadButton
- function SourceLoadButtonPushed(app, ~)
- [file,path] = uigetfile({'*.mat;*.spk', 'MATLAB file or SpekPy spectrum file (*.mat, *.spk)'}, 'Load Saved Source File');
- if ischar(file)
- [~, name, ext] = fileparts(file);
- app.Source1DropDown.Items{end+1} = name;
- app.Source2DropDown.Items{end+1} = name;
- try
- if ext == ".spk"
- loaded_source = source_fromfile(fullfile(path, file));
- else
- loaded_source = load(fullfile(path, file), 'source').source;
+ function SourceLoadButtonPushed(app, event)
+
+ [sourceFile, sourcePath] = uigetfile( ...
+ {"*.mat;*.spk", "DECTSim source files (*.mat, *.spk)"; ...
+ "*.mat", "MAT-files (*.mat)"; ...
+ "*.spk", "Spectrum files (*.spk)"}, ...
+ "Load Source");
+
+ if isequal(sourceFile, 0)
+ return;
+ end
+
+ fullSourcePath = fullfile(sourcePath, sourceFile);
+ [~, sourceName, sourceExtension] = fileparts(sourceFile);
+
+ try
+ if strcmpi(sourceExtension, ".spk")
+ loadedSource = source_fromfile(fullSourcePath);
+ else
+ loadedData = load(fullSourcePath, "source");
+
+ if ~isfield(loadedData, "source")
+ error("The selected MAT-file does not contain a variable named 'source'.");
end
- catch ME
- % If there is an error loading the source - let the user know
- uialert(app.UIFigure, ME.message, 'Invalid Source File');
+
+ loadedSource = loadedData.source;
end
- app.Source1DropDown.ItemsData{end+1} = loaded_source;
- app.Source2DropDown.ItemsData{end+1} = loaded_source;
- end % Nothing selected
+ catch ME
+ uialert( ...
+ app.UIFigure, ...
+ sprintf("The source could not be loaded:\n\n%s", ME.message), ...
+ "Load Source Error");
+ return;
+ end
+
+ app.Source1DropDown.Items{end + 1} = sourceName;
+ app.Source1DropDown.ItemsData{end + 1} = loadedSource;
+
+ app.Source2DropDown.Items{end + 1} = sourceName;
+ app.Source2DropDown.ItemsData{end + 1} = loadedSource;
end
% Menu selected function: ResetInContextMenu, ResetMenu
@@ -511,8 +619,12 @@ function ResetMenuSelected(app, ~)
app.Source2DropDown.Items = {'None', 'Low Energy (40 kvp)', 'High Energy (80 kvp)'};
app.Source2DropDown.ItemsData = {'None', 'Low Energy (40 kvp)', 'High Energy (80 kvp)'};
app.Source2DropDown.Value = 'None';
- app.PhantomListBox.Items = {'Example 1', 'Example 2'};
- app.PhantomListBox.ItemsData = {'Example 1', 'Example 2'};
+
+ app.PhantomListBox.Items = {"Modified Shepp Logan","Example 2","Example 3","Example 4"};
+ app.PhantomListBox.ItemsData = {"Modified Shepp Logan","Example 2", "Example 3", "Example 4"};
+ app.PhantomListBox.Value = "Modified Shepp Logan";
+ app.phantom_files = {'PhantomExample1.mat', 'PhantomExample2.mat', 'PhantomExample3.mat','PhantomExample4.mat'};
+ app.UpdatePhantomPreview();
% Reset the edit fields
app.VoxelSizeEditField.Value = 1;
@@ -595,6 +707,7 @@ function SaveStateSelected(app, ~)
state.phantom_selected = app.PhantomListBox.Value;
state.phantoms = app.PhantomListBox.Items;
state.phantom_data = app.PhantomListBox.ItemsData;
+ state.phantom_files = app.phantom_files;
state.voxel_size = app.VoxelSizeEditField.Value;
state.voxel_units = app.VoxelSizeUnits.Value;
@@ -620,13 +733,27 @@ function SaveStateSelected(app, ~)
% Menu selected function: LoadStateInContextMenu, LoadStateMenu
function LoadStateSelected(app, event)
+ [stateFile, statePath] = uigetfile("*.mat", "Load State");
+ if isequal(stateFile, 0)
+ return;
+ end
+
try
- state_file = uigetfile('*.mat','Load State');
- state = load(state_file, 'state').state;
+ loadedData = load(fullfile(statePath, stateFile), "state");
+
+ if ~isfield(loadedData, "state")
+ error("The selected file does not contain a variable named 'state'.");
+ end
+
+ state = loadedData.state;
catch ME
- uialert(app.UIFigure, ME.message, 'Invalid State File');
+ uialert( ...
+ app.UIFigure, ...
+ sprintf("The state file could not be loaded:\n\n%s", ME.message), ...
+ "Load State Error");
+ return;
end
- if ~ischar(state_file); return; end % Nothing selected
+
app.ReconstructionPanel.Visible = state.recon_visible;
app.DetectorPanel.Visible = state.detector_visible;
app.PhantomPanel.Visible = state.phantom_visible;
@@ -655,6 +782,9 @@ function LoadStateSelected(app, event)
app.PhantomListBox.Value = state.phantom_selected;
app.VoxelSizeEditField.Value = state.voxel_size;
app.VoxelSizeUnits.Value = state.voxel_units;
+ if isfield(state, "phantom_files")
+ app.phantom_files = state.phantom_files;
+ end
% Set the gantry information
app.DistToDetectorField.Value = state.dist_to_detector;
@@ -677,16 +807,41 @@ function LoadStateSelected(app, event)
app.ShowDropDownChanged(event);
app.DetectorShapeDropDownValueChanged(event);
app.SourceTypeDropDownValueChanged(event);
+ app.UpdatePhantomPreview();
end
% Menu selected function: ReconstructionHelpMenu
function ReconstructionHelpMenuSelected(~, ~)
- doc iradon
- web('docs/build/html/user_guide/gui.html#reconstruction')
+ % doc iradon
+ web("https://dectsim.readthedocs.io/en/latest/user_guide/gui.html#reconstruction","-browser");
end
% Menu selected function: ExportMenu, ExporttoScriptMenu
function ExporttoScriptMenuSelected(app, ~)
+
+ %When using exe export is possible but files cannot be used.
+ if isdeployed
+ choice = uiconfirm( ...
+ app.UIFigure, ...
+ [ ...
+ "DECTSim can export the simulation as a MATLAB script, " ...
+ "but MATLAB Runtime cannot execute MATLAB scripts." newline newline ...
+ "To run the exported script, you will need a licensed MATLAB " ...
+ "installation with the required toolboxes." newline newline ...
+ "Would you still like to export the script?" ...
+ ], ...
+ "MATLAB Required to Run Exported Script", ...
+ "Options", ["Export Script", "Cancel"], ...
+ "DefaultOption", "Export Script", ...
+ "CancelOption", "Cancel", ...
+ "Icon", "warning");
+
+ if choice == "Cancel"
+ return;
+ end
+ end
+
+
% Create a script to run the simulation
[file,path] = uiputfile('*.m','Save Script');
if ~ischar(file); return; end % Nothing selected
@@ -711,7 +866,7 @@ function ExporttoScriptMenuSelected(app, ~)
has_source2 = source2_selected > 1;
if has_source2 && source2_selected <= 3
fprintf(fid, "source2 = load('%s', 'source').source;\n", ...
- fullfile(pathToMLAPP, app.source_files{source2_selected}));
+ fullfile(pathToMLAPP, app.source_files{source2_selected - 1}));
elseif source2_selected > 3
source2 = app.Source2DropDown.ItemsData{source2_selected};
save(fullfile(path, 'source2.mat'), "source2");
@@ -838,46 +993,92 @@ function ExporttoScriptMenuSelected(app, ~)
end
% Menu selected function: DocumentationMenu
- function DocumentationMenuSelected(~, ~)
- web('docs\build\html\index.html')
+ function DocumentationMenuSelected(app, event)
+ web("https://dectsim.readthedocs.io/en/latest/", "-browser");
end
- % Menu selected function: SourceHelpMenu_run
- function SourceHelpMenu_runSelected(~, ~)
- web('docs/build/html/user_guide/gui.html#source')
+ % Menu selected function: AboutMenu
+ function AboutMenuSelected(app, event)
+ message = sprintf([ ...
+ 'DECTSim 1.0.0\n\n' ...
+ 'Dual-energy computed tomography simulation application.\n\n' ...
+ 'MATLAB®. © 1984 - 2026 The MathWorks, Inc.\n\n' ...
+ 'This application was created using MATLAB Compiler and ' ...
+ 'requires MATLAB Runtime.\n\n' ...
+ 'DECTSim is distributed under the BSD 3-Clause License.\n' ...
+ 'Copyright (c) 2023, Joshua Gray and Sofia Pearson.\n\n' ...
+ 'See APPLICATION_LICENSE.txt,' ...
+ 'for the applicable terms and notices.' ...
+ ]);
+
+ uialert( ...
+ app.UIFigure, ...
+ message, ...
+ 'About DECTSim', ...
+ 'Icon', 'info');
end
+
% Menu selected function: PhantomHelpMenu_run
function PhantomHelpMenu_runSelected(~, ~)
- web('docs/build/html/user_guide/gui.html#phantom')
+ web("https://dectsim.readthedocs.io/en/latest/user_guide/gui.html#phantom","-browser");
end
% Menu selected function: DetectorHelpMenu_run
function DetectorContextMenuSelected(~, ~)
- web('docs/build/html/user_guide/gui.html#detector')
+ web("https://dectsim.readthedocs.io/en/latest/user_guide/gui.html#detector","-browser");
end
% Menu selected function: ScatterHelpMenu
function ScatterContextMenuSelected(~, ~)
- web('docs/build/html/user_guide/gui.html#scatter')
+ web("https://dectsim.readthedocs.io/en/latest/user_guide/gui.html#scatter","-browser");
end
% Menu selected function: ReconstructionMenu_3, SinogramMenu_3
- function OpeninImageViewerMenuSelected(app, event)
- show_sinogram = strcmp(event.Source.Text, 'Sinogram');
+ function OpeninImageViewerMenuSelected(~, ~)
+
+ showSinogram = strcmp(event.Source.Text, "Sinogram");
+
if isempty(app.recons{1})
- errordlg('No sinogram to view', 'Invalid Sinogram');return;
+ uialert( ...
+ app.UIFigure, ...
+ "Run a simulation before opening an image.", ...
+ "No Image Available");
+ return;
+ end
+
+ if showSinogram
+ imageData = ...
+ app.ShowDropDown.ItemsData{app.ShowDropDown.ValueIndex};
+ windowTitle = "DECTSim Sinogram";
+ else
+ imageData = app.recons{app.ShowDropDown.ValueIndex};
+ windowTitle = "DECTSim Reconstruction";
end
+
try
- if show_sinogram
- imageViewer(app.ShowDropDown.ItemsData{app.ShowDropDown.ValueIndex});
+ if isdeployed
+ % imageViewer is unavailable in MATLAB Runtime.
+ % Use a standard MATLAB figure instead.
+ imageFigure = figure( ...
+ "Name", windowTitle, ...
+ "NumberTitle", "off");
+
+ imageAxes = axes(imageFigure);
+
+ imshow(imageData, ...
+ "Parent", imageAxes, ...
+ "InitialMagnification", "fit");
else
- imageViewer(app.recons{app.ShowDropDown.ValueIndex})
+ % imageViewer is available in normal MATLAB.
+ %#exclude imageViewer
+ imageViewer(imageData);
end
catch ME
- % If there is an error opening the image viewer - tell the user the error and suggest they install the image processing toolbox
- message = sprintf('Error opening the image viewer: %s\nIt is possible that the image processing toolbox is not installed. You can install it from the Add-Ons menu.', ME.message);
- uialert(app.UIFigure, message, 'Error opening image viewer');
+ uialert( ...
+ app.UIFigure, ...
+ sprintf("The image could not be opened:\n\n%s", ME.message), ...
+ "Image Display Error");
end
end
end
@@ -936,6 +1137,13 @@ function createComponents(app)
app.DocumentationMenu.MenuSelectedFcn = createCallbackFcn(app, @DocumentationMenuSelected, true);
app.DocumentationMenu.Text = 'Documentation';
+ % Create AboutMenu
+ app.AboutMenu = uimenu(app.HelpMenu);
+ app.AboutMenu.MenuSelectedFcn = ...
+ createCallbackFcn(app, @AboutMenuSelected, true);
+ app.AboutMenu.Separator = 'on';
+ app.AboutMenu.Text = 'About DECTSim';
+
% Create TabGroup
app.TabGroup = uitabgroup(app.UIFigure);
app.TabGroup.TabLocation = 'bottom';
@@ -1145,6 +1353,7 @@ function createComponents(app)
app.PhantomListBox = uilistbox(app.PhantomPanel);
app.PhantomListBox.Items = {'Modified Shepp Logan', 'Example 2', 'Example 3', 'Example 4'};
app.PhantomListBox.ItemsData = {'Modified Shepp Logan', 'Example 2', 'Example 3', 'Example 4'};
+ app.PhantomListBox.ValueChangedFcn = createCallbackFcn(app, @PhantomListBoxValueChanged, true);
app.PhantomListBox.Tooltip = {'Select the available phantoms'};
app.PhantomListBox.Position = [95 51 158 74];
app.PhantomListBox.Value = 'Modified Shepp Logan';
@@ -1608,6 +1817,7 @@ function createComponents(app)
app.ScatterFactorSpinner.ContextMenu = app.ScatterContextMenu;
% Show the figure after all components are created
+ app.UpdatePhantomPreview();
app.UIFigure.Visible = 'on';
end
end
diff --git a/resources/project/GSuWaUF5mY44VY9K0v8oH9L0JjE/yJ5HhId127bnvMTz_nJJ2d9M4dod.xml b/resources/project/GSuWaUF5mY44VY9K0v8oH9L0JjE/yJ5HhId127bnvMTz_nJJ2d9M4dod.xml
deleted file mode 100644
index 7a6326b..0000000
--- a/resources/project/GSuWaUF5mY44VY9K0v8oH9L0JjE/yJ5HhId127bnvMTz_nJJ2d9M4dod.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/resources/project/GSuWaUF5mY44VY9K0v8oH9L0JjE/yJ5HhId127bnvMTz_nJJ2d9M4dop.xml b/resources/project/GSuWaUF5mY44VY9K0v8oH9L0JjE/yJ5HhId127bnvMTz_nJJ2d9M4dop.xml
deleted file mode 100644
index 28deae1..0000000
--- a/resources/project/GSuWaUF5mY44VY9K0v8oH9L0JjE/yJ5HhId127bnvMTz_nJJ2d9M4dop.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
\ No newline at end of file
diff --git a/src/compute_sinogram.m b/src/compute_sinogram.m
index c2baa67..eb45974 100644
--- a/src/compute_sinogram.m
+++ b/src/compute_sinogram.m
@@ -167,4 +167,13 @@
% Convert the signal to a sinogram
sinogram = sensor_unit.get_image(photon_signal + scatter_signal, air_signal);
+
+%Adding some checks
+
+if any(isnan(sinogram), "all")
+ error( ...
+ "DECTSim:InvalidSinogram", ...
+ "The calculated sinogram contains NaN values.");
+end
+
end
\ No newline at end of file
diff --git a/src/voxels/save_phantom_preview.m b/src/voxels/save_phantom_preview.m
new file mode 100644
index 0000000..5c50ca6
--- /dev/null
+++ b/src/voxels/save_phantom_preview.m
@@ -0,0 +1,97 @@
+function [preview, alpha] = save_phantom_preview( ...
+ phantom, output_file)
+%SAVE_PHANTOM_PREVIEW Save the central axial phantom slice as a PNG.
+%
+% SAVE_PHANTOM_PREVIEW(PHANTOM, OUTPUT_FILE) evaluates the central
+% x-y slice of a voxel_array. Different phantom objects are shown as
+% grayscale levels, while the world/background material is transparent.
+%
+% [PREVIEW, ALPHA] = SAVE_PHANTOM_PREVIEW(...) also returns the image
+% and alpha arrays.
+
+ arguments
+ phantom (1, 1) voxel_array
+ output_file (1, 1) string
+ end
+
+ % num_planes counts boundaries, so subtract one to get voxel cells.
+ num_voxels = phantom.num_planes - 1;
+
+ nx = num_voxels(1);
+ ny = num_voxels(2);
+ nz = num_voxels(3);
+
+ % Select the central z voxel.
+ z_index = ceil(nz / 2);
+
+ % Construct every x-y voxel index on the central slice.
+ [x_indices, y_indices] = ndgrid(1:nx, 1:ny);
+
+ indices = [
+ x_indices(:).'
+ y_indices(:).'
+ repmat(z_index, 1, numel(x_indices))
+ ];
+
+ % Obtain the object/material ID at each voxel.
+ object_indices = phantom.get_object_idxs(indices);
+
+ % Convert vector back to a 2-D image.
+ %
+ % Transpose because image rows represent y and columns represent x.
+ object_slice = reshape(object_indices, nx, ny);
+ object_slice = rot90(object_slice, 1);
+
+ % phantom.nobj is the world material, which is air by default.
+ foreground = object_slice ~= phantom.nobj;
+
+ % Map each object index to its material attenuation coefficient (arbitrary energy)
+ reference_energy = 50;
+ mu_values = double(phantom.get_mu_arr(reference_energy));
+ attenuation_slice = reshape(mu_values(object_slice(:)), size(object_slice));
+
+ preview = zeros(size(object_slice), "double");
+
+ if any(foreground, "all")
+ foreground_values = ...
+ attenuation_slice(foreground);
+
+ minimum_value = min(foreground_values);
+ maximum_value = max(foreground_values);
+
+ % Keep the phantom between dark grey and light grey.
+ minimum_grey = 0.15;
+ maximum_grey = 0.85;
+
+ if maximum_value > minimum_value
+ preview(foreground) = ...
+ minimum_grey + ...
+ (maximum_grey - minimum_grey) .* ...
+ (attenuation_slice(foreground) - minimum_value) ./ ...
+ (maximum_value - minimum_value);
+ else
+ preview(foreground) = (minimum_grey + maximum_grey) / 2;
+ end
+end
+
+
+
+
+
+
+
+ % Fully transparent background, fully opaque phantom.
+ alpha = double(foreground);
+
+ output_folder = fileparts(output_file);
+
+ if output_folder ~= "" && ~isfolder(output_folder)
+ mkdir(output_folder);
+ end
+
+ imwrite( ...
+ preview, ...
+ output_file, ...
+ "png", ...
+ "Alpha", alpha);
+end
\ No newline at end of file