From c2a32468ef65642c1d63d9cdcfad7f9ef203ee23 Mon Sep 17 00:00:00 2001 From: Aaron Marburg Date: Sun, 25 Jan 2026 16:54:54 -0800 Subject: [PATCH] Major restructure --- .drone.yml | 59 ---- .github/dependabot.yml | 6 + .github/mergify.yml | 18 ++ .github/workflows/ci.yaml | 31 ++ .github/workflows/format.yaml | 34 ++ .pre-commit-config.yaml | 61 ++-- CMakeLists.txt | 195 ++---------- README.md | 115 +------ cmake/BuildNonROS.cmake | 98 ++++++ cmake/BuildROS1.cmake | 45 +++ cmake/BuildROS2.cmake | 36 +++ cmake/drawsonarConfig.cmake.in | 10 + fips | 17 - fips.yml | 15 - helpers/make_color_map.py | 38 ++- .../AbstractSonarInterface.h | 12 +- .../ColorMaps.h | 8 +- .../DataStructures.h | 4 +- .../DrawSonar.h | 30 +- include/libdrawsonar/HistogramGenerator.h | 24 ++ include/libdrawsonar/Logger.h | 145 +++++++++ .../OverlayImage.h | 4 +- .../SonarDrawer.h | 47 ++- include/sonar_image_proc/HistogramGenerator.h | 24 -- launch/sonar_postproc.launch | 50 --- lib/AbstractSonarInterface.cpp | 6 +- lib/ColorMaps.cpp | 6 +- lib/HistogramGenerator.cpp | 28 +- lib/OldDrawSonar.cpp | 17 +- lib/SonarDrawer.cpp | 39 ++- libdrawsonar.repos | 5 + package.xml | 30 +- python/draw_sonar/__init__.py | 6 +- python/histogram_drawer | 30 +- python/ndarray_converter.cpp | 42 +-- python/ndarray_converter.h | 11 +- python/old_setup.py | 73 ++--- python/py_draw_sonar.cpp | 4 +- python/sonar_image_proc/sonar_msg_metadata.py | 1 - python/test.py | 4 +- ros/cfg/DrawSonar.cfg | 18 -- .../sonar_image_msg_interface.h | 237 -------------- ros/src/draw_sonar_node.cpp | 19 -- ros/src/draw_sonar_nodelet.cpp | 240 --------------- ros/src/sonar_postprocessor_node.cpp | 19 -- ros/src/sonar_postprocessor_nodelet.cpp | 114 ------- ros/tools/bag2sonar.cpp | 176 ----------- scripts/sonar_fov.py | 185 ----------- scripts/sonar_pointcloud.py | 291 ------------------ setup.py | 8 +- sonar_image_proc.rosinstall | 1 - test/unit/test_draw_sonar.cpp | 4 +- 52 files changed, 749 insertions(+), 1991 deletions(-) delete mode 100644 .drone.yml create mode 100644 .github/dependabot.yml create mode 100644 .github/mergify.yml create mode 100644 .github/workflows/ci.yaml create mode 100644 .github/workflows/format.yaml create mode 100644 cmake/BuildNonROS.cmake create mode 100644 cmake/BuildROS1.cmake create mode 100644 cmake/BuildROS2.cmake create mode 100644 cmake/drawsonarConfig.cmake.in delete mode 100755 fips delete mode 100644 fips.yml rename include/{sonar_image_proc => libdrawsonar}/AbstractSonarInterface.h (96%) rename include/{sonar_image_proc => libdrawsonar}/ColorMaps.h (95%) rename include/{sonar_image_proc => libdrawsonar}/DataStructures.h (84%) rename include/{sonar_image_proc => libdrawsonar}/DrawSonar.h (73%) create mode 100644 include/libdrawsonar/HistogramGenerator.h create mode 100644 include/libdrawsonar/Logger.h rename include/{sonar_image_proc => libdrawsonar}/OverlayImage.h (98%) rename include/{sonar_image_proc => libdrawsonar}/SonarDrawer.h (86%) delete mode 100644 include/sonar_image_proc/HistogramGenerator.h delete mode 100644 launch/sonar_postproc.launch create mode 100644 libdrawsonar.repos delete mode 100644 ros/cfg/DrawSonar.cfg delete mode 100644 ros/include/sonar_image_proc/sonar_image_msg_interface.h delete mode 100644 ros/src/draw_sonar_node.cpp delete mode 100644 ros/src/draw_sonar_nodelet.cpp delete mode 100644 ros/src/sonar_postprocessor_node.cpp delete mode 100644 ros/src/sonar_postprocessor_nodelet.cpp delete mode 100644 ros/tools/bag2sonar.cpp delete mode 100755 scripts/sonar_fov.py delete mode 100755 scripts/sonar_pointcloud.py delete mode 100644 sonar_image_proc.rosinstall diff --git a/.drone.yml b/.drone.yml deleted file mode 100644 index 4538130..0000000 --- a/.drone.yml +++ /dev/null @@ -1,59 +0,0 @@ -kind: pipeline -type: docker -name: build - -platform: - os: linux - arch: amd64 - -steps: - ## Biuld in fips (debug with unittesting) -- name: build-fips-unittest - image: amarburg/lsdslam-dev-host - commands: - - ./fips set config linux-make-unittest - - ./fips build - - ## Build in ros melodic -# - name: build-ros-melodic -# image: amarburg/drone-ci-ros-melodic:latest -# commands: -# - wget -O- https://gitlab.com/amarburg/ros_drone_ci/-/raw/master/bootstrap.sh | /bin/bash -# environment: -# WSTOOL_RECURSIVE: true -# ROS_PACKAGES_TO_INSTALL: cv-bridge image-transport - - ## Build in ros noetic -- name: build-ros-noetic - image: amarburg/drone-ci-ros-noetic:latest - commands: - - wget -O- https://gitlab.com/amarburg/ros_drone_ci/-/raw/master/bootstrap.sh | /bin/bash - environment: - WSTOOL_RECURSIVE: true - ROS_PACKAGES_TO_INSTALL: cv-bridge image-transport - - ## Trigger downstream builds on Github -# - name: trigger-github-downstream -# image: plugins/downstream -# settings: -# server: https://github.drone.camhd.science -# fork: true -# token: -# from_secret: github_drone_token -# repositories: -# - apl-ocean-engineering/serdp_common -# when: -# event: -# exclude: -# - pull_request - -- name: slack - image: plugins/slack - settings: - webhook: - from_secret: slack_webhook - when: - status: [ success, failure ] - event: - exclude: - - pull_request diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..79fc83a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/mergify.yml b/.github/mergify.yml new file mode 100644 index 0000000..38eb702 --- /dev/null +++ b/.github/mergify.yml @@ -0,0 +1,18 @@ +pull_request_rules: + + - name: ask to resolve conflict + conditions: + - conflict + - author!=mergify[bot] + actions: + comment: + message: This pull request is in conflict. Could you fix it @{{author}}? + + - name: development targets main branch + conditions: + - base!=main + - author!=mergify[bot] + actions: + comment: + message: | + Please target the `main` branch for development, we will backport the changes to {{base}} for you if approved and if they don't break API. diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..f92fe45 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,31 @@ +name: ROS Industrial CI + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +jobs: + industrial-ci: + name: Run Industrial CI + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + env: + # We include humble because there are still 22.04 systems (Jetson) out there + - {UPSTREAM_WORKSPACE: sonar_image_proc.repos, ROS_DISTRO: humble} + - {UPSTREAM_WORKSPACE: sonar_image_proc.repos, ROS_DISTRO: kilted} + - {UPSTREAM_WORKSPACE: sonar_image_proc.repos, ROS_DISTRO: rolling} + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + submodules: recursive + + - name: Run ROS Industrial CI + uses: ros-industrial/industrial_ci@master + env: ${{matrix.env}} diff --git a/.github/workflows/format.yaml b/.github/workflows/format.yaml new file mode 100644 index 0000000..b558fa2 --- /dev/null +++ b/.github/workflows/format.yaml @@ -0,0 +1,34 @@ +name: Formatting (pre-commit) + +on: + pull_request: + push: + branches: + - main + workflow_dispatch: + +jobs: + pre-commit: + name: Format + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version: "3.10" + + - name: Install clang-format-14 + run: sudo apt-get install clang-format-14 + + - name: Run pre-commit + uses: pre-commit/action@v3.0.1 + id: precommit + + - name: Upload pre-commit changes + if: failure() && steps.precommit.outcome == 'failure' + uses: rhaschke/upload-git-patch-action@main + with: + name: pre-commit diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f8d8571..d019757 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,26 +1,43 @@ -# See https://pre-commit.com for more information -# See https://pre-commit.com/hooks.html for more hooks repos: -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.2.0 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.12.3 hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - - id: check-yaml - - id: check-json - - id: check-xml - - id: check-added-large-files - - id: mixed-line-ending -- repo: https://github.com/pre-commit/mirrors-clang-format - rev: v15.0.4 + # Run the linter + - id: ruff-check + args: ["--fix", "--exit-non-zero-on-fix"] + # Run the formatter. + - id: ruff-format + + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v20.1.7 hooks: - id: clang-format - # Respect .clang-format if it exists, otherwise use Google - args: ["--fallback-style=Google"] -# -# Uncomment the following to enable cpplint -# -#- repo: https://gitlab.com/daverona/pre-commit/cpp -# rev: 0.8.0 -# hooks: -# - id: cpplint + types_or: [c++, c] + + # CMake formatter + - repo: https://github.com/BlankSpruce/gersemi + rev: 0.19.3 + hooks: + - id: gersemi + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-added-large-files + - id: check-case-conflict + - id: check-executables-have-shebangs + - id: check-json + - id: check-merge-conflict + - id: check-shebang-scripts-are-executable + - id: check-symlinks + - id: check-toml + - id: check-xml + - id: check-yaml + - id: debug-statements + - id: destroyed-symlinks + - id: detect-private-key + - id: end-of-file-fixer + - id: fix-byte-order-marker + - id: mixed-line-ending + - id: trailing-whitespace + args: [--markdown-linebreak-ext=md] diff --git a/CMakeLists.txt b/CMakeLists.txt index c8e8987..7e0bc3e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,178 +1,37 @@ -cmake_minimum_required(VERSION 3.5) -project(sonar_image_proc) +cmake_minimum_required(VERSION 3.14.4) +project(libdrawsonar) -if(FIPS_CONFIG AND NOT FIPS_IMPORT) - cmake_minimum_required(VERSION 3.5) - - get_filename_component(FIPS_ROOT_DIR "../fips" ABSOLUTE) - include("${FIPS_ROOT_DIR}/cmake/fips.cmake") - - fips_setup() -else() - find_package(catkin QUIET) -endif() +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +add_compile_options(-std=c++17) +# == Code common to all builds ======================================= list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake) -# find_package(Eigen3 REQUIRED ) -find_package(Boost COMPONENTS filesystem system program_options REQUIRED) - -# # Default OpenCV version -list(APPEND OPENCV_COMPONENTS core highgui imgproc) -find_package(OpenCV COMPONENTS ${OPENCV_COMPONENTS} REQUIRED) - -if(FIPS_CONFIG) - # # Set global include paths - fips_include_directories( - ${CMAKE_SOURCE_DIR}/include - ${OpenCV_INCLUDE_DIRS} - ) - - # The local library - fips_begin_module(sonar_image_proc) - - fips_src(lib *.cpp) - fips_libs(${Boost_LIBRARIES} ${OpenCV_LIBS}) - - fips_end_module() - - if(NOT FIPS_IMPORT) - if(FIPS_UNITTESTS) - gtest_begin(sonar_image_proc) - fips_src(test/unit/) - fips_deps(sonar_image_proc) - gtest_end() - endif() - endif() - - fips_finish() - -else() - find_package(catkin REQUIRED - marine_acoustic_msgs - cv_bridge - image_transport - nodelet - nodelet_topic_tools - OpenCV - rosbag_storage - dynamic_reconfigure - ) - - catkin_python_setup() - - # This needs to come after `catkin_python_setup` and before `catkin_package` - generate_dynamic_reconfigure_options( - ros/cfg/DrawSonar.cfg - ) - catkin_package( - INCLUDE_DIRS include/ ros/include/ - LIBRARIES sonar_image_proc - DEPENDS OpenCV - - # CATKIN_DEPENDS - ) - - include_directories( - include/ - ros/include/ - ${catkin_INCLUDE_DIRS} - ${OpenCV_INCLUDE_DIRS} - ) - - set(sonar_image_proc_SRCS +set(drawsonar_SRCS lib/AbstractSonarInterface.cpp lib/ColorMaps.cpp lib/SonarDrawer.cpp lib/OldDrawSonar.cpp lib/HistogramGenerator.cpp - ) - - # "true" library containing core functionality - add_library(sonar_image_proc ${sonar_image_proc_SRCS}) - target_link_libraries(sonar_image_proc ${catkin_LIBRARIES}) - set_property(TARGET sonar_image_proc PROPERTY CXX_STANDARD 14) - add_dependencies(sonar_image_proc ${PROJECT_NAME}_gencfg) - - # Dynamically loadable libraries containing nodelets - add_library(sonar_image_proc_nodelets - ros/src/draw_sonar_nodelet.cpp - ros/src/sonar_postprocessor_nodelet.cpp) - target_link_libraries(sonar_image_proc_nodelets ${catkin_LIBRARIES} sonar_image_proc) - set_property(TARGET sonar_image_proc_nodelets PROPERTY CXX_STANDARD 14) - - # # Standalone nodes - add_executable(draw_sonar_node ros/src/draw_sonar_node.cpp) - target_link_libraries(draw_sonar_node - sonar_image_proc - ${catkin_LIBRARIES}) - add_dependencies(draw_sonar_node ${catkin_EXPORTED_TARGETS}) - set_property(TARGET draw_sonar_node PROPERTY CXX_STANDARD 14) - - add_executable(sonar_postprocessor_node ros/src/sonar_postprocessor_node.cpp) - target_link_libraries(sonar_postprocessor_node - sonar_image_proc - ${catkin_LIBRARIES}) - add_dependencies(sonar_postprocessor_node ${catkin_EXPORTED_TARGETS}) - set_property(TARGET sonar_postprocessor_node PROPERTY CXX_STANDARD 14) - - # bag2sonar is **not** a conventional ROS node, but a standalone - # executable which uses ros_storage to read ros bags - add_executable(bag2sonar ros/tools/bag2sonar.cpp) - target_link_libraries(bag2sonar - sonar_image_proc - ${catkin_LIBRARIES}) - add_dependencies(bag2sonar ${catkin_EXPORTED_TARGETS}) - set_property(TARGET bag2sonar PROPERTY CXX_STANDARD 14) - - install(TARGETS draw_sonar_node - sonar_postprocessor_node - bag2sonar - RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}) - - install(TARGETS - sonar_image_proc_nodelets - sonar_image_proc - LIBRARY DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION}) - - install(DIRECTORY launch/ - DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}/launch) - - # install( DIRECTORY rqt_config/ - # DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}/rqt_config ) - install(FILES nodelet_plugins.xml - DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}) - - # # Install headers - install(DIRECTORY include/${PROJECT_NAME}/ - DESTINATION ${CATKIN_PACKAGE_INCLUDE_DESTINATION} - FILES_MATCHING PATTERN "*.hpp" PATTERN "*.h" - PATTERN ".git" EXCLUDE) - - install(DIRECTORY ros/include/${PROJECT_NAME}/ - DESTINATION ${CATKIN_PACKAGE_INCLUDE_DESTINATION} - FILES_MATCHING PATTERN "*.hpp" PATTERN "*.h" - PATTERN ".git" EXCLUDE) - - # Install python scripts and modules - catkin_install_python(PROGRAMS - scripts/sonar_pointcloud.py - scripts/sonar_fov.py - DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} - ) - - # # Disable python wrapper for now ... - # - # # == Stuff related to the Python wrapper == - # pybind_add_module(py_draw_sonar SHARED - # python/py_draw_sonar.cpp - # python/ndarray_converter.cpp - # lib/DrawSonar.cpp - # lib/DataStructures.cpp - # lib/ColorMaps.cpp - # ) - # target_link_libraries( py_draw_sonar ${OpenCV_LIBS} draw_sonar ) - # - # install(TARGETS py_draw_sonar - # DESTINATION ${CATKIN_GLOBAL_PYTHON_DESTINATION} ) #${CATKIN_PACKAGE_PYTHON_DESTINATION}) +) + +# ROS build, determine which version... +if(DEFINED ENV{ROS_VERSION}) + find_package(ros_environment) + + set(ROS_VERSION $ENV{ROS_VERSION}) + + if(${ROS_VERSION} EQUAL 2) + include(cmake/BuildROS2.cmake) + elseif(${ROS_VERSION} EQUAL 1) + include(cmake/BuildROS1.cmake) + else() + message( + "Unsure what sort of build to do. Not in a ROS1 or ROS2 environment" + ) + endif() +else() + message(NOTICE "!! Performing non-ROS build !!") + include(cmake/BuildNonROS.cmake) endif() diff --git a/README.md b/README.md index b2501a1..4e48a8c 100644 --- a/README.md +++ b/README.md @@ -1,118 +1,8 @@ # sonar_image_proc -[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit) - -Code to draw data from forward-looking imaging sonars. - -If built for ROS, it will build a node/nodelet -[draw_sonar](https://github.com/apl-ocean-engineering/libdraw_sonar/tree/master/src_ros) -which subscribes to an -[marine_acoustic_msgs/ProjectedSonarImage](https://github.com/apl-ocean-engineering/marine_msgs/blob/main/marine_acoustic_msgs/msg/ProjectedSonarImage.msg) -and publishes a -[sensor_msgs/Image](https://docs.ros.org/en/melodic/api/sensor_msgs/html/msg/Image.html). - -The core library contains no ROS dependencies, and can be linked into non-ROS applications. - -# ROS Interfaces (draw_sonar_node) - -`rosrun sonar_image_proc draw_sonar_node` - -## Subscribers - -Subscribes to the topic `sonar_image` of type [marine_acoustic_msgs/ProjectedSonarImage](https://github.com/apl-ocean-engineering/marine_msgs/blob/main/marine_acoustic_msgs/msg/ProjectedSonarImage.msg). - - -## Publishers - -By default publishes three [sensor_msgs/Image](https://docs.ros.org/en/melodic/api/sensor_msgs/html/msg/Image.html) topics: - -* `drawn_sonar` contains an image of the sonar drawn with a cartesian projection -with the origin (range = 0) of the sonar centered on the bottom edge of -the image, and azimuth=0 extending vertically upwards in the image. By default, -the image height is set by the number range bins, and the image width is -automatically determined based on the height and the min/max azimuth of the -sonar image. The color map used to convert the sonar intensity to RGB is set -in code. - -![](drawn_sonar.png) - -* `drawn_sonar_rect` is the contents of the SonarImage data mapped directly from polar to cartesian coordinates. -Since the intensity data in the SonarImage is stored azimuth-major, the data is -mapped into the image space with range in the X direction and azimuth in the Y -direction: - - * Image width is the number of range bins in the data, with the minimum range - on the left side and maximum range on the right side. - - * Image height is the number of azimuth bins in the data, with the lowest - azimuth (typically the most negative) at the top, and most positive at the - bottom. - -![](drawn_sonar_rect.png) - -* `drawn_sonar_osd` adds guidelines and annotations to `drawn_sonar`. Overlay parameters can be configured in realtime. - -If the param `publish_timing` is `true`, the node will track the elapsed time to -draw each sonar image and publish that information to the topic `sonar_image_proc_timing` -as a [std_msgs/String](http://docs.ros.org/en/noetic/api/std_msgs/html/msg/String.html) -containing a JSON dict. - -If the param `publish_old` is `true`, the node will also draw the sonar using -the old `draw_sonar` algorithm for comparison. - -## Params - -If `max_range` is set to a non-zero value, images will be clipped/dropped to that max range (or the actual sonar range, whichever is smaller). - -If `publish_timing` is `true` the node will publish performance information as a -JSON [string](http://docs.ros.org/en/noetic/api/std_msgs/html/msg/String.html) -to the topic `sonar_image_proc_timing`. Defaults to `true` - -If `publish_old` is `true` the node will also draw the sonar using the old -algorithm to the topic `old_drawn_sonar`. Defaults to `false` - -If `publish_histogram` is `true` the node will publish a "raw" histogram information as a `UInt32MultiArray` to the topic `histogram`. It contains a vector of unsigned ints giving the count for each intensity value -- so for 8 bit data the vector will be 256 elements in length, and for 16-bit data it will be 65536 elements in length. - -# bag2sonar - -The program `bag2sonar` reads in a bagfile containing a `ProjectedSonarImage` topic, draws the sonar image and writes those images to *new* bagfile in a `Image` topic. - -Usage: - -``` -$ rosrun sonar_image_proc bag2sonar -Usage: - - bag2sonar [options] - -Draw sonar from a bagfile: - -h [ --help ] Display this help message - -l [ --logscale ] Do logscale - --min-db arg (=0) Min db - --max-db arg (=0) Max db - --osd If set, include the on-screen display - in output - -o [ --output-bag ] arg Name of output bagfile - -t [ --output-topic ] arg (=/drawn_sonar) - Topic for images in output bagfile -``` - -Note that `bag2sonar` is not a conventional ROS node, it is intended to run as a standalone commandline program. It uses `ros_storage` to read the input bagfile sequentially, rather than subscribing to a topic. - -# histogram_drawer - -`python/histogram_drawer` is a Python script which subscribes to the `histogram` topic and uses numpy+Matplotlib to bin the data (into a fixed set of 128 bin right now), and draw a plot to the topic `drawn_histogram`. - -# Python API - -Long term, I'd like to be able to call this drawing function from Python, -however we're not there yet. - -There IS a totally separate python node that publishes a pointcloud -for visualization in rviz: - -`rosrun sonar_image_proc sonar_pointcloud.py` +Library to draw data from forward-looking imaging sonars. +This repo is designed to build in Colcon (ROS2), Catkin (ROS1) and cmake environments, but it contains **no** ROS-specific code. ROS1,2 wrappers are found in [sonar_image_proc](https://github.com/apl-ocean-engineering/sonar_image_proc) # API @@ -122,6 +12,7 @@ A convenience function [drawSonar](include/sonar_image_proc/DrawSonar.h) is also # Related Packages +* [sonar_image_proc](https://github.com/apl-ocean-engineering/sonar_image_proc) provides ROS{1,2} nodes for creating images from sonar data. * [liboculus](https://github.com/apl-ocean-engineering/liboculus) provides network IO and data parsing for the Oculus sonar (non-ROS). * [oculus_sonar_driver](https://gitlab.com/apl-ocean-engineering/oculus_sonar_driver) provides a ROS node for interfacing with the Oculus sonar. * [marine_acoustic_msgs](https://github.com/apl-ocean-engineering/marine_msgs/blob/main/marine_acoustic_msgs) defines the ROS [ProjectedSonarImage](https://github.com/apl-ocean-engineering/marine_msgs/blob/main/marine_acoustic_msgs/msg/ProjectedSonarImage.msg) message type published by [oculus_sonar_driver](https://gitlab.com/apl-ocean-engineering/oculus_sonar_driver). diff --git a/cmake/BuildNonROS.cmake b/cmake/BuildNonROS.cmake new file mode 100644 index 0000000..def4b02 --- /dev/null +++ b/cmake/BuildNonROS.cmake @@ -0,0 +1,98 @@ +# Specify the minimum version for CMake +cmake_minimum_required(VERSION 3.12) +project(libdrawsonar) + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3 -march=native -Wl,--no-as-needed") + +# Set the output folder where your program will be created +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/bin) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/lib) + +# ########################################### +# The following folders will be included # +# ########################################### +include_directories("${PROJECT_SOURCE_DIR}/include/") + +find_package(spdlog) +find_package(OpenCV COMPONENTS core imgproc) + +include_directories(${install_dir}/include/) + +# ##################### +# Add Execuatables # +# ##################### +link_directories(${Boost_LIBRARY_DIRS}) + +# Create Library +add_library(drawsonar SHARED ${drawsonar_SRCS}) +set_target_properties(drawsonar PROPERTIES LIBRARY_OUTPUT_NAME drawsonar) +target_link_libraries( + drawsonar + PUBLIC spdlog::spdlog opencv_core opencv_imgproc opencv_highgui +) + +# ============================================= +# to allow find_package() +# ============================================= +# +# The following is borrowed heavily from: +# https://github.com/RossHartley/invariant-ekf +# I am responsible for all mistakes +# +# the following case be used in an external project requiring drawsonar: +# ... +# find_package(drawsonar) +# include_directories(${drawsonar_INCLUDE_DIRS}) +# ... + +# NOTE: the following will support find_package for 1) local build (make) and 2) for installed files (make install) + +# 1- local build + +# Register the local build in case one doesn't use "make install" +export(PACKAGE drawsonar) + +# Create variable for the local build tree +# set_target_properties(drawsonar PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}) +get_property( + drawsonar_include_dirs + DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + PROPERTY INCLUDE_DIRECTORIES +) +get_property( + drawsonar_library_dirs + TARGET drawsonar + PROPERTY LIBRARY_OUTPUT_DIRECTORY +) +get_property(drawsonar_libraries TARGET drawsonar PROPERTY LIBRARY_OUTPUT_NAME) + +message("drawsonar_include_dirs: " ${drawsonar_include_dirs}) +message("drawsonar_library_dirs: " ${drawsonar_library_dirs}) +message("drawsonar_libraries: " ${drawsonar_libraries}) + +# Configure config file for local build tree +configure_file( + cmake/drawsonarConfig.cmake.in + "${PROJECT_BINARY_DIR}/drawsonarConfig.cmake" + @ONLY +) + +message("PROJECT_BINARY_DIR: " ${PROJECT_BINARY_DIR}) + +# 2- installation build # + +# Change the include location for the case of an install location +set(drawsonar_include_dirs ${CMAKE_INSTALL_PREFIX}/include ${EIGEN_INCLUDE_DIR}) + +# We put the generated file for installation in a different repository (i.e., ./CMakeFiles/) +configure_file( + cmake/drawsonarConfig.cmake.in + "${PROJECT_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/drawsonarConfig.cmake" + @ONLY +) + +install( + FILES "${PROJECT_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/drawsonarConfig.cmake" + DESTINATION share/drawsonar/cmake + COMPONENT dev +) diff --git a/cmake/BuildROS1.cmake b/cmake/BuildROS1.cmake new file mode 100644 index 0000000..0da0fdc --- /dev/null +++ b/cmake/BuildROS1.cmake @@ -0,0 +1,45 @@ +# Catkin/ROS1 section ===== + +catkin_package( + INCLUDE_DIRS include + LIBRARIES libdrawsonar +) + +add_library(libdrawsonar ${drawsonar_SRCS}) + +include_directories(libdrawsonar include ${catkin_INCLUDE_DIRS}) + +target_link_libraries(libdrawsonar ${catkin_LIBRARIES}) + +install( + TARGETS libdrawsonar + ARCHIVE DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION} + LIBRARY DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION} + RUNTIME DESTINATION ${CATKIN_GLOBAL_BIN_DESTINATION} +) + +## Install headers +install( + DIRECTORY include/${PROJECT_NAME}/ + DESTINATION ${CATKIN_PACKAGE_INCLUDE_DESTINATION} + FILES_MATCHING + PATTERN "*.hpp" + PATTERN "*.h" + PATTERN ".git" EXCLUDE +) + +if(CATKIN_ENABLE_TESTING) + add_definitions(-DTEST_DATA_PATH="${CMAKE_CURRENT_SOURCE_DIR}/test/data") + include_directories(test/data/) + + file(GLOB drawsonar_test_SRCS test/unit/*cpp) + + catkin_add_gtest(drawsonar_test ${drawsonar_test_SRCS}) + + target_link_libraries( + drawsonar_test + ${catkin_LIBRARIES} + libdrawsonar + Boost::system + ) +endif() diff --git a/cmake/BuildROS2.cmake b/cmake/BuildROS2.cmake new file mode 100644 index 0000000..8e12f31 --- /dev/null +++ b/cmake/BuildROS2.cmake @@ -0,0 +1,36 @@ +# == ament/ROS2 section ================================= + +find_package(ament_cmake REQUIRED) +find_package(OpenCV REQUIRED) + +add_library(drawsonar SHARED ${drawsonar_SRCS}) +target_link_libraries(drawsonar PUBLIC opencv_core) + +target_include_directories( + drawsonar + PUBLIC + "$" + "$" +) + +install( + TARGETS drawsonar + EXPORT export_${PROJECT_NAME} + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + RUNTIME DESTINATION bin +) + +install( + DIRECTORY include/ + DESTINATION include + FILES_MATCHING + PATTERN "*.hpp" + PATTERN "*.h" + PATTERN ".git" EXCLUDE +) + +ament_export_targets(export_${PROJECT_NAME} HAS_LIBRARY_TARGET) +ament_export_libraries(drawsonar) + +ament_package() diff --git a/cmake/drawsonarConfig.cmake.in b/cmake/drawsonarConfig.cmake.in new file mode 100644 index 0000000..2e22029 --- /dev/null +++ b/cmake/drawsonarConfig.cmake.in @@ -0,0 +1,10 @@ +# - Config file for the drawsonar package +# It defines the following variables +# drawsonar_INCLUDE_DIRS - include directories for drawsonar +# drawsonar_LIBRARY_DIRS - directories for drawsonar library + +# Compute paths +get_filename_component(drawsonar_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" PATH) +set(drawsonar_INCLUDE_DIRS "@drawsonar_include_dirs@") +set(drawsonar_LIBRARY_DIRS "@drawsonar_library_dirs@") +set(drawsonar_LIBRARIES "@drawsonar_libraries@") diff --git a/fips b/fips deleted file mode 100755 index efdeb88..0000000 --- a/fips +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env python -"""fips main entry""" -import os -import sys -import subprocess -proj_path = os.path.dirname(os.path.abspath(__file__)) -fips_path = os.path.dirname(proj_path) + '/fips' -if not os.path.isdir(fips_path) : - print("\033[93m=== cloning fips build system to '{}':\033[0m".format(fips_path)) - subprocess.call(['git', 'clone', 'https://github.com/amarburg/fips.git', fips_path]) -sys.path.insert(0,fips_path) -try : - from mod import fips -except ImportError : - print("\033[91m[ERROR]\033[0m failed to initialize fips build system in '{}'".format(proj_path)) - sys.exit(10) -fips.run(fips_path, proj_path, sys.argv) diff --git a/fips.yml b/fips.yml deleted file mode 100644 index ede8bb8..0000000 --- a/fips.yml +++ /dev/null @@ -1,15 +0,0 @@ -imports: - fips-googletest: - git: https://github.com/amarburg/fips-googletest.git - cond: "FIPS_UNITTESTS" - -exports: - modules: - draw_sonar: . - header-dirs: - - include - -defines: - FIPS_EXCEPTIONS: ON - FIPS_RTTI: ON - FIPS_UNITTESTS_RUN_AFTER_BUILD: ON diff --git a/helpers/make_color_map.py b/helpers/make_color_map.py index 0cad913..f1f4a4a 100755 --- a/helpers/make_color_map.py +++ b/helpers/make_color_map.py @@ -5,16 +5,22 @@ import numpy as np if __name__ == "__main__": - - parser = argparse.ArgumentParser(description='Make .cpp source file for a colormap') - - parser.add_argument('--prologue', type=Path, default="prologue.txt", - help='Test file containing boilerplate for the beginning of file') - parser.add_argument('--epilogue', type=Path, default='epilogue.txt', - help="Test file containing boilerplate for end of file") - - parser.add_argument('--csv', type=Path, nargs="*", - help='CSV file containing') + parser = argparse.ArgumentParser(description="Make .cpp source file for a colormap") + + parser.add_argument( + "--prologue", + type=Path, + default="prologue.txt", + help="Test file containing boilerplate for the beginning of file", + ) + parser.add_argument( + "--epilogue", + type=Path, + default="epilogue.txt", + help="Test file containing boilerplate for end of file", + ) + + parser.add_argument("--csv", type=Path, nargs="*", help="CSV file containing") args = parser.parse_args() @@ -24,23 +30,23 @@ print(line.strip()) for csv_file in args.csv: - data = np.loadtxt(csv_file, delimiter=',', dtype=float) + data = np.loadtxt(csv_file, delimiter=",", dtype=float) print("const float ColorMap::float_data[%d][3] = {" % len(data)) for entry in data: - print("{%f,%f,%f}," % (entry[0],entry[1],entry[2])) + print("{%f,%f,%f}," % (entry[0], entry[1], entry[2])) print("};") print() print("const float ColorMap::char_data[%d][3] = {" % len(data)) for entry in data: - print("{%d,%d,%d}," % (int(entry[0]*255), - int(entry[1]*255), - int(entry[2]*255))) + print( + "{%d,%d,%d}," + % (int(entry[0] * 255), int(entry[1] * 255), int(entry[2] * 255)) + ) print("};") print() - if args.epilogue: with open(args.epilogue) as fp: for line in fp: diff --git a/include/sonar_image_proc/AbstractSonarInterface.h b/include/libdrawsonar/AbstractSonarInterface.h similarity index 96% rename from include/sonar_image_proc/AbstractSonarInterface.h rename to include/libdrawsonar/AbstractSonarInterface.h index a5eff34..abb98fb 100644 --- a/include/sonar_image_proc/AbstractSonarInterface.h +++ b/include/libdrawsonar/AbstractSonarInterface.h @@ -10,7 +10,7 @@ #include #include -namespace sonar_image_proc { +namespace libdrawsonar { typedef std::pair Bounds_t; extern const Bounds_t UnsetBounds; @@ -28,7 +28,7 @@ struct AzimuthRangeIndices { // Designed as a "common abstact type" between the Blueprint // Subsea SimplePingResult and ROS ImagingSonarMsg struct AbstractSonarInterface { - public: +public: AbstractSonarInterface(); enum DataType_t { @@ -54,7 +54,7 @@ struct AbstractSonarInterface { int nAzimuth() const { return azimuths().size(); } int nAzimuths() const { return azimuths().size(); - } // Whoops, should be consistent + } // Whoops, should be consistent float azimuth(int n) const { return azimuths().at(n); } Bounds_t azimuthBounds() const; @@ -128,13 +128,13 @@ struct AbstractSonarInterface { } __attribute__((deprecated)); - private: +private: // In a few cases, need to "check and potentially calculate cached // value" without actually getting the value void checkRangeBounds() const; void checkAzimuthBounds() const; - private: +private: // Since we search extensively for the bounds // (rather than assuming the first and last are the bounds), // cache the results @@ -143,4 +143,4 @@ struct AbstractSonarInterface { mutable float _maxRangeSquared; }; -} // namespace sonar_image_proc +} // namespace libdrawsonar diff --git a/include/sonar_image_proc/ColorMaps.h b/include/libdrawsonar/ColorMaps.h similarity index 95% rename from include/sonar_image_proc/ColorMaps.h rename to include/libdrawsonar/ColorMaps.h index f098dba..fc17ccf 100644 --- a/include/sonar_image_proc/ColorMaps.h +++ b/include/libdrawsonar/ColorMaps.h @@ -5,11 +5,11 @@ #include -#include "sonar_image_proc/AbstractSonarInterface.h" +#include "libdrawsonar/AbstractSonarInterface.h" -namespace sonar_image_proc { +namespace libdrawsonar { -using sonar_image_proc::AbstractSonarInterface; +using libdrawsonar::AbstractSonarInterface; struct SonarColorMap { // A ColorMap is a mapping from one pixel of sonar data to @@ -106,4 +106,4 @@ struct InfernoSaturationColorMap : public InfernoColorMap { } }; -} // namespace sonar_image_proc +} // namespace libdrawsonar diff --git a/include/sonar_image_proc/DataStructures.h b/include/libdrawsonar/DataStructures.h similarity index 84% rename from include/sonar_image_proc/DataStructures.h rename to include/libdrawsonar/DataStructures.h index 36fc720..a1cf07f 100644 --- a/include/sonar_image_proc/DataStructures.h +++ b/include/libdrawsonar/DataStructures.h @@ -8,7 +8,7 @@ #include -namespace sonar_image_proc { +namespace libdrawsonar { // \todo. Are these used anymore? struct SonarPoint { @@ -19,4 +19,4 @@ struct SonarPoint { SonarPoint bearingRange2Cartesian(float bearing, float range); -} // namespace sonar_image_proc +} // namespace libdrawsonar diff --git a/include/sonar_image_proc/DrawSonar.h b/include/libdrawsonar/DrawSonar.h similarity index 73% rename from include/sonar_image_proc/DrawSonar.h rename to include/libdrawsonar/DrawSonar.h index 74e1538..9f60565 100644 --- a/include/sonar_image_proc/DrawSonar.h +++ b/include/libdrawsonar/DrawSonar.h @@ -11,13 +11,13 @@ #include #include -#include "sonar_image_proc/AbstractSonarInterface.h" -#include "sonar_image_proc/ColorMaps.h" -#include "sonar_image_proc/SonarDrawer.h" +#include "libdrawsonar/AbstractSonarInterface.h" +#include "libdrawsonar/ColorMaps.h" +#include "libdrawsonar/SonarDrawer.h" -namespace sonar_image_proc { +namespace libdrawsonar { -inline cv::Mat drawSonar(const sonar_image_proc::AbstractSonarInterface &ping, +inline cv::Mat drawSonar(const libdrawsonar::AbstractSonarInterface &ping, const SonarColorMap &colorMap = InfernoColorMap(), const cv::Mat &image = cv::Mat(0, 0, CV_8UC3)) { SonarDrawer drawer; @@ -38,10 +38,10 @@ inline cv::Mat drawSonar(const sonar_image_proc::AbstractSonarInterface &ping, // // Cell (nRange,nBearing) is the data at the max range, most positive bearing // -inline cv::Mat drawSonarRectImage( - const sonar_image_proc::AbstractSonarInterface &ping, - const SonarColorMap &colorMap = InfernoColorMap(), - const cv::Mat &rectImage = cv::Mat(0, 0, CV_8UC3)) { +inline cv::Mat +drawSonarRectImage(const libdrawsonar::AbstractSonarInterface &ping, + const SonarColorMap &colorMap = InfernoColorMap(), + const cv::Mat &rectImage = cv::Mat(0, 0, CV_8UC3)) { SonarDrawer drawer; return drawer.drawRectSonarImage(ping, colorMap, rectImage); } @@ -61,15 +61,15 @@ namespace old_api { // // If set, maxRange is used in lieu of the ping's native max range, // allowing truncation of the image -cv::Size calculateImageSize( - const sonar_image_proc::AbstractSonarInterface &ping, cv::Size hint, - int pixPerRangeBin = 2, float maxRange = -1.0); +cv::Size calculateImageSize(const libdrawsonar::AbstractSonarInterface &ping, + cv::Size hint, int pixPerRangeBin = 2, + float maxRange = -1.0); -cv::Mat drawSonar(const sonar_image_proc::AbstractSonarInterface &ping, +cv::Mat drawSonar(const libdrawsonar::AbstractSonarInterface &ping, cv::Mat &mat, const SonarColorMap &colorMap = InfernoColorMap(), float maxRange = -1.0); -} // namespace old_api +} // namespace old_api -} // namespace sonar_image_proc +} // namespace libdrawsonar diff --git a/include/libdrawsonar/HistogramGenerator.h b/include/libdrawsonar/HistogramGenerator.h new file mode 100644 index 0000000..9bb1d98 --- /dev/null +++ b/include/libdrawsonar/HistogramGenerator.h @@ -0,0 +1,24 @@ +// Copyright 2021 University of Washington Applied Physics Laboratory +// + +#pragma once + +#include "libdrawsonar/AbstractSonarInterface.h" + +namespace libdrawsonar { + +class HistogramGenerator { +public: + static std::vector Generate(const AbstractSonarInterface &ping); + + static std::vector + GenerateUint8(const AbstractSonarInterface &ping); + static std::vector + GenerateUint16(const AbstractSonarInterface &ping); + + // Histogram generator for 32bit data produces 256 bins of log10(intensity) + static std::vector + GenerateUint32(const AbstractSonarInterface &ping); +}; + +} // namespace libdrawsonar diff --git a/include/libdrawsonar/Logger.h b/include/libdrawsonar/Logger.h new file mode 100644 index 0000000..a0ecb2d --- /dev/null +++ b/include/libdrawsonar/Logger.h @@ -0,0 +1,145 @@ + +/* + * Copyright (c) 2017-2025 University of Washington + * Author: Aaron Marburg + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of University of Washington nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include +#include +#include + +namespace libdrawsonar { + +class Logger { +public: + static std::shared_ptr & + get_instance(const std::shared_ptr &logger_in = nullptr) { + static std::shared_ptr s_logger(init(logger_in)); + if ((logger_in) && (logger_in != s_logger->logger_)) + s_logger->logger_ = logger_in; + return s_logger; + } + + static std::shared_ptr &get_logger() { + return Logger::get_instance()->logger_; + } + + static void set_logger(const std::shared_ptr &s) { + Logger::get_instance(s); + } + + static void add_sink(const spdlog::sink_ptr &s) { + // n.b. per the spdlog documentation, this is _not_ thread safe + Logger::get_logger()->sinks().push_back(s); + } + + ~Logger() {}; + +private: + static std::shared_ptr + init(const std::shared_ptr &logger_in = nullptr) { + // Use new to access private constructor + return std::shared_ptr(new Logger(logger_in)); + } + + Logger(const std::shared_ptr &l = nullptr) : logger_(l) { + if (!logger_) { + logger_ = std::make_shared("libdrawsonar"); + spdlog::register_logger(logger_); + } + }; + + std::shared_ptr logger_; + + Logger(const Logger &) = delete; + Logger &operator=(const Logger &) = delete; +}; + +// Convenience wrappers around "oclog::" +namespace oclog { +using spdlog::format_string_t; +using spdlog::source_loc; + +template +inline void log(source_loc source, spdlog::level::level_enum lvl, + format_string_t fmt, Args &&...args) { + Logger::get_logger()->log(source, lvl, fmt, std::forward(args)...); +} + +template +inline void log(spdlog::level::level_enum lvl, format_string_t fmt, + Args &&...args) { + Logger::get_logger()->log(source_loc{}, lvl, fmt, + std::forward(args)...); +} + +template +inline void trace(format_string_t fmt, Args &&...args) { + Logger::get_logger()->trace(fmt, std::forward(args)...); +} + +template +inline void debug(format_string_t fmt, Args &&...args) { + Logger::get_logger()->debug(fmt, std::forward(args)...); +} + +template +inline void info(format_string_t fmt, Args &&...args) { + Logger::get_logger()->info(fmt, std::forward(args)...); +} + +template +inline void warn(format_string_t fmt, Args &&...args) { + Logger::get_logger()->warn(fmt, std::forward(args)...); +} + +template +inline void error(format_string_t fmt, Args &&...args) { + Logger::get_logger()->error(fmt, std::forward(args)...); +} + +template +inline void critical(format_string_t fmt, Args &&...args) { + Logger::get_logger()->critical(fmt, std::forward(args)...); +} + +template +inline void log(source_loc source, spdlog::level::level_enum lvl, + const T &msg) { + Logger::get_logger()->log(source, lvl, msg); +} + +template +inline void log(spdlog::level::level_enum lvl, const T &msg) { + Logger::get_logger()->log(lvl, msg); +} +} // namespace oclog + +} // namespace libdrawsonar diff --git a/include/sonar_image_proc/OverlayImage.h b/include/libdrawsonar/OverlayImage.h similarity index 98% rename from include/sonar_image_proc/OverlayImage.h rename to include/libdrawsonar/OverlayImage.h index 14df320..c26d50a 100644 --- a/include/sonar_image_proc/OverlayImage.h +++ b/include/libdrawsonar/OverlayImage.h @@ -1,7 +1,7 @@ #include #include -namespace sonar_image_proc { +namespace libdrawsonar { using cv::Mat; using cv::Vec; @@ -111,4 +111,4 @@ void overlayImage(const Mat &bg, const Mat &fg, Mat &dst) { // // } // // } // } -}; // namespace sonar_image_proc +}; // namespace libdrawsonar diff --git a/include/sonar_image_proc/SonarDrawer.h b/include/libdrawsonar/SonarDrawer.h similarity index 86% rename from include/sonar_image_proc/SonarDrawer.h rename to include/libdrawsonar/SonarDrawer.h index 63b4a93..f6a317c 100644 --- a/include/sonar_image_proc/SonarDrawer.h +++ b/include/libdrawsonar/SonarDrawer.h @@ -10,35 +10,35 @@ #include #include -#include "sonar_image_proc/AbstractSonarInterface.h" -#include "sonar_image_proc/ColorMaps.h" +#include "libdrawsonar/AbstractSonarInterface.h" +#include "libdrawsonar/ColorMaps.h" -namespace sonar_image_proc { +namespace libdrawsonar { -using sonar_image_proc::AbstractSonarInterface; +using libdrawsonar::AbstractSonarInterface; // Function in lib/OverlayImage.cpp void overlayImage(const cv::Mat &background, const cv::Mat &foreground, cv::Mat &output); -#define OVERLAY_RW(var, tp, set, get) \ - OverlayConfig &set(tp i) { \ - var = i; \ - return *this; \ - } \ +#define OVERLAY_RW(var, tp, set, get) \ + OverlayConfig &set(tp i) { \ + var = i; \ + return *this; \ + } \ tp get() const { return var; } class SonarDrawer { - public: +public: struct OverlayConfig { - public: + public: int DEFAULT_LINE_THICKNESS = 1; float DEFAULT_LINE_ALPHA = 0.5; // range spacing of 0 means "calculate automatically" float DEFAULT_RANGE_SPACING = 0; - float DEFAULT_RADIAL_SPACING = 20; // degrees + float DEFAULT_RADIAL_SPACING = 20; // degrees bool DEFAULT_RADIAL_AT_ZERO = false; float DEFAULT_FONT_SCALE = 1.0; @@ -49,8 +49,7 @@ class SonarDrawer { range_spacing_m_(DEFAULT_RANGE_SPACING), radial_spacing_deg_(DEFAULT_RADIAL_SPACING), radial_at_zero_(DEFAULT_RADIAL_AT_ZERO), - font_scale_(DEFAULT_FONT_SCALE), - line_color_(255, 255, 255) {} + font_scale_(DEFAULT_FONT_SCALE), line_color_(255, 255, 255) {} OVERLAY_RW(line_alpha_, float, setLineAlpha, lineAlpha) OVERLAY_RW(line_thickness_, int, setLineThickness, lineThickness) @@ -76,7 +75,7 @@ class SonarDrawer { (lineColor() != other.lineColor()); } - private: + private: int line_thickness_; float line_alpha_; float range_spacing_m_; @@ -125,7 +124,7 @@ class SonarDrawer { OverlayConfig &overlayConfig() { return overlay_config_; } - private: +private: OverlayConfig overlay_config_; // Utility class which can generate and store the two cv::Mats @@ -134,10 +133,10 @@ class SonarDrawer { // Also stores meta-information to determine if the map is // invalid and needs to be regenerated. struct Cached { - public: + public: Cached() { ; } - protected: + protected: virtual bool isValid(const AbstractSonarInterface &ping) const; // Meta-information to validate map @@ -146,13 +145,13 @@ class SonarDrawer { }; struct CachedMap : public Cached { - public: + public: CachedMap() : Cached() { ; } typedef std::pair MapPair; MapPair operator()(const AbstractSonarInterface &ping); - private: + private: bool isValid(const AbstractSonarInterface &ping) const override; void create(const AbstractSonarInterface &ping); @@ -160,14 +159,14 @@ class SonarDrawer { } _map; struct CachedOverlay : public Cached { - public: + public: CachedOverlay() : Cached() { ; } const cv::Mat &operator()(const AbstractSonarInterface &ping, const cv::Mat &sonarImage, const OverlayConfig &config); - private: + private: bool isValid(const AbstractSonarInterface &ping, const cv::Mat &sonarImage, const OverlayConfig &config) const; @@ -179,6 +178,6 @@ class SonarDrawer { } _overlay; -}; // namespace sonar_image_procclassSonarDrawer +}; // namespace classSonarDrawer -} // namespace sonar_image_proc +} // namespace libdrawsonar diff --git a/include/sonar_image_proc/HistogramGenerator.h b/include/sonar_image_proc/HistogramGenerator.h deleted file mode 100644 index 1bbcb47..0000000 --- a/include/sonar_image_proc/HistogramGenerator.h +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2021 University of Washington Applied Physics Laboratory -// - -#pragma once - -#include "sonar_image_proc/AbstractSonarInterface.h" - -namespace sonar_image_proc { - -class HistogramGenerator { - public: - static std::vector Generate(const AbstractSonarInterface &ping); - - static std::vector GenerateUint8( - const AbstractSonarInterface &ping); - static std::vector GenerateUint16( - const AbstractSonarInterface &ping); - - // Histogram generator for 32bit data produces 256 bins of log10(intensity) - static std::vector GenerateUint32( - const AbstractSonarInterface &ping); -}; - -} // namespace sonar_image_proc diff --git a/launch/sonar_postproc.launch b/launch/sonar_postproc.launch deleted file mode 100644 index 48ea524..0000000 --- a/launch/sonar_postproc.launch +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lib/AbstractSonarInterface.cpp b/lib/AbstractSonarInterface.cpp index 9c72024..e09e86c 100644 --- a/lib/AbstractSonarInterface.cpp +++ b/lib/AbstractSonarInterface.cpp @@ -1,12 +1,12 @@ // Copyright 2021 University of Washington Applied Physics Laboratory // -#include "sonar_image_proc/AbstractSonarInterface.h" +#include "libdrawsonar/AbstractSonarInterface.h" #include #include -namespace sonar_image_proc { +namespace libdrawsonar { const Bounds_t UnsetBounds = Bounds_t(-1, -1); @@ -42,4 +42,4 @@ void AbstractSonarInterface::checkAzimuthBounds() const { } } -} // namespace sonar_image_proc +} // namespace libdrawsonar diff --git a/lib/ColorMaps.cpp b/lib/ColorMaps.cpp index b7be98a..687c7a8 100644 --- a/lib/ColorMaps.cpp +++ b/lib/ColorMaps.cpp @@ -8,9 +8,9 @@ // // This file autogenerated with helpers/make_color_map.py -#include "sonar_image_proc/ColorMaps.h" +#include "libdrawsonar/ColorMaps.h" -namespace sonar_image_proc { +namespace libdrawsonar { const float InfernoColorMap::_inferno_data_float[256][3] = { {0.001462, 0.000466, 0.013866}, {0.002267, 0.001270, 0.018570}, @@ -210,4 +210,4 @@ const float InfernoColorMap::_inferno_data_uint8[256][3] = { {247, 251, 153}, {249, 252, 157}, {250, 253, 160}, {252, 254, 164}, }; -} // namespace sonar_image_proc +} // namespace libdrawsonar diff --git a/lib/HistogramGenerator.cpp b/lib/HistogramGenerator.cpp index a82e9aa..ff4ebc5 100644 --- a/lib/HistogramGenerator.cpp +++ b/lib/HistogramGenerator.cpp @@ -1,14 +1,14 @@ // Copyright 2021 University of Washington Applied Physics Laboratory // -#include "sonar_image_proc/HistogramGenerator.h" +#include "libdrawsonar/HistogramGenerator.h" #include -namespace sonar_image_proc { +namespace libdrawsonar { -std::vector HistogramGenerator::Generate( - const AbstractSonarInterface &ping) { +std::vector +HistogramGenerator::Generate(const AbstractSonarInterface &ping) { if (ping.data_type() == AbstractSonarInterface::TYPE_UINT8) return HistogramGenerator::GenerateUint8(ping); else if (ping.data_type() == AbstractSonarInterface::TYPE_UINT16) @@ -20,8 +20,8 @@ std::vector HistogramGenerator::Generate( } // \todo Some repetition, but these functions are pretty simple -std::vector HistogramGenerator::GenerateUint8( - const AbstractSonarInterface &ping) { +std::vector +HistogramGenerator::GenerateUint8(const AbstractSonarInterface &ping) { std::vector result(256, 0); for (int r = 0; r < ping.nRanges(); r++) { @@ -34,8 +34,8 @@ std::vector HistogramGenerator::GenerateUint8( return result; } -std::vector HistogramGenerator::GenerateUint16( - const AbstractSonarInterface &ping) { +std::vector +HistogramGenerator::GenerateUint16(const AbstractSonarInterface &ping) { std::vector result(65536, 0); for (int r = 0; r < ping.nRanges(); r++) { @@ -49,8 +49,8 @@ std::vector HistogramGenerator::GenerateUint16( return result; } -std::vector HistogramGenerator::GenerateUint32( - const AbstractSonarInterface &ping) { +std::vector +HistogramGenerator::GenerateUint32(const AbstractSonarInterface &ping) { std::vector result(256, 0); const float logMax = log10(UINT32_MAX); @@ -59,7 +59,8 @@ std::vector HistogramGenerator::GenerateUint32( for (int b = 0; b < ping.nBearings(); b++) { const auto val = ping.intensity_uint32(AzimuthRangeIndices(b, r)); - if (val == 0) continue; + if (val == 0) + continue; const float l = log10(val); @@ -68,7 +69,8 @@ std::vector HistogramGenerator::GenerateUint32( // std::cerr << "val = " << val << "; l = " << l << "; idx = " << idx << // std::endl; - if ((idx < 0) || (idx > result.size())) continue; + if ((idx < 0) || (idx > result.size())) + continue; result[idx]++; } } @@ -76,4 +78,4 @@ std::vector HistogramGenerator::GenerateUint32( return result; } -} // namespace sonar_image_proc +} // namespace libdrawsonar diff --git a/lib/OldDrawSonar.cpp b/lib/OldDrawSonar.cpp index 4cc018b..39e7f5b 100644 --- a/lib/OldDrawSonar.cpp +++ b/lib/OldDrawSonar.cpp @@ -6,13 +6,13 @@ #include #include -#include "sonar_image_proc/DrawSonar.h" +#include "libdrawsonar/DrawSonar.h" #ifndef THETA_SHIFT #define THETA_SHIFT PI; #endif -namespace sonar_image_proc { +namespace libdrawsonar { namespace old_api { @@ -45,8 +45,10 @@ cv::Size calculateImageSize(const AbstractSonarInterface &ping, cv::Size hint, } // Ensure w and h are both divisible by zero - if (w % 2) w++; - if (h % 2) h++; + if (w % 2) + w++; + if (h % 2) + h++; return Size(w, h); } @@ -112,7 +114,8 @@ cv::Mat drawSonar(const AbstractSonarInterface &ping, const Mat &mat, } for (int r = 0; r < nRanges; ++r) { - if (ping.range(r) > rangeMax) continue; + if (ping.range(r) > rangeMax) + continue; for (int b = 0; b < nBeams; ++b) { const float range = ping.range(r); @@ -135,5 +138,5 @@ cv::Mat drawSonar(const AbstractSonarInterface &ping, const Mat &mat, return out; } -} // namespace old_api -} // namespace sonar_image_proc +} // namespace old_api +} // namespace libdrawsonar diff --git a/lib/SonarDrawer.cpp b/lib/SonarDrawer.cpp index 392bfc5..db92e93 100644 --- a/lib/SonarDrawer.cpp +++ b/lib/SonarDrawer.cpp @@ -5,12 +5,12 @@ #include #include -#include "sonar_image_proc/DrawSonar.h" -#include "sonar_image_proc/OverlayImage.h" +#include "libdrawsonar/DrawSonar.h" +#include "libdrawsonar/OverlayImage.h" -namespace sonar_image_proc { +namespace libdrawsonar { -using sonar_image_proc::AbstractSonarInterface; +using libdrawsonar::AbstractSonarInterface; static float deg2radf(float deg) { return deg * M_PI / 180.0; } static float rad2degf(float rad) { return rad * 180.0 / M_PI; } @@ -97,10 +97,11 @@ bool SonarDrawer::Cached::isValid(const AbstractSonarInterface &ping) const { // ==== SonarDrawer::CachedMap ==== -SonarDrawer::CachedMap::MapPair SonarDrawer::CachedMap::operator()( - const AbstractSonarInterface &ping) { +SonarDrawer::CachedMap::MapPair +SonarDrawer::CachedMap::operator()(const AbstractSonarInterface &ping) { // _scMap[12] are mutable to break out of const - if (!isValid(ping)) create(ping); + if (!isValid(ping)) + create(ping); return std::make_pair(_scMap1, _scMap2); } @@ -121,7 +122,8 @@ void SonarDrawer::CachedMap::create(const AbstractSonarInterface &ping) { const int originx = abs(minusWidth); const cv::Size imgSize(width, nRanges); - if ((width <= 0) || (nRanges <= 0)) return; + if ((width <= 0) || (nRanges <= 0)) + return; newmap.create(imgSize, CV_32FC2); @@ -171,7 +173,8 @@ void SonarDrawer::CachedMap::create(const AbstractSonarInterface &ping) { } bool SonarDrawer::CachedMap::isValid(const AbstractSonarInterface &ping) const { - if (_scMap1.empty() || _scMap2.empty()) return false; + if (_scMap1.empty() || _scMap2.empty()) + return false; return Cached::isValid(ping); } @@ -181,17 +184,21 @@ bool SonarDrawer::CachedMap::isValid(const AbstractSonarInterface &ping) const { bool SonarDrawer::CachedOverlay::isValid(const AbstractSonarInterface &ping, const cv::Mat &sonarImage, const OverlayConfig &config) const { - if (sonarImage.size() != _overlay.size()) return false; + if (sonarImage.size() != _overlay.size()) + return false; - if (_config_used != config) return false; + if (_config_used != config) + return false; return Cached::isValid(ping); } -const cv::Mat &SonarDrawer::CachedOverlay::operator()( - const AbstractSonarInterface &ping, const cv::Mat &sonarImage, - const OverlayConfig &config) { - if (!isValid(ping, sonarImage, config)) create(ping, sonarImage, config); +const cv::Mat & +SonarDrawer::CachedOverlay::operator()(const AbstractSonarInterface &ping, + const cv::Mat &sonarImage, + const OverlayConfig &config) { + if (!isValid(ping, sonarImage, config)) + create(ping, sonarImage, config); return _overlay; } @@ -318,4 +325,4 @@ void SonarDrawer::CachedOverlay::create(const AbstractSonarInterface &ping, _config_used = config; } -} // namespace sonar_image_proc +} // namespace libdrawsonar diff --git a/libdrawsonar.repos b/libdrawsonar.repos new file mode 100644 index 0000000..90261ee --- /dev/null +++ b/libdrawsonar.repos @@ -0,0 +1,5 @@ +repositories: + marine_msgs: + type: git + url: https://github.com/apl-ocean-engineering/marine_msgs + version: ros2 diff --git a/package.xml b/package.xml index e63c7c7..a64647e 100644 --- a/package.xml +++ b/package.xml @@ -1,36 +1,20 @@ - - sonar_image_proc + + libdrawsonar 0.0.1 Common code for manipulating imaging sonar data. + Aaron Marburg Aaron Marburg - - - BSD - catkin - - roscpp - rospy - - marine_acoustic_msgs - cv_bridge - image_transport - nodelet_topic_tools - std_msgs - - dynamic_reconfigure - nodelet - rosbag_storage - - dynamic_reconfigure - nodelet + catkin + ament_cmake - + catkin + ament_cmake diff --git a/python/draw_sonar/__init__.py b/python/draw_sonar/__init__.py index 705f190..b71854b 100644 --- a/python/draw_sonar/__init__.py +++ b/python/draw_sonar/__init__.py @@ -1,6 +1,6 @@ +from py_draw_sonar import * # NOQA -from py_draw_sonar import * ## A trivial test function to check if the module is loading properly -def pyadd(a,b): - return a+b +def pyadd(a, b): + return a + b diff --git a/python/histogram_drawer b/python/histogram_drawer index 93a32a1..53b6c95 100755 --- a/python/histogram_drawer +++ b/python/histogram_drawer @@ -9,49 +9,49 @@ from cv_bridge import CvBridge import io import numpy as np import matplotlib -matplotlib.use('agg') # turn off interactive backend + +matplotlib.use("agg") # turn off interactive backend import matplotlib.pyplot as plt -class HistogramDrawer: +class HistogramDrawer: def __init__(self): - self.pub = rospy.Publisher("drawn_histogram", Image, queue_size=10) self.sub = rospy.Subscriber("histogram", UInt32MultiArray, self.rx_histogram) self.bin_max = 0 def rx_histogram(self, data): - #print(data.data) + # print(data.data) xmax = len(data.data) ## Regardless, compress to 128 bins .. this algorithm assumes the new # number of bins is a factor of the original histogram bins - d = np.reshape(data.data, (128,-1), 'C') + d = np.reshape(data.data, (128, -1), "C") - new_histogram = np.sum(d,axis=1) + new_histogram = np.sum(d, axis=1) self.bin_max = np.max((self.bin_max, max(new_histogram))) - #print(new_histogram) + # print(new_histogram) fig, ax = plt.subplots() plt.yscale("log") - ax.plot(np.linspace(0,xmax,128), new_histogram,'b.-') + ax.plot(np.linspace(0, xmax, 128), new_histogram, "b.-") ax.set_xlabel("Intensity") ax.set_ylabel("Count") - ax.set_ylim([1, self.bin_max*2]) + ax.set_ylim([1, self.bin_max * 2]) with io.BytesIO() as buff: - fig.savefig(buff, format='raw') + fig.savefig(buff, format="raw") buff.seek(0) data = np.frombuffer(buff.getvalue(), dtype=np.uint8) w, h = fig.canvas.get_width_height() im = data.reshape((int(h), int(w), -1)) # Drop alpha channel - im = im[:,:,:3] + im = im[:, :, :3] bridge = CvBridge() image_message = bridge.cv2_to_imgmsg(im, encoding="rgb8") @@ -61,11 +61,9 @@ class HistogramDrawer: plt.close(fig) - if __name__ == "__main__": + rospy.init_node("histogram_drawer", anonymous=True) - rospy.init_node('histogram_drawer', anonymous=True) - - drawer = HistogramDrawer() + drawer = HistogramDrawer() - rospy.spin(); + rospy.spin() diff --git a/python/ndarray_converter.cpp b/python/ndarray_converter.cpp index 158999c..8a38376 100644 --- a/python/ndarray_converter.cpp +++ b/python/ndarray_converter.cpp @@ -45,36 +45,36 @@ static int failmsg(const char *fmt, ...) { } class PyAllowThreads { - public: +public: PyAllowThreads() : _state(PyEval_SaveThread()) {} ~PyAllowThreads() { PyEval_RestoreThread(_state); } - private: +private: PyThreadState *_state; }; class PyEnsureGIL { - public: +public: PyEnsureGIL() : _state(PyGILState_Ensure()) {} ~PyEnsureGIL() { PyGILState_Release(_state); } - private: +private: PyGILState_STATE _state; }; -#define ERRWRAP2(expr) \ - try { \ - PyAllowThreads allowThreads; \ - expr; \ - } catch (const cv::Exception &e) { \ - PyErr_SetString(opencv_error, e.what()); \ - return 0; \ +#define ERRWRAP2(expr) \ + try { \ + PyAllowThreads allowThreads; \ + expr; \ + } catch (const cv::Exception &e) { \ + PyErr_SetString(opencv_error, e.what()); \ + return 0; \ } using namespace cv; class NumpyAllocator : public MatAllocator { - public: +public: NumpyAllocator() { stdAllocator = Mat::getStdAllocator(); } ~NumpyAllocator() {} @@ -83,7 +83,8 @@ class NumpyAllocator : public MatAllocator { UMatData *u = new UMatData(this); u->data = u->origdata = (uchar *)PyArray_DATA((PyArrayObject *)o); npy_intp *_strides = PyArray_STRIDES((PyArrayObject *)o); - for (int i = 0; i < dims - 1; i++) step[i] = (size_t)_strides[i]; + for (int i = 0; i < dims - 1; i++) + step[i] = (size_t)_strides[i]; step[dims - 1] = CV_ELEM_SIZE(type); u->size = sizes[0] * step[0]; u->userdata = o; @@ -120,8 +121,10 @@ class NumpyAllocator : public MatAllocator { : f * NPY_ULONGLONG + (f ^ 1) * NPY_UINT; int i, dims = dims0; cv::AutoBuffer _sizes(dims + 1); - for (i = 0; i < dims; i++) _sizes[i] = sizes[i]; - if (cn > 1) _sizes[dims++] = cn; + for (i = 0; i < dims; i++) + _sizes[i] = sizes[i]; + if (cn > 1) + _sizes[dims++] = cn; PyObject *o = PyArray_SimpleNew(dims, _sizes, typenum); if (!o) CV_Error_(Error::StsError, @@ -141,7 +144,8 @@ class NumpyAllocator : public MatAllocator { } void deallocate(UMatData *u) const { - if (!u) return; + if (!u) + return; PyEnsureGIL gil; CV_Assert(u->urefcount >= 0); CV_Assert(u->refcount >= 0); @@ -160,7 +164,8 @@ NumpyAllocator g_numpyAllocator; bool NDArrayConverter::toMat(PyObject *o, Mat &m) { bool allowND = true; if (!o || o == Py_None) { - if (!m.data) m.allocator = &g_numpyAllocator; + if (!m.data) + m.allocator = &g_numpyAllocator; return true; } @@ -317,7 +322,8 @@ bool NDArrayConverter::toMat(PyObject *o, Mat &m) { } PyObject *NDArrayConverter::toNDArray(const cv::Mat &m) { - if (!m.data) Py_RETURN_NONE; + if (!m.data) + Py_RETURN_NONE; Mat temp, *p = (Mat *)&m; if (!p->u || p->allocator != &g_numpyAllocator) { temp.allocator = &g_numpyAllocator; diff --git a/python/ndarray_converter.h b/python/ndarray_converter.h index 3b87f94..81ba002 100644 --- a/python/ndarray_converter.h +++ b/python/ndarray_converter.h @@ -6,7 +6,7 @@ #include class NDArrayConverter { - public: +public: // must call this first, or the other routines don't work! static bool init_numpy(); @@ -23,9 +23,8 @@ class NDArrayConverter { namespace pybind11 { namespace detail { -template <> -struct type_caster { - public: +template <> struct type_caster { +public: PYBIND11_TYPE_CASTER(cv::Mat, _("numpy.ndarray")); bool load(handle src, bool) { @@ -37,7 +36,7 @@ struct type_caster { } }; -} // namespace detail -} // namespace pybind11 +} // namespace detail +} // namespace pybind11 #endif diff --git a/python/old_setup.py b/python/old_setup.py index 7289d88..9a610a7 100644 --- a/python/old_setup.py +++ b/python/old_setup.py @@ -2,12 +2,8 @@ # Much of this copied from https://github.com/edmBernard/pybind11_opencv_numpy # -import fnmatch -import os -from os.path import dirname, exists, join from setuptools import find_packages, setup, Extension from setuptools.command.build_ext import build_ext -import subprocess import sys import setuptools @@ -15,20 +11,23 @@ # pybind-specific compilation stuff # + class get_pybind_include(object): """Helper class to determine the pybind11 include path The purpose of this class is to postpone importing pybind11 until it is actually installed, so that the ``get_include()`` - method can be invoked. """ + method can be invoked.""" def __init__(self, user=False): self.user = user def __str__(self): import pybind11 + return pybind11.get_include(self.user) + # As of Python 3.6, CCompiler has a `has_flag` method. # cf http://bugs.python.org/issue26689 def has_flag(compiler, flagname): @@ -36,8 +35,9 @@ def has_flag(compiler, flagname): the specified compiler. """ import tempfile - with tempfile.NamedTemporaryFile('w', suffix='.cpp') as f: - f.write('int main (int argc, char **argv) { return 0; }') + + with tempfile.NamedTemporaryFile("w", suffix=".cpp") as f: + f.write("int main (int argc, char **argv) { return 0; }") try: compiler.compile([f.name], extra_postargs=[flagname]) except setuptools.distutils.errors.CompileError: @@ -50,24 +50,24 @@ def cpp_flag(compiler): The c++14 is preferred over c++11 (when it is available). """ - if has_flag(compiler, '-std=c++14'): - return '-std=c++14' - elif has_flag(compiler, '-std=c++11'): - return '-std=c++11' + if has_flag(compiler, "-std=c++14"): + return "-std=c++14" + elif has_flag(compiler, "-std=c++11"): + return "-std=c++11" else: - raise RuntimeError('Unsupported compiler -- at least C++11 support ' - 'is needed!') + raise RuntimeError("Unsupported compiler -- at least C++11 support is needed!") class BuildExt(build_ext): """A custom build extension for adding compiler-specific options.""" + c_opts = { - 'msvc': ['/EHsc'], - 'unix': [], + "msvc": ["/EHsc"], + "unix": [], } - if sys.platform == 'darwin': - c_opts['unix'] += ['-stdlib=libc++', '-mmacosx-version-min=10.7'] + if sys.platform == "darwin": + c_opts["unix"] += ["-stdlib=libc++", "-mmacosx-version-min=10.7"] def build_extensions(self): ct = self.compiler.compiler_type @@ -76,45 +76,46 @@ def build_extensions(self): for arg in ext_modules[0].extra_compile_args: opts.append(arg) - if ct == 'unix': - opts.append('-s') # strip - opts.append('-g0') # remove debug symbols + if ct == "unix": + opts.append("-s") # strip + opts.append("-g0") # remove debug symbols opts.append(cpp_flag(self.compiler)) - if has_flag(self.compiler, '-fvisibility=hidden'): - opts.append('-fvisibility=hidden') + if has_flag(self.compiler, "-fvisibility=hidden"): + opts.append("-fvisibility=hidden") for ext in self.extensions: ext.extra_compile_args = opts build_ext.build_extensions(self) + ext_modules = [ Extension( - 'serdp_common', + "serdp_common", [ - 'serdp_common_py.cpp', - 'ndarray_converter.cpp', - '../lib/DrawSonar.cpp', - '../lib/DataStructures.cpp' + "serdp_common_py.cpp", + "ndarray_converter.cpp", + "../lib/DrawSonar.cpp", + "../lib/DataStructures.cpp", ], include_dirs=[ # Path to pybind11 headers get_pybind_include(), get_pybind_include(user=True), - "../include/" + "../include/", ], - extra_compile_args=['-DABSTRACT_SONAR_INTERFACE_ONLY'], - libraries=['opencv_core', 'opencv_highgui'], - language='c++', + extra_compile_args=["-DABSTRACT_SONAR_INTERFACE_ONLY"], + libraries=["opencv_core", "opencv_highgui"], + language="c++", ), ] setup( - name='serdp_common', - version='0.1', - author='Aaron Marburg', - author_email='amarburg@uw.edu', + name="serdp_common", + version="0.1", + author="Aaron Marburg", + author_email="amarburg@uw.edu", packages=find_packages(), ext_modules=ext_modules, install_requires=None, - cmdclass={'build_ext': BuildExt}, + cmdclass={"build_ext": BuildExt}, zip_safe=False, ) diff --git a/python/py_draw_sonar.cpp b/python/py_draw_sonar.cpp index 7024a5e..5907a2f 100644 --- a/python/py_draw_sonar.cpp +++ b/python/py_draw_sonar.cpp @@ -63,12 +63,12 @@ cv::Mat cloneimg(cv::Mat image) { return image.clone(); } int add(int i, int j) { return i + j; } class AddClass { - public: +public: AddClass(int value) : value(value) {} cv::Mat add(cv::Mat input) { return input + this->value; } - private: +private: int value; }; diff --git a/python/sonar_image_proc/sonar_msg_metadata.py b/python/sonar_image_proc/sonar_msg_metadata.py index fd400d4..4ddc1f2 100644 --- a/python/sonar_image_proc/sonar_msg_metadata.py +++ b/python/sonar_image_proc/sonar_msg_metadata.py @@ -1,4 +1,3 @@ -#! /usr/bin/env python3 """ Copyright 2023 University of Washington Applied Physics Laboratory Author: Marc Micatka & Laura Lindzey diff --git a/python/test.py b/python/test.py index ef044ea..778f4f4 100644 --- a/python/test.py +++ b/python/test.py @@ -1,4 +1,4 @@ import draw_sonar -print( draw_sonar.pyadd(1,2) ) -print( draw_sonar.cppadd(1,2) ) +print(draw_sonar.pyadd(1, 2)) +print(draw_sonar.cppadd(1, 2)) diff --git a/ros/cfg/DrawSonar.cfg b/ros/cfg/DrawSonar.cfg deleted file mode 100644 index ea7fe22..0000000 --- a/ros/cfg/DrawSonar.cfg +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python -PACKAGE = "sonar_image_proc" - -from dynamic_reconfigure.parameter_generator_catkin import * - -gen = ParameterGenerator() - -gen.add("line_alpha", double_t, 0, "Alpha for lines", 0.5, 0, 1.0) -gen.add("line_thickness", int_t, 0, "Line thickness", 1, 1, 5) - -gen.add("range_spacing", double_t, 0, "Spacing of range marks (0 for auto)", 0, 0, 100) -gen.add("bearing_spacing", double_t, 0, "Spacing of bearing radials", 20, 0, 100) - -gen.add("log_scale", bool_t, 0, "Use log scale for intensity", False) -gen.add("min_db", double_t, 0, "Minimum intensity in log scale (in db, 0 for full range)", 0, -221, 0) -gen.add("max_db", double_t, 0, "Maximum intensity in log scale (in db, 0 for full range)", 0, -221, 0) - -exit(gen.generate(PACKAGE, "sonar_image_proc", "DrawSonar")) diff --git a/ros/include/sonar_image_proc/sonar_image_msg_interface.h b/ros/include/sonar_image_proc/sonar_image_msg_interface.h deleted file mode 100644 index 2b8b04e..0000000 --- a/ros/include/sonar_image_proc/sonar_image_msg_interface.h +++ /dev/null @@ -1,237 +0,0 @@ -// Copyright 2021 University of Washington Applied Physics Laboratory -// -// An implementation of an AbstractSonarInterface which -// wraps a ROS marine_acoustic_msgs::SonarImage -// - -#pragma once - -#include - -#include "sonar_image_proc/AbstractSonarInterface.h" - -namespace sonar_image_proc { - -using marine_acoustic_msgs::ProjectedSonarImage; -using sonar_image_proc::AbstractSonarInterface; -using std::vector; - -struct SonarImageMsgInterface - : public sonar_image_proc::AbstractSonarInterface { - explicit SonarImageMsgInterface( - const marine_acoustic_msgs::ProjectedSonarImage::ConstPtr &ping) - : _ping(ping), do_log_scale_(false) { - // Vertical field of view is determined by comparing - // z / sqrt(x^2 + y^2) to tan(elevation_beamwidth/2) - _verticalTanSquared = - // NOTE(lindzey): The old message assumed a constant elevation - // beamwidth; - // the multibeam people insisted that it be per-beam. For now, this - // assumes that they're all the same. - // TODO(lindzey): Look into whether averaging would be better, or if we - // should create an array of verticalTanSquared. - // TODO(lindzey): Handle empty-array case. - std::pow(std::tan(ping->ping_info.tx_beamwidths[0] / 2.0), 2); - - for (const auto pt : ping->beam_directions) { - auto az = atan2(-1 * pt.y, pt.z); - _ping_azimuths.push_back(az); - } - } - - // n.b. do_log_scale is a mode switch which causes the intensity_* - // functions in this SonarImageMsgInterface use a log (db) scale - // for 32-bit data **ONLY** -- functionality is unchanged for 8- - // and 16-bit data - // - // It maps a given 32-bit intensity value i to a fraction of the - // full scale of a 32-bit uint in the interval [0.0, 1.0] - // It then takes the db value of that: - // - // l = 10 * log( i / (2^32-1) ) - // - // Such that l is between - // ~ -96 for i = 1 - // and - // 0.0 for i = UINT32_MAX - // - // The intensity_* functions create a linear map m() from [min_db, max_db] - // to the full range of the returned type (e.g. uint8, uint16, etc), - // and returns m(l) - // - // If min_db is 0.0, then the full range value - // min_db = (10*log(1/(2^32-1))) is used - // If max_db is 0.0, then the full range value - // max_db = 0.0 is used - // - // So for example if i = 2^31 (half the full range of a uint32), l = -3 db - // This is approx 0.97 of the full scale in logspace - // - // With min_db = 0, max_db = 0 (full range) - // intensity_uint8(i) will return approx 247 (0.97 * 255) - // - // With min_db = -10, max_db = 0 - // intensity_uint8(i) will return approx 178 (0.7 * 255) - // - // With min_db = -1, max_db = 0 - // l(i) is less than min_db and intensity_uint8(i) will return 0 - // - // With min_db = 0, max_db = -10 - // l(i) is greater than max_db and intensity_uint8(i) will return 255 - // - void do_log_scale(float min_db, float max_db) { - do_log_scale_ = true; - min_db_ = min_db; - max_db_ = max_db; - range_db_ = max_db - min_db; - } - - AbstractSonarInterface::DataType_t data_type() const override { - if (_ping->image.dtype == _ping->image.DTYPE_UINT8) - return AbstractSonarInterface::TYPE_UINT8; - else if (_ping->image.dtype == _ping->image.DTYPE_UINT16) - return AbstractSonarInterface::TYPE_UINT16; - else if (_ping->image.dtype == _ping->image.DTYPE_UINT32) - return AbstractSonarInterface::TYPE_UINT32; - - // - return AbstractSonarInterface::TYPE_NONE; - } - - const std::vector &ranges() const override { return _ping->ranges; } - - const std::vector &azimuths() const override { return _ping_azimuths; } - - float verticalTanSquared() const { return _verticalTanSquared; } - - // When the underlying data is 8-bit, returns that exact value - // from the underlying data - // - // If the underlying data is 16-bit, it returns a scaled value. - uint8_t intensity_uint8(const AzimuthRangeIndices &idx) const override { - if (do_log_scale_ && (_ping->image.dtype == _ping->image.DTYPE_UINT32)) { - return intensity_float_log(idx) * UINT8_MAX; - } - - if (_ping->image.dtype == _ping->image.DTYPE_UINT8) { - return read_uint8(idx); - } else if (_ping->image.dtype == _ping->image.DTYPE_UINT16) { - return read_uint16(idx) >> 8; - } else if (_ping->image.dtype == _ping->image.DTYPE_UINT32) { - return read_uint32(idx) >> 24; - } - - return 0; - } - - uint16_t intensity_uint16(const AzimuthRangeIndices &idx) const override { - // Truncate 32bit intensities - if (do_log_scale_ && (_ping->image.dtype == _ping->image.DTYPE_UINT32)) { - return intensity_float_log(idx) * UINT16_MAX; - } - - if (_ping->image.dtype == _ping->image.DTYPE_UINT8) { - return read_uint8(idx) << 8; - } else if (_ping->image.dtype == _ping->image.DTYPE_UINT16) { - return read_uint16(idx); - } else if (_ping->image.dtype == _ping->image.DTYPE_UINT32) { - return read_uint32(idx) >> 16; - } - return 0; - } - - uint32_t intensity_uint32(const AzimuthRangeIndices &idx) const override { - if (do_log_scale_ && (_ping->image.dtype == _ping->image.DTYPE_UINT32)) { - return intensity_float_log(idx) * UINT32_MAX; - } - - if (_ping->image.dtype == _ping->image.DTYPE_UINT8) { - return read_uint8(idx) << 24; - } else if (_ping->image.dtype == _ping->image.DTYPE_UINT16) { - return read_uint16(idx) << 16; - } else if (_ping->image.dtype == _ping->image.DTYPE_UINT32) { - return read_uint32(idx); - } - return 0; - } - - float intensity_float(const AzimuthRangeIndices &idx) const override { - if (do_log_scale_ && (_ping->image.dtype == _ping->image.DTYPE_UINT32)) { - return intensity_float_log(idx); - } - - if (_ping->image.dtype == _ping->image.DTYPE_UINT8) { - return static_cast(read_uint8(idx)) / UINT8_MAX; - } else if (_ping->image.dtype == _ping->image.DTYPE_UINT16) { - return static_cast(read_uint16(idx)) / UINT16_MAX; - } else if (_ping->image.dtype == _ping->image.DTYPE_UINT32) { - return static_cast(read_uint32(idx)) / UINT32_MAX; - } - return 0.0; - } - - protected: - marine_acoustic_msgs::ProjectedSonarImage::ConstPtr _ping; - - float _verticalTanSquared; - std::vector _ping_azimuths; - - size_t index(const AzimuthRangeIndices &idx) const { - int data_size; - if (_ping->image.dtype == _ping->image.DTYPE_UINT8) { - data_size = 1; - } else if (_ping->image.dtype == _ping->image.DTYPE_UINT16) { - data_size = 2; - } else if (_ping->image.dtype == _ping->image.DTYPE_UINT32) { - data_size = 4; - } else { - assert(false); - } - - return data_size * ((idx.range() * nBearings()) + idx.azimuth()); - } - - // "raw" read functions. Assumes the data type has already been checked - uint32_t read_uint8(const AzimuthRangeIndices &idx) const { - assert(_ping->image.dtype == _ping->image.DTYPE_UINT8); - const auto i = index(idx); - - return (_ping->image.data[i]); - } - - uint32_t read_uint16(const AzimuthRangeIndices &idx) const { - assert(_ping->image.dtype == _ping->image.DTYPE_UINT16); - const auto i = index(idx); - - return (static_cast(_ping->image.data[i]) | - (static_cast(_ping->image.data[i + 1]) << 8)); - } - - uint32_t read_uint32(const AzimuthRangeIndices &idx) const { - assert(_ping->image.dtype == _ping->image.DTYPE_UINT32); - const auto i = index(idx); - - const uint32_t v = - (static_cast(_ping->image.data[i]) | - (static_cast(_ping->image.data[i + 1]) << 8) | - (static_cast(_ping->image.data[i + 2]) << 16) | - (static_cast(_ping->image.data[i + 3]) << 24)); - return v; - } - - float intensity_float_log(const AzimuthRangeIndices &idx) const { - const auto intensity = read_uint32(idx); - const auto v = - log(static_cast(std::max((uint)1, intensity)) / UINT32_MAX) * - 10; // dbm - - const auto min_db = (min_db_ == 0 ? log(1.0 / UINT32_MAX) * 10 : min_db_); - - return std::min(1.0, std::max(0.0, (v - min_db) / range_db_)); - } - - bool do_log_scale_; - float min_db_, max_db_, range_db_; -}; - -} // namespace sonar_image_proc diff --git a/ros/src/draw_sonar_node.cpp b/ros/src/draw_sonar_node.cpp deleted file mode 100644 index b8ece56..0000000 --- a/ros/src/draw_sonar_node.cpp +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2021 University of Washington Applied Physics Laboratory -// - -#include "nodelet/loader.h" -#include "ros/ros.h" - -int main(int argc, char **argv) { - ros::init(argc, argv, "draw_sonar"); - - nodelet::Loader nodelet; - nodelet::M_string remap(ros::names::getRemappings()); - nodelet::V_string nargv; - - nodelet.load(ros::this_node::getName(), "sonar_image_proc/draw_sonar", remap, - nargv); - - ros::spin(); - return 0; -} diff --git a/ros/src/draw_sonar_nodelet.cpp b/ros/src/draw_sonar_nodelet.cpp deleted file mode 100644 index b402a00..0000000 --- a/ros/src/draw_sonar_nodelet.cpp +++ /dev/null @@ -1,240 +0,0 @@ -// Copyright 2021-2022 University of Washington Applied Physics Laboratory -// Author: Aaron Marburg - -#include -#include -#include -#include - -#include "marine_acoustic_msgs/ProjectedSonarImage.h" -#include "nodelet/nodelet.h" -#include "ros/ros.h" - -// For uploading drawn sonar images to Image topic -#include -#include -#include -#include -#include -#include - -#include "sonar_image_proc/ColorMaps.h" -#include "sonar_image_proc/DrawSonar.h" -#include "sonar_image_proc/HistogramGenerator.h" -#include "sonar_image_proc/SonarDrawer.h" -#include "sonar_image_proc/sonar_image_msg_interface.h" - -// Subscribes to sonar message topic, draws using opencv then publishes -// result - -namespace draw_sonar { - -using namespace std; -using namespace cv; - -using sonar_image_proc::SonarImageMsgInterface; -using std_msgs::UInt32MultiArray; - -using sonar_image_proc::HistogramGenerator; - -using sonar_image_proc::InfernoColorMap; -using sonar_image_proc::InfernoSaturationColorMap; -using sonar_image_proc::SonarColorMap; - -class DrawSonarNodelet : public nodelet::Nodelet { - public: - DrawSonarNodelet() - : Nodelet(), - _maxRange(0.0), - _colorMap(new InfernoColorMap) // Set a reasonable default - { - ; - } - - virtual ~DrawSonarNodelet() { ; } - - private: - virtual void onInit() { - ros::NodeHandle nh = getMTNodeHandle(); - ros::NodeHandle pnh = getMTPrivateNodeHandle(); - - pnh.param("max_range", _maxRange, 0.0); - - pnh.param("publish_old", _publishOldApi, false); - pnh.param("publish_timing", _publishTiming, true); - - pnh.param("publish_histogram", _publishHistogram, false); - - std::string colorMapName; - pnh.param("color_map", colorMapName, "inferno"); - - // Set color map - setColorMap(colorMapName); - - if (_maxRange > 0.0) { - NODELET_INFO_STREAM("Only drawing to max range " << _maxRange); - } - - subSonarImage_ = nh.subscribe( - "sonar_image", 10, &DrawSonarNodelet::sonarImageCallback, this); - - pub_ = nh.advertise("drawn_sonar", 10); - osdPub_ = nh.advertise("drawn_sonar_osd", 10); - rectPub_ = nh.advertise("drawn_sonar_rect", 10); - - if (_publishOldApi) - oldPub_ = nh.advertise("old_drawn_sonar", 10); - - if (_publishTiming) - timingPub_ = - nh.advertise("sonar_image_proc_timing", 10); - - if (_publishHistogram) - histogramPub_ = nh.advertise("histogram", 10); - - dyn_cfg_server_.reset(new DynamicReconfigureServer(pnh)); - dyn_cfg_server_->setCallback(std::bind(&DrawSonarNodelet::dynCfgCallback, - this, std::placeholders::_1, - std::placeholders::_2)); - - ROS_DEBUG("draw_sonar ready to run..."); - } - - void cvBridgeAndPublish( - const marine_acoustic_msgs::ProjectedSonarImage::ConstPtr &msg, - const cv::Mat &mat, ros::Publisher &pub) { - cv_bridge::CvImage img_bridge(msg->header, - sensor_msgs::image_encodings::RGB8, mat); - - sensor_msgs::Image output_msg; - img_bridge.toImageMsg(output_msg); - pub.publish(output_msg); - } - - void sonarImageCallback( - const marine_acoustic_msgs::ProjectedSonarImage::ConstPtr &msg) { - ROS_FATAL_COND(!_colorMap, "Colormap is undefined, this shouldn't happen"); - - SonarImageMsgInterface interface(msg); - if (log_scale_) { - interface.do_log_scale(min_db_, max_db_); - } - - ros::WallDuration oldApiElapsed, rectElapsed, mapElapsed, histogramElapsed; - - if (_publishOldApi) { - ros::WallTime begin = ros::WallTime::now(); - - // Used to be a configurable parameter, but now only meaningful - // in the deprecated API - const int pixPerRangeBin = 2; - - cv::Size sz = sonar_image_proc::old_api::calculateImageSize( - interface, cv::Size(0, 0), pixPerRangeBin, _maxRange); - cv::Mat mat(sz, CV_8UC3); - mat = sonar_image_proc::old_api::drawSonar(interface, mat, *_colorMap, - _maxRange); - - cvBridgeAndPublish(msg, mat, oldPub_); - - oldApiElapsed = ros::WallTime::now() - begin; - } - - if (_publishHistogram) { - ros::WallTime begin = ros::WallTime::now(); - - auto histogramOut = UInt32MultiArray(); - histogramOut.data = HistogramGenerator::Generate(interface); - - histogramPub_.publish(histogramOut); - - histogramElapsed = ros::WallTime::now() - begin; - } - - { - ros::WallTime begin = ros::WallTime::now(); - - cv::Mat rectMat = _sonarDrawer.drawRectSonarImage(interface, *_colorMap); - - // Rotate rectangular image to the more expected format where zero range - // is at the bottom of the image, with negative azimuth to the right - // aka (rotated 90 degrees CCW) - cv::Mat rotatedRect; - cv::rotate(rectMat, rotatedRect, cv::ROTATE_90_COUNTERCLOCKWISE); - cvBridgeAndPublish(msg, rotatedRect, rectPub_); - - rectElapsed = ros::WallTime::now() - begin; - begin = ros::WallTime::now(); - - cv::Mat sonarMat = _sonarDrawer.remapRectSonarImage(interface, rectMat); - cvBridgeAndPublish(msg, sonarMat, pub_); - - if (osdPub_.getNumSubscribers() > 0) { - cv::Mat osdMat = _sonarDrawer.drawOverlay(interface, sonarMat); - cvBridgeAndPublish(msg, osdMat, osdPub_); - } - - mapElapsed = ros::WallTime::now() - begin; - } - - if (_publishTiming) { - ostringstream output; - - output << "{"; - output << "\"draw_total\" : " << (mapElapsed + rectElapsed).toSec(); - output << ", \"rect\" : " << rectElapsed.toSec(); - output << ", \"map\" : " << mapElapsed.toSec(); - - if (_publishOldApi) output << ", \"old_api\" : " << oldApiElapsed.toSec(); - if (_publishHistogram) - output << ", \"histogram\" : " << histogramElapsed.toSec(); - - output << "}"; - - std_msgs::String out_msg; - out_msg.data = output.str(); - - timingPub_.publish(out_msg); - } - } - - void dynCfgCallback(sonar_image_proc::DrawSonarConfig &config, - uint32_t level) { - _sonarDrawer.overlayConfig() - .setRangeSpacing(config.range_spacing) - .setRadialSpacing(config.bearing_spacing) - .setLineAlpha(config.line_alpha) - .setLineThickness(config.line_thickness); - - log_scale_ = config.log_scale; - min_db_ = config.min_db; - max_db_ = config.max_db; - } - - void setColorMap(const std::string &colorMapName) { - // TBD actually implement the parameter processing here... - _colorMap.reset(new InfernoSaturationColorMap()); - } - - ros::Subscriber subSonarImage_; - ros::Publisher pub_, rectPub_, osdPub_, oldPub_, timingPub_, histogramPub_; - - typedef dynamic_reconfigure::Server - DynamicReconfigureServer; - std::shared_ptr dyn_cfg_server_; - - sonar_image_proc::SonarDrawer _sonarDrawer; - - float _maxRange; - bool _publishOldApi, _publishTiming, _publishHistogram; - - float min_db_, max_db_; - bool log_scale_; - - std::unique_ptr _colorMap; -}; - -} // namespace draw_sonar - -#include -PLUGINLIB_EXPORT_CLASS(draw_sonar::DrawSonarNodelet, nodelet::Nodelet); diff --git a/ros/src/sonar_postprocessor_node.cpp b/ros/src/sonar_postprocessor_node.cpp deleted file mode 100644 index 91fa87f..0000000 --- a/ros/src/sonar_postprocessor_node.cpp +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2021 University of Washington Applied Physics Laboratory -// - -#include "nodelet/loader.h" -#include "ros/ros.h" - -int main(int argc, char **argv) { - ros::init(argc, argv, "sonar_postprocessor"); - - nodelet::Loader nodelet; - nodelet::M_string remap(ros::names::getRemappings()); - nodelet::V_string nargv; - - nodelet.load(ros::this_node::getName(), - "sonar_image_proc/sonar_postprocessor", remap, nargv); - - ros::spin(); - return 0; -} diff --git a/ros/src/sonar_postprocessor_nodelet.cpp b/ros/src/sonar_postprocessor_nodelet.cpp deleted file mode 100644 index 04bc625..0000000 --- a/ros/src/sonar_postprocessor_nodelet.cpp +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright 2021-2022 University of Washington Applied Physics Laboratory -// Author: Aaron Marburg - -#include - -#include "marine_acoustic_msgs/ProjectedSonarImage.h" -#include "nodelet/nodelet.h" -#include "ros/ros.h" -#include "sonar_image_proc/sonar_image_msg_interface.h" - -namespace sonar_postprocessor { - -using marine_acoustic_msgs::ProjectedSonarImage; -using sonar_image_proc::SonarImageMsgInterface; - -class SonarPostprocessorNodelet : public nodelet::Nodelet { - public: - SonarPostprocessorNodelet() : Nodelet() { ; } - - virtual ~SonarPostprocessorNodelet() { ; } - - private: - virtual void onInit() { - ros::NodeHandle nh = getMTNodeHandle(); - ros::NodeHandle pnh = getMTPrivateNodeHandle(); - - pnh.param("gain", gain_, 1.0); - pnh.param("gamma", gamma_, 0.0); - - subSonarImage_ = nh.subscribe( - "sonar_image", 10, &SonarPostprocessorNodelet::sonarImageCallback, - this); - - pubSonarImage_ = - nh.advertise("sonar_image_postproc", 10); - - ROS_DEBUG("sonar_processor ready to run..."); - } - - void sonarImageCallback( - const marine_acoustic_msgs::ProjectedSonarImage::ConstPtr &msg) { - SonarImageMsgInterface interface(msg); - - // For now, only postprocess 32bit images - if (msg->image.dtype != msg->image.DTYPE_UINT32) { - pubSonarImage_.publish(msg); - return; - } - - // Expect this will copy - marine_acoustic_msgs::ProjectedSonarImage out = *msg; - - // For now, only 8-bit output is supported - out.image.dtype = out.image.DTYPE_UINT8; - out.image.data.clear(); - out.image.data.reserve(interface.ranges().size() * - interface.azimuths().size()); - - double logmin, logmax; - - for (unsigned int r_idx = 0; r_idx < interface.nRanges(); ++r_idx) { - for (unsigned int a_idx = 0; a_idx < interface.nAzimuth(); ++a_idx) { - sonar_image_proc::AzimuthRangeIndices idx(a_idx, r_idx); - - // const uint32_t pix = interface.intensity_uint32(idx); - // const auto range = interface.range(r_idx); - - // Avoid log(0) - auto intensity = interface.intensity_uint32(idx); - auto v = log(std::max((uint)1, intensity)) / log(UINT32_MAX); - - if ((r_idx == 0) && (a_idx == 0)) { - logmin = v; - logmax = v; - } else { - logmin = std::min(v, logmin); - logmax = std::max(v, logmax); - } - - const float vmax = 1.0, threshold = 0.74; - - v = (v - threshold) / (vmax - threshold); - v = std::min(1.0, std::max(0.0, v)); - out.image.data.push_back(UINT8_MAX * v); - - // Just do the math in float for now - // float i = static_cast(pix)/UINT32_MAX; - - // //ROS_INFO_STREAM(a_idx << "," << r_idx << " : " << pix << " => " << - // i); - - // i *= gain_; - - // UINT8_MAX * i); - } - } - - // - float dr = exp(logmax - logmin); - pubSonarImage_.publish(out); - } - - protected: - ros::Subscriber subSonarImage_; - ros::Publisher pubSonarImage_; - - float gain_, gamma_; -}; - -} // namespace sonar_postprocessor - -#include -PLUGINLIB_EXPORT_CLASS(sonar_postprocessor::SonarPostprocessorNodelet, - nodelet::Nodelet); diff --git a/ros/tools/bag2sonar.cpp b/ros/tools/bag2sonar.cpp deleted file mode 100644 index 275194a..0000000 --- a/ros/tools/bag2sonar.cpp +++ /dev/null @@ -1,176 +0,0 @@ -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include "sonar_image_proc/SonarDrawer.h" - -namespace po = boost::program_options; - -using std::string; -using std::vector; - -class OutputWrapper { - public: - virtual void write( - const marine_acoustic_msgs::ProjectedSonarImage::ConstPtr &msg, - const cv::Mat &mat) = 0; -}; - -class BagOutput : public OutputWrapper { - public: - BagOutput(const std::string &bagfile, const std::string topic) - : _bag(bagfile, rosbag::bagmode::Write), _topic(topic) {} - - void write(const marine_acoustic_msgs::ProjectedSonarImage::ConstPtr &msg, - const cv::Mat &mat) override { - cv_bridge::CvImage img_bridge(msg->header, - sensor_msgs::image_encodings::RGB8, mat); - - sensor_msgs::Image output_msg; - img_bridge.toImageMsg(output_msg); - - // Retain original message's timestamp - _bag.write(_topic, msg->header.stamp, output_msg); - } - - rosbag::Bag _bag; - std::string _topic; -}; - -void print_help(const po::options_description &description) { - std::cout << "Usage:" << std::endl; - std::cout << std::endl; - std::cout << " bag2sonar [options] " << std::endl; - std::cout << std::endl; - std::cout << description; - exit(0); -} - -int main(int argc, char **argv) { - po::options_description public_description("Draw sonar from a bagfile"); - - // The following code with three differente po::option_descriptions in a - // slight-of-hand to hide the positional argment "input-files" - // otherwise it shows up in print_help() which is ugly - // - // clang-format off - public_description.add_options() - ("help,h", "Display this help message") - ("logscale,l", po::bool_switch()->default_value(true), "Do logscale") - ("min-db", po::value()->default_value(0), "Min db") - ("max-db", po::value()->default_value(0), "Max db") - ("osd", po::bool_switch()->default_value(true), "If set, include the on-screen display in output") - ("output-bag,o", po::value(), "Name of output bagfile") - ("output-topic,t", po::value()->default_value("/drawn_sonar"), "Topic for images in output bagfile"); - - po::options_description hidden_description(""); - hidden_description.add_options() - ("input-files", po::value>()->required(), "Input files"); - // clang-format on - - po::options_description full_description(""); - full_description.add(public_description).add(hidden_description); - - po::positional_options_description p; - p.add("input-files", -1); - - po::variables_map vm; - - try { - po::store(po::command_line_parser(argc, argv) - .options(full_description) - .positional(p) - .run(), - vm); - po::notify(vm); - - } catch (const boost::program_options::required_option &e) { - // This exception occurs when a required option (input-files) isn't supplied - // Catch that and display a help message instead. - print_help(public_description); - } catch (const std::exception &e) { - // Catch any other random errors and exit - std::cerr << e.what() << std::endl; - exit(-1); - } - - if (vm.count("help")) { - print_help(public_description); - } else if (vm.count("input-files")) { - if (vm.count("input-files") > 1) { - std::cerr << "Can only process one file at a time" << std::endl; - exit(-1); - } - - std::vector> outputs; - - if (vm.count("output-bag")) { - outputs.push_back(std::make_shared( - vm["output-bag"].as(), vm["output-topic"].as())); - } - - sonar_image_proc::SonarDrawer sonar_drawer; - std::unique_ptr color_map( - new sonar_image_proc::InfernoColorMap); - - std::vector files = - vm["input-files"].as>(); - for (std::string file : files) { - std::cout << "Processing input file " << file << std::endl; - - rosbag::Bag bag(file, rosbag::bagmode::Read); - - std::cout << "Bagfile " << file << " is " << bag.getSize() << " bytes" - << std::endl; - - rosbag::View view( - bag, rosbag::TypeQuery("marine_acoustic_msgs/ProjectedSonarImage")); - - int count = 0; - - BOOST_FOREACH (rosbag::MessageInstance const m, view) { - marine_acoustic_msgs::ProjectedSonarImage::ConstPtr msg = - m.instantiate(); - - sonar_image_proc::SonarImageMsgInterface interface(msg); - if (vm["logscale"].as()) { - interface.do_log_scale(vm["min-db"].as(), - vm["max-db"].as()); - } - - cv::Mat rectMat = - sonar_drawer.drawRectSonarImage(interface, *color_map); - - cv::Mat sonarMat = sonar_drawer.remapRectSonarImage(interface, rectMat); - - cv::Mat outMat; - if (vm["osd"].as()) { - outMat = sonar_drawer.drawOverlay(interface, sonarMat); - } else { - outMat = sonarMat; - } - - for (auto output : outputs) { - output->write(msg, outMat); - } - - count++; - - if ((count % 100) == 0) { - std::cout << "Processed " << count << " sonar frames" << std::endl; - } - } - - bag.close(); - } - } - - exit(0); -} diff --git a/scripts/sonar_fov.py b/scripts/sonar_fov.py deleted file mode 100755 index 6c8d2d0..0000000 --- a/scripts/sonar_fov.py +++ /dev/null @@ -1,185 +0,0 @@ -#! /usr/bin/env python3 -""" -Copyright 2023 University of Washington Applied Physics Laboratory -Author: Marc Micatka & Laura Lindzey -""" -from __future__ import annotations # use type of class in member function annotation. -import numpy as np -import rospy - -from marine_acoustic_msgs.msg import ProjectedSonarImage -from geometry_msgs.msg import Point -from std_msgs.msg import Header -from visualization_msgs.msg import Marker, MarkerArray - -from sonar_image_proc.sonar_msg_metadata import SonarImageMetadata - - -def build_vector_list(msg_metadata): - """ - From sonar parameters, build the wedge as list of vectors - """ - xx = [] - yy = [] - zz = [] - - # Calculate ending arc of the FOV in 3D space - for elevation in [msg_metadata.min_elevation, msg_metadata.max_elevation]: - ce = np.cos(elevation) - se = np.sin(elevation) - ca = np.cos(msg_metadata.azimuths) - sa = np.sin(msg_metadata.azimuths) - - z = msg_metadata.max_range * ce * ca - y = -1 * msg_metadata.max_range * ce * sa - x = np.full_like(z, fill_value=msg_metadata.max_range * se) - xx.extend(x) - yy.extend(y) - zz.extend(z) - - # Add a point at the origin - xx_new = np.insert(np.array(xx), 0, 0) - yy_new = np.insert(np.array(yy), 0, 0) - zz_new = np.insert(np.array(zz), 0, 0) - - # Vertices are all the points on the edge of the wedge - # Points are at the origin and then at the end of every arc - vertices = np.zeros(shape=(len(xx_new), 3)) - vertices[:, 0] = xx_new - vertices[:, 1] = yy_new - vertices[:, 2] = zz_new - - # The vertices, faces, and connections are directional in nature - # Because the mesh is one-sided and will be invisible certain directions - # if the order is incorrect - - # Number of faces: - vertex_cnt = len(xx_new) + 1 - face_cnt = vertex_cnt // 2 - 2 - split = (len(xx_new) - 1) // 2 - - pt0 = np.zeros(shape=(face_cnt), dtype=np.uint) - pt1 = np.arange(1, face_cnt + 1, dtype=np.uint) - pt2 = pt1 + 1 - pt3 = pt2 + face_cnt - pt4 = pt3 + 1 - top_face = np.stack([pt0, pt1, pt2]).T - btm_face = np.stack([pt4, pt3, pt0]).T - - # Add triangles on sides: - left_face = np.array([[0, split + 1, 1]], dtype=np.uint) - right_face = np.array([[0, split, len(xx_new) - 1]], dtype=np.uint) - - # Add triangles between elevations: - first_pt = np.arange(1, split, dtype=np.uint) - second_pt = first_pt + 1 - third_pt = second_pt + split - 1 - fourth_pt = third_pt + 1 - - first_connection = np.stack([first_pt, third_pt, fourth_pt]).T - second_connection = np.stack([first_pt, fourth_pt, second_pt]).T - elevation_faces = np.vstack([first_connection, second_connection]) - - faces = np.vstack([top_face, btm_face, left_face, right_face, elevation_faces]) - - # Create array of vectors - vectors = [] - for f in faces: - for j in range(3): - x, y, z = vertices[f[j], :] - pt = Point(x, y, z) - vectors.append(pt) - return vectors - - -class SonarFOV: - def __init__(self): - self.sub = rospy.Subscriber( - "sonar_image", - ProjectedSonarImage, - self.sonar_image_callback, - queue_size=1, - buff_size=1000000, - ) - self.pub_fov = rospy.Publisher("sonar_fov", MarkerArray, queue_size=10) - - # Alpha transparency for wedge - self.alpha = rospy.get_param("~alpha", 1.0) - if isinstance(self.alpha, str): - self.alpha = float(self.alpha) - - # RGB color for wedge - self.color = rospy.get_param("~color", [0.0, 1.0, 0.0]) - if isinstance(self.color, str): - self.color = eval(self.color) - - self.vector_list = None - self.sonar_msg_metadata = None - - def generate_marker_array(self, vector_list) -> MarkerArray: - """ - Create MarkerArray from the pre-computed FOV wedge. - Sets the color and alpha from rosparams - """ - obj = Marker() - obj.type = Marker.TRIANGLE_LIST - obj.id = 1 - obj.points = vector_list - obj.frame_locked = True - - obj.scale.x = 1.0 - obj.scale.y = 1.0 - obj.scale.z = 1.0 - - obj.color.r = self.color[0] - obj.color.g = self.color[1] - obj.color.b = self.color[2] - obj.color.a = self.alpha - - wedge = MarkerArray() - wedge.markers = [obj] - return wedge - - def sonar_image_callback(self, sonar_image_msg: ProjectedSonarImage): - """ - Callback to publish the marker array containing the FOV wedge. - Only updates the geometry if parameters change. - """ - rospy.logdebug( - "Received new image, seq %d at %f" - % (sonar_image_msg.header.seq, sonar_image_msg.header.stamp.to_sec()) - ) - - # For the sonar_fov script, the elevation steps need to be 2 - # The logic to draw the mesh is not generic to step counts - new_metadata = SonarImageMetadata(sonar_image_msg) - generate_fov_flag = False - if self.sonar_msg_metadata is None: - self.sonar_msg_metadata = new_metadata - generate_fov_flag = True # generate the fov stl - else: - # Because the dictionary contains numpy arrays, a simple check for a == b does not work. - # Using allclose because the range occasionally changes by fractions - if ( - self.sonar_msg_metadata is None - or self.sonar_msg_metadata != new_metadata - ): - # things have changed, regenerate the fov - rospy.logdebug("Updating Parameters of FOV") - self.sonar_msg_metadata = new_metadata - generate_fov_flag = True - - if generate_fov_flag: - rospy.logdebug("Generating FOV mesh...") - self.vector_list = build_vector_list(self.sonar_msg_metadata) - - wedge = self.generate_marker_array(self.vector_list) - for obj in wedge.markers: - obj.header = sonar_image_msg.header - self.pub_fov.publish(wedge) - - -if __name__ == "__main__": - rospy.init_node("sonar_fov") - fov_publisher = SonarFOV() - rospy.spin() diff --git a/scripts/sonar_pointcloud.py b/scripts/sonar_pointcloud.py deleted file mode 100755 index df90cef..0000000 --- a/scripts/sonar_pointcloud.py +++ /dev/null @@ -1,291 +0,0 @@ -#! /usr/bin/env python3 -""" -Copyright 2023 University of Washington Applied Physics Laboratory -Author: Marc Micatka & Laura Lindzey -""" - -from __future__ import annotations # use type of class in member function annotation. -from matplotlib import cm -import numpy as np -import rospy -import time -from marine_acoustic_msgs.msg import ProjectedSonarImage, SonarImageData -from sensor_msgs.msg import PointCloud2, PointField -from std_msgs.msg import Header - -from sonar_image_proc.sonar_msg_metadata import SonarImageMetadata - - -def make_geometry(sonar_msg_metadata: SonarImageMetadata, elevations) -> np.array: - """ - Vectorized geometry generation. - Regenerates when there are changing parameters from the sonar. - - The original make_geometry() fxn was nested loops. We replaced it with a vectorized solution but - for the purposes of making the indexing and geometry creation more clear, - here is the original implementation: - - ces = np.cos(elevations) - ses = np.sin(elevations) - cas = np.cos(self.azimuths) - sas = np.sin(self.azimuths) - for kk, (ce, se) in enumerate(zip(ces, ses)): - for ii, (ca, sa) in enumerate(zip(cas, sas)): - for jj, distance in enumerate(self.ranges): - idx = ii + jj * self.num_angles - zz = distance * ce * ca - yy = distance * ce * sa - xx = distance * se - points[kk][idx] = [xx, yy, zz] - """ - rospy.logdebug("Making Geometry") - begin_time = time.time() - # Pre-compute these values to speed up the loop - # Compute the idxs to properly index into the points array - idxs = np.arange(0, sonar_msg_metadata.num_angles * sonar_msg_metadata.num_ranges) - idxs = idxs.reshape( - sonar_msg_metadata.num_ranges, sonar_msg_metadata.num_angles - ).flatten(order="F") - - # Compute the cosines and sines of elevations and azimuths - ces = np.cos(elevations) - ses = np.sin(elevations) - cas = np.cos(sonar_msg_metadata.azimuths) - sas = np.sin(sonar_msg_metadata.azimuths) - - # Allocate points - new_shape = ( - len(elevations), - sonar_msg_metadata.num_ranges * sonar_msg_metadata.num_angles, - 3, - ) - points = np.zeros(shape=(new_shape)) - - x_temp = np.tile( - sonar_msg_metadata.ranges[np.newaxis, :] * ses[:, np.newaxis], - reps=sonar_msg_metadata.num_angles, - ).flatten() - y_temp = ( - sonar_msg_metadata.ranges[np.newaxis, np.newaxis, :] - * ces[:, np.newaxis, np.newaxis] - * sas[np.newaxis, :, np.newaxis] - ).flatten() - z_temp = ( - sonar_msg_metadata.ranges[np.newaxis, np.newaxis, :] - * ces[:, np.newaxis, np.newaxis] - * cas[np.newaxis, :, np.newaxis] - ).flatten() - - points[:, idxs, :] = np.stack([x_temp, y_temp, z_temp], axis=1).reshape(new_shape) - - total_time = time.time() - begin_time - rospy.logdebug(f"Creating geometry took {1000*total_time:0.2f} ms") - return points - - -def make_color_lookup() -> np.array: - """ - Generates a lookup table from matplotlib inferno colormapping - """ - color_lookup = np.zeros(shape=(256, 4)) - - for aa in range(256): - r, g, b, _ = cm.inferno(aa) - alpha = aa / 256 - - color_lookup[aa, :] = [r, g, b, alpha] - return color_lookup - - -class SonarPointcloud(object): - """ - Subscribe to ProjectedSonarImage messages and publish slices of the - intensity array as rviz MarkerArrays. - """ - - def __init__(self): - # NB: if queue_size is set, have to be sure buff_size is sufficiently large, - # since it can only discard messages that are fully in the buffer. - # https://stackoverflow.com/questions/26415699/ros-subscriber-not-up-to-date - # (200k didn't work, and sys.getsizeof doesn't return an accurate size of the whole object) - self.sub = rospy.Subscriber( - "sonar_image", - ProjectedSonarImage, - self.sonar_image_callback, - queue_size=1, - buff_size=1000000, - ) - self.pub = rospy.Publisher("sonar_cloud", PointCloud2, queue_size=1) - - # Flag to determine whether we publish ALL points or only non-zero points - self.publish_all_points = rospy.get_param("~publish_all_points", False) - - # cmin is a color scaling parameter. The log-scaled and normed intensity values - # get scaled to (cmin, 1.0) - self.cmin = rospy.get_param("~cmin", 0.74) - - # threshold is the publish parameter if publish_all_points is false - self.threshold = rospy.get_param("~threshold", 0) - - # Number of planes to add to the pointcloud projection. - # Steps are evenly distributed between the max and min elevation angles. - elev_steps = rospy.get_param("~elev_steps", 2) - if isinstance(elev_steps, (float, str)): - elev_steps = int(elev_steps) - min_elev_deg = rospy.get_param("~min_elev_deg", -10) - max_elev_deg = rospy.get_param("~max_elev_deg", 10) - assert max_elev_deg >= min_elev_deg - min_elev = np.radians(min_elev_deg) - max_elev = np.radians(max_elev_deg) - self.elevations = np.linspace(min_elev, max_elev, elev_steps) - - # x,y,z coordinates of points - # [ELEVATION_IDX, INTENSITY_IDX, DIMENSION_IDX] - # Shape chosen for ease of mapping coordinates to intensities - self.geometry = None - - # We need to detect whether the geometry has changed. - self.sonar_msg_metadata = None - - self.color_lookup = None - self.output_points = None - - def normalize_intensity_array(self, image: SonarImageData): - """ - Process an intensity array into a parseable format for pointcloud generation - Can handle 8bit or 32bit input data, will log scale output data - - Input intensities are on the range [0, INT_MAX]. - Output intensities are log-scaled, normalized to [0, 1] - """ - if image.dtype == image.DTYPE_UINT8: - data_type = np.uint8 - elif image.dtype == image.DTYPE_UINT32: - data_type = np.uint32 - else: - raise Exception("Only 8 bit and 32 bit data is supported!") - - intensities = np.frombuffer(image.data, dtype=data_type) - new_intensites = intensities.astype(np.float32) - # Scaling calculation is taken from sonar_image_proc/ros/src/sonar_postprocessor_nodelet.cpp - # First we log scale the intensities - - # Avoid log(0) and scale full range of possible intensities to [0,1] - vv = np.log(np.maximum(1, new_intensites)) / np.log(np.iinfo(data_type).max) - return vv - - def sonar_image_callback(self, sonar_image_msg: ProjectedSonarImage): - """ - Convert img_msg into point cloud with color mappings via numpy. - """ - begin_time = time.time() - rospy.logdebug( - "Received new image, seq %d at %f" - % (sonar_image_msg.header.seq, sonar_image_msg.header.stamp.to_sec()) - ) - header = Header() - header = sonar_image_msg.header - - frame_id = rospy.get_param("~frame_id", None) - if frame_id: - header.frame_id = frame_id - - fields = [ - PointField("x", 0, PointField.FLOAT32, 1), - PointField("y", 4, PointField.FLOAT32, 1), - PointField("z", 8, PointField.FLOAT32, 1), - PointField("r", 12, PointField.FLOAT32, 1), - PointField("g", 16, PointField.FLOAT32, 1), - PointField("b", 20, PointField.FLOAT32, 1), - PointField("a", 24, PointField.FLOAT32, 1), - ] - - new_metadata = SonarImageMetadata(sonar_image_msg) - if self.sonar_msg_metadata is None or self.sonar_msg_metadata != new_metadata: - rospy.logdebug( - f"Metadata updated! \n" - f"Old: {self.sonar_msg_metadata} \n" - f"New: {new_metadata}" - ) - - self.sonar_msg_metadata = new_metadata - self.geometry = make_geometry(self.sonar_msg_metadata, self.elevations) - - if self.color_lookup is None: - self.color_lookup = make_color_lookup() - t0 = time.time() - - normalized_intensities = self.normalize_intensity_array(sonar_image_msg.image) - - # Threshold pointcloud based on intensity if not publishing all points - if self.publish_all_points: - selected_intensities = normalized_intensities - geometry = self.geometry - else: - pos_intensity_idx = np.where(normalized_intensities > self.threshold) - selected_intensities = normalized_intensities[pos_intensity_idx] - geometry = self.geometry[:, pos_intensity_idx[0]] - - num_original = len(normalized_intensities) - num_selected = len(selected_intensities) - rospy.logdebug( - f"Filtering Results: (Pts>Thresh, Total): {num_selected, num_original}. " - f"Frac: {(num_selected/num_original):.3f}" - ) - - # Finally, apply colormap to intensity values. - v_max = 1.0 - colors = (selected_intensities - self.cmin) / (v_max - self.cmin) - c_clipped = np.clip(colors, a_min=0.0, a_max=1.0) - c_uint8 = (np.iinfo(np.uint8).max * c_clipped).astype(np.uint8) - - # Allocate our output points - npts = len(selected_intensities) - self.output_points = np.zeros( - (len(self.elevations) * npts, 7), dtype=np.float32 - ) - - # Expand out intensity array (for fast comparison) - # The np.where call setting colors requires an array of shape (npts, 4). - # selected_intensities has shape (npts,), but np.repeat requires (npts, 1). - c_expanded = np.repeat(c_uint8[..., np.newaxis], 4, axis=1) - elev_points = np.empty((npts, 7)) - elev_points[:, 3:] = self.color_lookup[c_expanded[:, 0]] - - # Fill the output array - for i in range(len(self.elevations)): - elev_points[:, 0:3] = geometry[i, :, :] - step = i * npts - next_step = step + npts - self.output_points[step:next_step, :] = elev_points - - t1 = time.time() - n_pub = len(self.output_points) - cloud_msg = PointCloud2( - header=header, - height=1, - width=n_pub, - is_dense=True, - is_bigendian=False, - fields=fields, - point_step=7 * 4, - row_step=7 * 4 * n_pub, - data=self.output_points.tobytes(), - ) - - dt1 = time.time() - t1 - dt0 = t1 - t0 - total_time = time.time() - begin_time - self.pub.publish(cloud_msg) - - rospy.logdebug( - f"published pointcloud: npts = {npts}, Find Pts = {dt0:0.5f} sec, " - f"Convert to Cloud = {dt1:0.5f} sec. " - f"Total Time = {total_time:0.3f} sec" - ) - - -if __name__ == "__main__": - rospy.init_node("sonar_pointcloud") - pointcloud_publisher = SonarPointcloud() - rospy.spin() diff --git a/setup.py b/setup.py index 2f5941f..16d148e 100644 --- a/setup.py +++ b/setup.py @@ -5,8 +5,8 @@ from distutils.core import setup setup( - version='0.0.1', - scripts=['python/histogram_drawer'], - packages=['sonar_image_proc'], - package_dir={'': 'python/'} + version="0.0.1", + scripts=["python/histogram_drawer"], + packages=["sonar_image_proc"], + package_dir={"": "python/"}, ) diff --git a/sonar_image_proc.rosinstall b/sonar_image_proc.rosinstall deleted file mode 100644 index f1f7beb..0000000 --- a/sonar_image_proc.rosinstall +++ /dev/null @@ -1 +0,0 @@ -- git: {local-name: hydrographic_msgs, uri: "https://github.com/apl-ocean-engineering/hydrographic_msgs.git"} diff --git a/test/unit/test_draw_sonar.cpp b/test/unit/test_draw_sonar.cpp index 7931dec..bf33e96 100644 --- a/test/unit/test_draw_sonar.cpp +++ b/test/unit/test_draw_sonar.cpp @@ -5,7 +5,7 @@ #include using namespace std; -#include "sonar_image_proc/DrawSonar.h" -using namespace sonar_image_proc; +#include "libdrawsonar/DrawSonar.h" +using namespace libdrawsonar; TEST(TestDrawSonar, NullTest) { ASSERT_TRUE(true); }