diff --git a/applications/CMakeLists.txt b/applications/CMakeLists.txt index 631de8505..1f36a3907 100644 --- a/applications/CMakeLists.txt +++ b/applications/CMakeLists.txt @@ -17,13 +17,16 @@ add_holohub_application(adv_networking_bench DEPENDS OPERATORS advanced_network) +add_holohub_application(aja_video_capture DEPENDS + OPERATORS aja_source) + add_holohub_application(basic_networking_ping DEPENDS OPERATORS basic_network) add_holohub_application(body_pose_estimation DEPENDS OPERATORS OPTIONAL dds_video_subscriber dds_video_publisher) -add_holohub_application(colonoscopy_segmentation) +add_holohub_application(colonoscopy_segmentation DEPENDS OPERATORS aja_source) add_holohub_application(cvcuda_basic DEPENDS OPERATORS cvcuda_holoscan_interop) @@ -42,11 +45,12 @@ add_subdirectory(distributed) add_holohub_application(endoscopy_depth_estimation) -add_holohub_application(endoscopy_out_of_body_detection) +add_holohub_application(endoscopy_out_of_body_detection DEPENDS OPERATORS aja_source) add_holohub_application(endoscopy_tool_tracking DEPENDS OPERATORS lstm_tensor_rt_inference tool_tracking_postprocessor + aja_source OPTIONAL deltacast_videomaster yuan_qcap vtk_renderer) add_subdirectory(h264) @@ -62,15 +66,15 @@ add_holohub_application(hyperspectral_segmentation) add_subdirectory(laser_detection_latency) -add_holohub_application(multiai_endoscopy) +add_holohub_application(multiai_endoscopy DEPENDS OPERATORS aja_source) -add_holohub_application(multiai_ultrasound) +add_holohub_application(multiai_ultrasound DEPENDS OPERATORS aja_source) add_holohub_application(simple_radar_pipeline) add_holohub_application(simple_pdw_pipeline DEPENDS OPERATORS basic_network) -add_holohub_application(object_detection_torch) +add_holohub_application(object_detection_torch DEPENDS OPERATORS aja_source) add_holohub_application(openigtlink_3dslicer DEPENDS OPERATORS openigtlink) @@ -98,7 +102,7 @@ add_holohub_application(psd_pipeline DEPENDS vita49_psd_packetizer data_writer) -add_holohub_application(ultrasound_segmentation) +add_holohub_application(ultrasound_segmentation DEPENDS OPERATORS aja_source) add_holohub_application(velodyne_lidar_app DEPENDS OPERATORS velodyne_lidar diff --git a/applications/aja_video_capture/CMakeLists.txt b/applications/aja_video_capture/CMakeLists.txt new file mode 100644 index 000000000..ea280c271 --- /dev/null +++ b/applications/aja_video_capture/CMakeLists.txt @@ -0,0 +1,17 @@ +# 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. + +add_subdirectory(cpp) +add_subdirectory(python) diff --git a/applications/aja_video_capture/README.md b/applications/aja_video_capture/README.md new file mode 100644 index 000000000..e508c7bbc --- /dev/null +++ b/applications/aja_video_capture/README.md @@ -0,0 +1,59 @@ +# AJA Capture + +Minimal example to demonstrate the use of the aja source operator to capture device input and stream to holoviz operator. + +*Visit the [SDK User Guide](https://docs.nvidia.com/holoscan/sdk-user-guide/aja_setup.html) to setup the AJA Card.* + +## C++ Run instructions + +* **using deb package install or NGC container**: + ```bash + /opt/nvidia/holoscan/examples/aja_capture/cpp/aja_capture + ``` +* **source (dev container)**: + ```bash + ./run launch # optional: append `install` for install tree + ./examples/aja_capture/cpp/aja_capture + +* **source (local env)**: + ```bash + ${BUILD_OR_INSTALL_DIR}/examples/aja_capture/cpp/aja_capture + ``` + +## Python Run instructions + +* **using python wheel**: + ```bash + # [Prerequisite] Download example .py file below to `APP_DIR` + # [Optional] Start the virtualenv where holoscan is installed + python3 /aja_capture.py + ``` +* **using deb package install**: + ```bash + export PYTHONPATH=/opt/nvidia/holoscan/python/lib + python3 /opt/nvidia/holoscan/examples/aja_capture/python/aja_capture.py + ``` +* **from NGC container**: + ```bash + python3 /opt/nvidia/holoscan/examples/aja_capture/python/aja_capture.py + ``` +* **source (dev container)**: + ```bash + ./run launch # optional: append `install` for install tree + python3 ./examples/aja_capture/python/aja_capture.py + ``` +* **source (local env)**: + ```bash + export PYTHONPATH=${BUILD_OR_INSTALL_DIR}/python/lib + python3 ${BUILD_OR_INSTALL_DIR}/examples/aja_capture/python/aja_capture.py + ``` +## Settings + + To evaluate the AJA example using alternative resolutions, you may modify the aja_capture.yaml configuration file as needed. For instance, to test a resolution format of 1280 x 720 at 60 Hz, you can specify the following parameters in the aja section of the configuration : + + ```bash + aja: + width: 1280 + height: 720 + framerate: 60 + ``` \ No newline at end of file diff --git a/applications/aja_video_capture/cpp/CMakeLists.txt b/applications/aja_video_capture/cpp/CMakeLists.txt new file mode 100644 index 000000000..8a8562a1d --- /dev/null +++ b/applications/aja_video_capture/cpp/CMakeLists.txt @@ -0,0 +1,104 @@ +# SPDX-FileCopyrightText: Copyright (c) 2022-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. + +# Create example +add_executable(aja_capture + aja_capture.cpp +) + +target_link_libraries(aja_capture + PRIVATE + holoscan::core + holoscan::ops::holoviz + holoscan::aja +) + +# Copy config file +add_custom_target(aja_capture_yaml + COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/aja_capture.yaml" ${CMAKE_CURRENT_BINARY_DIR} + DEPENDS "aja_capture.yaml" + BYPRODUCTS "aja_capture.yaml" +) + +add_dependencies(aja_capture aja_capture_yaml) + +# Testing +if(BUILD_TESTING) + + set(RECORDING_DIR ${CMAKE_CURRENT_BINARY_DIR}/recording_output) + set(SOURCE_VIDEO_BASENAME video_replayer_output) + set(VALIDATION_FRAMES_DIR ${Holoscan-examples_SOURCE_DIR}/../tests/data/validation_frames/aja_capture/) + + file(MAKE_DIRECTORY ${RECORDING_DIR}) + + file(READ ${CMAKE_CURRENT_SOURCE_DIR}/aja_capture.yaml CONFIG_STRING) + string(REPLACE "count: -1" "count: 10" CONFIG_STRING "${CONFIG_STRING}") + set(CONFIG_FILE ${CMAKE_CURRENT_BINARY_DIR}/cpp_aja_capture_config.yaml) + file(WRITE ${CONFIG_FILE} "${CONFIG_STRING}") + + # Patch the current example to enable recording the rendering window + add_custom_command(OUTPUT aja_capture_test.cpp + COMMAND patch -u -o aja_capture_test.cpp ${CMAKE_CURRENT_SOURCE_DIR}/aja_capture.cpp + ${Holoscan-examples_SOURCE_DIR}/../tests/data/validation_frames/aja_capture/cpp_aja_capture.patch + ) + + # Create the test executable + add_executable(aja_capture_test + aja_capture_test.cpp + ) + + target_include_directories(aja_capture_test + PRIVATE ${CMAKE_SOURCE_DIR}/tests) + + target_compile_definitions(aja_capture_test + PRIVATE RECORD_OUTPUT RECORDING_DIR="${RECORDING_DIR}" + PRIVATE SOURCE_VIDEO_BASENAME="${SOURCE_VIDEO_BASENAME}" + ) + + target_link_libraries(aja_capture_test + PRIVATE + holoscan::core + holoscan::ops::holoviz + holoscan::ops::video_stream_replayer + holoscan::ops::video_stream_recorder + holoscan::ops::format_converter + holoscan::aja + ) + + # Add the test and make sure it runs + add_test(NAME EXAMPLE_CPP_AJA_CAPTURE_TEST + COMMAND ${CMAKE_CURRENT_BINARY_DIR}/aja_capture_test ${CONFIG_FILE} + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + ) + set_tests_properties(EXAMPLE_CPP_AJA_CAPTURE_TEST PROPERTIES + PASS_REGULAR_EXPRESSION "Scheduler stopped: Some entities are waiting for execution" + ) + + # Add a test to check the validity of the frames + add_test(NAME EXAMPLE_CPP_AJA_CAPTURE_RENDER_TEST + COMMAND python3 ${Holoscan-examples_SOURCE_DIR}/../scripts/video_validation.py + --source_video_dir ${RECORDING_DIR} + --source_video_basename ${SOURCE_VIDEO_BASENAME} + --output_dir ${RECORDING_DIR} + --validation_frames_dir ${VALIDATION_FRAMES_DIR} + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + ) + + set_tests_properties(EXAMPLE_CPP_AJA_CAPTURE_RENDER_TEST PROPERTIES + DEPENDS EXAMPLE_CPP_AJA_CAPTURE_TEST + PASS_REGULAR_EXPRESSION "Valid video output!" + ) + +endif() diff --git a/applications/aja_video_capture/cpp/aja_capture.cpp b/applications/aja_video_capture/cpp/aja_capture.cpp new file mode 100644 index 000000000..890ec9ab4 --- /dev/null +++ b/applications/aja_video_capture/cpp/aja_capture.cpp @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2022-2024 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. + */ + +#include +#include + +#include +class App : public holoscan::Application { + public: + void compose() override { + using namespace holoscan; + + auto source = make_operator("aja", from_config("aja"), + make_condition(from_config("aja.count"))); + auto visualizer = make_operator("holoviz", from_config("holoviz")); + + // Flow definition + add_flow(source, visualizer, {{"video_buffer_output", "receivers"}}); + } +}; + +int main(int argc, char** argv) { + App app; + + // Get the configuration + auto config_path = std::filesystem::canonical(argv[0]).parent_path(); + config_path /= std::filesystem::path("aja_capture.yaml"); + if (argc >= 2) { config_path = argv[1]; } + + app.config(config_path); + + app.run(); + + return 0; +} diff --git a/applications/aja_video_capture/cpp/aja_capture.yaml b/applications/aja_video_capture/cpp/aja_capture.yaml new file mode 100644 index 000000000..283618020 --- /dev/null +++ b/applications/aja_video_capture/cpp/aja_capture.yaml @@ -0,0 +1,33 @@ +%YAML 1.2 +# SPDX-FileCopyrightText: Copyright (c) 2022-2024 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. +--- +aja: + width: 1920 + height: 1080 + framerate: 60 + rdma: true + enable_overlay: false + overlay_rdma: true + count: -1 + +holoviz: + width: 854 + height: 480 + tensors: + - name: "" + type: color + opacity: 1.0 + priority: 0 diff --git a/applications/aja_video_capture/cpp/metadata.json b/applications/aja_video_capture/cpp/metadata.json new file mode 100644 index 000000000..51bed9a41 --- /dev/null +++ b/applications/aja_video_capture/cpp/metadata.json @@ -0,0 +1,37 @@ +{ + "application": { + "name": "AJA Video Capture", + "authors": [ + { + "name": "Holoscan Team", + "affiliation": "NVIDIA" + } + ], + "language": "C++", + "version": "3.0", + "changelog": { + "1.0": "Initial Release", + "2.0": "Update to support Holoscan SDK 2.1.0", + "3.0": "Moved examples to Holohub Application" + }, + "holoscan_sdk": { + "minimum_required_version": "1.0.3", + "tested_versions": [ + "1.0.3", + "2.0.0", + "2.1.0", + "2.2.0", + "3.0.0" + ] + }, + "platforms": ["amd64", "arm64"], + "tags": ["Video", "AJA"], + "ranking": 0, + "dependencies": { + }, + "run": { + "command": "/aja_capture", + "workdir": "holohub_bin" + } + } +} diff --git a/applications/aja_video_capture/python/CMakeLists.txt b/applications/aja_video_capture/python/CMakeLists.txt new file mode 100644 index 000000000..95093439e --- /dev/null +++ b/applications/aja_video_capture/python/CMakeLists.txt @@ -0,0 +1,73 @@ +# SPDX-FileCopyrightText: Copyright (c) 2022-2024 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. + +# Copy aja_capture application file +add_custom_target(python_aja_capture ALL + COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/aja_capture.py" ${CMAKE_CURRENT_BINARY_DIR} + DEPENDS "aja_capture.py" + BYPRODUCTS "aja_capture.py" +) + +# Testing +if(BUILD_TESTING) + + set(RECORDING_DIR ${CMAKE_CURRENT_BINARY_DIR}/recording_output) + set(SOURCE_VIDEO_BASENAME python_aja_capture_output) + set(VALIDATION_FRAMES_DIR ${Holoscan-examples_SOURCE_DIR}/../tests/data/validation_frames/aja_capture/) + + file(MAKE_DIRECTORY ${RECORDING_DIR}) + + file(READ ${CMAKE_CURRENT_SOURCE_DIR}/aja_capture.yaml CONFIG_STRING) + string(REPLACE "count: -1" "count: 10" CONFIG_STRING ${CONFIG_STRING}) + string(APPEND CONFIG_STRING " enable_render_buffer_output: true\n\nrecorder:\n directory: \"${RECORDING_DIR}\"\n basename: \"${SOURCE_VIDEO_BASENAME}\"") + set(CONFIG_FILE ${CMAKE_CURRENT_BINARY_DIR}/python_aja_capture_config.yaml) + file(WRITE ${CONFIG_FILE} ${CONFIG_STRING}) + + # Patch the current example to enable recording the rendering window + add_custom_command(OUTPUT aja_capture_test.py + COMMAND patch -u -o aja_capture_test.py ${CMAKE_CURRENT_SOURCE_DIR}/aja_capture.py + ${Holoscan-examples_SOURCE_DIR}/../tests/data/validation_frames/aja_capture/python_aja_capture.patch + ) + + add_custom_target(python_aja_capture_test ALL + DEPENDS "aja_capture_test.py" + ) + + add_test(NAME EXAMPLE_PYTHON_AJA_CAPTURE_TEST + COMMAND python3 aja_capture_test.py --config python_aja_capture_config.yaml + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + ) + + set_tests_properties(EXAMPLE_PYTHON_AJA_CAPTURE_TEST PROPERTIES + DEPENDS "aja_capture_test.py" + PASS_REGULAR_EXPRESSION "Scheduler stopped: Some entities are waiting for execution" + ) + + # Add a test to check the validity of the frames + add_test(NAME EXAMPLE_PYTHON_AJA_CAPTURE_RENDER_TEST + COMMAND python3 ${Holoscan-examples_SOURCE_DIR}/../scripts/video_validation.py + --source_video_dir ${RECORDING_DIR} + --source_video_basename ${SOURCE_VIDEO_BASENAME} + --output_dir ${RECORDING_DIR} + --validation_frames_dir ${VALIDATION_FRAMES_DIR} + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + ) + + set_tests_properties(EXAMPLE_PYTHON_AJA_CAPTURE_RENDER_TEST PROPERTIES + DEPENDS EXAMPLE_PYTHON_AJA_CAPTURE_TEST + PASS_REGULAR_EXPRESSION "Valid video output!" + ) + +endif() diff --git a/applications/aja_video_capture/python/aja_capture.py b/applications/aja_video_capture/python/aja_capture.py new file mode 100644 index 000000000..70767e3be --- /dev/null +++ b/applications/aja_video_capture/python/aja_capture.py @@ -0,0 +1,58 @@ +""" +SPDX-FileCopyrightText: Copyright (c) 2022-2024 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. +""" # noqa: E501 + +import os + +from holoscan.conditions import CountCondition +from holoscan.core import Application +from holoscan.operators import AJASourceOp, HolovizOp + + +class AJACaptureApp(Application): + """ + Example of an application that uses the following operators: + + - AJASourceOp + - HolovizOp + + The AJASourceOp reads frames from an AJA input device and sends it to the HolovizOp. + The HolovizOp displays the frames. + """ + + def compose(self): + args_aja = self.kwargs("aja") + + count = args_aja["count"] + args_aja.pop("count") + + source = AJASourceOp(self, CountCondition(self, count), name="aja", **args_aja) + + visualizer = HolovizOp(self, name="holoviz", **self.kwargs("holoviz")) + + self.add_flow(source, visualizer, {("video_buffer_output", "receivers")}) + + +def main(config_file): + app = AJACaptureApp() + # if the --config command line argument was provided, it will override this config_file + app.config(config_file) + app.run() + + +if __name__ == "__main__": + config_file = os.path.join(os.path.dirname(__file__), "aja_capture.yaml") + main(config_file=config_file) diff --git a/applications/aja_video_capture/python/aja_capture.yaml b/applications/aja_video_capture/python/aja_capture.yaml new file mode 100644 index 000000000..283618020 --- /dev/null +++ b/applications/aja_video_capture/python/aja_capture.yaml @@ -0,0 +1,33 @@ +%YAML 1.2 +# SPDX-FileCopyrightText: Copyright (c) 2022-2024 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. +--- +aja: + width: 1920 + height: 1080 + framerate: 60 + rdma: true + enable_overlay: false + overlay_rdma: true + count: -1 + +holoviz: + width: 854 + height: 480 + tensors: + - name: "" + type: color + opacity: 1.0 + priority: 0 diff --git a/applications/aja_video_capture/python/aja_capture_59Hz.yaml b/applications/aja_video_capture/python/aja_capture_59Hz.yaml new file mode 100644 index 000000000..1de3b80ae --- /dev/null +++ b/applications/aja_video_capture/python/aja_capture_59Hz.yaml @@ -0,0 +1,33 @@ +%YAML 1.2 +# SPDX-FileCopyrightText: Copyright (c) 2022-2024 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. +--- +aja: + width: 1280 + height: 720 + rdma: true + enable_overlay: false + overlay_rdma: true + framerate: 59 + count: -1 + +holoviz: + width: 854 + height: 480 + tensors: + - name: "" + type: color + opacity: 1.0 + priority: 0 diff --git a/applications/aja_video_capture/python/aja_capture_60Hz.yaml b/applications/aja_video_capture/python/aja_capture_60Hz.yaml new file mode 100644 index 000000000..e8220c445 --- /dev/null +++ b/applications/aja_video_capture/python/aja_capture_60Hz.yaml @@ -0,0 +1,33 @@ +%YAML 1.2 +# SPDX-FileCopyrightText: Copyright (c) 2022-2024 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. +--- +aja: + width: 1280 + height: 720 + rdma: true + enable_overlay: false + overlay_rdma: true + framerate: 60 + count: -1 + +holoviz: + width: 854 + height: 480 + tensors: + - name: "" + type: color + opacity: 1.0 + priority: 0 diff --git a/applications/aja_video_capture/python/metadata.json b/applications/aja_video_capture/python/metadata.json new file mode 100644 index 000000000..fb5abedd2 --- /dev/null +++ b/applications/aja_video_capture/python/metadata.json @@ -0,0 +1,36 @@ +{ + "application": { + "name": "AJA Video Capture", + "authors": [ + { + "name": "Holoscan Team", + "affiliation": "NVIDIA" + } + ], + "language": "Python", + "version": "3.0", + "changelog": { + "1.0": "Initial Release", + "2.0": "Update to support Holoscan SDK 2.1.0", + "3.0": "Moved examples to Holohub Application" + }, + "holoscan_sdk": { + "minimum_required_version": "1.0.3", + "tested_versions": [ + "1.0.3", + "2.0.0", + "2.1.0", + "2.2.0" + ] + }, + "platforms": ["amd64", "arm64"], + "tags": ["Video", "AJA"], + "ranking": 0, + "dependencies": { + }, + "run": { + "command": "python3 /aja_capture.py", + "workdir": "holohub_bin" + } + } +} diff --git a/applications/colonoscopy_segmentation/CMakeLists.txt b/applications/colonoscopy_segmentation/CMakeLists.txt index be918bc35..dad4fd085 100644 --- a/applications/colonoscopy_segmentation/CMakeLists.txt +++ b/applications/colonoscopy_segmentation/CMakeLists.txt @@ -69,7 +69,7 @@ if(BUILD_TESTING) --data "${HOLOHUB_DATA_DIR}/colonoscopy_segmentation" WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) set_property(TEST colonoscopy_segmentation_python_test PROPERTY ENVIRONMENT - "PYTHONPATH=${GXF_LIB_DIR}/../python/lib") + "PYTHONPATH=${GXF_LIB_DIR}/../python/lib:${CMAKE_BINARY_DIR}/python/lib") set_tests_properties(colonoscopy_segmentation_python_test PROPERTIES PASS_REGULAR_EXPRESSION "Reach end of file or playback count reaches to the limit. Stop ticking.;" diff --git a/applications/colonoscopy_segmentation/colonoscopy_segmentation.py b/applications/colonoscopy_segmentation/colonoscopy_segmentation.py index ff9a438c5..9e9db2f98 100644 --- a/applications/colonoscopy_segmentation/colonoscopy_segmentation.py +++ b/applications/colonoscopy_segmentation/colonoscopy_segmentation.py @@ -18,7 +18,6 @@ from holoscan.core import Application from holoscan.operators import ( - AJASourceOp, FormatConverterOp, HolovizOp, InferenceOp, @@ -27,6 +26,8 @@ ) from holoscan.resources import BlockMemoryPool, CudaStreamPool, MemoryStorageType +from holohub.aja_source import AJASourceOp + class ColonoscopyApp(Application): def __init__(self, data, source="replayer"): diff --git a/applications/endoscopy_out_of_body_detection/CMakeLists.txt b/applications/endoscopy_out_of_body_detection/CMakeLists.txt index 394f9da91..c475e2a5a 100644 --- a/applications/endoscopy_out_of_body_detection/CMakeLists.txt +++ b/applications/endoscopy_out_of_body_detection/CMakeLists.txt @@ -28,11 +28,11 @@ add_executable(endoscopy_out_of_body_detection target_link_libraries(endoscopy_out_of_body_detection PRIVATE holoscan::core - holoscan::ops::aja holoscan::ops::video_stream_replayer holoscan::ops::format_converter holoscan::ops::inference holoscan::ops::inference_processor + holoscan::aja ) # Download the associated dataset if needed diff --git a/applications/endoscopy_out_of_body_detection/main.cpp b/applications/endoscopy_out_of_body_detection/main.cpp index c652f146b..9c8fa7044 100644 --- a/applications/endoscopy_out_of_body_detection/main.cpp +++ b/applications/endoscopy_out_of_body_detection/main.cpp @@ -18,12 +18,15 @@ #include #include -#include #include #include #include #include +#ifdef AJA_SOURCE +#include +#endif + class App : public holoscan::Application { public: void set_source(const std::string& source) { diff --git a/applications/endoscopy_tool_tracking/cpp/CMakeLists.txt b/applications/endoscopy_tool_tracking/cpp/CMakeLists.txt index 0e53ac074..c5ce6d4cd 100644 --- a/applications/endoscopy_tool_tracking/cpp/CMakeLists.txt +++ b/applications/endoscopy_tool_tracking/cpp/CMakeLists.txt @@ -26,13 +26,13 @@ add_executable(endoscopy_tool_tracking target_link_libraries(endoscopy_tool_tracking PRIVATE holoscan::core - holoscan::ops::aja holoscan::ops::video_stream_replayer holoscan::ops::video_stream_recorder holoscan::ops::format_converter holoscan::ops::holoviz lstm_tensor_rt_inference tool_tracking_postprocessor + holoscan::aja ) target_link_libraries(endoscopy_tool_tracking PRIVATE $) diff --git a/applications/endoscopy_tool_tracking/cpp/main.cpp b/applications/endoscopy_tool_tracking/cpp/main.cpp index 48fbd77ad..9198a7632 100644 --- a/applications/endoscopy_tool_tracking/cpp/main.cpp +++ b/applications/endoscopy_tool_tracking/cpp/main.cpp @@ -18,7 +18,6 @@ #include #include -#include #include #include #include @@ -29,6 +28,10 @@ #include #endif +#ifdef AJA_SOURCE +#include +#endif + #ifdef DELTACAST_VIDEOMASTER #include #include diff --git a/applications/endoscopy_tool_tracking/python/endoscopy_tool_tracking.py b/applications/endoscopy_tool_tracking/python/endoscopy_tool_tracking.py index d4bc98ff4..bb55c5076 100644 --- a/applications/endoscopy_tool_tracking/python/endoscopy_tool_tracking.py +++ b/applications/endoscopy_tool_tracking/python/endoscopy_tool_tracking.py @@ -18,7 +18,6 @@ from holoscan.core import Application from holoscan.operators import ( - AJASourceOp, FormatConverterOp, HolovizOp, VideoStreamRecorderOp, @@ -26,6 +25,7 @@ ) from holoscan.resources import BlockMemoryPool, CudaStreamPool, MemoryStorageType +from holohub.aja_source import AJASourceOp from holohub.lstm_tensor_rt_inference import LSTMTensorRTInferenceOp # Enable this line for Yuam capture card diff --git a/applications/monai_endoscopic_tool_seg/tool_segmentation.py b/applications/monai_endoscopic_tool_seg/tool_segmentation.py index 9d67abeda..055400c72 100755 --- a/applications/monai_endoscopic_tool_seg/tool_segmentation.py +++ b/applications/monai_endoscopic_tool_seg/tool_segmentation.py @@ -18,7 +18,6 @@ from holoscan.core import Application from holoscan.operators import ( - AJASourceOp, FormatConverterOp, HolovizOp, InferenceOp, @@ -27,6 +26,8 @@ ) from holoscan.resources import BlockMemoryPool, CudaStreamPool, MemoryStorageType +from holohub.aja_source import AJASourceOp + class EndoToolSegApp(Application): def __init__(self, data, source="replayer"): diff --git a/applications/multiai_endoscopy/cpp/post-proc-cpu/CMakeLists.txt b/applications/multiai_endoscopy/cpp/post-proc-cpu/CMakeLists.txt index d6142b97b..e87e11774 100644 --- a/applications/multiai_endoscopy/cpp/post-proc-cpu/CMakeLists.txt +++ b/applications/multiai_endoscopy/cpp/post-proc-cpu/CMakeLists.txt @@ -27,12 +27,12 @@ add_executable(multiai_endoscopy target_link_libraries(multiai_endoscopy PRIVATE holoscan::core - holoscan::ops::aja holoscan::ops::video_stream_replayer holoscan::ops::format_converter holoscan::ops::inference holoscan::ops::segmentation_postprocessor holoscan::ops::holoviz + holoscan::aja ) # Download the associated dataset if needed @@ -80,13 +80,13 @@ if(BUILD_TESTING) target_link_libraries(multiai_endoscopy_cpp_test PRIVATE holoscan::core - holoscan::ops::aja holoscan::ops::video_stream_replayer holoscan::ops::video_stream_recorder holoscan::ops::format_converter holoscan::ops::inference holoscan::ops::segmentation_postprocessor holoscan::ops::holoviz + holoscan::aja ) # Add the test and make sure it runs diff --git a/applications/multiai_endoscopy/cpp/post-proc-cpu/multi_ai.cpp b/applications/multiai_endoscopy/cpp/post-proc-cpu/multi_ai.cpp index 5702b23bb..516cd0243 100644 --- a/applications/multiai_endoscopy/cpp/post-proc-cpu/multi_ai.cpp +++ b/applications/multiai_endoscopy/cpp/post-proc-cpu/multi_ai.cpp @@ -18,13 +18,17 @@ #include #include -#include #include #include #include #include #include #include + +#ifdef AJA_SOURCE +#include +#endif + #if __has_include("gxf/std/dlpack_utils.hpp") #define GXF_HAS_DLPACK_SUPPORT 1 #else diff --git a/applications/multiai_endoscopy/cpp/post-proc-matx-cpu/CMakeLists.txt b/applications/multiai_endoscopy/cpp/post-proc-matx-cpu/CMakeLists.txt index 87bcf3a3e..debe80163 100644 --- a/applications/multiai_endoscopy/cpp/post-proc-matx-cpu/CMakeLists.txt +++ b/applications/multiai_endoscopy/cpp/post-proc-matx-cpu/CMakeLists.txt @@ -44,12 +44,12 @@ add_executable(multi_ai target_link_libraries(multi_ai PRIVATE holoscan::core - holoscan::ops::aja holoscan::ops::video_stream_replayer holoscan::ops::format_converter holoscan::ops::inference holoscan::ops::segmentation_postprocessor holoscan::ops::holoviz + holoscan::aja matx::matx ) diff --git a/applications/multiai_endoscopy/cpp/post-proc-matx-cpu/multi_ai.cpp b/applications/multiai_endoscopy/cpp/post-proc-matx-cpu/multi_ai.cpp index ea1ba63d4..4bceb6855 100644 --- a/applications/multiai_endoscopy/cpp/post-proc-matx-cpu/multi_ai.cpp +++ b/applications/multiai_endoscopy/cpp/post-proc-matx-cpu/multi_ai.cpp @@ -18,12 +18,16 @@ #include #include -#include #include #include #include #include #include + +#ifdef AJA_SOURCE +#include +#endif + #include "gxf/std/tensor.hpp" #include "matx.h" diff --git a/applications/multiai_endoscopy/cpp/post-proc-matx-gpu/CMakeLists.txt b/applications/multiai_endoscopy/cpp/post-proc-matx-gpu/CMakeLists.txt index 89a0f9faa..84c413fcf 100644 --- a/applications/multiai_endoscopy/cpp/post-proc-matx-gpu/CMakeLists.txt +++ b/applications/multiai_endoscopy/cpp/post-proc-matx-gpu/CMakeLists.txt @@ -40,7 +40,6 @@ add_executable(multi_ai target_link_libraries(multi_ai PRIVATE holoscan::core - holoscan::ops::aja holoscan::ops::video_stream_replayer holoscan::ops::format_converter holoscan::ops::inference diff --git a/applications/multiai_endoscopy/cpp/post-proc-matx-gpu/multi_ai.cu b/applications/multiai_endoscopy/cpp/post-proc-matx-gpu/multi_ai.cu index 6ea84baa9..ddbd34d94 100644 --- a/applications/multiai_endoscopy/cpp/post-proc-matx-gpu/multi_ai.cu +++ b/applications/multiai_endoscopy/cpp/post-proc-matx-gpu/multi_ai.cu @@ -18,7 +18,6 @@ #include #include -#include #include #include #include @@ -27,6 +26,9 @@ #include "gxf/std/tensor.hpp" #include "matx.h" +#ifdef AJA_SOURCE +#include +#endif #define CUDA_TRY(stmt) \ { \ diff --git a/applications/multiai_endoscopy/python/multi_ai.py b/applications/multiai_endoscopy/python/multi_ai.py index 52a34d49b..13fefe8a8 100644 --- a/applications/multiai_endoscopy/python/multi_ai.py +++ b/applications/multiai_endoscopy/python/multi_ai.py @@ -21,7 +21,6 @@ import numpy as np from holoscan.core import Application, Operator, OperatorSpec from holoscan.operators import ( - AJASourceOp, FormatConverterOp, HolovizOp, InferenceOp, @@ -30,6 +29,8 @@ ) from holoscan.resources import UnboundedAllocator +from holohub.aja_source import AJASourceOp + class DetectionPostprocessorOp(Operator): """Example of an operator post processing the tensor from inference component. diff --git a/applications/multiai_endoscopy/testing/cpp_multiai_endoscopy.patch b/applications/multiai_endoscopy/testing/cpp_multiai_endoscopy.patch index d813d1bf2..97b6675ae 100644 --- a/applications/multiai_endoscopy/testing/cpp_multiai_endoscopy.patch +++ b/applications/multiai_endoscopy/testing/cpp_multiai_endoscopy.patch @@ -1,14 +1,13 @@ --- applications/multiai_endoscopy/cpp/post-proc-cpu/multi_ai.cpp 2024-01-10 18:43:58.365526539 +0000 +++ applications/multiai_endoscopy/cpp/post-proc-cpu/multi_ai_test.cpp 2024-02-28 09:17:11.116207162 +0000 -@@ -20,6 +20,7 @@ +@@ -20,5 +20,6 @@ #include - #include #include +#include #include #include #include -@@ -344,6 +345,24 @@ +@@ -367,6 +368,24 @@ add_flow(segmentation_preprocessor, inference, {{"", "receivers"}}); add_flow(inference, segmentation_postprocessor, {{"transmitter", ""}}); add_flow(segmentation_postprocessor, holoviz, {{"", "receivers"}}); diff --git a/applications/multiai_ultrasound/cpp/CMakeLists.txt b/applications/multiai_ultrasound/cpp/CMakeLists.txt index 8cca9afd6..1dcfb8d36 100644 --- a/applications/multiai_ultrasound/cpp/CMakeLists.txt +++ b/applications/multiai_ultrasound/cpp/CMakeLists.txt @@ -26,13 +26,13 @@ add_executable(multiai_ultrasound target_link_libraries(multiai_ultrasound PRIVATE holoscan::core - holoscan::ops::aja holoscan::ops::video_stream_recorder holoscan::ops::video_stream_replayer holoscan::ops::format_converter holoscan::ops::holoviz holoscan::ops::inference holoscan::ops::inference_processor + holoscan::aja visualizer_icardio ) diff --git a/applications/multiai_ultrasound/cpp/main.cpp b/applications/multiai_ultrasound/cpp/main.cpp index a362eb590..3b02ea623 100644 --- a/applications/multiai_ultrasound/cpp/main.cpp +++ b/applications/multiai_ultrasound/cpp/main.cpp @@ -18,7 +18,6 @@ #include #include -#include #include #include #include @@ -28,6 +27,10 @@ #include +#ifdef AJA_SOURCE +#include +#endif + #include #define HOLOSCAN_VERSION \ diff --git a/applications/multiai_ultrasound/python/multiai_ultrasound.py b/applications/multiai_ultrasound/python/multiai_ultrasound.py index 5550e9080..4e96044c7 100644 --- a/applications/multiai_ultrasound/python/multiai_ultrasound.py +++ b/applications/multiai_ultrasound/python/multiai_ultrasound.py @@ -19,7 +19,6 @@ from holoscan import __version__ as holoscan_version from holoscan.core import Application from holoscan.operators import ( - AJASourceOp, FormatConverterOp, HolovizOp, InferenceOp, @@ -30,6 +29,7 @@ from holoscan.resources import BlockMemoryPool, CudaStreamPool, MemoryStorageType from packaging.version import Version +from holohub.aja_source import AJASourceOp from holohub.visualizer_icardio import VisualizerICardioOp diff --git a/applications/object_detection_torch/CMakeLists.txt b/applications/object_detection_torch/CMakeLists.txt index fc338b513..45c43c6df 100644 --- a/applications/object_detection_torch/CMakeLists.txt +++ b/applications/object_detection_torch/CMakeLists.txt @@ -26,13 +26,13 @@ add_executable(object_detection_torch target_link_libraries(object_detection_torch PRIVATE holoscan::core - holoscan::ops::aja holoscan::ops::video_stream_replayer holoscan::ops::video_stream_recorder holoscan::ops::format_converter holoscan::ops::inference holoscan::ops::inference_processor holoscan::ops::holoviz + holoscan::aja ) # Download the cars sample data diff --git a/applications/object_detection_torch/main.cpp b/applications/object_detection_torch/main.cpp index 4aa82cd54..60382e537 100644 --- a/applications/object_detection_torch/main.cpp +++ b/applications/object_detection_torch/main.cpp @@ -18,7 +18,6 @@ #include #include -#include #include #include #include @@ -26,6 +25,10 @@ #include #include +#ifdef AJA_SOURCE +#include +#endif + class App : public holoscan::Application { public: void set_source(const std::string &source) { diff --git a/applications/orsi/CMakeLists.txt b/applications/orsi/CMakeLists.txt index a68cea98a..63027d0e0 100644 --- a/applications/orsi/CMakeLists.txt +++ b/applications/orsi/CMakeLists.txt @@ -30,6 +30,7 @@ set(HOLOSCAN_OPERATORS orsi_segmentation_postprocessor orsi_segmentation_preprocessor orsi_visualizer + aja_source ) if(ORSI_VIDEO_MASTER_SDK_FOUND) diff --git a/applications/orsi/lib/CMakeLists.txt b/applications/orsi/lib/CMakeLists.txt index 9c3c2efd0..2b0bb591e 100644 --- a/applications/orsi/lib/CMakeLists.txt +++ b/applications/orsi/lib/CMakeLists.txt @@ -28,8 +28,8 @@ target_link_libraries(orsi_app_lib PUBLIC holoscan::core holoscan::ops::video_stream_replayer - holoscan::ops::aja holoscan::orsi::format_converter + holoscan::aja ) set(VIDEOMASTER_OPERATOR "") diff --git a/applications/orsi/lib/orsi_app.cpp b/applications/orsi/lib/orsi_app.cpp index 36c23eb12..1d1200dc9 100644 --- a/applications/orsi/lib/orsi_app.cpp +++ b/applications/orsi/lib/orsi_app.cpp @@ -21,7 +21,11 @@ #ifdef USE_VIDEOMASTER #include #endif -#include + +#ifdef AJA_SOURCE +#include +#endif + #include #include diff --git a/applications/ssd_detection_endoscopy_tools/ssd_step1.py b/applications/ssd_detection_endoscopy_tools/ssd_step1.py index df7ebac73..305a52577 100755 --- a/applications/ssd_detection_endoscopy_tools/ssd_step1.py +++ b/applications/ssd_detection_endoscopy_tools/ssd_step1.py @@ -25,15 +25,11 @@ import torch.nn.functional as F from holoscan.core import Application, Operator, OperatorSpec from holoscan.gxf import Entity -from holoscan.operators import ( - AJASourceOp, - FormatConverterOp, - HolovizOp, - InferenceOp, - VideoStreamReplayerOp, -) +from holoscan.operators import FormatConverterOp, HolovizOp, InferenceOp, VideoStreamReplayerOp from holoscan.resources import BlockMemoryPool, MemoryStorageType, UnboundedAllocator +from holohub.aja_source import AJASourceOp + torch.cuda.set_device(torch.device("cuda:0")) debug_tensor_values_preproc = False diff --git a/applications/ssd_detection_endoscopy_tools/ssd_step2_route1.py b/applications/ssd_detection_endoscopy_tools/ssd_step2_route1.py index 05936bafb..199222ca8 100755 --- a/applications/ssd_detection_endoscopy_tools/ssd_step2_route1.py +++ b/applications/ssd_detection_endoscopy_tools/ssd_step2_route1.py @@ -24,15 +24,11 @@ import torch.nn.functional as F from holoscan.core import Application, Operator, OperatorSpec from holoscan.gxf import Entity -from holoscan.operators import ( - AJASourceOp, - FormatConverterOp, - HolovizOp, - InferenceOp, - VideoStreamReplayerOp, -) +from holoscan.operators import FormatConverterOp, HolovizOp, InferenceOp, VideoStreamReplayerOp from holoscan.resources import BlockMemoryPool, MemoryStorageType, UnboundedAllocator +from holohub.aja_source import AJASourceOp + try: import cupy as cp except ImportError: diff --git a/applications/ssd_detection_endoscopy_tools/ssd_step2_route2.py b/applications/ssd_detection_endoscopy_tools/ssd_step2_route2.py index 718ef8036..4c5568166 100755 --- a/applications/ssd_detection_endoscopy_tools/ssd_step2_route2.py +++ b/applications/ssd_detection_endoscopy_tools/ssd_step2_route2.py @@ -18,15 +18,11 @@ import numpy as np from holoscan.core import Application, Operator, OperatorSpec -from holoscan.operators import ( - AJASourceOp, - FormatConverterOp, - HolovizOp, - InferenceOp, - VideoStreamReplayerOp, -) +from holoscan.operators import FormatConverterOp, HolovizOp, InferenceOp, VideoStreamReplayerOp from holoscan.resources import BlockMemoryPool, MemoryStorageType, UnboundedAllocator +from holohub.aja_source import AJASourceOp + try: import cupy as cp except ImportError: diff --git a/applications/ssd_detection_endoscopy_tools/ssd_step2_route2_render_labels.py b/applications/ssd_detection_endoscopy_tools/ssd_step2_route2_render_labels.py index 0a2b990c1..dcd965400 100755 --- a/applications/ssd_detection_endoscopy_tools/ssd_step2_route2_render_labels.py +++ b/applications/ssd_detection_endoscopy_tools/ssd_step2_route2_render_labels.py @@ -18,15 +18,11 @@ import numpy as np from holoscan.core import Application, Operator, OperatorSpec -from holoscan.operators import ( - AJASourceOp, - FormatConverterOp, - HolovizOp, - InferenceOp, - VideoStreamReplayerOp, -) +from holoscan.operators import FormatConverterOp, HolovizOp, InferenceOp, VideoStreamReplayerOp from holoscan.resources import BlockMemoryPool, MemoryStorageType, UnboundedAllocator +from holohub.aja_source import AJASourceOp + try: import cupy as cp except ImportError: diff --git a/applications/ultrasound_segmentation/cpp/CMakeLists.txt b/applications/ultrasound_segmentation/cpp/CMakeLists.txt index 4a47cc017..31136c7f7 100644 --- a/applications/ultrasound_segmentation/cpp/CMakeLists.txt +++ b/applications/ultrasound_segmentation/cpp/CMakeLists.txt @@ -26,12 +26,12 @@ add_executable(ultrasound_segmentation target_link_libraries(ultrasound_segmentation PRIVATE holoscan::core - holoscan::ops::aja holoscan::ops::video_stream_replayer holoscan::ops::format_converter holoscan::ops::inference holoscan::ops::segmentation_postprocessor holoscan::ops::holoviz + holoscan::aja ) # Download the associated dataset if needed @@ -78,13 +78,13 @@ if(BUILD_TESTING) target_link_libraries(ultrasound_segmentation_cpp_test PRIVATE holoscan::core - holoscan::ops::aja holoscan::ops::video_stream_replayer holoscan::ops::video_stream_recorder holoscan::ops::format_converter holoscan::ops::inference holoscan::ops::segmentation_postprocessor holoscan::ops::holoviz + holoscan::aja ) # Add the test and make sure it runs diff --git a/applications/ultrasound_segmentation/cpp/main.cpp b/applications/ultrasound_segmentation/cpp/main.cpp index 80f7bf548..3d67041b6 100644 --- a/applications/ultrasound_segmentation/cpp/main.cpp +++ b/applications/ultrasound_segmentation/cpp/main.cpp @@ -18,13 +18,15 @@ #include #include "holoscan/holoscan.hpp" -#include #include #include #include #include #include +#ifdef AJA_SOURCE +#include +#endif class App : public holoscan::Application { public: void set_source(const std::string& source) { diff --git a/applications/ultrasound_segmentation/python/CMakeLists.txt b/applications/ultrasound_segmentation/python/CMakeLists.txt index fbbd9378d..72558fef2 100644 --- a/applications/ultrasound_segmentation/python/CMakeLists.txt +++ b/applications/ultrasound_segmentation/python/CMakeLists.txt @@ -50,7 +50,7 @@ if(BUILD_TESTING) --data "${HOLOHUB_DATA_DIR}/ultrasound_segmentation" WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) set_property(TEST ultrasound_segmentation_python_test PROPERTY ENVIRONMENT - "PYTHONPATH=${GXF_LIB_DIR}/../python/lib") + "PYTHONPATH=${GXF_LIB_DIR}/../python/lib:${CMAKE_BINARY_DIR}/python/lib") set_tests_properties(ultrasound_segmentation_python_test PROPERTIES PASS_REGULAR_EXPRESSION "Reach end of file or playback count reaches to the limit. Stop ticking.;" diff --git a/applications/ultrasound_segmentation/python/ultrasound_segmentation.py b/applications/ultrasound_segmentation/python/ultrasound_segmentation.py index f7a8f6758..db35938f1 100644 --- a/applications/ultrasound_segmentation/python/ultrasound_segmentation.py +++ b/applications/ultrasound_segmentation/python/ultrasound_segmentation.py @@ -18,7 +18,6 @@ from holoscan.core import Application from holoscan.operators import ( - AJASourceOp, FormatConverterOp, HolovizOp, InferenceOp, @@ -27,6 +26,8 @@ ) from holoscan.resources import BlockMemoryPool, CudaStreamPool, MemoryStorageType +from holohub.aja_source import AJASourceOp + class UltrasoundApp(Application): def __init__(self, data, source="replayer"): diff --git a/applications/ultrasound_segmentation/testing/cpp_ultrasound_segmentation.patch b/applications/ultrasound_segmentation/testing/cpp_ultrasound_segmentation.patch index 51c2e1076..6c6bc8a59 100644 --- a/applications/ultrasound_segmentation/testing/cpp_ultrasound_segmentation.patch +++ b/applications/ultrasound_segmentation/testing/cpp_ultrasound_segmentation.patch @@ -1,8 +1,7 @@ --- applications/ultrasound_segmentation/cpp/main.cpp 2024-01-10 18:43:58.437526279 +0000 +++ applications/ultrasound_segmentation/cpp/main_test.cpp 2024-02-28 09:02:37.697730376 +0000 -@@ -20,6 +20,7 @@ +@@ -20,5 +20,6 @@ #include "holoscan/holoscan.hpp" - #include #include +#include #include diff --git a/applications/yolo_model_deployment/yolo_detection.py b/applications/yolo_model_deployment/yolo_detection.py index 1ea3879cc..b43e3c7a1 100644 --- a/applications/yolo_model_deployment/yolo_detection.py +++ b/applications/yolo_model_deployment/yolo_detection.py @@ -20,15 +20,11 @@ import numpy as np from holoscan.core import Application, Operator, OperatorSpec from holoscan.gxf import Entity -from holoscan.operators import ( - AJASourceOp, - FormatConverterOp, - HolovizOp, - InferenceOp, - VideoStreamReplayerOp, -) +from holoscan.operators import FormatConverterOp, HolovizOp, InferenceOp, VideoStreamReplayerOp from holoscan.resources import BlockMemoryPool, MemoryStorageType, UnboundedAllocator +from holohub.aja_source import AJASourceOp + try: import cupy as cp except ImportError: diff --git a/operators/CMakeLists.txt b/operators/CMakeLists.txt index cc7556cae..c4ea169fe 100644 --- a/operators/CMakeLists.txt +++ b/operators/CMakeLists.txt @@ -15,6 +15,7 @@ # Add operators (in alphabetical order) add_holohub_operator(advanced_network) +add_holohub_operator(aja_source) add_holohub_operator(apriltag_detector) add_holohub_operator(basic_network) add_holohub_operator(cvcuda_holoscan_interop) diff --git a/operators/aja_source/CMakeLists.txt b/operators/aja_source/CMakeLists.txt new file mode 100644 index 000000000..f5489e612 --- /dev/null +++ b/operators/aja_source/CMakeLists.txt @@ -0,0 +1,65 @@ +# SPDX-FileCopyrightText: Copyright (c) 2023-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(aja_source) + +find_package(holoscan 1.0 REQUIRED CONFIG + PATHS "/opt/nvidia/holoscan" "/workspace/holoscan-sdk/install") + +# Fetch AJA nvt2 repository +include(FetchContent) +FetchContent_Declare( + ajantv2 + GIT_REPOSITORY https://github.com/nvidia-holoscan/libajantv2.git + GIT_TAG d4250c556bcf1ebade627a3ef7a2027de7dc85ee +) + +set(ENV{NTV2_VERSION_BUILD} 1) +set(AJANTV2_DISABLE_DEMOS ON) +set(AJANTV2_DISABLE_DRIVER ON) +set(AJANTV2_DISABLE_PLUGINS ON) +set(AJANTV2_DISABLE_TESTS ON) +set(AJANTV2_DISABLE_TOOLS ON) +set(AJA_INSTALL_HEADERS OFF) +set(AJA_INSTALL_SOURCES OFF) +FetchContent_MakeAvailable(ajantv2) +# Disable all the warnings for AJA +target_compile_options(ajantv2 PRIVATE -w) + +add_library(AJA::ajantv2 ALIAS ajantv2) + +add_library(aja_source SHARED + aja_source.hpp + ntv2channel.hpp + aja_source.cpp + ) + +add_library(holoscan::aja ALIAS aja_source) +target_include_directories(aja_source INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) +target_compile_definitions(aja_source INTERFACE AJA_SOURCE) + +target_link_libraries(aja_source + PUBLIC + holoscan::core + AJA::ajantv2 + CUDA::cuda_driver + PRIVATE + CUDA::cudart + GXF::multimedia +) + +if(HOLOHUB_BUILD_PYTHON) + add_subdirectory(python) +endif() diff --git a/operators/aja_source/aja_source.cpp b/operators/aja_source/aja_source.cpp new file mode 100644 index 000000000..a18f916c3 --- /dev/null +++ b/operators/aja_source/aja_source.cpp @@ -0,0 +1,631 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2022-2024 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. + */ + +#include "aja_source.hpp" + +#include +#include + +#include +#include +#include + +#include "gxf/multimedia/video.hpp" +#include "holoscan/core/condition.hpp" +#include "holoscan/core/execution_context.hpp" +#include "holoscan/core/gxf/entity.hpp" +#include "holoscan/core/io_spec.hpp" +#include "holoscan/core/operator_spec.hpp" + +namespace holoscan::ops { + +// used in more than one function +constexpr uint32_t kNumBuffers = 2; + +AJASourceOp::AJASourceOp() {} + +void AJASourceOp::setup(OperatorSpec& spec) { + auto& video_buffer_output = spec.output("video_buffer_output"); + auto& overlay_buffer_input = + spec.input("overlay_buffer_input").condition(ConditionType::kNone); + auto& overlay_buffer_output = spec.output("overlay_buffer_output"); + + constexpr char kDefaultDevice[] = "0"; + constexpr NTV2Channel kDefaultChannel = NTV2_CHANNEL1; + constexpr uint32_t kDefaultWidth = 1920; + constexpr uint32_t kDefaultHeight = 1080; + constexpr uint32_t kDefaultFramerate = 60; + constexpr bool kDefaultInterlaced = false; + constexpr bool kDefaultRDMA = false; + constexpr bool kDefaultEnableOverlay = false; + constexpr bool kDefaultOverlayRDMA = false; + constexpr NTV2Channel kDefaultOverlayChannel = NTV2_CHANNEL2; + + spec.param(video_buffer_output_, + "video_buffer_output", + "VideoBufferOutput", + "Output for the video buffer.", + &video_buffer_output); + spec.param( + device_specifier_, "device", "Device", "Device specifier.", std::string(kDefaultDevice)); + spec.param(channel_, "channel", "Channel", "NTV2Channel to use.", kDefaultChannel); + spec.param(width_, "width", "Width", "Width of the stream.", kDefaultWidth); + spec.param(height_, "height", "Height", "Height of the stream.", kDefaultHeight); + spec.param(framerate_, "framerate", "Framerate", "Framerate of the stream.", kDefaultFramerate); + spec.param(interlaced_, "interlaced", "Interlaced", "Interlaced or not.", kDefaultInterlaced); + spec.param(use_rdma_, "rdma", "RDMA", "Enable RDMA.", kDefaultRDMA); + spec.param( + enable_overlay_, "enable_overlay", "EnableOverlay", "Enable overlay.", kDefaultEnableOverlay); + spec.param(overlay_channel_, + "overlay_channel", + "OverlayChannel", + "NTV2Channel to use for overlay output.", + kDefaultOverlayChannel); + spec.param( + overlay_rdma_, "overlay_rdma", "OverlayRDMA", "Enable overlay RDMA.", kDefaultOverlayRDMA); + spec.param(overlay_buffer_output_, + "overlay_buffer_output", + "OverlayBufferOutput", + "Output for an empty overlay buffer.", + &overlay_buffer_output); + spec.param(overlay_buffer_input_, + "overlay_buffer_input", + "OverlayBufferInput", + "Input for a filled overlay buffer.", + &overlay_buffer_input); +} + +AJAStatus AJASourceOp::DetermineVideoFormat() { + video_format_ = NTV2_FORMAT_UNKNOWN; + + if (interlaced_) { + if (width_ == 1920 && height_ == 1080) { + if (framerate_ == 50) { + video_format_ = NTV2_FORMAT_1080i_5000; + } else if (framerate_ == 59) { + video_format_ = NTV2_FORMAT_1080i_5994; + } else if (framerate_ == 60) { + video_format_ = NTV2_FORMAT_1080i_6000; + } + } + } else { + if (width_ == 1280 && height_ == 720) { + if (framerate_ == 50) { + video_format_ = NTV2_FORMAT_720p_5000; + } else if (framerate_ == 59) { + video_format_ = NTV2_FORMAT_720p_5994; + } else if (framerate_ == 60) { + video_format_ = NTV2_FORMAT_720p_6000; + } + } else if (width_ == 1920 && height_ == 1080) { + if (framerate_ == 23) { + video_format_ = NTV2_FORMAT_1080p_2398; + } else if (framerate_ == 24) { + video_format_ = NTV2_FORMAT_1080p_2400; + } else if (framerate_ == 25) { + video_format_ = NTV2_FORMAT_1080p_2500; + } else if (framerate_ == 29) { + video_format_ = NTV2_FORMAT_1080p_2997; + } else if (framerate_ == 30) { + video_format_ = NTV2_FORMAT_1080p_3000; + } else if (framerate_ == 50) { + video_format_ = NTV2_FORMAT_1080p_5000_A; + } else if (framerate_ == 59) { + video_format_ = NTV2_FORMAT_1080p_5994_A; + } else if (framerate_ == 60) { + video_format_ = NTV2_FORMAT_1080p_6000_A; + } + } else if (width_ == 3840 && height_ == 2160) { + if (framerate_ == 23) { + video_format_ = NTV2_FORMAT_3840x2160p_2398; + } else if (framerate_ == 24) { + video_format_ = NTV2_FORMAT_3840x2160p_2400; + } else if (framerate_ == 25) { + video_format_ = NTV2_FORMAT_3840x2160p_2500; + } else if (framerate_ == 29) { + video_format_ = NTV2_FORMAT_3840x2160p_2997; + } else if (framerate_ == 30) { + video_format_ = NTV2_FORMAT_3840x2160p_3000; + } else if (framerate_ == 50) { + video_format_ = NTV2_FORMAT_3840x2160p_5000; + } else if (framerate_ == 59) { + video_format_ = NTV2_FORMAT_3840x2160p_5994; + } else if (framerate_ == 60) { + video_format_ = NTV2_FORMAT_3840x2160p_6000; + } + } else if (width_ == 4096 && height_ == 2160) { + if (framerate_ == 23) { + video_format_ = NTV2_FORMAT_4096x2160p_2398; + } else if (framerate_ == 24) { + video_format_ = NTV2_FORMAT_4096x2160p_2400; + } else if (framerate_ == 25) { + video_format_ = NTV2_FORMAT_4096x2160p_2500; + } else if (framerate_ == 29) { + video_format_ = NTV2_FORMAT_4096x2160p_2997; + } else if (framerate_ == 30) { + video_format_ = NTV2_FORMAT_4096x2160p_3000; + } else if (framerate_ == 50) { + video_format_ = NTV2_FORMAT_4096x2160p_5000; + } else if (framerate_ == 59) { + video_format_ = NTV2_FORMAT_4096x2160p_5994; + } else if (framerate_ == 60) { + video_format_ = NTV2_FORMAT_4096x2160p_6000; + } + } + } + + return (video_format_ == NTV2_FORMAT_UNKNOWN) ? AJA_STATUS_UNSUPPORTED : AJA_STATUS_SUCCESS; +} + +AJAStatus AJASourceOp::OpenDevice() { + // Get the requested device. + if (!CNTV2DeviceScanner::GetFirstDeviceFromArgument(device_specifier_, device_)) { + HOLOSCAN_LOG_ERROR("Device {} not found.", device_specifier_.get()); + return AJA_STATUS_OPEN; + } + + // Check if the device is ready. + if (!device_.IsDeviceReady(false)) { + HOLOSCAN_LOG_ERROR("Device {} not ready.", device_specifier_.get()); + return AJA_STATUS_INITIALIZE; + } + + // Get the device ID. + device_id_ = device_.GetDeviceID(); + + // Detect Kona HDMI device. + is_kona_hdmi_ = NTV2DeviceGetNumHDMIVideoInputs(device_id_) > 1; + + // Check if a TSI 4x format is needed. + if (is_kona_hdmi_) { use_tsi_ = GetNTV2VideoFormatTSI(&video_format_); } + + // Check device capabilities. + if (!NTV2DeviceCanDoVideoFormat(device_id_, video_format_)) { + HOLOSCAN_LOG_ERROR("AJA device does not support requested video format."); + return AJA_STATUS_UNSUPPORTED; + } + if (!NTV2DeviceCanDoFrameBufferFormat(device_id_, pixel_format_)) { + HOLOSCAN_LOG_ERROR("AJA device does not support requested pixel format."); + return AJA_STATUS_UNSUPPORTED; + } + if (!NTV2DeviceCanDoCapture(device_id_)) { + HOLOSCAN_LOG_ERROR("AJA device cannot capture video."); + return AJA_STATUS_UNSUPPORTED; + } + if (!NTV2_IS_VALID_CHANNEL(channel_)) { + HOLOSCAN_LOG_ERROR("Invalid AJA channel: {}", static_cast(channel_.get())); + return AJA_STATUS_UNSUPPORTED; + } + + // Check overlay capabilities. + if (enable_overlay_) { + if (!NTV2_IS_VALID_CHANNEL(overlay_channel_)) { + HOLOSCAN_LOG_ERROR("Invalid overlay channel: {}", static_cast(overlay_channel_.get())); + return AJA_STATUS_UNSUPPORTED; + } + + if (NTV2DeviceGetNumVideoChannels(device_id_) < 2) { + HOLOSCAN_LOG_ERROR("Insufficient number of video channels"); + return AJA_STATUS_UNSUPPORTED; + } + + if (NTV2DeviceGetNumFrameStores(device_id_) < 2) { + HOLOSCAN_LOG_ERROR("Insufficient number of frame stores"); + return AJA_STATUS_UNSUPPORTED; + } + + if (NTV2DeviceGetNumMixers(device_id_) < 1) { + HOLOSCAN_LOG_ERROR("Hardware mixing not supported"); + return AJA_STATUS_UNSUPPORTED; + } + + if (!NTV2DeviceHasBiDirectionalSDI(device_id_)) { + HOLOSCAN_LOG_ERROR("BiDirectional SDI not supported"); + return AJA_STATUS_UNSUPPORTED; + } + } + + return AJA_STATUS_SUCCESS; +} + +AJAStatus AJASourceOp::SetupVideo() { + constexpr size_t kWarmupFrames = 5; + + NTV2InputSourceKinds input_kind = is_kona_hdmi_ ? NTV2_INPUTSOURCES_HDMI : NTV2_INPUTSOURCES_SDI; + NTV2InputSource input_src = ::NTV2ChannelToInputSource(channel_, input_kind); + NTV2Channel tsi_channel = static_cast(channel_ + 1); + + if (!IsRGBFormat(pixel_format_)) { + HOLOSCAN_LOG_ERROR("YUV formats not yet supported"); + return AJA_STATUS_UNSUPPORTED; + } + + // Detect if the source is YUV or RGB (i.e. if CSC is required or not). + bool is_input_rgb(false); + if (input_kind == NTV2_INPUTSOURCES_HDMI) { + NTV2LHIHDMIColorSpace input_color; + device_.GetHDMIInputColor(input_color, channel_); + is_input_rgb = (input_color == NTV2_LHIHDMIColorSpaceRGB); + } + + // Setup the input routing. + device_.ClearRouting(); + device_.EnableChannel(channel_); + if (use_tsi_) { + device_.SetTsiFrameEnable(true, channel_); + device_.EnableChannel(tsi_channel); + } + device_.SetMode(channel_, NTV2_MODE_CAPTURE); + if (NTV2DeviceHasBiDirectionalSDI(device_id_) && NTV2_INPUT_SOURCE_IS_SDI(input_src)) { + device_.SetSDITransmitEnable(channel_, false); + } + device_.SetVideoFormat(video_format_, false, false, channel_); + device_.SetFrameBufferFormat(channel_, pixel_format_); + if (use_tsi_) { device_.SetFrameBufferFormat(tsi_channel, pixel_format_); } + device_.EnableInputInterrupt(channel_); + device_.SubscribeInputVerticalEvent(channel_); + + NTV2OutputXptID input_output_xpt = + GetInputSourceOutputXpt(input_src, /*DS2*/ false, is_input_rgb, /*Quadrant*/ 0); + NTV2InputXptID fb_input_xpt(GetFrameBufferInputXptFromChannel(channel_)); + if (use_tsi_) { + if (!is_input_rgb) { + if (NTV2DeviceGetNumCSCs(device_id_) < 4) { + HOLOSCAN_LOG_ERROR("CSCs not available for TSI input."); + return AJA_STATUS_UNSUPPORTED; + } + device_.Connect(NTV2_XptFrameBuffer1Input, NTV2_Xpt425Mux1ARGB); + device_.Connect(NTV2_XptFrameBuffer1DS2Input, NTV2_Xpt425Mux1BRGB); + device_.Connect(NTV2_XptFrameBuffer2Input, NTV2_Xpt425Mux2ARGB); + device_.Connect(NTV2_XptFrameBuffer2DS2Input, NTV2_Xpt425Mux2BRGB); + device_.Connect(NTV2_Xpt425Mux1AInput, NTV2_XptCSC1VidRGB); + device_.Connect(NTV2_Xpt425Mux1BInput, NTV2_XptCSC2VidRGB); + device_.Connect(NTV2_Xpt425Mux2AInput, NTV2_XptCSC3VidRGB); + device_.Connect(NTV2_Xpt425Mux2BInput, NTV2_XptCSC4VidRGB); + device_.Connect(NTV2_XptCSC1VidInput, NTV2_XptHDMIIn1); + device_.Connect(NTV2_XptCSC2VidInput, NTV2_XptHDMIIn1Q2); + device_.Connect(NTV2_XptCSC3VidInput, NTV2_XptHDMIIn1Q3); + device_.Connect(NTV2_XptCSC4VidInput, NTV2_XptHDMIIn1Q4); + } else { + device_.Connect(NTV2_XptFrameBuffer1Input, NTV2_Xpt425Mux1ARGB); + device_.Connect(NTV2_XptFrameBuffer1DS2Input, NTV2_Xpt425Mux1BRGB); + device_.Connect(NTV2_XptFrameBuffer2Input, NTV2_Xpt425Mux2ARGB); + device_.Connect(NTV2_XptFrameBuffer2DS2Input, NTV2_Xpt425Mux2BRGB); + device_.Connect(NTV2_Xpt425Mux1AInput, NTV2_XptHDMIIn1RGB); + device_.Connect(NTV2_Xpt425Mux1BInput, NTV2_XptHDMIIn1Q2RGB); + device_.Connect(NTV2_Xpt425Mux2AInput, NTV2_XptHDMIIn1Q3RGB); + device_.Connect(NTV2_Xpt425Mux2BInput, NTV2_XptHDMIIn1Q4RGB); + } + } else if (!is_input_rgb) { + if (NTV2DeviceGetNumCSCs(device_id_) <= static_cast(channel_)) { + HOLOSCAN_LOG_ERROR("No CSC available for NTV2_CHANNEL{}", static_cast(channel_) + 1); + return AJA_STATUS_UNSUPPORTED; + } + NTV2InputXptID csc_input = GetCSCInputXptFromChannel(channel_); + NTV2OutputXptID csc_output = + GetCSCOutputXptFromChannel(channel_, /*inIsKey*/ false, /*inIsRGB*/ true); + device_.Connect(fb_input_xpt, csc_output); + device_.Connect(csc_input, input_output_xpt); + } else { + device_.Connect(fb_input_xpt, input_output_xpt); + } + + if (enable_overlay_) { + // Setup output channel. + device_.SetReference(NTV2_REFERENCE_INPUT1); + device_.SetMode(overlay_channel_, NTV2_MODE_DISPLAY); + device_.SetSDITransmitEnable(overlay_channel_, true); + device_.SetVideoFormat(video_format_, false, false, overlay_channel_); + device_.SetFrameBufferFormat(overlay_channel_, NTV2_FBF_ABGR); + + // Setup mixer controls. + device_.SetMixerFGInputControl(0, NTV2MIXERINPUTCONTROL_SHAPED); + device_.SetMixerBGInputControl(0, NTV2MIXERINPUTCONTROL_FULLRASTER); + device_.SetMixerCoefficient(0, 0x10000); + device_.SetMixerFGMatteEnabled(0, false); + device_.SetMixerBGMatteEnabled(0, false); + + // Setup routing (overlay frame to CSC, CSC and SDI input to mixer, mixer to SDI output). + NTV2OutputDestination output_dst = ::NTV2ChannelToOutputDestination(overlay_channel_); + device_.Connect(GetCSCInputXptFromChannel(overlay_channel_), + GetFrameBufferOutputXptFromChannel(overlay_channel_, true /*RGB*/)); + device_.Connect(NTV2_XptMixer1FGVidInput, + GetCSCOutputXptFromChannel(overlay_channel_, false /*Key*/)); + device_.Connect(NTV2_XptMixer1FGKeyInput, + GetCSCOutputXptFromChannel(overlay_channel_, true /*Key*/)); + device_.Connect(NTV2_XptMixer1BGVidInput, input_output_xpt); + device_.Connect(GetOutputDestInputXpt(output_dst), NTV2_XptMixer1VidYUV); + + // Set initial output frame (overlay uses HW frames 2 and 3). + current_overlay_hw_frame_ = 2; + device_.SetOutputFrame(overlay_channel_, current_overlay_hw_frame_); + } + + // Wait for a number of frames to acquire video signal. + current_hw_frame_ = 0; + device_.SetInputFrame(channel_, current_hw_frame_); + device_.WaitForInputVerticalInterrupt(channel_, kWarmupFrames); + + return AJA_STATUS_SUCCESS; +} + +bool AJASourceOp::AllocateBuffers(std::vector& buffers, size_t num_buffers, + size_t buffer_size, bool rdma) { + buffers.resize(num_buffers); + for (auto& buf : buffers) { + if (rdma) { + if (is_igpu_) { + cudaHostAlloc(&buf, buffer_size, cudaHostAllocDefault); + } else { + cudaMalloc(&buf, buffer_size); + } + unsigned int syncFlag = 1; + if (cuPointerSetAttribute( + &syncFlag, CU_POINTER_ATTRIBUTE_SYNC_MEMOPS, reinterpret_cast(buf))) { + HOLOSCAN_LOG_ERROR("Failed to set SYNC_MEMOPS CUDA attribute for RDMA"); + return false; + } + } else { + buf = malloc(buffer_size); + } + + if (!buf) { + HOLOSCAN_LOG_ERROR("Failed to allocate buffer memory"); + return false; + } + + if (!device_.DMABufferLock(static_cast(buf), buffer_size, true, rdma)) { + HOLOSCAN_LOG_ERROR("Failed to map buffer for DMA"); + return false; + } + } + + return true; +} + +void AJASourceOp::FreeBuffers(std::vector& buffers, bool rdma) { + for (auto& buf : buffers) { + if (rdma) { + if (is_igpu_) { + cudaFreeHost(buf); + } else { + cudaFree(buf); + } + } else { + free(buf); + } + } + buffers.clear(); +} + +AJAStatus AJASourceOp::SetupBuffers() { + auto size = GetVideoWriteSize(video_format_, pixel_format_); + + if (!AllocateBuffers(buffers_, kNumBuffers, size, use_rdma_)) { return AJA_STATUS_INITIALIZE; } + + if (enable_overlay_) { + if (!AllocateBuffers(overlay_buffers_, kNumBuffers, size, overlay_rdma_)) { + return AJA_STATUS_INITIALIZE; + } + } + + return AJA_STATUS_SUCCESS; +} + +void AJASourceOp::initialize() { + register_converter(); + + // Pre-initialize the 'enable_overlay' parameter. + auto enable_overlay_arg = std::find_if(args().rbegin(), args().rend(), [](const auto& arg) { + return (arg.name() == "enable_overlay"); + }); + if (enable_overlay_arg != args().rend()) { + auto& param_wrap = spec()->params()["enable_overlay"]; + ArgumentSetter::set_param(param_wrap, (*enable_overlay_arg)); + } + if (!enable_overlay_.has_value()) { enable_overlay_.set_default_value(); } + // If overlay is disabled, insert ConditionType::kNone + // condition so that its default condition (DownstreamMessageAffordableCondition) is not added + // during Operator::initialize(). + if (!enable_overlay_.get()) { + spec()->outputs()["overlay_buffer_output"]->condition(ConditionType::kNone); + } + + Operator::initialize(); +} + +void AJASourceOp::start() { + // Determine whether or not we're using the iGPU. + // TODO(unknown): This assumes we're using the first GPU device (as does the rest of the + // operator). + cudaDeviceProp prop; + cudaGetDeviceProperties(&prop, 0); + is_igpu_ = prop.integrated; + + float framerate; + if (framerate_ == 23) { + framerate = 23.98F; + } else if (framerate_ == 29) { + framerate = 29.97F; + } else if (framerate_ == 59) { + framerate = 59.94F; + } else { + framerate = framerate_; + } + HOLOSCAN_LOG_INFO("AJA Source: Capturing {}x{}@{}Hz {}from NTV2_CHANNEL{}", + width_, + height_, + framerate, + (interlaced_ ? "(interlaced) " : ""), + (channel_.get() + 1)); + HOLOSCAN_LOG_INFO("AJA Source: RDMA is {}", use_rdma_ ? "enabled" : "disabled"); + if (enable_overlay_) { + HOLOSCAN_LOG_INFO("AJA Source: Outputting overlay to NTV2_CHANNEL{}", + (overlay_channel_.get() + 1)); + HOLOSCAN_LOG_INFO("AJA Source: Overlay RDMA is {}", overlay_rdma_ ? "enabled" : "disabled"); + } else { + HOLOSCAN_LOG_INFO("AJA Source: Overlay output is disabled"); + } + + AJAStatus status = DetermineVideoFormat(); + if (AJA_FAILURE(status)) { + throw std::runtime_error("Video format could not be determined or is not supported."); + } + + status = OpenDevice(); + if (AJA_FAILURE(status)) { + throw std::runtime_error(fmt::format("Failed to open device {}", device_specifier_.get())); + } + + status = SetupVideo(); + if (AJA_FAILURE(status)) { + throw std::runtime_error(fmt::format("Failed to setup device {}", device_specifier_.get())); + } + + status = SetupBuffers(); + if (AJA_FAILURE(status)) { throw std::runtime_error("Failed to setup AJA buffers."); } +} + +void AJASourceOp::compute(InputContext& op_input, OutputContext& op_output, + ExecutionContext& context) { + // holoscan::gxf::Entity + bool have_overlay_in = false; + holoscan::gxf::Entity overlay_in_message; + auto maybe_overlay_message = op_input.receive("overlay_buffer_input"); + if (!maybe_overlay_message || maybe_overlay_message.value().is_null()) { + HOLOSCAN_LOG_TRACE("Operator '{}' failed to find overlay_buffer_input", name_); + } else { + overlay_in_message = maybe_overlay_message.value(); + have_overlay_in = true; + } + + if (enable_overlay_ && have_overlay_in) { + nvidia::gxf::Handle overlay_buffer; + try { + overlay_buffer = holoscan::gxf::get_videobuffer(overlay_in_message); + // Overlay uses HW frames 2 and 3. + current_overlay_hw_frame_ = ((current_overlay_hw_frame_ + 1) % 2) + 2; + + ULWord* ptr = reinterpret_cast(overlay_buffer->pointer()); + device_.DMAWriteFrame(current_overlay_hw_frame_, ptr, overlay_buffer->size()); + device_.SetOutputFrame(overlay_channel_, current_overlay_hw_frame_); + device_.SetMixerMode(0, NTV2MIXERMODE_MIX); + } catch (const std::runtime_error& r_) { + HOLOSCAN_LOG_TRACE("Failed to read VideoBuffer with error: {}", std::string(r_.what())); + } + } + + // Update the next input frame and wait until it starts. + uint32_t next_hw_frame = (current_hw_frame_ + 1) % 2; + device_.SetInputFrame(channel_, next_hw_frame); + device_.WaitForInputFieldID(NTV2_FIELD0, channel_); + + // Read the last completed frame. + auto size = GetVideoWriteSize(video_format_, pixel_format_); + auto ptr = static_cast(buffers_[current_buffer_]); + device_.DMAReadFrame(current_hw_frame_, ptr, size); + + // Set the frame to read for the next tick. + current_hw_frame_ = next_hw_frame; + + // Common (output and overlay) buffer info + nvidia::gxf::VideoTypeTraits video_type; + nvidia::gxf::VideoFormatSize color_format; + auto color_planes = color_format.getDefaultColorPlanes(width_, height_); + nvidia::gxf::VideoBufferInfo info{width_, + height_, + video_type.value, + std::move(color_planes), + nvidia::gxf::SurfaceLayout::GXF_SURFACE_LAYOUT_PITCH_LINEAR}; + + if (enable_overlay_) { + // Pass an overlay buffer downstream. + auto overlay_output = nvidia::gxf::Entity::New(context.context()); + if (!overlay_output) { + HOLOSCAN_LOG_ERROR("Failed to allocate overlay output; terminating."); + return; + } + + auto overlay_buffer = overlay_output.value().add(); + if (!overlay_buffer) { + HOLOSCAN_LOG_ERROR("Failed to allocate overlay buffer; terminating."); + return; + } + + auto overlay_storage_type = overlay_rdma_ ? nvidia::gxf::MemoryStorageType::kDevice + : nvidia::gxf::MemoryStorageType::kHost; + overlay_buffer.value()->wrapMemory( + info, size, overlay_storage_type, overlay_buffers_[current_buffer_], nullptr); + + auto overlay_result = gxf::Entity(std::move(overlay_output.value())); + op_output.emit(overlay_result, "overlay_buffer_output"); + } + + // Pass the video output buffer downstream. + auto video_output = nvidia::gxf::Entity::New(context.context()); + if (!video_output) { + throw std::runtime_error("Failed to allocate video output; terminating."); + return; + } + + auto video_buffer = video_output.value().add(); + if (!video_buffer) { + throw std::runtime_error("Failed to allocate video buffer; terminating."); + return; + } + + auto storage_type = + use_rdma_ ? nvidia::gxf::MemoryStorageType::kDevice : nvidia::gxf::MemoryStorageType::kHost; + video_buffer.value()->wrapMemory(info, size, storage_type, buffers_[current_buffer_], nullptr); + + auto result = gxf::Entity(std::move(video_output.value())); + op_output.emit(result, "video_buffer_output"); + + // Update the current buffer (index shared between video and overlay) + current_buffer_ = (current_buffer_ + 1) % kNumBuffers; +} + +void AJASourceOp::stop() { + device_.UnsubscribeInputVerticalEvent(channel_); + device_.DMABufferUnlockAll(); + + if (enable_overlay_) { device_.SetMixerMode(0, NTV2MIXERMODE_FOREGROUND_OFF); } + + FreeBuffers(buffers_, use_rdma_); + FreeBuffers(overlay_buffers_, overlay_rdma_); +} + +bool AJASourceOp::GetNTV2VideoFormatTSI(NTV2VideoFormat* format) { + switch (*format) { + case NTV2_FORMAT_3840x2160p_2400: + *format = NTV2_FORMAT_4x1920x1080p_2400; + return true; + case NTV2_FORMAT_3840x2160p_6000: + *format = NTV2_FORMAT_4x1920x1080p_6000; + return true; + case NTV2_FORMAT_4096x2160p_2400: + *format = NTV2_FORMAT_4x2048x1080p_2400; + return true; + case NTV2_FORMAT_4096x2160p_6000: + *format = NTV2_FORMAT_4x2048x1080p_6000; + return true; + default: + return false; + } +} + +} // namespace holoscan::ops diff --git a/operators/aja_source/aja_source.hpp b/operators/aja_source/aja_source.hpp new file mode 100644 index 000000000..401ca2632 --- /dev/null +++ b/operators/aja_source/aja_source.hpp @@ -0,0 +1,140 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2022-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. + */ + +#ifndef HOLOSCAN_OPERATORS_AJA_SOURCE_AJA_SOURCE_HPP +#define HOLOSCAN_OPERATORS_AJA_SOURCE_AJA_SOURCE_HPP + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Woverloaded-virtual" +// AJA headers are not clean C++ and generate warnings +// when compiled with -Werror. +// Since ajantv2 is 3rd party code, we disable the warning for now. +#include +#include +#pragma GCC diagnostic pop +#include + +#include +#include +#include + +#include "holoscan/core/io_context.hpp" +#include "holoscan/core/io_spec.hpp" +#include "holoscan/core/operator.hpp" +#include "holoscan/core/operator_spec.hpp" +#include "./ntv2channel.hpp" + +namespace holoscan::ops { + +/** + * @brief Operator class to get the video stream from AJA capture card. + * + * ==Named Inputs== + * + * - **overlay_buffer_input** : `nvidia::gxf::VideoBuffer` (optional) + * - The operator does not require a message on this input port in order for `compute` to + * be called. If a message is found, and `enable_overlay` is true, the image will be + * mixed with the image captured by the AJA card. If `enable_overlay` is false, any message + * on this port will be ignored. + * + * ==Named Outputs== + * + * - **video_buffer_output** : `nvidia::gxf::VideoBuffer` + * - The output video frame from the AJA capture card. If `overlay_rdma` is true, this + * video buffer will be on the device, otherwise it will be in pinned host memory. + * - **overlay_buffer_output** : `nvidia::gxf::VideoBuffer` (optional) + * - This output port will only emit a video buffer when `enable_overlay` is true. If + * `overlay_rdma` is true, this video buffer will be on the device, otherwise it will be + * in pinned host memory. + * + * ==Parameters== + * + * - **device**: The device to target (e.g., "0" for device 0). Optional (default: "0"). + * - **channel**: The camera `NTV2Channel` to use for output (e.g., `NTV2Channel::NTV2_CHANNEL1` + * (`0`) or "NTV2_CHANNEL1" (in YAML) for the first channel). Optional (default: + * `NTV2Channel::NTV2_CHANNEL1` in C++ or `"NTV2_CHANNEL1"` in YAML). + * - **width**: Width of the video stream. Optional (default: `1920`). + * - **height**: Height of the video stream. Optional (default: `1080`). + * - **framerate**: Frame rate of the video stream. Optional (default: `60`). + * - **interlaced**: Whether the frame is interlaced (true) or progressive (false). Optional (default: `false`). + * - **rdma**: Boolean indicating whether RDMA is enabled. Optional (default: `false`). + * - **enable_overlay**: Boolean indicating whether a separate overlay channel is enabled. Optional + * (default: `false`). + * - **overlay_channel**: The camera `NTV2Channel` to use for overlay output. Optional (default: + * `NTV2Channel::NTV2_CHANNEL2` in C++ or `"NTV2_CHANNEL2"` in YAML). + * - **overlay_rdma**: Boolean indicating whether RDMA is enabled for the overlay. Optional + * (default: `true`). + */ +class AJASourceOp : public holoscan::Operator { + public: + HOLOSCAN_OPERATOR_FORWARD_ARGS(AJASourceOp) + + AJASourceOp(); + + void setup(OperatorSpec& spec) override; + + void initialize() override; + void start() override; + void compute(InputContext& op_input, OutputContext& op_output, + ExecutionContext& context) override; + void stop() override; + + private: + AJAStatus DetermineVideoFormat(); + AJAStatus OpenDevice(); + AJAStatus SetupVideo(); + AJAStatus SetupBuffers(); + AJAStatus StartAutoCirculate(); + bool AllocateBuffers(std::vector& buffers, size_t num_buffers, size_t buffer_size, + bool rdma); + void FreeBuffers(std::vector& buffers, bool rdma); + bool GetNTV2VideoFormatTSI(NTV2VideoFormat* format); + + Parameter video_buffer_output_; + Parameter device_specifier_; + Parameter channel_; + Parameter width_; + Parameter height_; + Parameter framerate_; + Parameter interlaced_; + Parameter use_rdma_; + Parameter enable_overlay_; + Parameter overlay_channel_; + Parameter overlay_rdma_; + Parameter overlay_buffer_input_; + Parameter overlay_buffer_output_; + + // internal state + CNTV2Card device_; + NTV2DeviceID device_id_ = DEVICE_ID_NOTFOUND; + NTV2VideoFormat video_format_ = NTV2_FORMAT_UNKNOWN; + NTV2PixelFormat pixel_format_ = NTV2_FBF_ABGR; + bool use_tsi_ = false; + bool is_kona_hdmi_ = false; + + std::vector buffers_; + std::vector overlay_buffers_; + uint8_t current_buffer_ = 0; + uint8_t current_hw_frame_ = 0; + uint8_t current_overlay_hw_frame_ = 0; + + bool is_igpu_ = false; +}; + +} // namespace holoscan::ops + +#endif /* HOLOSCAN_OPERATORS_AJA_SOURCE_AJA_SOURCE_HPP */ diff --git a/operators/aja_source/metadata.json b/operators/aja_source/metadata.json new file mode 100644 index 000000000..718f7c3e9 --- /dev/null +++ b/operators/aja_source/metadata.json @@ -0,0 +1,32 @@ +{ + "operator": { + "name": "aja_source", + "authors": [ + { + "name": "Holoscan Team", + "affiliation": "NVIDIA" + } + ], + "version": "1.0", + "changelog": { + "1.0": "Initial Release" + }, + "holoscan_sdk": { + "minimum_required_version": "1.0.3", + "tested_versions": [ + "2.0.0" + ] + }, + "platforms": [ + "amd64", + "arm64" + ], + "tags": [ + "Camera", + "AJA" + ], + "ranking": 1, + "dependencies": { + } + } +} diff --git a/operators/aja_source/ntv2channel.hpp b/operators/aja_source/ntv2channel.hpp new file mode 100644 index 000000000..d8fc61e31 --- /dev/null +++ b/operators/aja_source/ntv2channel.hpp @@ -0,0 +1,58 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2022-2023 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. + */ + +#ifndef HOLOSCAN_OPERATORS_AJA_SOURCE_NTV2CHANNEL_HPP +#define HOLOSCAN_OPERATORS_AJA_SOURCE_NTV2CHANNEL_HPP + +#include +#include + +#include +#include + +template <> +struct YAML::convert { + static Node encode(const NTV2Channel& rhs) { + Node node; + auto channel = static_cast(rhs) + 1; // 0 => NTV2_CHANNEL1 + std::stringstream ss; + ss << "NTV2_CHANNEL"; + ss << channel; + node.push_back(ss.str()); + YAML::Node value_node = node[0]; + return value_node; + } + + static bool decode(const Node& node, NTV2Channel& rhs) { + if (!node.IsScalar()) return false; + + const std::string prefix("NTV2_CHANNEL"); + auto value = node.Scalar(); + if (value.find(prefix) != 0) return false; + value = value.substr(prefix.length()); + + try { + size_t len; + const auto index = std::stoi(value, &len); + if (index < 1 || index > NTV2_MAX_NUM_CHANNELS || len != value.length()) { return false; } + rhs = static_cast(index - 1); + return true; + } catch (...) { return false; } + } +}; + +#endif /* HOLOSCAN_OPERATORS_AJA_SOURCE_NTV2CHANNEL_HPP */ diff --git a/operators/aja_source/python/CMakeLists.txt b/operators/aja_source/python/CMakeLists.txt new file mode 100644 index 000000000..0eb9d6aab --- /dev/null +++ b/operators/aja_source/python/CMakeLists.txt @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: Copyright (c) 2023 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. + +include(pybind11_add_holohub_module) +pybind11_add_holohub_module( + CPP_CMAKE_TARGET aja_source + CLASS_NAME "AJASourceOp" + SOURCES aja_source.cpp +) diff --git a/operators/aja_source/python/__init__.py b/operators/aja_source/python/__init__.py new file mode 100644 index 000000000..c6ee6acdd --- /dev/null +++ b/operators/aja_source/python/__init__.py @@ -0,0 +1,22 @@ +""" +SPDX-FileCopyrightText: Copyright (c) 2023-2024 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. +""" # noqa: E501 + +import holoscan.core # noqa: F401 + +from ._aja_source import AJASourceOp, NTV2Channel + +__all__ = ["AJASourceOp", "NTV2Channel"] diff --git a/operators/aja_source/python/aja_source.cpp b/operators/aja_source/python/aja_source.cpp new file mode 100644 index 000000000..e2232f4c7 --- /dev/null +++ b/operators/aja_source/python/aja_source.cpp @@ -0,0 +1,167 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2022-2024 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. + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../operator_util.hpp" +#include "./aja_source_pydoc.hpp" + +#include "holoscan/core/fragment.hpp" +#include "holoscan/core/operator.hpp" +#include "holoscan/core/operator_spec.hpp" +#include "../aja_source.hpp" + +using std::string_literals::operator""s; +using pybind11::literals::operator""_a; + +namespace py = pybind11; + +namespace holoscan::ops { + +namespace { + +// using constexpr constructor instead of unordered_map here to make clang-tidy happy +// (avoids warning of type: fuchsia-statically-constructed-objects) +constexpr std::array, 8> NTV2ChannelMapping = { + {{"NTV2_CHANNEL1", NTV2Channel::NTV2_CHANNEL1}, + {"NTV2_CHANNEL2", NTV2Channel::NTV2_CHANNEL2}, + {"NTV2_CHANNEL3", NTV2Channel::NTV2_CHANNEL3}, + {"NTV2_CHANNEL4", NTV2Channel::NTV2_CHANNEL4}, + {"NTV2_CHANNEL5", NTV2Channel::NTV2_CHANNEL5}, + {"NTV2_CHANNEL6", NTV2Channel::NTV2_CHANNEL6}, + {"NTV2_CHANNEL7", NTV2Channel::NTV2_CHANNEL7}, + {"NTV2_CHANNEL8", NTV2Channel::NTV2_CHANNEL8}}}; + +constexpr NTV2Channel ToNTV2Channel(std::string_view value) { + for (const auto& [name, channel] : NTV2ChannelMapping) { + if (name == value) { return channel; } + } + return NTV2Channel::NTV2_CHANNEL_INVALID; +} + +} // namespace + +/* Trampoline class for handling Python kwargs + * + * These add a constructor that takes a Fragment for which to initialize the operator. + * The explicit parameter list and default arguments take care of providing a Pythonic + * kwarg-based interface with appropriate default values matching the operator's + * default parameters in the C++ API `setup` method. + * + * The sequence of events in this constructor is based on Fragment::make_operator + */ + +class PyAJASourceOp : public AJASourceOp { + public: + /* Inherit the constructors */ + using AJASourceOp::AJASourceOp; + + // Define a constructor that fully initializes the object. + PyAJASourceOp( + Fragment* fragment, const py::args& args, const std::string& device = "0"s, + const std::variant& channel = NTV2Channel::NTV2_CHANNEL1, + uint32_t width = 1920, uint32_t height = 1080, uint32_t framerate = 60, + bool interlaced = false, bool rdma = false, bool enable_overlay = false, + const std::variant& overlay_channel = NTV2Channel::NTV2_CHANNEL2, + bool overlay_rdma = true, const std::string& name = "aja_source") + : AJASourceOp(ArgList{Arg{"device", device}, + Arg{"width", width}, + Arg{"height", height}, + Arg{"framerate", framerate}, + Arg{"interlaced", interlaced}, + Arg{"rdma", rdma}, + Arg{"enable_overlay", enable_overlay}, + Arg{"overlay_rdma", overlay_rdma}}) { + add_positional_condition_and_resource_args(this, args); + if (std::holds_alternative(channel)) { + this->add_arg(Arg("channel", ToNTV2Channel(std::get(channel)))); + } else { + this->add_arg(Arg("channel", std::get(channel))); + } + if (std::holds_alternative(overlay_channel)) { + this->add_arg(Arg("overlay_channel", ToNTV2Channel(std::get(overlay_channel)))); + } else { + this->add_arg(Arg("overlay_channel", std::get(overlay_channel))); + } + name_ = name; + fragment_ = fragment; + spec_ = std::make_shared(fragment); + setup(*spec_); + } +}; + +/* The python module */ + +PYBIND11_MODULE(_aja_source, m) { + m.doc() = R"pbdoc( + Holoscan SDK AJASourceOp Python Bindings + --------------------------------------- + .. currentmodule:: _aja_source + )pbdoc"; + + py::enum_(m, "NTV2Channel") + .value("NTV2_CHANNEL1", NTV2Channel::NTV2_CHANNEL1) + .value("NTV2_CHANNEL2", NTV2Channel::NTV2_CHANNEL2) + .value("NTV2_CHANNEL3", NTV2Channel::NTV2_CHANNEL3) + .value("NTV2_CHANNEL4", NTV2Channel::NTV2_CHANNEL4) + .value("NTV2_CHANNEL5", NTV2Channel::NTV2_CHANNEL5) + .value("NTV2_CHANNEL6", NTV2Channel::NTV2_CHANNEL6) + .value("NTV2_CHANNEL7", NTV2Channel::NTV2_CHANNEL7) + .value("NTV2_CHANNEL8", NTV2Channel::NTV2_CHANNEL8) + .value("NTV2_MAX_NUM_CHANNELS", NTV2Channel::NTV2_MAX_NUM_CHANNELS) + .value("NTV2_CHANNEL_INVALID", NTV2Channel::NTV2_CHANNEL_INVALID); + + py::class_>( + m, "AJASourceOp", doc::AJASourceOp::doc_AJASourceOp) + .def(py::init, + uint32_t, + uint32_t, + uint32_t, + bool, + bool, + bool, + const std::variant, + bool, + const std::string&>(), + "fragment"_a, + "device"_a = "0"s, + "channel"_a = NTV2Channel::NTV2_CHANNEL1, + "width"_a = 1920, + "height"_a = 1080, + "framerate"_a = 60, + "interlaced"_a = false, + "rdma"_a = false, + "enable_overlay"_a = false, + "overlay_channel"_a = NTV2Channel::NTV2_CHANNEL2, + "overlay_rdma"_a = true, + "name"_a = "aja_source"s, + doc::AJASourceOp::doc_AJASourceOp); +} // PYBIND11_MODULE NOLINT +} // namespace holoscan::ops diff --git a/operators/aja_source/python/aja_source_pydoc.hpp b/operators/aja_source/python/aja_source_pydoc.hpp new file mode 100644 index 000000000..b5479b228 --- /dev/null +++ b/operators/aja_source/python/aja_source_pydoc.hpp @@ -0,0 +1,85 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2022-2024 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. + */ + +#ifndef PYHOLOSCAN_OPERATORS_AJA_SOURCE_PYDOC_HPP +#define PYHOLOSCAN_OPERATORS_AJA_SOURCE_PYDOC_HPP + +#include + +#include "macros.hpp" + +namespace holoscan::doc::AJASourceOp { + +// PyAJASourceOp Constructor +PYDOC(AJASourceOp, R"doc( +Operator to get a video stream from an AJA capture card. + +**==Named Inputs==** + + overlay_buffer_input : nvidia::gxf::VideoBuffer (optional) + The operator does not require a message on this input port in order for ``compute`` to be + called. If a message is found, and ``enable_overlay`` is ``True``, the image will be mixed + with the image captured by the AJA card. If ``enable_overlay`` is ``False``, any message on + this port will be ignored. + +**==Named Outputs==** + + video_buffer_output : nvidia::gxf::VideoBuffer + The output video frame from the AJA capture card. If ``overlay_rdma`` is ``True``, this + video buffer will be on the device, otherwise it will be in pinned host memory. + overlay_buffer_output : nvidia::gxf::VideoBuffer (optional) + This output port will only emit a video buffer when ``enable_overlay`` is ``True``. If + ``overlay_rdma`` is ``True``, this video buffer will be on the device, otherwise it will be + in pinned host memory. + +Parameters +---------- +fragment : holoscan.core.Fragment (constructor only) + The fragment that the operator belongs to. +device : str, optional + The device to target (e.g., "0" for device 0). Default value is ``"0"``. +channel : holoscan.operators.NTV2Channel or int, optional + The camera ``NTV2Channel`` to use for output (e.g., ``NTV2Channel.NTV2_CHANNEL1`` (``0``) or + "NTV2_CHANNEL1" (in YAML) for the first channel). Default value is ``NTV2Channel.NTV2_CHANNEL1`` + (``"NTV2_CHANNEL1"`` in YAML). +width : int, optional + Width of the video stream. Default value is ``1920``. +height : int, optional + Height of the video stream. Default value is ``1080``. +framerate : int, optional + Frame rate of the video stream. Default value is ``60``. +interlaced : bool, optional + Whether or not the video is an interlaced format. Default value is ``False`` + (``"false"`` in YAML). +rdma : bool, optional + Boolean indicating whether RDMA is enabled. Default value is ``False`` (``"false"`` in YAML). +enable_overlay : bool, optional + Boolean indicating whether a separate overlay channel is enabled. Default value is ``False`` + (``"false"`` in YAML). +overlay_channel : holoscan.operators.NTV2Channel or int, optional + The camera NTV2Channel to use for overlay output. Default value is ``NTV2Channel.NTV2_CHANNEL2`` + (``"NTV2_CHANNEL2"`` in YAML). +overlay_rdma : bool, optional + Boolean indicating whether RDMA is enabled for the overlay. Default value is ``False`` + (``"false"`` in YAML). +name : str, optional (constructor only) + The name of the operator. Default value is ``"aja_source"``. +)doc") + +} // namespace holoscan::doc::AJASourceOp + +#endif /* PYHOLOSCAN_OPERATORS_AJA_SOURCE_PYDOC_HPP */ diff --git a/tutorials/creating-multi-node-applications/scenario1/multi_ai.py b/tutorials/creating-multi-node-applications/scenario1/multi_ai.py index 3c46d45c9..0a32ba1c3 100644 --- a/tutorials/creating-multi-node-applications/scenario1/multi_ai.py +++ b/tutorials/creating-multi-node-applications/scenario1/multi_ai.py @@ -22,7 +22,6 @@ import numpy as np from holoscan.core import Application, Fragment, Operator, OperatorSpec from holoscan.operators import ( - AJASourceOp, FormatConverterOp, HolovizOp, InferenceOp, @@ -31,6 +30,8 @@ ) from holoscan.resources import UnboundedAllocator +from holohub.aja_source import AJASourceOp + class DetectionPostprocessorOp(Operator): """Example of an operator post processing the tensor from inference component. diff --git a/tutorials/creating-multi-node-applications/scenario2/endoscopy_distributed_app.py b/tutorials/creating-multi-node-applications/scenario2/endoscopy_distributed_app.py index f5bd4128c..9a0fd311a 100644 --- a/tutorials/creating-multi-node-applications/scenario2/endoscopy_distributed_app.py +++ b/tutorials/creating-multi-node-applications/scenario2/endoscopy_distributed_app.py @@ -18,7 +18,6 @@ from holoscan.core import Application, Fragment from holoscan.operators import ( - AJASourceOp, FormatConverterOp, HolovizOp, InferenceOp, @@ -33,6 +32,7 @@ UnboundedAllocator, ) +from holohub.aja_source import AJASourceOp from holohub.lstm_tensor_rt_inference import LSTMTensorRTInferenceOp # Enable this line for Yuam capture card diff --git a/utilities/aja_build.sh b/utilities/aja_build.sh new file mode 100755 index 000000000..439614be8 --- /dev/null +++ b/utilities/aja_build.sh @@ -0,0 +1,153 @@ +#!/bin/bash +# SPDX-FileCopyrightText: Copyright (c) 2024 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. + +# Read arguments. +POSITIONAL_ARGS=() + +while [[ $# -gt 0 ]]; do + case $1 in + --skip-sdk) + SKIP_SDK=1 + shift # past argument + ;; + --load-driver) + LOAD_DRIVER=1 + shift # past argument + ;; + -*|--*) + echo "Unknown option $1" + echo "Usage: $(basename $0) [--skip-sdk] [--load-driver]" + exit 1 + ;; + *) + POSITIONAL_ARGS+=("$1") # save positional arg + shift # past argument + ;; + esac +done + +set -- "${POSITIONAL_ARGS[@]}" # restore positional parameters +basedir=$(pwd) + +# Set the appropriate build flags. +echo "==========================================================" +echo -n " Building AJA driver " +if [ -z "$SKIP_SDK" ]; then + echo -n "and SDK " +fi +echo -n "with RDMA support for " +export AJA_RDMA=1 +if lsmod | grep -q nvgpu ; then + echo "iGPU" + export AJA_IGPU=1 +else + echo "dGPU" + unset AJA_IGPU +fi +echo "==========================================================" && echo + +# Ensure the open source dGPU driver is being used. +if [ -z "$AJA_IGPU" ]; then + LICENSE=$(modinfo -l nvidia) + if [ "$LICENSE" == "NVIDIA" ]; then + echo "ERROR: The open source NVIDIA drivers are required for RDMA support" + echo " but the closed source drivers are currently installed. Please" + echo " install the open source drivers then run this script again." + exit 1 + fi +fi + +# Ensure CMake is installed. +if [ -z "$SKIP_SDK" ]; then + if ! command -v cmake &> /dev/null; then + echo "ERROR: CMake is not installed. Install it with the following then" + echo " run this script again:" + echo " sudo apt install -y cmake" + exit 1 + fi +fi + +# Checkout the libajantv2 repo. +if [ ! -d libajantv2 ]; then + git clone https://github.com/nvidia-holoscan/libajantv2.git + if [ $? -ne 0 ]; then + echo "ERROR: Failed to checkout libajantv2 repo." + exit 1 + fi + cd libajantv2/ +else + cd libajantv2/ && git pull + if [ $? -ne 0 ]; then + echo "ERROR: Failed to checkout libajantv2 repo." + exit 1 + fi +fi + +# Build the driver. +make -j --directory driver/linux/ +if [ $? -ne 0 ]; then + echo "ERROR: Failed to build libajantv2 driver." + exit 1 +fi + +# Build the SDK. +if [ -z "$SKIP_SDK" ]; then + mkdir -p build && cd build + cmake .. -Wno-dev && make -j + if [ $? -ne 0 ]; then + echo "ERROR: Failed to build libajantv2 SDK." + exit 1 + fi + if ! [ -f tools/rdmawhacker/rdmawhacker ]; then + echo && echo "WARNING: rdmawhacker build was skipped. Is CUDA installed?" + fi +fi + +# Load the driver. +if [ -n "$LOAD_DRIVER" ]; then + echo && echo "==========================================================" + echo "Loading AJA driver..." + cd $basedir + sudo ./libajantv2/driver/bin/load_ajantv2 + if [ $? -ne 0 ]; then + echo "ERROR: Failed to load AJA driver." + exit 1 + fi + if [ -z "$SKIP_SDK" ]; then + echo && echo "Enumerating AJA Devices:" + ./libajantv2/build/demos/ntv2enumerateboards/ntv2enumerateboards + if [ $? -ne 0 ]; then + echo "ERROR: Failed to enumerate AJA devices." + exit 1 + fi + fi +fi + +# Finish up. +echo && echo "============================================================" +echo "SUCCESS!" +if [ -z "$LOAD_DRIVER" ]; then + echo "Load driver using 'sudo ./libajantv2/driver/bin/load_ajantv2'" + if [ -f ${basedir}/libajantv2/build/demos/ntv2enumerateboards/ntv2enumerateboards ]; then + echo "Use ntv2enumerateboards tool to list available AJA devices:" + echo " ./libajantv2/build/demos/ntv2enumerateboards/ntv2enumerateboards" + fi +fi +if [ -f ${basedir}/libajantv2/build/tools/rdmawhacker/rdmawhacker ]; then + echo "Use rdmawhacker tool to check RDMA is functional (CTRL-C to exit):" + echo " ./libajantv2/build/tools/rdmawhacker/rdmawhacker" +fi +exit 0