-
Notifications
You must be signed in to change notification settings - Fork 127
[NEW APP] BCI Visualization with Kernel Flow2 and Real-Time Volume Rendering #1322
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
9c36409
dfb9684
c84ed9c
44bbc7c
3ae88f4
2decb42
7289f65
f795871
9fe0564
c0e1e86
322cf03
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| cmake_minimum_required(VERSION 3.20) | ||
| project(bci_visualization LANGUAGES NONE) | ||
|
|
||
| find_package(holoscan 2.0 REQUIRED CONFIG | ||
| PATHS "/opt/nvidia/holoscan" "/workspace/holoscan-sdk/install") | ||
|
|
||
| add_subdirectory(operators) | ||
|
|
||
| # Enable the operators | ||
| add_library(bci_visualization INTERFACE) | ||
| target_link_libraries(bci_visualization INTERFACE holoscan::core holoscan::ops::holoviz color_buffer_passthrough) | ||
mimiliaogo marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
mimiliaogo marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| # syntax=docker/dockerfile:1 | ||
|
|
||
| # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
|
|
||
| ARG BASE_IMAGE | ||
| FROM ${BASE_IMAGE} AS base | ||
|
|
||
| ARG DEBIAN_FRONTEND=noninteractive | ||
|
|
||
| # Install curl for downloading data files | ||
| RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* | ||
|
|
||
| # Add schemas directory to PYTHONPATH to enable importing operators | ||
| ENV PYTHONPATH=$PYTHONPATH:/workspace/holohub | ||
mimiliaogo marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| # Install Python dependencies | ||
| RUN pip install nibabel nilearn | ||
|
|
||
| # Install Kernel's dependencies | ||
| RUN pip install \ | ||
| numpy \ | ||
| scipy \ | ||
| h5py | ||
mimiliaogo marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
mimiliaogo marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,54 @@ | ||||||
| # BCI Visualization | ||||||
|
|
||||||
| ## Running | ||||||
| 1. Download data | ||||||
| * Please download data from [here](https://drive.google.com/drive/folders/1RpQ6UzjIZAr90FdW9VIbtTFYR6-up7w2) and put everything under `data/bci_visualization`. | ||||||
|
||||||
| * Includes an activation volume and a segmentation volume. | ||||||
mimiliaogo marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||
| 2. Run application | ||||||
| ```bash | ||||||
| ./holohub run bci_visualization | ||||||
| ``` | ||||||
| This command will build the docker and run the application. | ||||||
|
|
||||||
| ## Expected Results | ||||||
|
|
||||||
|  | ||||||
mimiliaogo marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||
| ### Components | ||||||
|
|
||||||
| 1. **VoxelStreamToVolumeOp** (`operators/voxel_stream_to_volume/voxel_stream_to_volume.py`): Bridge operator that converts sparse voxel data to dense volume | ||||||
| - Inputs: `affine_4x4`, `hb_voxel_data` | ||||||
| - Outputs: `volume`, `spacing`, `permute_axis`, `flip_axes`, `mask_volume`, `mask_spacing`, `mask_permute_axis`, `mask_flip_axes` | ||||||
|
|
||||||
| 2. **VolumeRendererOp**: ClaraViz-based volume renderer | ||||||
| - Renders the 3D volume with transfer functions | ||||||
| - Supports interactive camera control | ||||||
|
|
||||||
| 3. **HolovizOp**: Interactive visualization | ||||||
| - Displays the rendered volume | ||||||
| - Provides camera pose feedback | ||||||
|
|
||||||
|
|
||||||
| ## Data Flow | ||||||
|
|
||||||
| ``` | ||||||
| Reconstruction → VoxelStreamToVolume → VolumeRenderer → Holoviz | ||||||
| ↑ ↓ | ||||||
| └─── camera ────┘ | ||||||
| ``` | ||||||
|
|
||||||
| ## Volume Renderer Configuration | ||||||
| Here are some of the important config: | ||||||
| 1. `timeSlot`: Rendering time in ms. The longer the better the quality. | ||||||
| 2. `TransferFunction` | ||||||
|
|
||||||
| a. `activeRegions`: (0: SKIN, 1: SKULL, 2: CSF, 3: GRAY MATER, 4: WHITE MATTER, 5: AIR). Here, we select [3, 4] for our ROI. Set everything else as opacity=0 (default). | ||||||
|
||||||
| a. `activeRegions`: (0: SKIN, 1: SKULL, 2: CSF, 3: GRAY MATER, 4: WHITE MATTER, 5: AIR). Here, we select [3, 4] for our ROI. Set everything else as opacity=0 (default). | |
| a. `activeRegions`: (0: SKIN, 1: SKULL, 2: CSF, 3: GRAY MATTER, 4: WHITE MATTER, 5: AIR). Here, we select [3, 4] for our ROI. Set everything else as opacity=0 (default). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,240 @@ | ||
| """ | ||
| SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. | ||
| SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| BCI Visualization Application - streams synthetic voxel data and renders as 3D volume. | ||
| """ | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| import os | ||
mimiliaogo marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| import argparse | ||
| from pathlib import Path | ||
|
|
||
|
|
||
| from holoscan.core import Application | ||
| from holoscan.operators import HolovizOp | ||
| from holoscan.resources import CudaStreamPool, UnboundedAllocator | ||
| from holoscan.schedulers import EventBasedScheduler, MultiThreadScheduler | ||
mimiliaogo marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| # Import local operators | ||
| from operators.voxel_stream_to_volume import VoxelStreamToVolumeOp | ||
|
|
||
| from holohub.color_buffer_passthrough import ColorBufferPassthroughOp | ||
| from holohub.volume_renderer import VolumeRendererOp | ||
|
|
||
| class BciVisualizationApp(Application): | ||
| """BCI Visualization Application with ClaraViz.""" | ||
|
||
|
|
||
| def __init__(self, | ||
| argv=None, | ||
| *args, | ||
| render_config_file, | ||
| density_min, | ||
| density_max, | ||
| label_path=None, | ||
| roi_labels=None, | ||
| mask_path=None, | ||
| **kwargs, | ||
| ): | ||
| self._rendering_config = render_config_file | ||
| self._density_min = density_min | ||
| self._density_max = density_max | ||
| self._label_path = label_path | ||
| self._roi_labels = roi_labels | ||
| self._mask_path = mask_path | ||
|
|
||
| super().__init__(argv, *args, **kwargs) | ||
|
|
||
| def compose(self): | ||
| volume_allocator = UnboundedAllocator(self, name="allocator") | ||
| cuda_stream_pool = CudaStreamPool( | ||
| self, | ||
| name="cuda_stream", | ||
| dev_id=0, | ||
| stream_flags=0, | ||
| stream_priority=0, | ||
| reserved_size=1, | ||
| max_size=5, | ||
| ) | ||
|
|
||
| # Selection | ||
| selected_channel = 0 # 0: HbO, 1: HbR | ||
mimiliaogo marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| # Operators | ||
| voxel_to_volume_args = { | ||
| "selected_channel": selected_channel, | ||
| } | ||
| if self._label_path: | ||
| voxel_to_volume_args["label_path"] = self._label_path | ||
| if self._roi_labels: | ||
| voxel_to_volume_args["roi_labels"] = self._roi_labels | ||
| if self._mask_path: | ||
| voxel_to_volume_args["mask_nifti_path"] = self._mask_path | ||
|
|
||
| voxel_to_volume = VoxelStreamToVolumeOp( | ||
| self, | ||
| name="voxel_to_volume", | ||
| pool=volume_allocator, | ||
| **voxel_to_volume_args, | ||
| ) | ||
|
|
||
| volume_renderer_args = {} | ||
| if self._density_min: | ||
| volume_renderer_args["density_min"] = self._density_min | ||
| if self._density_max: | ||
| volume_renderer_args["density_max"] = self._density_max | ||
|
|
||
| volume_renderer = VolumeRendererOp( | ||
| self, | ||
| name="volume_renderer", | ||
| config_file=self._rendering_config, | ||
| allocator=volume_allocator, | ||
| alloc_width=1024, | ||
| alloc_height=768, | ||
mimiliaogo marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| cuda_stream_pool=cuda_stream_pool, | ||
| **volume_renderer_args, | ||
| ) | ||
|
|
||
| holoviz = HolovizOp( | ||
| self, | ||
| name="holoviz", | ||
| window_title="BCI Visualization with ClaraViz", | ||
| enable_camera_pose_output=True, | ||
| cuda_stream_pool=cuda_stream_pool, | ||
| ) | ||
|
|
||
| color_buffer_passthrough = ColorBufferPassthroughOp( | ||
| self, | ||
| name="color_buffer_passthrough", | ||
| ) | ||
|
|
||
| # Connect operators | ||
|
|
||
| kernel_data = Path("/workspace/holohub/data/kernel") | ||
mimiliaogo marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| from streams.snirf import SNIRFStream | ||
| stream = SNIRFStream(kernel_data / "data.snirf") | ||
mimiliaogo marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| from reconstruction import ReconstructionApplication | ||
| reconstruction_application = ReconstructionApplication( | ||
| stream=stream, | ||
| jacobian_path=kernel_data / "flow_mega_jacobian.npy", | ||
| channel_mapping_path=kernel_data / "flow_channel_map.json", | ||
| voxel_info_dir=kernel_data / "voxel_info", | ||
| coefficients_path=kernel_data / "extinction_coefficients_mua.csv", | ||
| use_gpu=True, | ||
| ) | ||
| reconstruction_application.compose(self, voxel_to_volume) | ||
mimiliaogo marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| # voxel_to_volume → volume_renderer | ||
| self.add_flow(voxel_to_volume, volume_renderer, { | ||
| ("volume", "density_volume"), | ||
| ("spacing", "density_spacing"), | ||
| ("permute_axis", "density_permute_axis"), | ||
| ("flip_axes", "density_flip_axes"), | ||
| }) | ||
| # Add mask connections to VolumeRendererOp | ||
| self.add_flow(voxel_to_volume, volume_renderer, { | ||
mimiliaogo marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| ("mask_volume", "mask_volume"), | ||
| ("mask_spacing", "mask_spacing"), | ||
| ("mask_permute_axis", "mask_permute_axis"), | ||
| ("mask_flip_axes", "mask_flip_axes"), | ||
| }) | ||
|
|
||
| # volume_renderer ↔ holoviz | ||
| self.add_flow(volume_renderer, color_buffer_passthrough, | ||
| {("color_buffer_out", "color_buffer_in")}) | ||
| self.add_flow(color_buffer_passthrough, holoviz, {("color_buffer_out", "receivers")}) | ||
| self.add_flow(holoviz, volume_renderer, {("camera_pose_output", "camera_pose")}) | ||
|
|
||
|
|
||
| def main(): | ||
|
|
||
|
|
||
mimiliaogo marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| parser = argparse.ArgumentParser(description="BCI Visualization Application", add_help=False) | ||
| parser.add_argument( | ||
| "-c", | ||
| "--config", | ||
| action="store", | ||
| dest="config", | ||
| help="Name of the renderer JSON configuration file to load", | ||
| ) | ||
|
|
||
| parser.add_argument( | ||
| "-i", | ||
| "--density_min", | ||
| action="store", | ||
| type=int, | ||
| dest="density_min", | ||
| help="Set the minimum of the density element values. If not set this is calculated from the" | ||
| "volume data. In practice CT volumes have a minimum value of -1024 which corresponds to" | ||
| "the lower value of the Hounsfield scale range usually used.", | ||
| ) | ||
| parser.add_argument( | ||
| "-a", | ||
| "--density_max", | ||
| action="store", | ||
| type=int, | ||
| dest="density_max", | ||
| help="Set the maximum of the density element values. If not set this is calculated from the" | ||
| "volume data. In practice CT volumes have a maximum value of 3071 which corresponds to" | ||
| "the upper value of the Hounsfield scale range usually used.", | ||
| ) | ||
mimiliaogo marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| parser.add_argument( | ||
| "-l", | ||
| "--label_path", | ||
| action="store", | ||
| type=str, | ||
| dest="label_path", | ||
| help="Path to the NPZ file containing brain anatomy labels. If not provided, uses default path.", | ||
| ) | ||
|
|
||
| parser.add_argument( | ||
| "-r", | ||
| "--roi_labels", | ||
| action="store", | ||
| type=str, | ||
| dest="roi_labels", | ||
| help="Comma-separated list of label values to use as ROI (e.g., '3,4'). Default is '3,4'.", | ||
| ) | ||
|
|
||
| parser.add_argument( | ||
| "-m", | ||
| "--mask_path", | ||
| action="store", | ||
| type=str, | ||
| dest="mask_path", | ||
| help="Path to the mask NIfTI file containing 3D integer labels (I, J, K). Optional.", | ||
| ) | ||
|
|
||
| parser.add_argument( | ||
| "-h", "--help", action="help", default=argparse.SUPPRESS, help="Help message" | ||
| ) | ||
|
|
||
| args = parser.parse_args() | ||
|
|
||
| # Parse roi_labels from comma-separated string to list of integers | ||
mimiliaogo marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| roi_labels = None | ||
| if args.roi_labels: | ||
| try: | ||
| roi_labels = [int(label.strip()) for label in args.roi_labels.split(',')] | ||
| except ValueError: | ||
| print(f"Warning: Invalid roi_labels format '{args.roi_labels}'. Expected comma-separated integers.") | ||
| roi_labels = None | ||
|
|
||
| app = BciVisualizationApp( | ||
| render_config_file=args.config, | ||
| density_min=args.density_min, | ||
| density_max=args.density_max, | ||
| label_path=args.label_path, | ||
| roi_labels=roi_labels, | ||
| mask_path=args.mask_path, | ||
| ) | ||
|
|
||
| app.scheduler(EventBasedScheduler(app, worker_thread_number=5, stop_on_deadlock=True)) | ||
|
|
||
| app.run() | ||
|
|
||
| print("BCI Visualization Application has finished running.") | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update copyright year.
The copyright header must include the current year (2026).
🔎 Proposed fix
As per pipeline failure logs.
📝 Committable suggestion
🧰 Tools
🪛 GitHub Actions: Check Compliance
[error] 1-1: Copyright header incomplete: current year not included in the header.
🤖 Prompt for AI Agents