diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml new file mode 100644 index 0000000..6dfd923 --- /dev/null +++ b/.github/workflows/cmake-multi-platform.yml @@ -0,0 +1,77 @@ +# This starter workflow is for a CMake project running on multiple platforms. There is a different starter workflow if you just want a single platform. +# See: https://github.com/actions/starter-workflows/blob/main/ci/cmake-single-platform.yml +name: CMake on multiple platforms + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + runs-on: ${{ matrix.os }} + + strategy: + # Set fail-fast to false to ensure that feedback is delivered for all matrix combinations. Consider changing this to true when your workflow is stable. + fail-fast: false + + # Set up a matrix to run the following 3 configurations: + # 1. + # 2. + # 3. + # + # To add more build types (Release, Debug, RelWithDebInfo, etc.) customize the build_type list. + matrix: + os: [ubuntu-latest] + build_type: [Release] + c_compiler: [gcc, clang] + include: + - os: ubuntu-latest + c_compiler: gcc + cpp_compiler: g++ + # define the standard C++ version to use + env: + CXX_STANDARD: 20 + - os: ubuntu-latest + c_compiler: clang + cpp_compiler: clang++ + # define the standard C++ version to use + env: + CXX_STANDARD: 20 + + steps: + - uses: actions/checkout@v4 + + - name: Set reusable strings + # Turn repeated input strings (such as the build output directory) into step outputs. These step outputs can be used throughout the workflow file. + id: strings + shell: bash + run: | + echo "build-output-dir=${{ github.workspace }}/build" >> "$GITHUB_OUTPUT" + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get -y install libxrandr-dev libhdf5-serial-dev libxinerama-dev libxcursor-dev libxi-dev libgl1-mesa-dev + + + - name: Configure CMake + # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. + # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type + run: > + cmake -B ${{ steps.strings.outputs.build-output-dir }} + -DCMAKE_CXX_COMPILER=${{ matrix.cpp_compiler }} + -DCMAKE_C_COMPILER=${{ matrix.c_compiler }} + -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} + -S ${{ github.workspace }} + + - name: Build + # Build your program with the given configuration. Note that --config is needed because the default Windows generator is a multi-config generator (Visual Studio generator). + run: cmake --build ${{ steps.strings.outputs.build-output-dir }} --config ${{ matrix.build_type }} + + - name: Test + working-directory: ${{ steps.strings.outputs.build-output-dir }} + # Execute tests defined by the CMake configuration. Note that --build-config is needed because the default Windows generator is a multi-config generator (Visual Studio generator). + # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail + run: ctest --build-config ${{ matrix.build_type }} diff --git a/.github/workflows/deploy-documentation.yml b/.github/workflows/deploy-documentation.yml new file mode 100644 index 0000000..0a766e7 --- /dev/null +++ b/.github/workflows/deploy-documentation.yml @@ -0,0 +1,64 @@ +# Sample workflow for building and deploying a VitePress site to GitHub Pages +# +name: Deploy VitePress site to Pages + +on: + # Runs on pushes targeting the `main` branch. Change this to `master` if you're + # using the `master` branch as the default branch. + push: + branches: [main] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + # Build job + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Not needed if lastUpdated is not enabled + # - uses: pnpm/action-setup@v3 # Uncomment this if you're using pnpm + # - uses: oven-sh/setup-bun@v1 # Uncomment this if you're using Bun + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm # or pnpm / yarn + - name: Setup Pages + uses: actions/configure-pages@v4 + - name: Install dependencies + run: npm ci # or pnpm install / yarn install / bun install + - name: Build with VitePress + run: npm run docs:build # or pnpm docs:build / yarn docs:build / bun run docs:build + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs/.vitepress/dist + + # Deployment job + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + needs: build + runs-on: ubuntu-latest + name: Deploy + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index d45dd9d..24d1f22 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,16 +7,15 @@ project(opensaft LANGUAGES CXX) set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED true) +set(CMAKE_CXX_STANDARD_REQUIRED True) if (NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE RelWithDebInfo) endif() -message(STATUS "CMAKE_BUILD_TYPE: ${CMAKE_BUILD_TYPE}") - option(OPENSAFT_CUDA_SUPPORT "Enable CUDA support" OFF) option(OPENSAFT_TESTING "Build unit tests" ON) +option(OPENSAFT_GUI "build graphical user interface alongside" OFF) # prepare for cuda compilation if (OPENSAFT_CUDA_SUPPORT) @@ -37,16 +36,8 @@ set(CUDA_NVCC_FLAGS ${CUDA_NVCC_FLAGS}; -x cu -rdc=true -ftz=false -prec-div=tru set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/modules/") # find dependencies (most of them are handled through FetchContent) -find_package(ImGUI REQUIRED) -find_package(glfw3 REQUIRED) -find_package(OpenGL REQUIRED) -find_package(SDL2 REQUIRED) find_library(H5CPP_LIB hdf5_cpp) find_library(H5_LIB hdf5) # todo this sounds fishy that -find_package(ImPlot REQUIRED) -find_package(ImGUIFileDialog REQUIRED) -find_package(CVolume REQUIRED) -find_package(Backward REQUIRED) # main library holding the backbone of the application add_library(opensaft STATIC "") @@ -54,25 +45,57 @@ add_library(opensaft STATIC "") add_subdirectory(src/) target_link_libraries(opensaft PUBLIC - ${ImGUI_LIBRARIES} - ${implot_LIBRARIES} - ${GLFW_LIBRARIES} - ${cvolume_LIBRARIES} - ${ImGUIFileDialog_LIBRARIES} - ${backward_LIBRARIES} ) target_include_directories(opensaft PUBLIC ${CMAKE_SOURCE_DIR}/src - ${ImGUI_INCLUDE_DIR} - ${ImPlot_INCLUDE_DIR} - ${ImGUIFileDialog_INCLUDE_DIR} - ${OPENGL_INCLUDE_DIRS} - ${GLFW3_INCLUDE_DIR} - ${cvolume_INCLUDE_DIR} - ${backward_INCLUDE_DIR} ) +if (OPENSAFT_GUI) + + add_library(opensaft-gui) + + find_package(ImGUI REQUIRED) + find_package(GLFW REQUIRED) + find_package(OpenGL REQUIRED) + find_package(ImPlot REQUIRED) + find_package(ImGUIFileDialog REQUIRED) + find_package(Backward REQUIRED) + + + target_link_libraries(opensaft-gui PUBLIC + ${ImGUI_LIBRARIES} + ${implot_LIBRARIES} + ${GLFW_LIBRARIES} + ${cvolume_LIBRARIES} + ${ImGUIFileDialog_LIBRARIES} + ${backward_LIBRARIES} + ) + + target_include_directories(opensaft PUBLIC + ${CMAKE_SOURCE_DIR}/src + ${ImGUI_INCLUDE_DIR} + ${ImPlot_INCLUDE_DIR} + ${ImGUIFileDialog_INCLUDE_DIR} + ${OPENGL_INCLUDE_DIRS} + ${GLFW3_INCLUDE_DIR} + ${cvolume_INCLUDE_DIR} + ${backward_INCLUDE_DIR} + ) + + + # add an executable for the main graphical user interface + add_executable(OpensaftGui src/main.cpp) + if (OPENSAFT_CUDA_SUPPORT) + set_property(TARGET OpensaftGui PROPERTY CUDA_SEPARABLE_COMPILATION ON) + set_property(TARGET OpensaftGui PROPERTY CUDA_ARCHITECTURES 50 75 86) + endif() + target_link_libraries(OpensaftGui PUBLIC opensaft-gui) + + install(TARGETS OpensaftGui DESTINATION bin) + +endif (OPENSAFT_GUI) + find_library(TERM_LIB curses) @@ -80,12 +103,4 @@ if (OPENSAFT_TESTING) add_subdirectory(tests) endif() -# add an executable for the main graphical user interface -add_executable(main_exp src/main.cpp) -if (OPENSAFT_CUDA_SUPPORT) - set_property(TARGET main_exp PROPERTY CUDA_SEPARABLE_COMPILATION ON) - set_property(TARGET main_exp PROPERTY CUDA_ARCHITECTURES 50 75 86) -endif() -target_link_libraries(main_exp PUBLIC opensaft) -install(TARGETS main_exp DESTINATION bin) diff --git a/README.md b/README.md index fb043ee..32018a5 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # opensaft +::: warning +This project is currently under development and not yet ready for use. Please check back later. +::: + Volumetric synthetic aperture focusing technique (SAFT) for acoustic resolution optoacoustic microscopy and (soon) scanning acoustic microscopy. This repository contains an implementation of the synthetic aperture focsuing technique purely written in C++ and CUDA. You need a CUDA capable device and Linux installed on your PC to run the fast GPU version of it. A "slow" CPU version is implemented as well. Installation instructions will soon be provided for ArchLinux as well as dataset specifications. @@ -21,17 +25,18 @@ Required packages: - a capable C++ compiler which can be consumed by `CMake` - `CUDA` for GPU acceleration (can be disabled, not required) - `hdf5` for dataset import and export +- `libxrandr`, `libxinerama`, `libxcursor`, `libxi`, `libgl1-mesa-dev`: used for GUI framework Package installation command ArchLinux: ```bash -pacman -S hdf5 +pacman -S hdf5 libxrandr libxinerama ``` Package installation command Ubuntu: ```bash -apt-get install libhdf5-serial-dev +apt-get install libhdf5-serial-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libgl1-mesa-dev ``` Many other dependencies are directly managed through CMake based on `FetchContent`. @@ -62,12 +67,26 @@ SAFT induces a strong absorption bias over depth. Absorbance "reconstructed" at The documentation should be running with `vitepress` and is compiled into a static website that is also published to github pages. All documentation is written in markdown and can be found in the doc folder. -To run the documentation locally, run the following commands: +If this is the first time that you have a look at the documentation, you need to run + +```bash +npm install +``` + +Afterwards, to display the documentation run ```bash npm run docs:dev ``` +To build the static website, run + +```bash +npm run docs:build +``` + +The documentation is published on every merge to `main` to [github pages](https://hofmannu.github.io/opensaft/). + ## Literature - J. Turner et al.: Improved optoacoustic microscopy through three-dimensional spatial impulse response synthetic aperture focusing technique in Optics Letters 39 (12), pp. 3390 - 3393 (2014), [10.1364/OL.39.003390](https://doi.org/10.1364/OL.39.003390) diff --git a/cmake/modules/FindBackward.cmake b/cmake/modules/FindBackward.cmake index edc385d..b8efad1 100644 --- a/cmake/modules/FindBackward.cmake +++ b/cmake/modules/FindBackward.cmake @@ -1,13 +1,14 @@ include(FetchContent) -FetchContent_Declare(backward - GIT_REPOSITORY https://github.com/bombela/backward-cpp - GIT_TAG master # or a version tag, such as v1.6 - SYSTEM # optional, the Backward include directory will be treated as system directory +FetchContent_Declare( + backward + GIT_REPOSITORY https://github.com/bombela/backward-cpp + GIT_TAG master # or a version tag, such as v1.6 + SYSTEM # optional, the Backward include directory will be treated as system directory ) FetchContent_GetProperties(backward) -if(NOT backward_POPULATED) +if (NOT backward_POPULATED) FetchContent_Populate(backward) add_subdirectory(${backward_SOURCE_DIR} ${backward_BINARY_DIR}) endif() diff --git a/cmake/modules/FindCVolume.cmake b/cmake/modules/FindCVolume.cmake deleted file mode 100644 index cff5fb0..0000000 --- a/cmake/modules/FindCVolume.cmake +++ /dev/null @@ -1,19 +0,0 @@ -# https://github.com/hofmannu/CVolume.git - -include(FetchContent) - -FetchContent_declare( - CVolume - GIT_REPOSITORY https://github.com/hofmannu/CVolume.git - GIT_TAG v0.1 - ) - -FetchContent_GetProperties(CVolume) -if (NOT cvolume_POPULATED) - FetchContent_Populate(CVolume) - add_subdirectory(${cvolume_SOURCE_DIR} ${cvolume_BINARY_DIR}) -endif() - -set(cvolume_FOUND TRUE) -set(cvolume_INCLUDE_DIR "${cvolume_SOURCE_DIR}/src") -set(cvolume_LIBRARIES "Volume") \ No newline at end of file diff --git a/cmake/modules/FindCatch2.cmake b/cmake/modules/FindCatch2.cmake index 309a17a..20ba4c8 100644 --- a/cmake/modules/FindCatch2.cmake +++ b/cmake/modules/FindCatch2.cmake @@ -9,7 +9,7 @@ FetchContent_Declare( ) FetchContent_GetProperties(catch2) -if(NOT catch2_POPULATED) +if (NOT catch2_POPULATED) FetchContent_Populate(catch2) add_subdirectory(${catch2_SOURCE_DIR} ${catch2_BINARY_DIR}) endif() diff --git a/cmake/modules/FindImGUI.cmake b/cmake/modules/FindImGUI.cmake index 699b40b..ccf6390 100644 --- a/cmake/modules/FindImGUI.cmake +++ b/cmake/modules/FindImGUI.cmake @@ -45,7 +45,7 @@ if (NOT imgui_POPULATED) # glfw3_LIBRARIES is empty target_link_libraries(ImGUI_target PRIVATE - ${glfw3_LIBRARIES} + ${glfw_LIBRARIES} ${OPENGL_LIBRARIES} ) diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 2caca14..40d4352 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -11,20 +11,37 @@ export default defineConfig({ { text: 'Physics', link: '/physics' }, { text: 'Software', link: '/software' }, { text: 'Hardware', link: '/hardware' }, + { text: 'Literature', link: '/literature' }, ], + outline: [2, 4], + socialLinks: [ + { icon: 'github', link: 'https://github.com/hofmannu/opensaft' } + ], + footer: { + message: 'Created by Urs Hofmann', + copyright: 'Copyright © 2024 Urs Hofmann' + }, - sidebar: [ - { - text: 'Examples', - items: [ - { text: 'Markdown Examples', link: '/markdown-examples' }, - { text: 'Runtime API Examples', link: '/api-examples' } - ] + lastUpdated: { + text: 'Updated at', + formatOptions: { + dateStyle: 'full', + timeStyle: 'medium' } - ], + }, + + i18nRouting: true, + search: { + provider: 'local' + }, + + editLink: { + pattern: 'https://github.com/hofmannu/opensaft/edit/main/blog/:path', + }, + + markdown: { + math: true + }, - socialLinks: [ - { icon: 'github', link: 'https://github.com/hofmannu/opensaft' } - ] } }) diff --git a/docs/contributing.md b/docs/contributing.md index e390fce..1728d3d 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -1,15 +1,18 @@ # Contributing - - ## Coding conventions - no `return` statement at the end of void functions -- declaration of `public` members comes before `private` members - avoid `enum` in favor of `enum class` - all includes of external libraries are done through `<>` brackets and the ones defined within this repository should be added using `""` -- all repository include paths should be defined relative to `src` directory +- all repository include paths should be defined relative to `src` directory, so even if a file is in the same subdirectory, it should be included with `#include "subdirectory/file.h"` instead of `#include "file.h"` ## Class structure -- `public` members first \ No newline at end of file +- `public` members first, then `protected`, then `private`: public interface is most relevant and needs to be the first thing visible +- `#pragma once` at the top of each header file + + +## Namespace + +- the entire library should be encapsulated into `opensaft` namespace to make the life of any consumer easier diff --git a/docs/index.md b/docs/index.md index 2ea02c6..efb70ab 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,22 +4,26 @@ layout: home hero: name: "opensaft" - text: "An open source implementation of the SAFT technique for optoacoustic imaging" - tagline: My great project tagline + text: + tagline: An open source implementation of the SAFT technique for optoacoustic imaging actions: - - theme: brand - text: Markdown Examples - link: /markdown-examples - theme: alt - text: API Examples - link: /api-examples + text: Physics + link: /physics + - theme: alt + text: Software + link: /software + - theme: alt + text: Hardware + link: /hardware features: - - title: Feature A - details: Lorem ipsum dolor sit amet, consectetur adipiscing elit - - title: Feature B - details: Lorem ipsum dolor sit amet, consectetur adipiscing elit - - title: Feature C - details: Lorem ipsum dolor sit amet, consectetur adipiscing elit ---- + - title: Open source + details: This repository is and will remain open source + - title: Performant + details: The code is optimized for performance using CUDA or can run on multicore CPU if you don't have a GPU at hand + - title: Informative + details: Through this documentation you can learn more about the technique and how to use it as well as potential hardware implementations + - title: Simulation + details: We are planning to add a simulation toolbox to generate test datasets. diff --git a/docs/literature.md b/docs/literature.md new file mode 100644 index 0000000..573b526 --- /dev/null +++ b/docs/literature.md @@ -0,0 +1,3 @@ +# Literature + +Here we will add a throughout literature review for the technique \ No newline at end of file diff --git a/docs/software.md b/docs/software.md new file mode 100644 index 0000000..32f83b8 --- /dev/null +++ b/docs/software.md @@ -0,0 +1,10 @@ +# Software + +## Memory model + +We have a container for the raw ultrasound signals coming in. +Thereby, each ultrasound waveform holds properties such as the 3D position, sampling rate etc. +Mutliple waveforms are then combined into an acquisition that also has properties which are shared between all the waveforms. + +## Reconstruction + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3e6f0ed..3507110 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,13 +1,9 @@ target_sources(opensaft PUBLIC - ColorMapper.h - Interface.h ReconSettings.h Transducer.h Saft.h PRIVATE - ColorMapper.cpp - Interface.cpp ReconSettings.cpp Transducer.cpp Saft.cpp @@ -19,11 +15,17 @@ if (OPENSAFT_CUDA_SUPPORT) target_sources(opensaft PUBLIC Saft.cuh - ColorMapper.cuh + Slicer/ColorMapper.cuh PRIVATE Saft.cu - ColorMapper.cu + Slicer/ColorMapper.cu ) endif() -add_subdirectory(Util) \ No newline at end of file +add_subdirectory(Util) +add_subdirectory(Memory) + +if (OPENSAFT_GUI) + add_subdirectory(Gui) + add_subdirectory(Slicer) +endif () \ No newline at end of file diff --git a/src/Gui/CMakeLists.txt b/src/Gui/CMakeLists.txt new file mode 100644 index 0000000..a151fcd --- /dev/null +++ b/src/Gui/CMakeLists.txt @@ -0,0 +1,6 @@ +target_sources(opensaft-gui +PUBLIC + Interface.h +PRIVATE + Interface.cpp + ) \ No newline at end of file diff --git a/src/Gui/Interface.cpp b/src/Gui/Interface.cpp new file mode 100644 index 0000000..374e10d --- /dev/null +++ b/src/Gui/Interface.cpp @@ -0,0 +1,710 @@ +#include "Interface.h" +#include +#include +#include + +namespace opensaft { + +static void glfw_error_callback(int error, const char* description) { + fprintf(stderr, "GLFW Error %d: %s\n", error, description); +} + +Interface::Interface() { + // get all subdefinitions of saft objects as pointers + trans = recon.get_ptrans(); // pointer to transducer class + sett = recon.get_psett(); // pointer to reconstruction settings + inputDataVol = recon.get_ppreprocData(); // pointer to preprocessed volume + reconDataVol = recon.get_preconData(); // pointer to reconstructed volume +} + +// displays a small help marker next to the text +static void HelpMarker(const char* desc) { + ImGui::TextDisabled("(?)"); + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); + ImGui::TextUnformatted(desc); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } + return; +} + +void Interface::SetupWorkspace(ImGuiID& dockspace_id) { + ImGui::DockBuilderRemoveNode(dockspace_id); + ImGuiDockNodeFlags dockSpaceFlags = 0; + ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace); + ImGui::DockBuilderSetNodeSize(dockspace_id, ImGui::GetMainViewport()->Size); +} + +void Interface::InitWindow(int* argcp, char** argv) { + glfwSetErrorCallback(glfw_error_callback); + if (!glfwInit()) { + throw std::runtime_error("Failed to initialize GLFW"); + } + +#if defined(IMGUI_IMPL_OPENGL_ES2) + // GL ES 2.0 + GLSL 100 + const char* glsl_version = "#version 100"; + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); + glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API); +#elif defined(__APPLE__) + // GL 3.2 + GLSL 150 + const char* glsl_version = "#version 150"; + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 3.2+ only + glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // Required on Mac +#else + // GL 3.0 + GLSL 130 + const char* glsl_version = "#version 130"; + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); +#endif + + // todo add window flags again here + GLFWwindow* window = + glfwCreateWindow(1024, 512, m_windowTitle, nullptr, nullptr); + if (window == nullptr) { + throw std::runtime_error("Failed to create GLFW window"); + } + glfwMakeContextCurrent(window); + glfwSwapInterval(1); // Enable vsync + + // Setup Dear ImGui context + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); + (void)io; + io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + io.ConfigWindowsMoveFromTitleBarOnly = true; + + // Setup Platform/Renderer backend + ImGui_ImplGlfw_InitForOpenGL(window, true); + ImGui_ImplOpenGL3_Init(glsl_version); + + bool firstUse = true; + while (!glfwWindowShouldClose(window)) { + glfwPollEvents(); + + // Start the Dear ImGui frame + ImGui_ImplOpenGL3_NewFrame(); + ImGui_ImplGlfw_NewFrame(); + ImGui::NewFrame(); + ImGuiID dockspaceId = ImGui::DockSpaceOverViewport( + ImGui::GetMainViewport(), ImGuiDockNodeFlags_PassthruCentralNode); + + // if we are here for the first time, let's setup the workspace + if (firstUse) { + firstUse = false; + SetupWorkspace(dockspaceId); + ImGui::DockBuilderFinish(dockspaceId); + } + + MainDisplayCode(); + ImGui::Render(); + + int display_w, display_h; + glfwGetFramebufferSize(window, &display_w, &display_h); + glViewport(0, 0, display_w, display_h); + + glClearColor(m_clearColor[0] * m_clearColor[3], + m_clearColor[1] * m_clearColor[3], + m_clearColor[2] * m_clearColor[3], m_clearColor[3]); + + glClear(GL_COLOR_BUFFER_BIT); + ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + + // Update and Render additional Platform Windows + // (Platform functions may change the current OpenGL context, so we + // save/restore it to make it easier to paste this code elsewhere. + // For this specific demo app we could also call + // glfwMakeContextCurrent(window) directly) + if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) { + GLFWwindow* backup_current_context = glfwGetCurrentContext(); + ImGui::UpdatePlatformWindows(); + ImGui::RenderPlatformWindowsDefault(); + glfwMakeContextCurrent(backup_current_context); + } + + glfwSwapBuffers(window); + } + + // Cleanup + ImGui_ImplOpenGL3_Shutdown(); + ImGui_ImplGlfw_Shutdown(); + ImGui::DestroyContext(); + + glfwDestroyWindow(window); + glfwTerminate(); +} + +// window to define all the transducer settings +void Interface::TransducerWindow() { + ImGui::Begin("Transducer properties", &show_transducer_window); + + ImGui::Columns(1); + ImGui::InputFloat("Focal distance [mm]", trans->get_pfocalDistance()); + ImGui::SameLine(); + HelpMarker( + "Curvature radius of sensitive element, defines z level of focal plane"); + ImGui::InputFloat("Aperture radius [mm]", trans->get_prAperture()); + ImGui::SameLine(); + HelpMarker("Used to define the size of the cone where we reconstruct"); + ImGui::InputFloat("Hole radius [mm]", trans->get_rHole()); + ImGui::SameLine(); + HelpMarker("So far unused, lateron for sensnitivity field building."); + ImGui::End(); + + return; +} + +// defining all the reconstruction settings +void Interface::SettingsWindow() { + ImGui::Begin("Reconstruction settings", &show_settings_window); + + // cropping applied along x y and t + float tCropMicro[2]; + tCropMicro[0] = sett->get_cropTMin() * 1e3; // stored as ms, sett as micros + tCropMicro[1] = sett->get_cropTMax() * 1e3; // stored as ms, sett as micros + ImGui::InputFloat2("t cropping [micros]", &tCropMicro[0]); + sett->set_crop(0, tCropMicro[0] * 1e-3, tCropMicro[1] * 1e-3); // store as ms + ImGui::InputFloat2("x cropping [mm]", sett->get_pcropX()); + ImGui::InputFloat2("y cropping [mm]", sett->get_pcropY()); + float rMinMm = sett->get_rMin() * 1e3; + ImGui::InputFloat("min. r_recon [mm]", &rMinMm); + sett->set_rMin(rMinMm * 1e-3); + ImGui::SameLine(); + HelpMarker( + "This defines the lower limit which for the radius of the used arcs. " + "Usually this value should be close to the expected diffraction limit"); + ImGui::InputFloat("speed of sound [m/s]", sett->get_psos()); + ImGui::Checkbox("coherence factor weighting", sett->get_pflagCoherenceW()); + ImGui::SameLine(); + HelpMarker( + "If this option is enabled, each reconstructed voxel will be weighted by " + "the wave coherence. Can improve resolution and SNR."); + ImGui::Checkbox("sensitivity field weighting", sett->get_pflagSensW()); + ImGui::Checkbox("pulse-echo mode", sett->get_pflagUs()); + ImGui::SameLine(); + HelpMarker( + "While this program was originally developed to be used for OA image " + "reconstruction it can also be applied to US pulse echo measurements"); + ImGui::Checkbox("GPU", sett->get_pflagGpu()); + ImGui::SameLine(); + HelpMarker("Defines if we want to run the code on CPU or GPU"); + ImGui::End(); +} + +// interfacing window to conveniently load data from h5 files +void Interface::DataLoaderWindow() { + ImGui::Begin("Data Loader", &show_data_loader); + + if (ImGui::Button("Load data")) { + ImGuiFileDialog::Instance()->OpenDialog("ChooseFileDlgKey", "Choose File", + ".h5\0.mat\0", "."); + } + + if (ImGuiFileDialog::Instance()->Display("ChooseFileDlgKey")) { + if (ImGuiFileDialog::Instance()->IsOk() == true) { + std::string inputFilePath = + ImGuiFileDialog::Instance()->GetFilePathName(); + inputDataVol->ReadFromFile(inputFilePath); + // inputDataVol->calcMinMax(); + isDataSetDefined = true; // set flag to data loaded + + // after data loading, update the cropping values + for (uint8_t iDim = 0; iDim < 3; iDim++) { + sett->set_crop(iDim, inputDataVol->get_minPos(iDim) * 1e3, + inputDataVol->get_maxPos(iDim) * 1e3); + } + zCropRaw[0] = inputDataVol->get_minPos(0); + zCropRaw[1] = inputDataVol->get_maxPos(0); + xCropRaw[0] = inputDataVol->get_minPos(1); + xCropRaw[1] = inputDataVol->get_maxPos(1); + yCropRaw[0] = inputDataVol->get_minPos(2); + yCropRaw[1] = inputDataVol->get_maxPos(2); + } + ImGuiFileDialog::Instance()->Close(); + } + + if (isDataSetDefined) { + if (ImGui::CollapsingHeader("Data information")) { + ImGui::Columns(2); + // print information about dataset if defined + ImGui::Text("Size"); + ImGui::NextColumn(); + + ImGui::Text("%u x %u x %u", inputDataVol->get_dim(0), + inputDataVol->get_dim(1), inputDataVol->get_dim(2)); + ImGui::NextColumn(); + + ImGui::Text("Resolution [microm]"); + ImGui::NextColumn(); + ImGui::Text("%.2f x %.2f", inputDataVol->get_res(1) * 1e6, + inputDataVol->get_res(2) * 1e6); + ImGui::NextColumn(); + + ImGui::Text("Sampling freqeucny [MHz]"); + ImGui::NextColumn(); + ImGui::Text("%.2f", 1 / inputDataVol->get_res(0) / 1e6); + ImGui::NextColumn(); + + ImGui::Text("t range [micros]"); + ImGui::NextColumn(); + ImGui::Text("%.2f ... %.2f", inputDataVol->get_minPos(0) * 1e6, + inputDataVol->get_maxPos(0) * 1e6); + ImGui::NextColumn(); + + ImGui::Text("x range [mm]"); + ImGui::NextColumn(); + ImGui::Text("%.2f ... %.2f", inputDataVol->get_minPos(1) * 1e3, + inputDataVol->get_maxPos(1) * 1e3); + ImGui::NextColumn(); + + ImGui::Text("y range [mm]"); + ImGui::NextColumn(); + ImGui::Text("%.2f ... %.2f", inputDataVol->get_minPos(2) * 1e3, + inputDataVol->get_maxPos(2) * 1e3); + ImGui::NextColumn(); + + ImGui::Text("Maximum value"); + ImGui::NextColumn(); + ImGui::Text("%f", inputDataVol->get_minVal()); + ImGui::NextColumn(); + + ImGui::Text("Minimum value"); + ImGui::NextColumn(); + ImGui::Text("%f", inputDataVol->get_maxVal()); + ImGui::NextColumn(); + ImGui::Columns(1); + } + + // if (ImGui::CollapsingHeader("Raw Data slicer")) { + // ImGui::SliderInt("zLayer", &currSliceZ, 0, inputDataVol->get_dim(0) - + // 1); float tSlice = inputDataVol->get_pos(currSliceZ, 0); + // ImGui::Text("Time of current layer: %f micros", tSlice * 1e6); + // ImGui::Text("Approximated z layer: %f mm", tSlice * sett->get_sos()); + // ImImagesc(inputDataVol->get_psliceZ((uint64_t)currSliceZ), + // inputDataVol->get_dim(1), inputDataVol->get_dim(2), + // &inDataTexture, inDataMapper); + + // int width = 550; + // int height = (float)width / inputDataVol->get_length(1) * + // inputDataVol->get_length(2); + // ImGui::Image((void*)(intptr_t)inDataTexture, ImVec2(width, height)); + + // ImGui::SliderInt("yLayer", &currSliceY, 0, inputDataVol->get_dim(2) - + // 1); + + // ImImagesc(inputDataVol->get_psliceY((uint64_t)currSliceY), + // inputDataVol->get_dim(1), inputDataVol->get_dim(0), + // &inDataTextureSlice, inDataMapper); + // height = (float)width / inputDataVol->get_length(1) * + // inputDataVol->get_length(0) * sett->get_sos(); + // ImGui::Image((void*)(intptr_t)inDataTextureSlice, ImVec2(width, + // height)); + + // ImGui::SliderFloat("MinVal", inDataMapper.get_pminVal(), + // inputDataVol->get_minVal(), + // inputDataVol->get_maxVal(), + // "%.1f"); + // ImGui::SliderFloat("MaxVal", inDataMapper.get_pmaxVal(), + // inputDataVol->get_minVal(), + // inputDataVol->get_maxVal(), + // "%.1f"); + // ImGui::ColorEdit4("Min color", inDataMapper.get_pminCol(), + // ImGuiColorEditFlags_Float); + // ImGui::ColorEdit4("Max color", inDataMapper.get_pmaxCol(), + // ImGuiColorEditFlags_Float); + // } + + // if (ImGui::CollapsingHeader("MIP preview")) { + // zCropRawMm[0] = zCropRaw[0] * 1e6; + // zCropRawMm[1] = zCropRaw[1] * 1e6; + // xCropRawMm[0] = xCropRaw[0] * 1e3; + // xCropRawMm[1] = xCropRaw[1] * 1e3; + // yCropRawMm[0] = yCropRaw[0] * 1e3; + // yCropRawMm[1] = yCropRaw[1] * 1e3; + + // // read in users idea of z/x/y cropping + // ImGui::SliderFloat2("t crop [micros]", &zCropRawMm[0], + // inputDataVol->get_minPos(0) * 1e6, + // inputDataVol->get_maxPos(0) * 1e6); + // ImGui::SliderFloat2("x crop lower [mm]", &xCropRawMm[0], + // inputDataVol->get_minPos(1) * 1e3, + // inputDataVol->get_maxPos(1) * 1e3); + // ImGui::SliderFloat2("y crop upper [mm]", &yCropRawMm[0], + // inputDataVol->get_minPos(2) * 1e3, + // inputDataVol->get_maxPos(2) * 1e3); + // ImGui::SliderFloat("z stretch", &zStretchRaw, 0.5, 10); + + // zCropRaw[0] = zCropRawMm[0] * 1e-6; + // zCropRaw[1] = zCropRawMm[1] * 1e-6; + // xCropRaw[0] = xCropRawMm[0] * 1e-3; + // xCropRaw[1] = xCropRawMm[1] * 1e-3; + // yCropRaw[0] = yCropRawMm[0] * 1e-3; + // yCropRaw[1] = yCropRawMm[1] * 1e-3; + + // inputDataVol->set_cropRangeZ(&zCropRaw[0]); + // inputDataVol->set_cropRangeX(&xCropRaw[0]); + // inputDataVol->set_cropRangeY(&yCropRaw[0]); + + // // update cropped mips if something changed + // if (inputDataVol->get_updatedCropRange()) + // inputDataVol->calcCroppedMips(); + + // const float xStart = (xCropRaw[0] - inputDataVol->get_minPos(1)) / + // inputDataVol->get_length(1); + // const float xEnd = (xCropRaw[1] - inputDataVol->get_minPos(1)) / + // inputDataVol->get_length(1); + // const float yStart = (yCropRaw[0] - inputDataVol->get_minPos(2)) / + // inputDataVol->get_length(2); + // const float yEnd = (yCropRaw[1] - inputDataVol->get_minPos(2)) / + // inputDataVol->get_length(2); + + // const int width = 550; + // const int height = (float)(width) / + // (inputDataVol->get_length(1) * (xEnd - xStart)) * + // (inputDataVol->get_length(2) * (yEnd - yStart)); + + // // plot MIP alonmg z + // ImImagesc(inputDataVol->get_croppedMipZ(), inputDataVol->get_dim(1), + // inputDataVol->get_dim(2), &inDataMipZ, mipRawMapper); + // ImGui::Image((void*)(intptr_t)inDataMipZ, ImVec2(width, height), + // ImVec2(xStart, yStart), // lower corner to crop + // ImVec2(xEnd, yEnd)); // upper corner to crop + + // ImImagesc(inputDataVol->get_croppedMipY(), inputDataVol->get_dim(1), + // inputDataVol->get_dim(0), &inDataMipY, mipRawMapper); + + // // plot MIP along Y + // const float zStart = (zCropRaw[0] - inputDataVol->get_minPos(0)) / + // inputDataVol->get_length(0); + // const float zEnd = (zCropRaw[1] - inputDataVol->get_minPos(0)) / + // inputDataVol->get_length(0); + + // const int height2 = + // ((float)width) / (inputDataVol->get_length(1) * (xEnd - xStart)) * + // (inputDataVol->get_length(0) * sett->get_sos() * (zEnd - zStart)) * + // zStretchRaw; + // // printf("width: %d, height %d\n", width, height2); + + // ImGui::Image((void*)(intptr_t)inDataMipY, ImVec2(width, height2), + // ImVec2(xStart, zStart), // lower corner to crop + // ImVec2(xEnd, zEnd)); // upper corner to crop + + // // printf("min and max val crop: %f, %f\n", + // // inputDataVol->get_minValCrop(), + // // inputDataVol->get_maxValCrop()); + + // ImGui::SliderFloat("MinVal", mipRawMapper.get_pminVal(), + // inputDataVol->get_minValCrop(), + // inputDataVol->get_maxValCrop(), "%.1f"); + // ImGui::SliderFloat("MaxVal", mipRawMapper.get_pmaxVal(), + // inputDataVol->get_minValCrop(), + // inputDataVol->get_maxValCrop(), "%.1f"); + // ImGui::ColorEdit4("Min color", mipRawMapper.get_pminCol(), + // ImGuiColorEditFlags_Float); + // ImGui::ColorEdit4("Max color", mipRawMapper.get_pmaxCol(), + // ImGuiColorEditFlags_Float); + // } + } + + ImGui::End(); + + return; +} + +// helper function to display stuff +void Interface::ImImagesc(const float* data, const uint64_t sizex, + const uint64_t sizey, GLuint* out_texture, + const ColorMapper myCMap) { + glDeleteTextures(1, out_texture); + + // Create an OpenGL texture identifier + GLuint image_texture; + glGenTextures(1, &image_texture); + glBindTexture(GL_TEXTURE_2D, image_texture); + + // setup filtering parameters for display + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + // use color transfer function to convert from float to rgba + unsigned char* data_conv = new unsigned char[4 * sizex * sizey]; + myCMap.convert_to_map(data, sizex * sizey, data_conv); + glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, sizex, sizey, 0, GL_RGBA, + GL_UNSIGNED_BYTE, data_conv); + + // give pointer back to main program + *out_texture = image_texture; + delete[] data_conv; // free memory for temporary array + return; +} + +// starts the reconstruction and takes care of previews of reconstructed volumes +void Interface::ReconWindow() { + ImGui::Begin("Reconstruction", &show_recon_window); + + if (!recon.get_isRunning()) { + if (isReconRunning) { + reconThread.join(); + // if this is the first reconstruction we have done, update the cropping + if (isReconDone == 0) { + zCropRecon[0] = reconDataVol->get_minPos(0); + zCropRecon[1] = reconDataVol->get_maxPos(0); + xCropRecon[0] = reconDataVol->get_minPos(1); + xCropRecon[1] = reconDataVol->get_maxPos(1); + yCropRecon[0] = reconDataVol->get_minPos(2); + yCropRecon[1] = reconDataVol->get_maxPos(2); + } + + isReconDone = 1; + isReconRunning = 0; + + // // recalculate cropped mips and update value limits + // reconDataVol->calcCroppedMips(); + // reconMipMapper.set_minVal(reconDataVol->get_minValCrop()); + // reconMipMapper.set_maxVal(reconDataVol->get_maxValCrop()); + printf("Finished reconstruction procedure!\n"); + } + + // show reconstruct button if dataset was defined + if (!isDataSetDefined) { + ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true); + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5f); + } + + if (ImGui::Button("Reconstruct")) { + printf("Starting reconstruction procedure...\n"); + // recon.recon(); + // isReconRunning = 1; + // saft* reconPtr = &recon; // get pointer to our reconstruction + // procedure reconThread = reconPtr->recon2thread(); isReconRunning = 1; + } + + if (!isDataSetDefined) { + ImGui::PopItemFlag(); + ImGui::PopStyleVar(); + } + + } else // here we simply show a disabled button and a progress bar + { + ImGui::Text("Reconstruction in progress..."); + time_t tt; + tt = std::chrono::system_clock::to_time_t(recon.get_tStart()); + + ImGui::Text("Started reconstruction at: %s", ctime(&tt)); + ImGui::Text("Estimated time to arrival: %.1f seconds", recon.get_tRemain()); + ImGui::ProgressBar(recon.get_percDone() / 100); + } + + // if (isReconDone) // this means that the reconstruction was performed at + // least + // // once + // { + // ImGui::Text("Last reconstruction took %.1f seconds.", + // recon.get_reconTime()); + + // // slice preview through reconstruction + // if (ImGui::CollapsingHeader("Slice preview recon")) { + // // show some information about current depth + // ImGui::SliderInt("zLayer recon slice [ID]", &currSliceZRecon, 0, + // reconDataVol->get_dim(0) - 1); + // ImGui::SliderInt("yLayer recon slice [ID]", &currSliceYRecon, 0, + // reconDataVol->get_dim(2) - 1); + // const float zSlice = reconDataVol->get_pos(currSliceZRecon, 0); + // ImGui::Text("Depth of recon layer: %f mm", zSlice); + + // // generate the actual image along z normal plane + // ImImagesc(reconDataVol->get_psliceZ((uint64_t)currSliceZRecon), + // reconDataVol->get_dim(1), reconDataVol->get_dim(2), + // &reconSliceZ, reconDataMapper); + + // const int width = 550; + // const int height = (float)width / reconDataVol->get_length(1) * + // reconDataVol->get_length(2); + // ImGui::Image((void*)(intptr_t)reconSliceZ, ImVec2(width, height)); + + // // generate the actual image along y normal plane + // ImImagesc(reconDataVol->get_psliceY((uint64_t)currSliceYRecon), + // reconDataVol->get_dim(1), reconDataVol->get_dim(0), + // &reconSliceY, reconDataMapper); + + // const int height2 = (float)width / reconDataVol->get_length(1) * + // reconDataVol->get_length(0); + // ImGui::Image((void*)(intptr_t)reconSliceY, ImVec2(width, height2)); + + // ImGui::SliderFloat("Min val recon slice", + // reconDataMapper.get_pminVal(), + // reconDataVol->get_minVal(), + // reconDataVol->get_maxVal(), + // "%.1f"); + // ImGui::SliderFloat("Max val recon slice", + // reconDataMapper.get_pmaxVal(), + // reconDataVol->get_minVal(), + // reconDataVol->get_maxVal(), + // "%.1f"); + // ImGui::ColorEdit4("Min color recon slice", + // reconDataMapper.get_pminCol(), + // ImGuiColorEditFlags_Float); + // ImGui::ColorEdit4("Max color recon slice", + // reconDataMapper.get_pmaxCol(), + // ImGuiColorEditFlags_Float); + // } + + // if (ImGui::CollapsingHeader("MIP preview recon")) { + // zCropReconMm[0] = zCropRecon[0] * 1e3; + // zCropReconMm[1] = zCropRecon[1] * 1e3; + // xCropReconMm[0] = xCropRecon[0] * 1e3; + // xCropReconMm[1] = xCropRecon[1] * 1e3; + // yCropReconMm[0] = yCropRecon[0] * 1e3; + // yCropReconMm[1] = yCropRecon[1] * 1e3; + + // // read in users idea of z/x/y cropping + // ImGui::SliderFloat2("z crop recon [mm]", &zCropReconMm[0], + // reconDataVol->get_minPos(0) * 1e3, + // reconDataVol->get_maxPos(0) * 1e3); + // ImGui::SliderFloat2("x crop recpm [mm]", &xCropReconMm[0], + // reconDataVol->get_minPos(1) * 1e3, + // reconDataVol->get_maxPos(1) * 1e3); + // ImGui::SliderFloat2("y crop recon [mm]", &yCropReconMm[0], + // reconDataVol->get_minPos(2) * 1e3, + // reconDataVol->get_maxPos(2) * 1e3); + // ImGui::SliderFloat("z stretch recon", &zStretchRecon, 0.5, 10); + + // zCropRecon[0] = zCropReconMm[0] * 1e-3; + // zCropRecon[1] = zCropReconMm[1] * 1e-3; + // xCropRecon[0] = xCropReconMm[0] * 1e-3; + // xCropRecon[1] = xCropReconMm[1] * 1e-3; + // yCropRecon[0] = yCropReconMm[0] * 1e-3; + // yCropRecon[1] = yCropReconMm[1] * 1e-3; + + // // check if all are in a good valid order, otherwise make sure that + // they + // // are sorted + // if (zCropRecon[0] > zCropRecon[1]) { + // const float midValZ = (zCropRecon[0] + zCropRecon[1]) / 2; + // zCropRecon[0] = midValZ; + // zCropRecon[1] = midValZ; + // } + + // if (xCropRecon[0] > xCropRecon[1]) { + // const float midValX = (xCropRecon[0] + xCropRecon[1]) / 2; + // xCropRecon[0] = midValX; + // xCropRecon[1] = midValX; + // } + + // if (yCropRecon[0] > yCropRecon[1]) { + // const float midValY = (yCropRecon[0] + yCropRecon[1]) / 2; + // yCropRecon[0] = midValY; + // yCropRecon[1] = midValY; + // } + + // reconDataVol->set_cropRangeZ(&zCropRecon[0]); + // reconDataVol->set_cropRangeX(&xCropRecon[0]); + // reconDataVol->set_cropRangeY(&yCropRecon[0]); + + // // update cropped mips if something changed + // if (reconDataVol->get_updatedCropRange()) + // reconDataVol->calcCroppedMips(); + + // const float xStart = (xCropRecon[0] - reconDataVol->get_minPos(1)) / + // reconDataVol->get_length(1); + // const float xEnd = (xCropRecon[1] - reconDataVol->get_minPos(1)) / + // reconDataVol->get_length(1); + // const float yStart = (yCropRecon[0] - reconDataVol->get_minPos(2)) / + // reconDataVol->get_length(2); + // const float yEnd = (yCropRecon[1] - reconDataVol->get_minPos(2)) / + // reconDataVol->get_length(2); + + // const int width = 550; + // const int height = (float)(width) / + // (reconDataVol->get_length(1) * (xEnd - xStart)) * + // (reconDataVol->get_length(2) * (yEnd - yStart)); + + // // plot MIP with normal axis along z + // ImImagesc(reconDataVol->get_croppedMipZ(), reconDataVol->get_dim(1), + // reconDataVol->get_dim(2), &reconMipZ, reconMipMapper); + + // ImGui::Image((void*)(intptr_t)reconMipZ, ImVec2(width, height), + // ImVec2(xStart, yStart), // lower corner to crop + // ImVec2(xEnd, yEnd)); // upper corner to crop + + // ImImagesc(reconDataVol->get_croppedMipY(), reconDataVol->get_dim(1), + // reconDataVol->get_dim(0), &reconMipY, reconMipMapper); + + // // plot MIP along Y + // const float zStart = (zCropRecon[0] - reconDataVol->get_minPos(0)) / + // reconDataVol->get_length(0); + // const float zEnd = (zCropRecon[1] - reconDataVol->get_minPos(0)) / + // reconDataVol->get_length(0); + + // const int height2 = + // ((float)width) / (reconDataVol->get_length(1) * (xEnd - xStart)) * + // (reconDataVol->get_length(0) * (zEnd - zStart)) * zStretchRecon; + // // printf("width: %d, height %d\n", width, height2); + + // ImGui::Image((void*)(intptr_t)reconMipY, ImVec2(width, height2), + // ImVec2(xStart, zStart), // lower corner to crop + // ImVec2(xEnd, zEnd)); // upper corner to crop + + // ImGui::SliderFloat("MinVal MIP", reconMipMapper.get_pminVal(), + // reconDataVol->get_minValCrop(), + // reconDataVol->get_maxValCrop(), "%.1f"); + // ImGui::SliderFloat("MaxVal MIP", reconMipMapper.get_pmaxVal(), + // reconDataVol->get_minValCrop(), + // reconDataVol->get_maxValCrop(), "%.1f"); + // ImGui::ColorEdit4("Min color MIP", reconMipMapper.get_pminCol(), + // ImGuiColorEditFlags_Float); + // ImGui::ColorEdit4("Max color MIP", reconMipMapper.get_pmaxCol(), + // ImGuiColorEditFlags_Float); + // } + + // // this is not implemented yet but at some point we want to have an + // option + // // here to export the previews + // if (ImGui::CollapsingHeader("Preview export functions")) { + // static char filePath[64]; + // ImGui::InputText("Image path", filePath, 64); + // string filePathString = filePath; + + // if (ImGui::Button("Export as png")) { + // } + // } + + // if (ImGui::CollapsingHeader("Volumetric export functions")) { + // static char filePath[64]; + // ImGui::InputText("Volume path", filePath, 64); + // string filePathString = filePath; + + // ImGui::Columns(2); + // if (ImGui::Button("Export as vtk")) { + // reconDataVol->exportVtk(filePathString); + // } + // ImGui::NextColumn(); + // if (ImGui::Button("Export as h5")) { + // reconDataVol->saveToFile(filePath); + // } + // } + // } + + ImGui::End(); + return; +} + +// main loop which we run through to update ImGui +void Interface::MainDisplayCode() { + TransducerWindow(); + SettingsWindow(); + DataLoaderWindow(); + ReconWindow(); + + return; +} + +} // namespace opensaft \ No newline at end of file diff --git a/src/Interface.h b/src/Gui/Interface.h similarity index 97% rename from src/Interface.h rename to src/Gui/Interface.h index 863668e..1331a8e 100644 --- a/src/Interface.h +++ b/src/Gui/Interface.h @@ -7,10 +7,10 @@ // #include #include "ColorMapper.h" +#include "Memory/Volume.h" #include "ReconSettings.h" #include "Saft.h" #include "Transducer.h" -#include "volume.h" #include #include #include @@ -37,8 +37,8 @@ class Interface { Transducer* trans; ReconSettings* sett; // todo swithc this guy to unique_ptr - volume* inputDataVol; - volume* reconDataVol; + Volume* inputDataVol; + Volume* reconDataVol; // for input dataset vizualization // todo make slicer a separate class diff --git a/src/Interface.cpp b/src/Interface.cpp deleted file mode 100644 index 7c9ce03..0000000 --- a/src/Interface.cpp +++ /dev/null @@ -1,697 +0,0 @@ -#include "Interface.h" -#include -#include -#include - -namespace opensaft { - -static void glfw_error_callback(int error, const char* description) { - fprintf(stderr, "GLFW Error %d: %s\n", error, description); -} - -Interface::Interface() { - // get all subdefinitions of saft objects as pointers - trans = recon.get_ptrans(); // pointer to transducer class - sett = recon.get_psett(); // pointer to reconstruction settings - inputDataVol = recon.get_ppreprocData(); // pointer to preprocessed volume - reconDataVol = recon.get_preconData(); // pointer to reconstructed volume -} - -// displays a small help marker next to the text -static void HelpMarker(const char* desc) { - ImGui::TextDisabled("(?)"); - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); - ImGui::TextUnformatted(desc); - ImGui::PopTextWrapPos(); - ImGui::EndTooltip(); - } - return; -} - -void Interface::SetupWorkspace(ImGuiID& dockspace_id) { - ImGui::DockBuilderRemoveNode(dockspace_id); - ImGuiDockNodeFlags dockSpaceFlags = 0; - ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace); - ImGui::DockBuilderSetNodeSize(dockspace_id, ImGui::GetMainViewport()->Size); -} - -void Interface::InitWindow(int* argcp, char** argv) { - glfwSetErrorCallback(glfw_error_callback); - if (!glfwInit()) { - throw std::runtime_error("Failed to initialize GLFW"); - } - -#if defined(IMGUI_IMPL_OPENGL_ES2) - // GL ES 2.0 + GLSL 100 - const char* glsl_version = "#version 100"; - glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2); - glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); - glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API); -#elif defined(__APPLE__) - // GL 3.2 + GLSL 150 - const char* glsl_version = "#version 150"; - glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); - glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); - glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 3.2+ only - glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // Required on Mac -#else - // GL 3.0 + GLSL 130 - const char* glsl_version = "#version 130"; - glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); - glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); -#endif - - // todo add window flags again here - GLFWwindow* window = - glfwCreateWindow(1024, 512, m_windowTitle, nullptr, nullptr); - if (window == nullptr) { - throw std::runtime_error("Failed to create GLFW window"); - } - glfwMakeContextCurrent(window); - glfwSwapInterval(1); // Enable vsync - - // Setup Dear ImGui context - IMGUI_CHECKVERSION(); - ImGui::CreateContext(); - ImGuiIO& io = ImGui::GetIO(); - (void)io; - io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; - io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; - io.ConfigWindowsMoveFromTitleBarOnly = true; - - // Setup Platform/Renderer backend - ImGui_ImplGlfw_InitForOpenGL(window, true); - ImGui_ImplOpenGL3_Init(glsl_version); - - bool firstUse = true; - while (!glfwWindowShouldClose(window)) { - glfwPollEvents(); - - // Start the Dear ImGui frame - ImGui_ImplOpenGL3_NewFrame(); - ImGui_ImplGlfw_NewFrame(); - ImGui::NewFrame(); - ImGuiID dockspaceId = ImGui::DockSpaceOverViewport( - ImGui::GetMainViewport(), ImGuiDockNodeFlags_PassthruCentralNode); - - // if we are here for the first time, let's setup the workspace - if (firstUse) { - firstUse = false; - SetupWorkspace(dockspaceId); - ImGui::DockBuilderFinish(dockspaceId); - } - - MainDisplayCode(); - ImGui::Render(); - - int display_w, display_h; - glfwGetFramebufferSize(window, &display_w, &display_h); - glViewport(0, 0, display_w, display_h); - - glClearColor(m_clearColor[0] * m_clearColor[3], - m_clearColor[1] * m_clearColor[3], - m_clearColor[2] * m_clearColor[3], m_clearColor[3]); - - glClear(GL_COLOR_BUFFER_BIT); - ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); - - // Update and Render additional Platform Windows - // (Platform functions may change the current OpenGL context, so we - // save/restore it to make it easier to paste this code elsewhere. - // For this specific demo app we could also call - // glfwMakeContextCurrent(window) directly) - if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) { - GLFWwindow* backup_current_context = glfwGetCurrentContext(); - ImGui::UpdatePlatformWindows(); - ImGui::RenderPlatformWindowsDefault(); - glfwMakeContextCurrent(backup_current_context); - } - - glfwSwapBuffers(window); - } - - // Cleanup - ImGui_ImplOpenGL3_Shutdown(); - ImGui_ImplGlfw_Shutdown(); - ImGui::DestroyContext(); - - glfwDestroyWindow(window); - glfwTerminate(); -} - -// window to define all the transducer settings -void Interface::TransducerWindow() { - ImGui::Begin("Transducer properties", &show_transducer_window); - - ImGui::Columns(1); - ImGui::InputFloat("Focal distance [mm]", trans->get_pfocalDistance()); - ImGui::SameLine(); - HelpMarker( - "Curvature radius of sensitive element, defines z level of focal plane"); - ImGui::InputFloat("Aperture radius [mm]", trans->get_prAperture()); - ImGui::SameLine(); - HelpMarker("Used to define the size of the cone where we reconstruct"); - ImGui::InputFloat("Hole radius [mm]", trans->get_rHole()); - ImGui::SameLine(); - HelpMarker("So far unused, lateron for sensnitivity field building."); - ImGui::End(); - - return; -} - -// defining all the reconstruction settings -void Interface::SettingsWindow() { - ImGui::Begin("Reconstruction settings", &show_settings_window); - - // cropping applied along x y and t - float tCropMicro[2]; - tCropMicro[0] = sett->get_cropTMin() * 1e3; // stored as ms, sett as micros - tCropMicro[1] = sett->get_cropTMax() * 1e3; // stored as ms, sett as micros - ImGui::InputFloat2("t cropping [micros]", &tCropMicro[0]); - sett->set_crop(0, tCropMicro[0] * 1e-3, tCropMicro[1] * 1e-3); // store as ms - ImGui::InputFloat2("x cropping [mm]", sett->get_pcropX()); - ImGui::InputFloat2("y cropping [mm]", sett->get_pcropY()); - float rMinMm = sett->get_rMin() * 1e3; - ImGui::InputFloat("min. r_recon [mm]", &rMinMm); - sett->set_rMin(rMinMm * 1e-3); - ImGui::SameLine(); - HelpMarker( - "This defines the lower limit which for the radius of the used arcs. " - "Usually this value should be close to the expected diffraction limit"); - ImGui::InputFloat("speed of sound [m/s]", sett->get_psos()); - ImGui::Checkbox("coherence factor weighting", sett->get_pflagCoherenceW()); - ImGui::SameLine(); - HelpMarker( - "If this option is enabled, each reconstructed voxel will be weighted by " - "the wave coherence. Can improve resolution and SNR."); - ImGui::Checkbox("sensitivity field weighting", sett->get_pflagSensW()); - ImGui::Checkbox("pulse-echo mode", sett->get_pflagUs()); - ImGui::SameLine(); - HelpMarker( - "While this program was originally developed to be used for OA image " - "reconstruction it can also be applied to US pulse echo measurements"); - ImGui::Checkbox("GPU", sett->get_pflagGpu()); - ImGui::SameLine(); - HelpMarker("Defines if we want to run the code on CPU or GPU"); - ImGui::End(); -} - -// interfacing window to conveniently load data from h5 files -void Interface::DataLoaderWindow() { - ImGui::Begin("Data Loader", &show_data_loader); - - if (ImGui::Button("Load data")) { - ImGuiFileDialog::Instance()->OpenDialog("ChooseFileDlgKey", "Choose File", - ".h5\0.mat\0", "."); - } - - if (ImGuiFileDialog::Instance()->Display("ChooseFileDlgKey")) { - if (ImGuiFileDialog::Instance()->IsOk() == true) { - std::string inputFilePath = - ImGuiFileDialog::Instance()->GetFilePathName(); - inputDataVol->readFromFile(inputFilePath); - inputDataVol->calcMinMax(); - isDataSetDefined = 1; // set flag to data loaded - - // after data loading, update the cropping values - for (uint8_t iDim = 0; iDim < 3; iDim++) { - sett->set_crop(iDim, inputDataVol->get_minPos(iDim) * 1e3, - inputDataVol->get_maxPos(iDim) * 1e3); - } - zCropRaw[0] = inputDataVol->get_minPos(0); - zCropRaw[1] = inputDataVol->get_maxPos(0); - xCropRaw[0] = inputDataVol->get_minPos(1); - xCropRaw[1] = inputDataVol->get_maxPos(1); - yCropRaw[0] = inputDataVol->get_minPos(2); - yCropRaw[1] = inputDataVol->get_maxPos(2); - } - ImGuiFileDialog::Instance()->Close(); - } - - if (isDataSetDefined) { - if (ImGui::CollapsingHeader("Data information")) { - ImGui::Columns(2); - // print information about dataset if defined - ImGui::Text("Size"); - ImGui::NextColumn(); - - ImGui::Text("%i x %i x %i", inputDataVol->get_dim(0), - inputDataVol->get_dim(1), inputDataVol->get_dim(2)); - ImGui::NextColumn(); - - ImGui::Text("Resolution [microm]"); - ImGui::NextColumn(); - ImGui::Text("%.2f x %.2f", inputDataVol->get_res(1) * 1e6, - inputDataVol->get_res(2) * 1e6); - ImGui::NextColumn(); - - ImGui::Text("Sampling freqeucny [MHz]"); - ImGui::NextColumn(); - ImGui::Text("%.2f", 1 / inputDataVol->get_res(0) / 1e6); - ImGui::NextColumn(); - - ImGui::Text("t range [micros]"); - ImGui::NextColumn(); - ImGui::Text("%.2f ... %.2f", inputDataVol->get_minPos(0) * 1e6, - inputDataVol->get_maxPos(0) * 1e6); - ImGui::NextColumn(); - - ImGui::Text("x range [mm]"); - ImGui::NextColumn(); - ImGui::Text("%.2f ... %.2f", inputDataVol->get_minPos(1) * 1e3, - inputDataVol->get_maxPos(1) * 1e3); - ImGui::NextColumn(); - - ImGui::Text("y range [mm]"); - ImGui::NextColumn(); - ImGui::Text("%.2f ... %.2f", inputDataVol->get_minPos(2) * 1e3, - inputDataVol->get_maxPos(2) * 1e3); - ImGui::NextColumn(); - - ImGui::Text("Maximum value"); - ImGui::NextColumn(); - ImGui::Text("%f", inputDataVol->get_minVal()); - ImGui::NextColumn(); - - ImGui::Text("Minimum value"); - ImGui::NextColumn(); - ImGui::Text("%f", inputDataVol->get_maxVal()); - ImGui::NextColumn(); - ImGui::Columns(1); - } - - if (ImGui::CollapsingHeader("Raw Data slicer")) { - ImGui::SliderInt("zLayer", &currSliceZ, 0, inputDataVol->get_dim(0) - 1); - float tSlice = inputDataVol->get_pos(currSliceZ, 0); - ImGui::Text("Time of current layer: %f micros", tSlice * 1e6); - ImGui::Text("Approximated z layer: %f mm", tSlice * sett->get_sos()); - ImImagesc(inputDataVol->get_psliceZ((uint64_t)currSliceZ), - inputDataVol->get_dim(1), inputDataVol->get_dim(2), - &inDataTexture, inDataMapper); - - int width = 550; - int height = (float)width / inputDataVol->get_length(1) * - inputDataVol->get_length(2); - ImGui::Image((void*)(intptr_t)inDataTexture, ImVec2(width, height)); - - ImGui::SliderInt("yLayer", &currSliceY, 0, inputDataVol->get_dim(2) - 1); - - ImImagesc(inputDataVol->get_psliceY((uint64_t)currSliceY), - inputDataVol->get_dim(1), inputDataVol->get_dim(0), - &inDataTextureSlice, inDataMapper); - height = (float)width / inputDataVol->get_length(1) * - inputDataVol->get_length(0) * sett->get_sos(); - ImGui::Image((void*)(intptr_t)inDataTextureSlice, ImVec2(width, height)); - - ImGui::SliderFloat("MinVal", inDataMapper.get_pminVal(), - inputDataVol->get_minVal(), inputDataVol->get_maxVal(), - "%.1f"); - ImGui::SliderFloat("MaxVal", inDataMapper.get_pmaxVal(), - inputDataVol->get_minVal(), inputDataVol->get_maxVal(), - "%.1f"); - ImGui::ColorEdit4("Min color", inDataMapper.get_pminCol(), - ImGuiColorEditFlags_Float); - ImGui::ColorEdit4("Max color", inDataMapper.get_pmaxCol(), - ImGuiColorEditFlags_Float); - } - - if (ImGui::CollapsingHeader("MIP preview")) { - zCropRawMm[0] = zCropRaw[0] * 1e6; - zCropRawMm[1] = zCropRaw[1] * 1e6; - xCropRawMm[0] = xCropRaw[0] * 1e3; - xCropRawMm[1] = xCropRaw[1] * 1e3; - yCropRawMm[0] = yCropRaw[0] * 1e3; - yCropRawMm[1] = yCropRaw[1] * 1e3; - - // read in users idea of z/x/y cropping - ImGui::SliderFloat2("t crop [micros]", &zCropRawMm[0], - inputDataVol->get_minPos(0) * 1e6, - inputDataVol->get_maxPos(0) * 1e6); - ImGui::SliderFloat2("x crop lower [mm]", &xCropRawMm[0], - inputDataVol->get_minPos(1) * 1e3, - inputDataVol->get_maxPos(1) * 1e3); - ImGui::SliderFloat2("y crop upper [mm]", &yCropRawMm[0], - inputDataVol->get_minPos(2) * 1e3, - inputDataVol->get_maxPos(2) * 1e3); - ImGui::SliderFloat("z stretch", &zStretchRaw, 0.5, 10); - - zCropRaw[0] = zCropRawMm[0] * 1e-6; - zCropRaw[1] = zCropRawMm[1] * 1e-6; - xCropRaw[0] = xCropRawMm[0] * 1e-3; - xCropRaw[1] = xCropRawMm[1] * 1e-3; - yCropRaw[0] = yCropRawMm[0] * 1e-3; - yCropRaw[1] = yCropRawMm[1] * 1e-3; - - inputDataVol->set_cropRangeZ(&zCropRaw[0]); - inputDataVol->set_cropRangeX(&xCropRaw[0]); - inputDataVol->set_cropRangeY(&yCropRaw[0]); - - // update cropped mips if something changed - if (inputDataVol->get_updatedCropRange()) inputDataVol->calcCroppedMips(); - - const float xStart = (xCropRaw[0] - inputDataVol->get_minPos(1)) / - inputDataVol->get_length(1); - const float xEnd = (xCropRaw[1] - inputDataVol->get_minPos(1)) / - inputDataVol->get_length(1); - const float yStart = (yCropRaw[0] - inputDataVol->get_minPos(2)) / - inputDataVol->get_length(2); - const float yEnd = (yCropRaw[1] - inputDataVol->get_minPos(2)) / - inputDataVol->get_length(2); - - const int width = 550; - const int height = (float)(width) / - (inputDataVol->get_length(1) * (xEnd - xStart)) * - (inputDataVol->get_length(2) * (yEnd - yStart)); - - // plot MIP alonmg z - ImImagesc(inputDataVol->get_croppedMipZ(), inputDataVol->get_dim(1), - inputDataVol->get_dim(2), &inDataMipZ, mipRawMapper); - ImGui::Image((void*)(intptr_t)inDataMipZ, ImVec2(width, height), - ImVec2(xStart, yStart), // lower corner to crop - ImVec2(xEnd, yEnd)); // upper corner to crop - - ImImagesc(inputDataVol->get_croppedMipY(), inputDataVol->get_dim(1), - inputDataVol->get_dim(0), &inDataMipY, mipRawMapper); - - // plot MIP along Y - const float zStart = (zCropRaw[0] - inputDataVol->get_minPos(0)) / - inputDataVol->get_length(0); - const float zEnd = (zCropRaw[1] - inputDataVol->get_minPos(0)) / - inputDataVol->get_length(0); - - const int height2 = - ((float)width) / (inputDataVol->get_length(1) * (xEnd - xStart)) * - (inputDataVol->get_length(0) * sett->get_sos() * (zEnd - zStart)) * - zStretchRaw; - // printf("width: %d, height %d\n", width, height2); - - ImGui::Image((void*)(intptr_t)inDataMipY, ImVec2(width, height2), - ImVec2(xStart, zStart), // lower corner to crop - ImVec2(xEnd, zEnd)); // upper corner to crop - - // printf("min and max val crop: %f, %f\n", - // inputDataVol->get_minValCrop(), - // inputDataVol->get_maxValCrop()); - - ImGui::SliderFloat("MinVal", mipRawMapper.get_pminVal(), - inputDataVol->get_minValCrop(), - inputDataVol->get_maxValCrop(), "%.1f"); - ImGui::SliderFloat("MaxVal", mipRawMapper.get_pmaxVal(), - inputDataVol->get_minValCrop(), - inputDataVol->get_maxValCrop(), "%.1f"); - ImGui::ColorEdit4("Min color", mipRawMapper.get_pminCol(), - ImGuiColorEditFlags_Float); - ImGui::ColorEdit4("Max color", mipRawMapper.get_pmaxCol(), - ImGuiColorEditFlags_Float); - } - } - - ImGui::End(); - - return; -} - -// helper function to display stuff -void Interface::ImImagesc(const float* data, const uint64_t sizex, - const uint64_t sizey, GLuint* out_texture, - const ColorMapper myCMap) { - glDeleteTextures(1, out_texture); - - // Create an OpenGL texture identifier - GLuint image_texture; - glGenTextures(1, &image_texture); - glBindTexture(GL_TEXTURE_2D, image_texture); - - // setup filtering parameters for display - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - - // use color transfer function to convert from float to rgba - unsigned char* data_conv = new unsigned char[4 * sizex * sizey]; - myCMap.convert_to_map(data, sizex * sizey, data_conv); - glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, sizex, sizey, 0, GL_RGBA, - GL_UNSIGNED_BYTE, data_conv); - - // give pointer back to main program - *out_texture = image_texture; - delete[] data_conv; // free memory for temporary array - return; -} - -// starts the reconstruction and takes care of previews of reconstructed volumes -void Interface::ReconWindow() { - ImGui::Begin("Reconstruction", &show_recon_window); - - if (!recon.get_isRunning()) { - if (isReconRunning) { - reconThread.join(); - // if this is the first reconstruction we have done, update the cropping - if (isReconDone == 0) { - zCropRecon[0] = reconDataVol->get_minPos(0); - zCropRecon[1] = reconDataVol->get_maxPos(0); - xCropRecon[0] = reconDataVol->get_minPos(1); - xCropRecon[1] = reconDataVol->get_maxPos(1); - yCropRecon[0] = reconDataVol->get_minPos(2); - yCropRecon[1] = reconDataVol->get_maxPos(2); - } - - isReconDone = 1; - isReconRunning = 0; - - // recalculate cropped mips and update value limits - reconDataVol->calcCroppedMips(); - reconMipMapper.set_minVal(reconDataVol->get_minValCrop()); - reconMipMapper.set_maxVal(reconDataVol->get_maxValCrop()); - printf("Finished reconstruction procedure!\n"); - } - - // show reconstruct button if dataset was defined - if (!isDataSetDefined) { - ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true); - ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5f); - } - - if (ImGui::Button("Reconstruct")) { - printf("Starting reconstruction procedure...\n"); - // recon.recon(); - // isReconRunning = 1; - // saft* reconPtr = &recon; // get pointer to our reconstruction procedure - // reconThread = reconPtr->recon2thread(); - // isReconRunning = 1; - } - - if (!isDataSetDefined) { - ImGui::PopItemFlag(); - ImGui::PopStyleVar(); - } - - } else // here we simply show a disabled button and a progress bar - { - ImGui::Text("Reconstruction in progress..."); - time_t tt; - tt = std::chrono::system_clock::to_time_t(recon.get_tStart()); - - ImGui::Text("Started reconstruction at: %s", ctime(&tt)); - ImGui::Text("Estimated time to arrival: %.1f seconds", recon.get_tRemain()); - ImGui::ProgressBar(recon.get_percDone() / 100); - } - - if (isReconDone) // this means that the reconstruction was performed at least - // once - { - ImGui::Text("Last reconstruction took %.1f seconds.", - recon.get_reconTime()); - - // slice preview through reconstruction - if (ImGui::CollapsingHeader("Slice preview recon")) { - // show some information about current depth - ImGui::SliderInt("zLayer recon slice [ID]", &currSliceZRecon, 0, - reconDataVol->get_dim(0) - 1); - ImGui::SliderInt("yLayer recon slice [ID]", &currSliceYRecon, 0, - reconDataVol->get_dim(2) - 1); - const float zSlice = reconDataVol->get_pos(currSliceZRecon, 0); - ImGui::Text("Depth of recon layer: %f mm", zSlice); - - // generate the actual image along z normal plane - ImImagesc(reconDataVol->get_psliceZ((uint64_t)currSliceZRecon), - reconDataVol->get_dim(1), reconDataVol->get_dim(2), - &reconSliceZ, reconDataMapper); - - const int width = 550; - const int height = (float)width / reconDataVol->get_length(1) * - reconDataVol->get_length(2); - ImGui::Image((void*)(intptr_t)reconSliceZ, ImVec2(width, height)); - - // generate the actual image along y normal plane - ImImagesc(reconDataVol->get_psliceY((uint64_t)currSliceYRecon), - reconDataVol->get_dim(1), reconDataVol->get_dim(0), - &reconSliceY, reconDataMapper); - - const int height2 = (float)width / reconDataVol->get_length(1) * - reconDataVol->get_length(0); - ImGui::Image((void*)(intptr_t)reconSliceY, ImVec2(width, height2)); - - ImGui::SliderFloat("Min val recon slice", reconDataMapper.get_pminVal(), - reconDataVol->get_minVal(), reconDataVol->get_maxVal(), - "%.1f"); - ImGui::SliderFloat("Max val recon slice", reconDataMapper.get_pmaxVal(), - reconDataVol->get_minVal(), reconDataVol->get_maxVal(), - "%.1f"); - ImGui::ColorEdit4("Min color recon slice", reconDataMapper.get_pminCol(), - ImGuiColorEditFlags_Float); - ImGui::ColorEdit4("Max color recon slice", reconDataMapper.get_pmaxCol(), - ImGuiColorEditFlags_Float); - } - - if (ImGui::CollapsingHeader("MIP preview recon")) { - zCropReconMm[0] = zCropRecon[0] * 1e3; - zCropReconMm[1] = zCropRecon[1] * 1e3; - xCropReconMm[0] = xCropRecon[0] * 1e3; - xCropReconMm[1] = xCropRecon[1] * 1e3; - yCropReconMm[0] = yCropRecon[0] * 1e3; - yCropReconMm[1] = yCropRecon[1] * 1e3; - - // read in users idea of z/x/y cropping - ImGui::SliderFloat2("z crop recon [mm]", &zCropReconMm[0], - reconDataVol->get_minPos(0) * 1e3, - reconDataVol->get_maxPos(0) * 1e3); - ImGui::SliderFloat2("x crop recpm [mm]", &xCropReconMm[0], - reconDataVol->get_minPos(1) * 1e3, - reconDataVol->get_maxPos(1) * 1e3); - ImGui::SliderFloat2("y crop recon [mm]", &yCropReconMm[0], - reconDataVol->get_minPos(2) * 1e3, - reconDataVol->get_maxPos(2) * 1e3); - ImGui::SliderFloat("z stretch recon", &zStretchRecon, 0.5, 10); - - zCropRecon[0] = zCropReconMm[0] * 1e-3; - zCropRecon[1] = zCropReconMm[1] * 1e-3; - xCropRecon[0] = xCropReconMm[0] * 1e-3; - xCropRecon[1] = xCropReconMm[1] * 1e-3; - yCropRecon[0] = yCropReconMm[0] * 1e-3; - yCropRecon[1] = yCropReconMm[1] * 1e-3; - - // check if all are in a good valid order, otherwise make sure that they - // are sorted - if (zCropRecon[0] > zCropRecon[1]) { - const float midValZ = (zCropRecon[0] + zCropRecon[1]) / 2; - zCropRecon[0] = midValZ; - zCropRecon[1] = midValZ; - } - - if (xCropRecon[0] > xCropRecon[1]) { - const float midValX = (xCropRecon[0] + xCropRecon[1]) / 2; - xCropRecon[0] = midValX; - xCropRecon[1] = midValX; - } - - if (yCropRecon[0] > yCropRecon[1]) { - const float midValY = (yCropRecon[0] + yCropRecon[1]) / 2; - yCropRecon[0] = midValY; - yCropRecon[1] = midValY; - } - - reconDataVol->set_cropRangeZ(&zCropRecon[0]); - reconDataVol->set_cropRangeX(&xCropRecon[0]); - reconDataVol->set_cropRangeY(&yCropRecon[0]); - - // update cropped mips if something changed - if (reconDataVol->get_updatedCropRange()) reconDataVol->calcCroppedMips(); - - const float xStart = (xCropRecon[0] - reconDataVol->get_minPos(1)) / - reconDataVol->get_length(1); - const float xEnd = (xCropRecon[1] - reconDataVol->get_minPos(1)) / - reconDataVol->get_length(1); - const float yStart = (yCropRecon[0] - reconDataVol->get_minPos(2)) / - reconDataVol->get_length(2); - const float yEnd = (yCropRecon[1] - reconDataVol->get_minPos(2)) / - reconDataVol->get_length(2); - - const int width = 550; - const int height = (float)(width) / - (reconDataVol->get_length(1) * (xEnd - xStart)) * - (reconDataVol->get_length(2) * (yEnd - yStart)); - - // plot MIP with normal axis along z - ImImagesc(reconDataVol->get_croppedMipZ(), reconDataVol->get_dim(1), - reconDataVol->get_dim(2), &reconMipZ, reconMipMapper); - - ImGui::Image((void*)(intptr_t)reconMipZ, ImVec2(width, height), - ImVec2(xStart, yStart), // lower corner to crop - ImVec2(xEnd, yEnd)); // upper corner to crop - - ImImagesc(reconDataVol->get_croppedMipY(), reconDataVol->get_dim(1), - reconDataVol->get_dim(0), &reconMipY, reconMipMapper); - - // plot MIP along Y - const float zStart = (zCropRecon[0] - reconDataVol->get_minPos(0)) / - reconDataVol->get_length(0); - const float zEnd = (zCropRecon[1] - reconDataVol->get_minPos(0)) / - reconDataVol->get_length(0); - - const int height2 = - ((float)width) / (reconDataVol->get_length(1) * (xEnd - xStart)) * - (reconDataVol->get_length(0) * (zEnd - zStart)) * zStretchRecon; - // printf("width: %d, height %d\n", width, height2); - - ImGui::Image((void*)(intptr_t)reconMipY, ImVec2(width, height2), - ImVec2(xStart, zStart), // lower corner to crop - ImVec2(xEnd, zEnd)); // upper corner to crop - - ImGui::SliderFloat("MinVal MIP", reconMipMapper.get_pminVal(), - reconDataVol->get_minValCrop(), - reconDataVol->get_maxValCrop(), "%.1f"); - ImGui::SliderFloat("MaxVal MIP", reconMipMapper.get_pmaxVal(), - reconDataVol->get_minValCrop(), - reconDataVol->get_maxValCrop(), "%.1f"); - ImGui::ColorEdit4("Min color MIP", reconMipMapper.get_pminCol(), - ImGuiColorEditFlags_Float); - ImGui::ColorEdit4("Max color MIP", reconMipMapper.get_pmaxCol(), - ImGuiColorEditFlags_Float); - } - - // this is not implemented yet but at some point we want to have an option - // here to export the previews - if (ImGui::CollapsingHeader("Preview export functions")) { - static char filePath[64]; - ImGui::InputText("Image path", filePath, 64); - string filePathString = filePath; - - if (ImGui::Button("Export as png")) { - } - } - - if (ImGui::CollapsingHeader("Volumetric export functions")) { - static char filePath[64]; - ImGui::InputText("Volume path", filePath, 64); - string filePathString = filePath; - - ImGui::Columns(2); - if (ImGui::Button("Export as vtk")) { - reconDataVol->exportVtk(filePathString); - } - ImGui::NextColumn(); - if (ImGui::Button("Export as h5")) { - reconDataVol->saveToFile(filePath); - } - } - } - - ImGui::End(); - return; -} - -// main loop which we run through to update ImGui -void Interface::MainDisplayCode() { - TransducerWindow(); - SettingsWindow(); - DataLoaderWindow(); - ReconWindow(); - - return; -} - - -} \ No newline at end of file diff --git a/src/Memory/CMakeLists.txt b/src/Memory/CMakeLists.txt new file mode 100644 index 0000000..32db885 --- /dev/null +++ b/src/Memory/CMakeLists.txt @@ -0,0 +1,8 @@ +target_sources(opensaft +PUBLIC + UltrasoundSignals.h + Volume.h +PRIVATE + Volume.cpp + UltrasoundSignals.cpp + ) \ No newline at end of file diff --git a/src/Memory/UltrasoundSignals.cpp b/src/Memory/UltrasoundSignals.cpp new file mode 100644 index 0000000..d801d68 --- /dev/null +++ b/src/Memory/UltrasoundSignals.cpp @@ -0,0 +1,19 @@ +#include "Memory/UltrasoundSignals.h" + +namespace opensaft { + +void TimeSignal::RemoveDC() { + // calculate mean + double mean = 0.0; + for (size_t i = 0; i < this->size(); i++) { + mean += this->at(i); + } + mean /= this->size(); + + // remove mean + for (size_t i = 0; i < this->size(); i++) { + this->at(i) -= mean; + } +} + +} // namespace opensaft \ No newline at end of file diff --git a/src/Memory/UltrasoundSignals.h b/src/Memory/UltrasoundSignals.h new file mode 100644 index 0000000..8838e5b --- /dev/null +++ b/src/Memory/UltrasoundSignals.h @@ -0,0 +1,34 @@ + +#include "VectorN.h" +#include + +#pragma once + +namespace opensaft { +struct TimeSignalProperties { + float sampleRate{1e9f}; //!< sampling rate of the acquisition [Hz] + float deltaT{0.0f}; //!< time between laser emission & first sample [sec] + Float3 pos{0.0f}; //!< transducer position in global coordinate system +}; + +class TimeSignal : public std::vector, TimeSignalProperties { +public: + TimeSignal() = default; + TimeSignal(const std::size_t nt) : std::vector(nt, 0.0f) {} + + /// removes the DC content of a signal so that its mean is zero + void RemoveDC(); + +private: +}; + +struct AcquisitionProperties {}; + +class UltrasoundSignals : public std::vector, + AcquisitionProperties { + +public: +private: +}; + +} // namespace opensaft \ No newline at end of file diff --git a/src/Memory/Volume.cpp b/src/Memory/Volume.cpp new file mode 100644 index 0000000..6b1f251 --- /dev/null +++ b/src/Memory/Volume.cpp @@ -0,0 +1,12 @@ + + +#include "Memory/Volume.h" + +namespace opensaft { + +void Volume::ReadFromFile(const std::string& filePath) { + // make sure that file extension is hdf + +} + +} // namespace opensaft \ No newline at end of file diff --git a/src/Memory/Volume.h b/src/Memory/Volume.h new file mode 100644 index 0000000..1b7e28b --- /dev/null +++ b/src/Memory/Volume.h @@ -0,0 +1,119 @@ + +#include "VectorN.h" +#include +#include +#include + +#pragma once + +namespace opensaft { + +enum class Dimension { X, Y, Z }; + +class Volume : public std::vector { +public: + Volume(const Size3& dims) + : std::vector(dims.InnerProduct(), 0.0f), m_dims(dims) {} + + /// volumetric access operators (const and non const) + [[nodiscard]] float& operator()(const std::size_t x, const std::size_t y, + const std::size_t z) { + return at(x + m_dims[0] * (y + m_dims[1] * z)); + } + + [[nodiscard]] float operator()(const std::size_t x, const std::size_t y, + const std::size_t z) const { + return at(x + m_dims[0] * (y + m_dims[1] * z)); + } + + void operator=(const float& setPoint) { + for (auto& elem : *this) + elem = setPoint; + } + + /// \return side length along that dimension + [[nodiscard]] float get_length(const uint8_t iDim) const { + return m_res[iDim] * m_dims[iDim]; + } + + /// \return the lower edge of the first voxel along that dimension + [[nodiscard]] float get_minPos(const uint8_t iDim) const { + return m_center[iDim] - get_length(iDim) * 0.5f; + } + + /// \return the upper edge of the last voxel along that dimension + [[nodiscard]] float get_maxPos(const uint8_t iDim) const { + return m_center[iDim] + get_length(iDim) * 0.5f; + } + + /// \returns the minimum value held by the data vector + [[nodiscard]] float get_minVal() const { + return *std::min_element(begin(), end()); + } + + /// \returns the maximum value held by the data vector + [[nodiscard]] float get_maxVal() const { + return *std::max_element(begin(), end()); + } + + /// \returns the resolution of the vector along xzy + [[nodiscard]] Float3 get_res() const { return m_res; } + + /// \returns the resolution of the voxel along a specified dimension + [[nodiscard]] float get_res(const uint8_t iDim) const { return m_res[iDim]; } + + /// \returns the dimensions of the volume as a 3 elem vector + [[nodiscard]] Size3 get_dims() const { return m_dims; } + + /// \returns the size of the volume in voxels along a certain dimension + [[nodiscard]] std::size_t get_dim(const uint8_t iDim) const { + return m_dims[iDim]; + } + + [[nodiscard]] float get_pos(const std::size_t idx, const uint8_t iDim) const { + return get_minPos(iDim) + get_res(iDim) * (idx + 0.5f); + } + + [[nodiscard]] Float3 get_pos(const Size3 idx) const { + return {get_pos(idx[0], Dimension::X), get_pos(idx[1], Dimension::Y), + get_pos(idx[2], Dimension::Z)}; + } + + [[nodiscard]] float get_pos(const std::size_t idx, + const Dimension iDim) const { + switch (iDim) { + case Dimension::X: + return get_pos(idx, 0); + case Dimension::Y: + return get_pos(idx, 1); + case Dimension::Z: + return get_pos(idx, 2); + } + } + + [[nodiscard]] Float3 get_center() const { return m_center; } + + [[nodiscard]] bool operator==(const Volume& other) { + if (m_center != other.get_center()) + return false; + if (m_res != other.get_res()) + return false; + if (m_dims != other.get_dims()) + return false; + for (std::size_t iElem = 0; iElem < size(); iElem++) { + if ((*this)[iElem] != other[iElem]) + return false; + } + return true; + } + + /// \returns reads the volume from a file (shouldbe externalized later) + void ReadFromFile(const std::string& filePath); + +private: + Float3 m_center{0.0f}; ///!< the center of the 3D volume + Float3 m_res{1.0f}; ///!< the resolution of the voxels in xyz + const Size3 m_dims; ///!< number of voxels along each dimension +}; + +} // namespace opensaft diff --git a/src/Saft.cpp b/src/Saft.cpp index e2f3a33..e1b76ff 100644 --- a/src/Saft.cpp +++ b/src/Saft.cpp @@ -1,138 +1,89 @@ #include "Saft.h" #include "Util/Logger.h" +#include "Util/Timer.h" #include -#include -#include namespace opensaft { -Saft::Saft() { - processor_count = std::thread::hardware_concurrency(); - Logger::Log( - std::format("[Saft] Found {} processor units...", processor_count)); +Saft::Saft() : m_processorCount(std::thread::hardware_concurrency()) { + Log(std::format("Found {} processor units...", m_processorCount)); } -// crop the image to the field of view -void Saft::crop() { - // check if all max values are bigger then the minimum values, otherwise swap - // them - sett.sortCropping(); - - // find index closest to minimum cropping positions - uint64_t idxMin[3]; - uint64_t idxMax[3]; - for (uint8_t iDim = 0; iDim < 3; iDim++) { - idxMin[iDim] = preprocData.get_idx(sett.get_cropMin(iDim) * 1e-3, iDim); - // printf("min idx along %d is %d \n", iDim, idxMin[iDim]); - // find index closest to maximum cropping positions - idxMax[iDim] = preprocData.get_idx(sett.get_cropMax(iDim) * 1e-3, iDim); - // printf("max idx alomg %d is %d \n", iDim, idxMax[iDim]); - } +// for each a scan first calculate the mean and then substract if from the +// vector +void Saft::RemoveDC() { - // push res origin and size over from original dataset - for (uint8_t iDim = 0; iDim < 3; iDim++) { - croppedData.set_origin(iDim, // push origin - preprocData.get_pos(idxMin[iDim], iDim)); + Log("Removing DC offset... "); + Timer T; + for (auto& sigs : *m_measuredData) + sigs.RemoveDC(); - croppedData.set_res(iDim, // push resolution - preprocData.get_res(iDim)); - // push new size - croppedData.set_dim(iDim, idxMax[iDim] - idxMin[iDim] + 1); - } - croppedData.alloc_memory(); - - // fill freshly allocated memory - uint64_t runIdx = 0; -#pragma unroll - for (uint64_t iy = idxMin[2]; iy < idxMax[2]; iy++) { - const uint64_t iyRel = iy - idxMin[2]; -#pragma unroll - for (uint64_t ix = idxMin[1]; ix < idxMax[1]; ix++) { - const uint64_t ixRel = ix - idxMin[1]; -#pragma unroll - for (uint64_t it = idxMin[0]; it < idxMax[0]; it++) { - const uint64_t itRel = it - idxMin[0]; - croppedData.set_value(itRel, ixRel, iyRel, - preprocData.get_value(it, ix, iy)); - runIdx++; - } - } + T.Stop(); + Log(std::format("DC offset removed in {} seconds", T.GetElapsedTime())); +} + +void Saft::Launch() { + // if input data was not specified, we cannot do anything + if (!m_measuredData.has_value()) { + Log(LogLevel::Warning, "No data available for reconstruction!"); + return; } - return; + m_start = std::chrono::high_resolution_clock::now(); + m_isRunning = true; + Log("Starting reconstruction..."); + m_reconThread = std::thread(&Saft::Recon, this); } -void* RemoveDc(void* threadarg) { - struct dcData* myData; - myData = (struct dcData*)threadarg; - -#pragma unroll - for (uint64_t iVec = myData->iVecStart; iVec <= myData->iVecEnd; iVec++) { - // calculate the full sum over the vector - float sum = 0; - uint64_t startIdx = iVec * myData->nT; - for (uint64_t iT = 0; iT < myData->nT; iT++) { - sum += myData->ptr[startIdx]; - startIdx++; - } - sum = sum / ((float)myData->nT); - - startIdx = iVec * myData->nT; - for (uint64_t iT = 0; iT < myData->nT; iT++) { - myData->ptr[startIdx] = myData->ptr[startIdx] - sum; - startIdx++; - } +void Saft::Recon() { + + m_percDone = 0.0f; + + // remove the DC component + RemoveDC(); + + // for now we just sleep a bit, then finish + for (std::size_t iRound = 0; iRound < 100; iRound++) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + m_percDone = static_cast(iRound); + // todo here we need to insert some sort of calculation } - pthread_exit(NULL); + + // indicate that we are finished and calculate reconstruction time + m_isRunning = false; + m_percDone = 100.0f; + auto end = std::chrono::high_resolution_clock::now(); + std::chrono::duration elapsed = end - m_start; + m_reconTime = elapsed.count(); } -// for each a scan first calculate the mean and then substract if from the -// vector -void Saft::remove_dc() { - Logger::Log("[Saft] Removing DC offset... "); - - pthread_t threads[processor_count]; - pthread_attr_t attr; - void* status; - - pthread_attr_init(&attr); - pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE); - - struct dcData td[processor_count]; - - uint64_t nVecs = croppedData.get_dim(1) * croppedData.get_dim(2); - const uint64_t nProcessorVecs = ceil((float)nVecs / ((float)processor_count)); - - // start individual threads taking care of dc removal - for (uint64_t iProcessor = 0; iProcessor < processor_count; iProcessor++) { - td[iProcessor].threadId = iProcessor; - td[iProcessor].iVecStart = iProcessor * nProcessorVecs; - td[iProcessor].iVecEnd = (iProcessor + 1) * nProcessorVecs; - if (td[iProcessor].iVecEnd >= nVecs) { - td[iProcessor].iVecEnd = nVecs - 1; - } - td[iProcessor].nT = croppedData.get_dim(0); - td[iProcessor].ptr = croppedData.get_pdata(); - - int rc = pthread_create(&threads[iProcessor], &attr, RemoveDc, - (void*)&td[iProcessor]); - if (rc) { - cout << "Error:unable to create thread," << rc << endl; - exit(-1); - } +void Saft::Wait() { + while (m_isRunning) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + Log(std::format("Reconstruction status at {}%", get_percDone())); } - // wait for ecxecution - pthread_attr_destroy(&attr); - for (uint64_t iProcessor = 0; iProcessor < processor_count; iProcessor++) { - int rc = pthread_join(threads[iProcessor], &status); - if (rc) { - cout << "Error: unable to join," << rc << endl; - exit(-1); - } + if (m_reconThread.joinable()) { + m_reconThread.join(); } - Logger::Log("[SAFT} DC offset removed..."); + Log(std::format("Reconstruction finished after {} seconds", m_reconTime)); +} + +std::optional Saft::GetVolume() const { + if (!m_reconData.has_value()) { + Log(LogLevel::Warning, "No volume data available"); + } + return m_reconData; +} + +float Saft::CalculateVoxel(Size3 idx) { + // calculates the response for a single position in the volume + // todo not implemented yet + // + Float3 posOutput = m_reconData.value().get_pos(idx); + + return 0.0f; } } // namespace opensaft \ No newline at end of file diff --git a/src/Saft.cu b/src/Saft.cu index 36f56d2..e21739e 100644 --- a/src/Saft.cu +++ b/src/Saft.cu @@ -1,4 +1,5 @@ #include "Saft.cuh" +#include "Saft.h" namespace opensaft { @@ -100,7 +101,7 @@ SAFT(float* outputVol, // reconstructed output volume [it, ix, iy] // run the kernel but on the CPU void Saft::saft_cpu() { - // declare a few varaibles for improved readabilit + // declare a few varaibles for improved readability const uint32_t nT = croppedData.get_dim(0); const uint32_t nX = croppedData.get_dim(1); const uint32_t nY = croppedData.get_dim(2); diff --git a/src/Saft.h b/src/Saft.h index 2e1aec8..4fb9a45 100644 --- a/src/Saft.h +++ b/src/Saft.h @@ -1,15 +1,12 @@ - -#pragma once - +#include "Memory/UltrasoundSignals.h" +#include "Memory/Volume.h" #include "ReconSettings.h" #include "Transducer.h" -#include "volume.h" -#include // used to stop time which is required for execution -#include -#include +#include "Util/Logger.h" +#include #include -using namespace std; +#pragma once namespace opensaft { @@ -21,52 +18,55 @@ struct dcData { float* ptr; }; -class Saft { +class Saft : LoggingClass { public: // class constructor Saft(); - Transducer* get_ptrans() { return &trans; }; - ReconSettings* get_psett() { return &sett; }; - volume* get_ppreprocData() { return &preprocData; }; - volume* get_preconData() { return &reconData; }; - volume* get_pcroppedData() { return &croppedData; }; + /// runs the actual reconstruction in a separate thread + void Launch(); + + /// waiting for the reconstruction to finish + void Wait(); + + /// define the input data for the reconstruction + void SetInput(UltrasoundSignals&& us) { m_measuredData = std::move(us); } - void recon(); - std::thread recon2thread(); + std::optional GetVolume() const; void saft_cpu(); // cpu version of the kernel, used for debugging and if no // GPU present - void crop(); - void remove_dc(); - [[nodiscard]] double get_reconTime() const noexcept { return reconTime; }; + /// substracts the DC component from the signal so that mean(sig) is close to + /// 0 for each individual waveform + void RemoveDC(); + + [[nodiscard]] double get_reconTime() const noexcept { return m_reconTime; }; [[nodiscard]] double get_tRemain() const noexcept { return tRemain; }; - std::chrono::time_point get_tStart() const { - return tStart; - }; - [[nodiscard]] bool get_isRunning() const noexcept { return isRunning; }; - [[nodiscard]] float get_percDone() const noexcept { return percDone; }; + [[nodiscard]] bool get_isRunning() const noexcept { return m_isRunning; }; + [[nodiscard]] float get_percDone() const noexcept { return m_percDone; }; private: - Transducer trans; // settings of the used transducer - ReconSettings sett; // reconstruction settings - volume preprocData; // preprocessed datasets - volume reconData; // reconstructed datasets - volume croppedData; // cropped dataset - - int processor_count = 1; - bool isRunning = - false; // flag indicating if reconstruction is currently running - float percDone = 0.0f; // perc of reconstruction done so far [%] - - // all we need to time the execution - std::chrono::time_point tStart; // start time - std::chrono::time_point tEnd; // end time - double tRemain = 0; - double reconTime = 0; // time required for last reconstruction + void Recon(); + + /// calculates the response of a single voxel in the output volume + float CalculateVoxel(Size3 idx); + + std::optional m_measuredData; //!< preprocessed datasets + std::optional m_reconData; //!< reconstructed datasets + std::thread m_reconThread; //!< thread for reconstruction + + std::chrono::time_point + m_start; //!< time of start + + const unsigned int m_processorCount; //!< total number of cores available + std::atomic m_isRunning = false; //!< is reconstruction running + std::atomic m_percDone = + 0.0f; //!< perc of reconstruction done so far [%] + double tRemain = 0; //!< remaining reconstruction time (estimate) + double m_reconTime = 0.0; //!< time of last reconstruction }; } // namespace opensaft \ No newline at end of file diff --git a/src/Slicer/CMakeLists.txt b/src/Slicer/CMakeLists.txt new file mode 100644 index 0000000..191b50c --- /dev/null +++ b/src/Slicer/CMakeLists.txt @@ -0,0 +1,8 @@ +target_sources(opensaft-gui +PUBLIC + ColorMapper.h + Slicer.h +PRIVATE + ColorMapper.cpp + Slicer.cpp + ) \ No newline at end of file diff --git a/src/ColorMapper.cpp b/src/Slicer/ColorMapper.cpp similarity index 100% rename from src/ColorMapper.cpp rename to src/Slicer/ColorMapper.cpp diff --git a/src/ColorMapper.cu b/src/Slicer/ColorMapper.cu similarity index 100% rename from src/ColorMapper.cu rename to src/Slicer/ColorMapper.cu diff --git a/src/ColorMapper.cuh b/src/Slicer/ColorMapper.cuh similarity index 100% rename from src/ColorMapper.cuh rename to src/Slicer/ColorMapper.cuh diff --git a/src/ColorMapper.h b/src/Slicer/ColorMapper.h similarity index 100% rename from src/ColorMapper.h rename to src/Slicer/ColorMapper.h diff --git a/src/Slicer/Slicer.cpp b/src/Slicer/Slicer.cpp new file mode 100644 index 0000000..9df997b --- /dev/null +++ b/src/Slicer/Slicer.cpp @@ -0,0 +1,3 @@ +#include "Slicer/Slicer.h" + +namespace opensaft {} \ No newline at end of file diff --git a/src/Slicer/Slicer.h b/src/Slicer/Slicer.h new file mode 100644 index 0000000..c7664fd --- /dev/null +++ b/src/Slicer/Slicer.h @@ -0,0 +1,11 @@ + + +namespace opensaft { + +/// converts a volume to a set of slices with a set of specified +/// options +class Slicer { +public: +private: +}; +} // namespace opensaft \ No newline at end of file diff --git a/src/Util/CMakeLists.txt b/src/Util/CMakeLists.txt index b461987..2f14e71 100644 --- a/src/Util/CMakeLists.txt +++ b/src/Util/CMakeLists.txt @@ -1,6 +1,8 @@ target_sources(opensaft PUBLIC Logger.h + Timer.h PRIVATE Logger.cpp + Timer.cpp ) \ No newline at end of file diff --git a/src/Util/Logger.cpp b/src/Util/Logger.cpp index ab7eaf8..f53516a 100644 --- a/src/Util/Logger.cpp +++ b/src/Util/Logger.cpp @@ -1,9 +1,21 @@ #include "Util/Logger.h" #include +#include #include namespace opensaft { +std::string demangle(const char* name) { + + int status = -4; // some arbitrary value to eliminate the compiler warning + + // enable c++11 by passing the flag -std=c++11 to g++ + std::unique_ptr res{ + abi::__cxa_demangle(name, NULL, NULL, &status), std::free}; + + return (status == 0) ? res.get() : name; +} + std::string Logger::GetTimestamp() { // get current time auto now = std::chrono::system_clock::now(); @@ -15,13 +27,19 @@ std::string Logger::GetTimestamp() { return ss.str(); } -void Logger::Log(LogLevel level, const std::string& message) { +void Logger::Log(LogHeader header, const std::string& message) { // log message to console - std::cout << GetTimestamp() << " [" - << LogLevelNames[static_cast(level)] << "]" << message - << std::endl; + std::cout << GetTimestamp() << " (" + << LogLevelNames[static_cast(header.level)] << ")"; + if (!header.origin.empty()) + std::cout << " [" << header.origin << "] "; + std::cout << message << std::endl; } void Logger::Log(const std::string& message) { Log(LogLevel::Info, message); } +std::string LoggingClass::GetName() const { + return demangle(typeid(*this).name()); +} + } // namespace opensaft \ No newline at end of file diff --git a/src/Util/Logger.h b/src/Util/Logger.h index 5bef635..10cc45c 100644 --- a/src/Util/Logger.h +++ b/src/Util/Logger.h @@ -15,12 +15,22 @@ enum class LogLevel : int { _Count //!< number of log types }; +struct LogHeader { + LogLevel level; + std::string origin = ""; + + LogHeader(LogLevel level, const std::string& origin) + : level(level), origin(origin) {} + + LogHeader(LogLevel level) : level(level) {} +}; + constexpr std::array(LogLevel::_Count)> LogLevelNames{"Debug", "Info", "Warning", "Error", "Critical"}; class Logger { public: - static void Log(LogLevel level, const std::string& message); + static void Log(LogHeader header, const std::string& message); /// will default to LogLevel::Info static void Log(const std::string& message); @@ -28,4 +38,19 @@ class Logger { private: static std::string GetTimestamp(); }; + +/// this class is used to inherit from and automatically populate the name tag +class LoggingClass { +public: + void Log(LogLevel level, const std::string& message) const { + Logger::Log({level, GetName()}, message); + } + + void Log(const std::string& message) const { + Logger::Log({LogLevel::Info, GetName()}, message); + } + +private: + virtual std::string GetName() const; +}; } // namespace opensaft \ No newline at end of file diff --git a/src/Util/Timer.cpp b/src/Util/Timer.cpp new file mode 100644 index 0000000..ad7d0fd --- /dev/null +++ b/src/Util/Timer.cpp @@ -0,0 +1,46 @@ + + +#include "Util/Timer.h" +#include + +namespace opensaft { + +Timer::Timer(const bool autoStart) { + if (autoStart) { + Start(); + } +} + +void Timer::Start() { + if (m_start.has_value()) { + throw std::runtime_error("Timer already started"); + } + m_start = std::chrono::high_resolution_clock::now(); +} + +void Timer::Stop() { + if (!m_start.has_value()) { + throw std::runtime_error("Timer not started"); + } + + if (m_stop.has_value()) { + throw std::runtime_error("Timer already stopped"); + } + + m_stop = std::chrono::high_resolution_clock::now(); +} + +double Timer::GetElapsedTime() const { + if (!m_start.has_value()) { + throw std::runtime_error("Timer not started"); + } + + if (!m_stop.has_value()) { + throw std::runtime_error("Timer not stopped"); + } + + return std::chrono::duration(m_stop.value() - m_start.value()) + .count(); +} + +} // namespace opensaft \ No newline at end of file diff --git a/src/Util/Timer.h b/src/Util/Timer.h new file mode 100644 index 0000000..cc78a6d --- /dev/null +++ b/src/Util/Timer.h @@ -0,0 +1,30 @@ + +#include +#include + +#pragma once + +namespace opensaft { + +/// a simple class that allows timing, estimating remaining progress etc +class Timer { +public: + Timer(bool autoStart = true); + + /// starts the timer, throws if already started + void Start(); + + /// stops the timer, throws if already stopped + void Stop(); + + /// returns the elapsed time in seconds + [[nodiscard]] double GetElapsedTime() const; + +private: + std::optional> + m_start; + std::optional> + m_stop; +}; + +} // namespace opensaft \ No newline at end of file diff --git a/src/VectorN.h b/src/VectorN.h index 2434fc9..77c7e50 100644 --- a/src/VectorN.h +++ b/src/VectorN.h @@ -4,6 +4,8 @@ #include #include +#pragma once + namespace opensaft { template @@ -119,8 +121,19 @@ class VectorN { } bool operator!=(const VectorN& other) const { return !(*this == other); } + + T InnerProduct() const { + T returnValue = 1.0f; + for (std::size_t i = 0; i < N; i++) { + returnValue *= data[i]; + } + return returnValue; + } }; +typedef VectorN Float3; typedef VectorN Float4; +typedef VectorN Size3; + } // namespace opensaft diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 2304981..f93ab3c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -2,7 +2,11 @@ find_package(Catch2 REQUIRED) add_executable(UnitTests TestMain.cpp + TestUltrasoundSignals.cpp TestVectorN.cpp + TestVolume.cpp + TestTimer.cpp + TestSaft.cpp ) target_link_libraries(UnitTests diff --git a/tests/TestSaft.cpp b/tests/TestSaft.cpp new file mode 100644 index 0000000..0c3e489 --- /dev/null +++ b/tests/TestSaft.cpp @@ -0,0 +1,16 @@ +#include "Memory/UltrasoundSignals.h" +#include "Saft.h" +#include "catch2/catch_test_macros.hpp" +#include + +using namespace opensaft; + +TEST_CASE("Saft: simple test recon") { + + UltrasoundSignals sig; + + Saft S; + S.SetInput(std::move(sig)); + S.Launch(); + S.Wait(); +} \ No newline at end of file diff --git a/tests/TestTimer.cpp b/tests/TestTimer.cpp new file mode 100644 index 0000000..d654196 --- /dev/null +++ b/tests/TestTimer.cpp @@ -0,0 +1,21 @@ +#include "Util/Timer.h" +#include "catch2/catch_test_macros.hpp" +#include + +using namespace opensaft; +TEST_CASE("Timer: autostart") { + Timer T; + SECTION("Normal stop") { + T.Stop(); + auto time = T.GetElapsedTime(); + } + + SECTION("Restart") { REQUIRE_THROWS(T.Start()); } + + SECTION("Restop") { + T.Stop(); + REQUIRE_THROWS(T.Stop()); + } + + SECTION("Get elapsed before stop") { REQUIRE_THROWS(T.GetElapsedTime()); } +} \ No newline at end of file diff --git a/tests/TestUltrasoundSignals.cpp b/tests/TestUltrasoundSignals.cpp new file mode 100644 index 0000000..717cd00 --- /dev/null +++ b/tests/TestUltrasoundSignals.cpp @@ -0,0 +1,31 @@ +#include "Memory/UltrasoundSignals.h" +#include "catch2/catch_test_macros.hpp" +#include + +using namespace opensaft; + +TEST_CASE("TimeSignals: basic tests") { + TimeSignal T(100); + REQUIRE(T.size() == 100); + for (const auto& elem : T) + REQUIRE(elem == 0.0f); +} + +TEST_CASE("TimeSignals: remove DC test") { + constexpr std::size_t nt = 100; + TimeSignal T(nt); + + // fill with sinosoidial shape + for (std::size_t it = 0; it < nt; it++) { + const float value = static_cast(it) / static_cast(nt) * M_PI; + T[it] = value + 1.23f; + } + + T.RemoveDC(); + + double avg = 0.0; + for (auto& elem : T) { + avg += elem; + } + REQUIRE(std::abs(avg) < 1e-4); +} \ No newline at end of file diff --git a/tests/TestVectorN.cpp b/tests/TestVectorN.cpp index 750e464..9f908c5 100644 --- a/tests/TestVectorN.cpp +++ b/tests/TestVectorN.cpp @@ -1,26 +1,28 @@ -#include #include "VectorN.h" +#include using namespace opensaft; TEST_CASE("VectorN: testing constructors") { - // empty constructor - { - VectorN vec1; - REQUIRE(vec1[0] == 0.0f); - REQUIRE(vec1[1] == 0.0f); - REQUIRE(vec1[2] == 0.0f); - REQUIRE(vec1[3] == 0.0f); - } + // empty constructor + { + VectorN vec1; + REQUIRE(vec1[0] == 0.0f); + REQUIRE(vec1[1] == 0.0f); + REQUIRE(vec1[2] == 0.0f); + REQUIRE(vec1[3] == 0.0f); + } + + // constructing all values to single float + { + float initValue = GENERATE(10.0f, -20.0f, 2.0f); + VectorN vec2(initValue); + REQUIRE(vec2[0] == initValue); + REQUIRE(vec2[1] == initValue); + REQUIRE(vec2[2] == initValue); + REQUIRE(vec2[3] == initValue); + } - // constructing all values to single float - { - float initValue = GENERATE(10.0f, -20.0f, 2.0f); - VectorN vec2(initValue); - REQUIRE(vec2[0] == initValue); - REQUIRE(vec2[1] == initValue); - REQUIRE(vec2[2] == initValue); - REQUIRE(vec2[3] == initValue); - } + /// todo add missing tests here before next merge } \ No newline at end of file diff --git a/tests/TestVolume.cpp b/tests/TestVolume.cpp new file mode 100644 index 0000000..286533f --- /dev/null +++ b/tests/TestVolume.cpp @@ -0,0 +1,37 @@ +#include "Memory/Volume.h" +#include "catch2/catch_test_macros.hpp" +#include + +using namespace opensaft; + +TEST_CASE("Volume: checking constructors") { + Volume V({100, 100, 100}); + for (const auto& elem : V) + REQUIRE(elem == 0.0f); + + REQUIRE(V.size() == 100 * 100 * 100); +} + +TEST_CASE("Volume: checking access operators") { + const std::size_t iy = GENERATE(1, 2, 0, 3); + Volume V({10, 10, 10}); + V(0, iy, 0) = 10.0f; + REQUIRE(V(0, iy, 0) == 10.0f); + REQUIRE(V[iy * 10] == 10.0f); + + // make sure all other elements are still 0.0f + for (std::size_t idx = 0; idx < 1000; idx++) { + if (idx != iy * 10) { + REQUIRE(V[idx] == 0.0f); + } + } +} + +TEST_CASE("Volume: assign to full volume") { + Volume V({2, 2, 2}); + const float setPoint = GENERATE(-1.0f, 2.0f, 3.12f); + V = setPoint; + for (const auto& elem : V) { + REQUIRE(elem == setPoint); + } +} \ No newline at end of file