diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 71999eea..3430909e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,30 @@ Changelog **Important: as of 0.13.0, the SDK is no longer compatible with firmware versions older than 2.1.0.** +[20241017] [0.13.1] +====================== + +* Add support for directly using IPv6 addresses for sensors in the CLI and in sensor clients. +* Typing '?' now displays the visualizer keyboard shortcuts in the visualizer window. +* Removed the ``async_client_example.cpp`` example. +* Un-deprecated ``ScanBatcher::ScanBatcher(size_t, const packet_format&)`` to remove a warning. (But please use ``ScanBatcher::ScanBatcher(const sensor_info&)`` instead.) + +* [BREAKING] Removed the ``input_row_major`` parameter from the ``dewarp`` function. (``dewarp`` now infers the array type.) +* [BREAKING] Renamed ``DEFAULT_HTTP_REQUEST_TIMEOUT_SECONDS`` to ``LONG_HTTP_REQUEST_TIMEOUT_SECONDS``. +* [BREAKING] Changed the default value of ``LidarScanVizAccumulatorsConfig.accum_min_dist_num`` from ``1`` to ``0``. +* [BUGFIX] Fixed a visualizer glitch causing drawables not to render if added after a call to ``PointViz::update()`` but before ``PointViz::run()`` or ``PointViz::run_once()``. +* [BUGFIX] Fixed a visualizer crash when using ``HIGHLIGHT_SECOND`` mode with single-return datasets. +* [BUGFIX] Fixed an issue with the 2d images not updating when cycled during pause. +* [BUGFIX] Fixed a bug that the first scan pose it not identity when using slice slam command on a slam output osf file +* [BUGFIX] Re-introduce the RAW field option + +Known Issues +------------ + +* Using an unbounded slice (e.g. with ``slice 100:`` during visualization can cause the source to loop back to the beginning (outside of the slice) when the source is a pcap file or an OSF saved with an earlier version of the SDK. +* A race condition in ``PointViz`` event handers occasionally causes a crash or unexpected results. + + [20240702] [0.13.0] ====================== @@ -50,7 +74,7 @@ ouster_client/Python SDK * [BREAKING] Remove ``ouster::make_xyz_lut(const ouster::sensor::sensor_info&)``. (Use ``make_xyz_lut(const sensor::sensor_info& sensor, bool use_extrinsics)`` instead.) -* [BREAKING] changed REFLECTIVITY channel field size to 8 bits. (Important - this makes the SDK incompatible with FW 2.0 and 2.1.) +* [BREAKING] changed REFLECTIVITY channel field size to 8 bits. (Important - this makes the SDK incompatible with FW 2.0.) * [BREAKING] Removed ``UDPPacketSource`` and ``BufferedUDPSource``. * [BREAKING] Removed ``ouster.sdk.util.firmware_version(hostname)`` please use ``ouster.sdk.client.SensorHttp.create(hostname).firmware_version()`` instead * [BREAKING] ``open_source`` no longer automatically finds and applies extrinsics from ``sensor_extrinsics.json`` files. Use the ``extrinsics`` argument instead to specify the path to the relevant extrinsics file instead. diff --git a/CMakeLists.txt b/CMakeLists.txt index 2b13753b..2ba18416 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,10 +13,10 @@ include(DefaultBuildType) include(VcpkgEnv) # ==== Project Name ==== -project(ouster-sdk VERSION 0.13.0) +project(ouster-sdk VERSION 0.13.1) # generate version header -set(OusterSDK_VERSION_STRING 0.13.0) +set(OusterSDK_VERSION_STRING 0.13.1) include(VersionGen) # ==== Options ==== diff --git a/docs/overview.rst b/docs/overview.rst index d60d14c7..6517443b 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -51,6 +51,7 @@ The following table indicates the compatibility of each released SDK version and ===================================== ======= ======= ======= ======= ======= ======= ======= ======= SDK Tag (Release) / Python SDK FW 2.0 FW 2.1 FW 2.2 FW 2.3 FW 2.4 FW 2.5 FW 3.0 FW 3.1 ===================================== ======= ======= ======= ======= ======= ======= ======= ======= +C++ SDK 20240703 / Python SDK 0.13.1 no **yes** **yes** **yes** **yes** **yes** **yes** **yes** C++ SDK 20240703 / Python SDK 0.13.0 no **yes** **yes** **yes** **yes** **yes** **yes** **yes** C++ SDK 20240703 / Python SDK 0.12.0 **yes** **yes** **yes** **yes** **yes** **yes** **yes** **yes** C++ SDK 20240423 / Python SDK 0.11.1 **yes** **yes** **yes** **yes** **yes** **yes** **yes** **yes** diff --git a/docs/versions.json b/docs/versions.json index 71d34b94..699d8ef7 100644 --- a/docs/versions.json +++ b/docs/versions.json @@ -1,4 +1,11 @@ [ + { + "version": "0.13.0", + "tags": { + "sdkx": "release-0.13.0", + "sdk": "release-0.13.0" + } + }, { "version": "0.12.0", "tags": { diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index ee256351..4ea04b7a 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -4,9 +4,6 @@ target_link_libraries(client_example PRIVATE OusterSDK::ouster_client) add_executable(client_packet_example client_packet_example.cpp) target_link_libraries(client_packet_example PRIVATE OusterSDK::ouster_client) -add_executable(async_client_example async_client_example.cpp) -target_link_libraries(async_client_example PRIVATE OusterSDK::ouster_client) - add_executable(config_example config_example.cpp) target_link_libraries(config_example PRIVATE OusterSDK::ouster_client) diff --git a/examples/async_client_example.cpp b/examples/async_client_example.cpp deleted file mode 100644 index 340f2124..00000000 --- a/examples/async_client_example.cpp +++ /dev/null @@ -1,249 +0,0 @@ -/** - * Copyright (c) 2023, Ouster, Inc. - * All rights reserved. - */ - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "ouster/client.h" -#include "ouster/impl/build.h" -#include "ouster/lidar_scan.h" -#include "ouster/sensor_client.h" -#include "ouster/types.h" - -using namespace ouster; - -const size_t N_SCANS = 5; - -void FATAL(const char* msg) { - std::cerr << msg << std::endl; - std::exit(EXIT_FAILURE); -} - -/* - * Display some stats about the captured Lidar Scan - */ -void display_scan_summary(const LidarScan& scan); - -/* - * Write output to CSV files. The output can be viewed in a point cloud - * viewer like CloudCompare: - * - * [0] https://github.com/cloudcompare/cloudcompare - */ -void write_cloud(const std::string& file_path, const LidarScan::Points& cloud); - -int main(int argc, char* argv[]) { - if (argc != 2 && argc != 3) { - std::cerr << "Version: " << ouster::SDK_VERSION_FULL << " (" - << ouster::BUILD_SYSTEM << ")" - << "\n\nUsage: async_client_example " - "[]" - "\n\n is optional: leave blank for " - "automatic destination detection" - << std::endl; - - return argc == 1 ? EXIT_SUCCESS : EXIT_FAILURE; - } - - // Limit ouster_client log statements to "info" and direct the output to log - // file rather than the console (default). - sensor::init_logger("info", "ouster.log"); - - std::cerr << "Ouster client example " << ouster::SDK_VERSION << std::endl; - /* - * The sensor client consists of the network client and a library for - * reading and working with data. - * - * The network client supports reading and writing a limited number of - * configuration parameters and receiving data without working directly with - * the socket APIs. See the `client.h` for more details. The minimum - * required parameters are the sensor hostname/ip and the data destination - * hostname/ip. - */ - const std::string sensor_hostname = argv[1]; - const std::string data_destination = (argc == 3) ? argv[2] : "@auto"; - - std::cerr << "Connecting to \"" << sensor_hostname << "\"...\n"; - - ouster::sensor::sensor_config config; - config.udp_dest = data_destination; - sensor::SensorClient client( - {ouster::sensor::Sensor(sensor_hostname, config)}); - std::cerr << "Connection to sensor succeeded" << std::endl; - - /* - * Configuration and calibration parameters can be queried directly from the - * sensor. These are required for parsing the packet stream and calculating - * accurate point clouds. - */ - std::cerr << "Gathering metadata..." << std::endl; - - // You can access the retrieved metadata from the SensorClient class - sensor::sensor_info info = client.get_sensor_info()[0]; - - size_t w = info.format.columns_per_frame; - size_t h = info.format.pixels_per_column; - - ouster::sensor::ColumnWindow column_window = info.format.column_window; - - std::cerr << " Firmware version: " << info.fw_rev - << "\n Serial number: " << info.sn - << "\n Product line: " << info.prod_line - << "\n Scan dimensions: " << w << " x " << h - << "\n Column window: [" << column_window.first << ", " - << column_window.second << "]" << std::endl; - - // A LidarScan holds lidar data for an entire rotation of the device - LidarScan scan{info}; - - // pre-compute a table for efficiently calculating point clouds from - // range - // the second argument specifies if sensor extrinsics should be applied to - // the output point cloud - XYZLut lut = ouster::make_xyz_lut(info, true); - // A an array of points to hold the projected representation of the scan - LidarScan::Points cloud; - - // A ScanBatcher can be used to batch packets into scans - ScanBatcher batch_to_scan(info); - - /* - * The network client provides some convenience wrappers around socket APIs - * to facilitate reading lidar and IMU data from the network. It is also - * possible to configure the sensor offline and read data directly from a - * UDP socket. - */ - - // Place to store raw packets as they pass between threads - ouster::sensor::LidarPacket lidar_packet; - ouster::sensor::ImuPacket imu_packet; - - /* - In this example we spin two threads one to receive lidar packets while the - other thread accumlates lidar packets of the same frame into a LidarScan - object, computes the xyz coordinates and then writes these coordiantes into - a file. The example is a show case of utilizing threads to decouple - reception of packets from processing the point cloud. For a more complete - examples on how to efficient stream and process lidar packets please refer - to the async_udp_source_example.cpp or the ouster_ros driver implementation - */ - size_t n_scans = 0; // counter to track the number of complete scans that - // we have successfully captured and processed. - std::mutex mtx; - std::condition_variable receiving_cv; - std::condition_variable processing_cv; - bool packet_processed = true; - - std::thread packet_receiving_thread([&]() { - while (n_scans < N_SCANS) { - // wait until sensor data is available - auto ev = client.get_packet(lidar_packet, imu_packet, 1.0); - - // check for timeout - if (ev.type == ouster::sensor::ClientEvent::PollTimeout) - FATAL("Client has timed out"); - - // check for error state - if (ev.type == ouster::sensor::ClientEvent::Error) - FATAL("Exit was requested"); - - // check for lidar data, read a packet and add it to the current - // batch - if (ev.type == ouster::sensor::ClientEvent::LidarPacket) { - std::unique_lock lock(mtx); - receiving_cv.wait( - lock, [&packet_processed] { return packet_processed; }); - packet_processed = false; - processing_cv.notify_one(); - } - - // check if IMU data is available (but don't do anything with it) - if (ev.type == ouster::sensor::ClientEvent::ImuPacket) { - std::unique_lock lock(mtx); - receiving_cv.wait( - lock, [&packet_processed] { return packet_processed; }); - // we are not going to processor imu data - // so we will keep packet_processed set to true - } - } - }); - - std::thread packet_processing_thread([&]() { - while (n_scans < N_SCANS) { - std::unique_lock lock(mtx); - processing_cv.wait( - lock, [&packet_processed] { return !packet_processed; }); - // batcher will return "true" when the current scan is complete - if (batch_to_scan(lidar_packet, scan)) { - // retry until we receive a full set of valid measurements - // (accounting for azimuth_window settings if any) - if (scan.complete(info.format.column_window)) { - display_scan_summary(scan); - std::cerr << "Computing point cloud... " << std::endl; - cloud = ouster::cartesian(scan, lut); - std::string file_name = - "cloud_" + std::to_string(n_scans) + ".csv"; - write_cloud(file_name, cloud); - ++n_scans; - } - } - - packet_processed = true; - receiving_cv.notify_one(); - } - }); - - packet_receiving_thread.join(); - packet_processing_thread.join(); - - std::cerr << "done" << std::endl; - - return EXIT_SUCCESS; -} - -void display_scan_summary(const LidarScan& scan) { - // channel fields can be queried as well - auto n_valid_first_returns = - (scan.field(sensor::ChanField::RANGE) != 0).count(); - - // LidarScan also provides access to header information such as - // status and timestamp - auto status = scan.status(); - auto it = std::find_if(status.data(), status.data() + status.size(), - [](const uint32_t s) { - return (s & 0x01); - }); // find first valid status - if (it != status.data() + status.size()) { - auto ts_ms = std::chrono::duration_cast( - std::chrono::nanoseconds(scan.timestamp()( - it - status.data()))); // get corresponding timestamp - - std::cerr << " Frame no. " << scan.frame_id << " with " - << n_valid_first_returns << " valid first returns at " - << ts_ms.count() << " ms" << std::endl; - } -} - -void write_cloud(const std::string& file_path, const LidarScan::Points& cloud) { - std::ofstream out; - out.open(file_path); - out << std::fixed << std::setprecision(4); - - // write each point, filtering out points without returns - for (int i = 0; i < cloud.rows(); i++) { - auto xyz = cloud.row(i); - if (!xyz.isApproxToConstant(0.0)) - out << xyz(0) << ", " << xyz(1) << ", " << xyz(2) << std::endl; - } - - out.close(); - std::cerr << " Wrote " << file_path << std::endl; -} diff --git a/examples/client_packet_example.cpp b/examples/client_packet_example.cpp index 98d3f39b..07ad45bb 100644 --- a/examples/client_packet_example.cpp +++ b/examples/client_packet_example.cpp @@ -30,8 +30,6 @@ int main(int argc, char* argv[]) { << ouster::BUILD_SYSTEM << ")" << "\n\nUsage: client_example " "[]..." - "\n\n is optional: leave blank for " - "automatic destination detection" << std::endl; return argc == 1 ? EXIT_SUCCESS : EXIT_FAILURE; diff --git a/ouster_client/include/ouster/client.h b/ouster_client/include/ouster/client.h index 36c26dae..9c003418 100644 --- a/ouster_client/include/ouster/client.h +++ b/ouster_client/include/ouster/client.h @@ -98,7 +98,7 @@ std::shared_ptr init_client( const std::string& hostname, const std::string& udp_dest_host, lidar_mode ld_mode = MODE_UNSPEC, timestamp_mode ts_mode = TIME_FROM_UNSPEC, int lidar_port = 0, int imu_port = 0, - int timeout_sec = DEFAULT_HTTP_REQUEST_TIMEOUT_SECONDS, + int timeout_sec = LONG_HTTP_REQUEST_TIMEOUT_SECONDS, bool persist_config = false); /** @@ -123,7 +123,7 @@ std::shared_ptr init_client( std::shared_ptr mtp_init_client( const std::string& hostname, const sensor_config& config, const std::string& mtp_dest_host, bool main, - int timeout_sec = DEFAULT_HTTP_REQUEST_TIMEOUT_SECONDS, + int timeout_sec = LONG_HTTP_REQUEST_TIMEOUT_SECONDS, bool persist_config = false); /** @}*/ @@ -232,8 +232,8 @@ bool read_imu_packet(const client& cli, ImuPacket& packet); * * @return a text blob of metadata parseable into a sensor_info struct. */ -std::string get_metadata( - client& cli, int timeout_sec = DEFAULT_HTTP_REQUEST_TIMEOUT_SECONDS); +std::string get_metadata(client& cli, + int timeout_sec = LONG_HTTP_REQUEST_TIMEOUT_SECONDS); /** * Get sensor config from the sensor. @@ -250,7 +250,7 @@ std::string get_metadata( */ bool get_config(const std::string& hostname, sensor_config& config, bool active = true, - int timeout_sec = DEFAULT_HTTP_REQUEST_TIMEOUT_SECONDS); + int timeout_sec = LONG_HTTP_REQUEST_TIMEOUT_SECONDS); // clang-format off /** @@ -280,7 +280,7 @@ enum config_flags : uint8_t { */ bool set_config(const std::string& hostname, const sensor_config& config, uint8_t config_flags = 0, - int timeout_sec = DEFAULT_HTTP_REQUEST_TIMEOUT_SECONDS); + int timeout_sec = LONG_HTTP_REQUEST_TIMEOUT_SECONDS); /** * Return the port used to listen for lidar UDP data. diff --git a/ouster_client/include/ouster/defaults.h b/ouster_client/include/ouster/defaults.h index d8d95fde..3ada1e0e 100644 --- a/ouster_client/include/ouster/defaults.h +++ b/ouster_client/include/ouster/defaults.h @@ -1,4 +1,5 @@ #pragma once -constexpr int DEFAULT_HTTP_REQUEST_TIMEOUT_SECONDS = 40; +constexpr int SHORT_HTTP_REQUEST_TIMEOUT_SECONDS = 4; +constexpr int LONG_HTTP_REQUEST_TIMEOUT_SECONDS = 40; constexpr int DEFAULT_COLUMNS_PER_PACKET = 16; diff --git a/ouster_client/include/ouster/lidar_scan.h b/ouster_client/include/ouster/lidar_scan.h index a61d0cd0..a18c6818 100644 --- a/ouster_client/include/ouster/lidar_scan.h +++ b/ouster_client/include/ouster/lidar_scan.h @@ -747,8 +747,6 @@ class ScanBatcher { * 2048. * @param[in] pf expected format of the incoming packets used for parsing. */ - [[deprecated("Use ScanBatcher::ScanBatcher(const sensor_info&) instead. " - "This is planned to be removed in Q4 2024.")]] ScanBatcher(size_t w, const sensor::packet_format& pf); // clang-format on diff --git a/ouster_client/include/ouster/sensor_http.h b/ouster_client/include/ouster/sensor_http.h index 4de0e70c..d92dd525 100644 --- a/ouster_client/include/ouster/sensor_http.h +++ b/ouster_client/include/ouster/sensor_http.h @@ -69,7 +69,8 @@ class SensorHttp { * * @return returns a Json object of the sensor metadata. */ - virtual Json::Value metadata(int timeout_sec = 1) const = 0; + virtual Json::Value metadata( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const = 0; /** * Queries the sensor_info. @@ -78,7 +79,8 @@ class SensorHttp { * * @return returns a Json object representing the sensor_info. */ - virtual Json::Value sensor_info(int timeout_sec = 1) const = 0; + virtual Json::Value sensor_info( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const = 0; /** * Queries active/staged configuration on the sensor @@ -88,8 +90,9 @@ class SensorHttp { * * @return a string represnting the active or staged config */ - virtual std::string get_config_params(bool active, - int timeout_sec = 1) const = 0; + virtual std::string get_config_params( + bool active, + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const = 0; /** * Set the value of a specfic configuration on the sensor, the changed @@ -99,9 +102,9 @@ class SensorHttp { * @param[in] value the new value to set for the selected configuration. * @param[in] timeout_sec The timeout for the request in seconds. */ - virtual void set_config_param(const std::string& key, - const std::string& value, - int timeout_sec = 1) const = 0; + virtual void set_config_param( + const std::string& key, const std::string& value, + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const = 0; /** * Retrieves the active configuration on the sensor @@ -110,7 +113,8 @@ class SensorHttp { * * @return active configuration parameters set on the sensor */ - virtual Json::Value active_config_params(int timeout_sec = 1) const = 0; + virtual Json::Value active_config_params( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const = 0; /** * Retrieves the staged configuration on the sensor @@ -119,14 +123,16 @@ class SensorHttp { * * @return staged configuration parameters set on the sensor */ - virtual Json::Value staged_config_params(int timeout_sec = 1) const = 0; + virtual Json::Value staged_config_params( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const = 0; /** * Enables automatic assignment of udp destination ports. * * @param[in] timeout_sec The timeout for the request in seconds. */ - virtual void set_udp_dest_auto(int timeout_sec = 1) const = 0; + virtual void set_udp_dest_auto( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const = 0; /** * Retrieves beam intrinsics of the sensor. @@ -135,7 +141,8 @@ class SensorHttp { * * @return beam_intrinsics retrieved from sensor */ - virtual Json::Value beam_intrinsics(int timeout_sec = 1) const = 0; + virtual Json::Value beam_intrinsics( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const = 0; /** * Retrieves imu intrinsics of the sensor. @@ -144,7 +151,8 @@ class SensorHttp { * * @return imu_intrinsics received from sensor */ - virtual Json::Value imu_intrinsics(int timeout_sec = 1) const = 0; + virtual Json::Value imu_intrinsics( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const = 0; /** * Retrieves lidar intrinsics of the sensor. @@ -153,7 +161,8 @@ class SensorHttp { * * @return lidar_intrinsics retrieved from sensor */ - virtual Json::Value lidar_intrinsics(int timeout_sec = 1) const = 0; + virtual Json::Value lidar_intrinsics( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const = 0; /** * Retrieves lidar data format. @@ -162,7 +171,8 @@ class SensorHttp { * * @return lidar_data_format received from sensor */ - virtual Json::Value lidar_data_format(int timeout_sec = 1) const = 0; + virtual Json::Value lidar_data_format( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const = 0; /** * Gets the calibaration status of the sensor. @@ -171,21 +181,24 @@ class SensorHttp { * * @return calibration status received from sensor */ - virtual Json::Value calibration_status(int timeout_sec = 1) const = 0; + virtual Json::Value calibration_status( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const = 0; /** * Restarts the sensor applying all staged configurations. * * @param[in] timeout_sec The timeout for the request in seconds. */ - virtual void reinitialize(int timeout_sec = 1) const = 0; + virtual void reinitialize( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const = 0; /** * Persist active configuration parameters to the sensor. * * @param[in] timeout_sec The timeout for the request in seconds. */ - virtual void save_config_params(int timeout_sec = 1) const = 0; + virtual void save_config_params( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const = 0; /** * Gets the user data stored on the sensor. @@ -194,7 +207,8 @@ class SensorHttp { * * @return user data retrieved from sensor */ - virtual std::string get_user_data(int timeout_sec = 1) const = 0; + virtual std::string get_user_data( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const = 0; /** * Gets the user data stored on the sensor and the retention policy. @@ -204,7 +218,7 @@ class SensorHttp { * @return user data and policy setting retrieved from the sensor */ virtual UserDataAndPolicy get_user_data_and_policy( - int timeout_sec = 1) const = 0; + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const = 0; /** * Sets the user data stored on the sensor. @@ -214,16 +228,28 @@ class SensorHttp { configuration is deleted from the sensor * @param[in] timeout_sec The timeout for the request in seconds. */ - virtual void set_user_data(const std::string& data, - bool keep_on_config_delete = true, - int timeout_sec = 1) const = 0; + virtual void set_user_data( + const std::string& data, bool keep_on_config_delete = true, + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const = 0; + + /** + * Gets sensor IP address information. + * + * @param[in] timeout_sec The timeout to use in seconds for the version + * request, this argument is optional. + * + * @return a JSON string containing sensor IP address information. + */ + virtual std::string network( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const = 0; /** * Deletes the user data stored on the sensor. * * @param[in] timeout_sec The timeout for the request in seconds. */ - virtual void delete_user_data(int timeout_sec = 1) const = 0; + virtual void delete_user_data( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const = 0; /** * Retrieves sensor firmware version information as a string. @@ -236,7 +262,7 @@ class SensorHttp { */ static std::string firmware_version_string( const std::string& hostname, - int timeout_sec = DEFAULT_HTTP_REQUEST_TIMEOUT_SECONDS); + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS); /** * Retrieves sensor firmware version information. @@ -249,7 +275,7 @@ class SensorHttp { */ static ouster::util::version firmware_version( const std::string& hostname, - int timeout_sec = DEFAULT_HTTP_REQUEST_TIMEOUT_SECONDS); + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS); /** * Creates an instance of the SensorHttp interface. @@ -262,7 +288,7 @@ class SensorHttp { */ static std::unique_ptr create( const std::string& hostname, - int timeout_sec = DEFAULT_HTTP_REQUEST_TIMEOUT_SECONDS); + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS); }; } // namespace util diff --git a/ouster_client/src/client.cpp b/ouster_client/src/client.cpp index 113dc7ee..f21167ad 100644 --- a/ouster_client/src/client.cpp +++ b/ouster_client/src/client.cpp @@ -233,7 +233,7 @@ Json::Value collect_metadata(SensorHttp& sensor_http, int timeout_sec) { bool get_config(SensorHttp& sensor_http, sensor_config& config, bool active = true, - int timeout_sec = DEFAULT_HTTP_REQUEST_TIMEOUT_SECONDS) { + int timeout_sec = LONG_HTTP_REQUEST_TIMEOUT_SECONDS) { auto res = sensor_http.get_config_params(active, timeout_sec); config = parse_config(res); return true; diff --git a/ouster_client/src/curl_client.h b/ouster_client/src/curl_client.h index 7114548d..8f68ece8 100644 --- a/ouster_client/src/curl_client.h +++ b/ouster_client/src/curl_client.h @@ -100,6 +100,7 @@ class CurlClient : public ouster::util::HttpClient { } std::lock_guard guard(mutex_); curl_easy_setopt(curl_handle, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl_handle, CURLOPT_DEFAULT_PROTOCOL, "http"); curl_easy_setopt(curl_handle, CURLOPT_TIMEOUT, timeout_seconds); if (type == RequestType::TYPE_GET) { curl_easy_setopt(curl_handle, CURLOPT_CUSTOMREQUEST, 0); diff --git a/ouster_client/src/http_client.h b/ouster_client/src/http_client.h index df579e41..837fefe6 100644 --- a/ouster_client/src/http_client.h +++ b/ouster_client/src/http_client.h @@ -10,6 +10,7 @@ #pragma once +#include #include namespace ouster { @@ -25,7 +26,14 @@ class HttpClient { * * @param[in] base_url_ url to the http server. */ - HttpClient(const std::string& base_url_) : base_url(base_url_) {} + HttpClient(const std::string& base_url_) { + // Properly escape URLs that look like IPv6 addresses (2 or more :'s) + if (std::count(base_url_.begin(), base_url_.end(), ':') >= 2) { + base_url = "[" + base_url_ + "]"; + } else { + base_url = base_url_; + } + } virtual ~HttpClient() {} diff --git a/ouster_client/src/sensor_client.cpp b/ouster_client/src/sensor_client.cpp index 3c5a936d..2991586f 100644 --- a/ouster_client/src/sensor_client.cpp +++ b/ouster_client/src/sensor_client.cpp @@ -5,6 +5,7 @@ #include "ouster/sensor_client.h" +#include "ouster/defaults.h" #include "ouster/impl/logging.h" using ouster::sensor::impl::Logger; @@ -54,7 +55,7 @@ std::shared_ptr Sensor::http_client() const { // construct the client if we haven't already if (!http_client_) { http_client_ = ouster::sensor::util::SensorHttp::create( - hostname_, 1); // todo figure out timeout + hostname_, SHORT_HTTP_REQUEST_TIMEOUT_SECONDS); } return http_client_; } diff --git a/ouster_client/src/sensor_http.cpp b/ouster_client/src/sensor_http.cpp index fbe406c8..8d8358f2 100644 --- a/ouster_client/src/sensor_http.cpp +++ b/ouster_client/src/sensor_http.cpp @@ -15,7 +15,7 @@ using namespace ouster::sensor::impl; string SensorHttp::firmware_version_string(const string& hostname, int timeout_sec) { - auto http_client = std::make_unique("http://" + hostname); + auto http_client = std::make_unique(hostname); auto fwjson = http_client->get("api/v1/system/firmware", timeout_sec); Json::Value root{}; diff --git a/ouster_client/src/sensor_http_imp.cpp b/ouster_client/src/sensor_http_imp.cpp index 29e50126..36f4ce13 100644 --- a/ouster_client/src/sensor_http_imp.cpp +++ b/ouster_client/src/sensor_http_imp.cpp @@ -7,7 +7,7 @@ using std::string; using namespace ouster::sensor::impl; SensorHttpImp::SensorHttpImp(const string& hostname) - : http_client(std::make_unique("http://" + hostname)) {} + : http_client(std::make_unique(hostname)) {} SensorHttpImp::~SensorHttpImp() = default; @@ -67,6 +67,10 @@ Json::Value SensorHttpImp::calibration_status(int timeout_sec) const { return get_json("api/v1/sensor/metadata/calibration_status", timeout_sec); } +std::string SensorHttpImp::network(int timeout_sec) const { + return get("api/v1/system/network", timeout_sec); +} + // reinitialize to activate new settings void SensorHttpImp::reinitialize(int timeout_sec) const { execute("api/v1/sensor/cmd/reinitialize", "{}", timeout_sec); diff --git a/ouster_client/src/sensor_http_imp.h b/ouster_client/src/sensor_http_imp.h index 33c6d030..c755edd9 100644 --- a/ouster_client/src/sensor_http_imp.h +++ b/ouster_client/src/sensor_http_imp.h @@ -38,14 +38,16 @@ class SensorHttpImp : public util::SensorHttp { * * @return returns a Json object of the sensor metadata. */ - Json::Value metadata(int timeout_sec = 1) const override; + Json::Value metadata( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const override; /** * Queries the sensor_info. * * @return returns a Json object representing the sensor_info. */ - Json::Value sensor_info(int timeout_sec = 1) const override; + Json::Value sensor_info( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const override; /** * Queries active/staged configuration on the sensor @@ -55,8 +57,9 @@ class SensorHttpImp : public util::SensorHttp { * * @return a string represnting the active or staged config */ - std::string get_config_params(bool active, - int timeout_sec = 1) const override; + std::string get_config_params( + bool active, + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const override; /** * Set the value of a specfic configuration on the sensor, the changed @@ -66,81 +69,105 @@ class SensorHttpImp : public util::SensorHttp { * @param[in] value the new value to set for the selected configuration. * @param[in] timeout_sec The timeout for the request in seconds. */ - void set_config_param(const std::string& key, const std::string& value, - int timeout_sec = 1) const override; + void set_config_param( + const std::string& key, const std::string& value, + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const override; /** * Retrieves the active configuration on the sensor */ - Json::Value active_config_params(int timeout_sec = 1) const override; + Json::Value active_config_params( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const override; /** * Retrieves the staged configuration on the sensor */ - Json::Value staged_config_params(int timeout_sec = 1) const override; + Json::Value staged_config_params( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const override; /** * Enables automatic assignment of udp destination ports. */ - void set_udp_dest_auto(int timeout_sec = 1) const override; + void set_udp_dest_auto( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const override; /** * Retrieves beam intrinsics of the sensor. */ - Json::Value beam_intrinsics(int timeout_sec = 1) const override; + Json::Value beam_intrinsics( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const override; /** * Retrieves imu intrinsics of the sensor. */ - Json::Value imu_intrinsics(int timeout_sec = 1) const override; + Json::Value imu_intrinsics( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const override; /** * Retrieves lidar intrinsics of the sensor. */ - Json::Value lidar_intrinsics(int timeout_sec = 1) const override; + Json::Value lidar_intrinsics( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const override; /** * Retrieves lidar data format. */ - Json::Value lidar_data_format(int timeout_sec = 1) const override; + Json::Value lidar_data_format( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const override; /** * Gets the calibaration status of the sensor. */ - Json::Value calibration_status(int timeout_sec = 1) const override; + Json::Value calibration_status( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const override; /** * Restarts the sensor applying all staged configurations. */ - void reinitialize(int timeout_sec = 1) const override; + void reinitialize( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const override; /** * Persist active configuration parameters to the sensor. */ - void save_config_params(int timeout_sec = 1) const override; + void save_config_params( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const override; /** * Gets the user data stored on the sensor. */ - std::string get_user_data(int timeout_sec = 1) const override; + std::string get_user_data( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const override; /** * Gets the user data stored on the sensor and the retention policy. */ util::UserDataAndPolicy get_user_data_and_policy( - int timeout_sec = 1) const override; + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const override; /** * Sets the user data stored on the sensor. */ - void set_user_data(const std::string& data, - bool keep_on_config_delete = true, - int timeout_sec = 1) const override; + void set_user_data( + const std::string& data, bool keep_on_config_delete = true, + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const override; /** * Deletes the user data stored on the sensor. */ - void delete_user_data(int timeout_sec = 1) const override; + void delete_user_data( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const override; + + /** + * Gets sensor IP address information. + * + * @param[in] timeout_sec The timeout to use in seconds for the version + * request, this argument is optional. + * + * @return a JSON string containing sensor IP address information. + */ + std::string network( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const override; protected: std::string get(const std::string& url, int timeout_sec) const; @@ -161,25 +188,27 @@ class SensorHttpImp_2_4_or_3 : public SensorHttpImp { /** * Gets the user data stored on the sensor. */ - std::string get_user_data(int timeout_sec = 1) const override; + std::string get_user_data( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const override; /** * Gets the user data stored on the sensor and the retention policy. */ util::UserDataAndPolicy get_user_data_and_policy( - int timeout_sec = 1) const override; + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const override; /** * Sets the user data stored on the sensor. */ - void set_user_data(const std::string& data, - bool keep_on_config_delete = true, - int timeout_sec = 1) const override; + void set_user_data( + const std::string& data, bool keep_on_config_delete = true, + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const override; /** * Deletes the user data stored on the sensor. */ - void delete_user_data(int timeout_sec = 1) const override; + void delete_user_data( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const override; }; // TODO: remove when firmware 2.2 has been fully phased out @@ -187,7 +216,8 @@ class SensorHttpImp_2_2 : public SensorHttpImp_2_4_or_3 { public: SensorHttpImp_2_2(const std::string& hostname); - void set_udp_dest_auto(int timeout_sec = 1) const override; + void set_udp_dest_auto( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const override; }; /** @@ -207,39 +237,46 @@ class SensorHttpImp_2_1 : public SensorHttpImp_2_2 { * * @return returns a Json object of the sensor metadata. */ - Json::Value metadata(int timeout_sec = 1) const override; + Json::Value metadata( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const override; /** * Queries the sensor_info. * * @return returns a Json object representing the sensor_info. */ - Json::Value sensor_info(int timeout_sec = 1) const override; + Json::Value sensor_info( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const override; /** * Retrieves beam intrinsics of the sensor. */ - Json::Value beam_intrinsics(int timeout_sec = 1) const override; + Json::Value beam_intrinsics( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const override; /** * Retrieves imu intrinsics of the sensor. */ - Json::Value imu_intrinsics(int timeout_sec = 1) const override; + Json::Value imu_intrinsics( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const override; /** * Retrieves lidar intrinsics of the sensor. */ - Json::Value lidar_intrinsics(int timeout_sec = 1) const override; + Json::Value lidar_intrinsics( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const override; /** * Retrieves lidar data format. */ - Json::Value lidar_data_format(int timeout_sec = 1) const override; + Json::Value lidar_data_format( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const override; /** * Gets the calibaration status of the sensor. */ - Json::Value calibration_status(int timeout_sec = 1) const override; + Json::Value calibration_status( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const override; }; } // namespace impl diff --git a/ouster_client/src/sensor_tcp_imp.cpp b/ouster_client/src/sensor_tcp_imp.cpp index 8825a8fd..b1c20866 100644 --- a/ouster_client/src/sensor_tcp_imp.cpp +++ b/ouster_client/src/sensor_tcp_imp.cpp @@ -125,6 +125,11 @@ void SensorTcpImp::delete_user_data(int /*timeout_sec*/) const { throw std::runtime_error("user data API not supported on this FW version"); } +std::string SensorTcpImp::network(int /*timeout_sec*/) const { + throw std::runtime_error( + "This endpoint is not supported on this FW version"); +} + SOCKET SensorTcpImp::cfg_socket(const char* addr) { struct addrinfo hints, *info_start, *ai; diff --git a/ouster_client/src/sensor_tcp_imp.h b/ouster_client/src/sensor_tcp_imp.h index 8e6ae7bb..725585f2 100644 --- a/ouster_client/src/sensor_tcp_imp.h +++ b/ouster_client/src/sensor_tcp_imp.h @@ -181,6 +181,17 @@ class SensorTcpImp : public util::SensorHttp { */ void delete_user_data(int timeout_sec = 1) const override; + /** + * Gets sensor IP address information. + * + * @param[in] timeout_sec The timeout to use in seconds for the version + * request, this argument is optional. + * + * @return a JSON string containing sensor IP address information. + */ + std::string network( + int timeout_sec = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) const override; + private: SOCKET cfg_socket(const char* addr); diff --git a/ouster_viz/include/ouster/point_viz.h b/ouster_viz/include/ouster/point_viz.h index e9dcd9e6..9df14cce 100644 --- a/ouster_viz/include/ouster/point_viz.h +++ b/ouster_viz/include/ouster/point_viz.h @@ -140,9 +140,17 @@ class PointViz { * @param[in] window_height Window height to set, * else uses the default_window_height */ - PointViz(const std::string& name, bool fix_aspect = false, - int window_width = default_window_width, - int window_height = default_window_height); + explicit PointViz(const std::string& name, bool fix_aspect = false, + int window_width = default_window_width, + int window_height = default_window_height); + + // Because PointViz uses the PIMPL pattern + // and the Impl owns the window context, + // we can't realistically copy or move instances of PointViz. + PointViz(const PointViz&) = delete; + PointViz(PointViz&&) = delete; + PointViz& operator=(PointViz&) = delete; + PointViz& operator=(PointViz&&) = delete; /** * Tears down the rendering context and closes the viz window @@ -187,28 +195,25 @@ class PointViz { /** * Update visualization state * - * Send state updates to be rendered on the next frame - * - * @return whether state was successfully sent. If not, will be sent on next - * call to update(). This can happen if update() is called more - * frequently than the frame rate. + * Send state updates to be rendered on the next frame. */ - bool update(); + void update(); /** * Add a callback for handling keyboard input * - * @param[in] f the callback. The second argument is the ascii value of the - * key pressed. Third argument is a bitmask of the modifier keys - * The callback's return value determines whether - * the remaining key callbacks should be called. + * @param[in] callback the callback. The second argument is the ascii value + * of the key pressed. Third argument is a bitmask of the modifier keys The + * callback's return value determines whether the remaining key callbacks + * should be called. */ - void push_key_handler(std::function&& f); + void push_key_handler( + std::function&& callback); /** * Add a callback for handling mouse button input * - * @param[in] f the callback. The callback's arguments are + * @param[in] callback the callback. The callback's arguments are * ctx: the context containing information about the buttons * pressed, the mouse position, and the viewport; * button: the mouse button pressed; @@ -221,12 +226,12 @@ class PointViz { std::function&& f); + ouster::viz::EventModifierKeys mods)>&& callback); /** * Add a callback for handling mouse scrolling input * - * @param[in] f the callback. The callback's arguments are + * @param[in] callback the callback. The callback's arguments are * ctx: the context containing information about the buttons * pressed, the mouse position, and the viewport; * x: the amount of scrolling in the x direction; @@ -235,12 +240,12 @@ class PointViz { * the remaining mouse scroll callbacks should be called. */ void push_scroll_handler( - std::function&& f); + std::function&& callback); /** * Add a callback for handling mouse movement * - * @param[in] f the callback. The callback's arguments are + * @param[in] callback the callback. The callback's arguments are * ctx: the context containing information about the buttons * pressed, the mouse position, and the viewport; * x: the mouse position in the x direction; @@ -249,7 +254,7 @@ class PointViz { * the remaining mouse position callbacks should be called. */ void push_mouse_pos_handler( - std::function&& f); + std::function&& callback); /** * Add a callback for processing every new draw frame buffer. @@ -258,12 +263,12 @@ class PointViz { * dramatically. Primary use to store frame buffer images to disk * for further processing. * - * @param[in] f function callback of a form f(fb_data, fb_width, fb_height) - * The callback's return value determines whether - * the remaining frame buffer callbacks should be called. + * @param[in] callback function callback of a form f(fb_data, fb_width, + * fb_height) The callback's return value determines whether the remaining + * frame buffer callbacks should be called. */ void push_frame_buffer_handler( - std::function&, int, int)>&& f); + std::function&, int, int)>&& callback); /** * Remove the last added callback for handling keyboard events @@ -292,12 +297,12 @@ class PointViz { /** * Add a callback for handling frame buffer resize events. - * @param[in] f function callback of the form f(const WindowCtx&). The - * callback's return value determines whether the remaining frame buffer + * @param[in] callback function callback of the form f(const WindowCtx&). + * The callback's return value determines whether the remaining frame buffer * resize callbacks should be called. */ void push_frame_buffer_resize_handler( - std::function&& f); + std::function&& callback); /** * Remove the last added callback for handling frame buffer resize events. @@ -488,6 +493,12 @@ struct WindowCtx { */ std::pair window_coordinates(double normalized_x, double normalized_y) const; + + /** + * @brief raises std::logic_error if this WindowCtx doesn't satisfy class + * invariants. + */ + void check_invariants() const; }; /** @@ -728,15 +739,17 @@ class Cloud { size_t w_{0}; mat4d extrinsic_{}; - bool range_changed_{false}; - bool key_changed_{false}; - bool mask_changed_{false}; - bool xyz_changed_{false}; - bool offset_changed_{false}; - bool transform_changed_{false}; - bool palette_changed_{false}; - bool pose_changed_{false}; - bool point_size_changed_{false}; + // set everything to changed so on GLCloud object reuse we properly draw + // everything first time + bool range_changed_{true}; + bool key_changed_{true}; + bool mask_changed_{true}; + bool xyz_changed_{true}; + bool offset_changed_{true}; + bool transform_changed_{true}; + bool palette_changed_{true}; + bool pose_changed_{true}; + bool point_size_changed_{true}; std::shared_ptr> range_data_{}; std::shared_ptr> key_data_{}; @@ -761,7 +774,7 @@ class Cloud { * @param[in] extrinsic sensor extrinsic calibration. 4x4 column-major * homogeneous transformation matrix */ - Cloud(size_t n, const mat4d& extrinsic = identity4d); + explicit Cloud(size_t n, const mat4d& extrinsic = identity4d); /** * Structured point cloud for visualization. @@ -778,6 +791,14 @@ class Cloud { Cloud(size_t w, size_t h, const float* dir, const float* off, const mat4d& extrinsic = identity4d); + /** + * Updates this cloud's state with the state of other, + * accounting for prior changes to this objects's state. + * + * @param[in] other the object to update the state from. + */ + void update_from(const Cloud& other); + /** * Clear dirty flags. * @@ -940,6 +961,14 @@ class Image { */ Image(); + /** + * Updates this image's state with the state of other, + * accounting for prior changes to this objects's state. + * + * @param[in] other the object to update the state from. + */ + void update_from(const Image& other); + /** * Clear dirty flags. * @@ -1098,6 +1127,14 @@ class Cuboid { */ Cuboid(const mat4d& transform, const vec4f& rgba); + /** + * Updates this cuboid's state with the state of other, + * accounting for prior changes to this objects's state. + * + * @param[in] other the object to update the state from. + */ + void update_from(const Cuboid& other); + /** * Clear dirty flags. * @@ -1163,6 +1200,14 @@ class Label { Label(const std::string& text, float x, float y, bool align_right = false, bool align_top = false); + /** + * Updates this label's state with the state of other, + * accounting for prior changes to this objects's state. + * + * @param[in] other the object to update the state from. + */ + void update_from(const Label& other); + /** * Clear dirty flags. * diff --git a/ouster_viz/src/point_viz.cpp b/ouster_viz/src/point_viz.cpp index ecd4f3c1..dc462695 100644 --- a/ouster_viz/src/point_viz.cpp +++ b/ouster_viz/src/point_viz.cpp @@ -63,9 +63,9 @@ class Indexed { bool remove(const std::shared_ptr& t) { auto res = std::find(back.begin(), back.end(), t); - if (res == back.end()) + if (res == back.end()) { return false; - else { + } else { res->reset(); return true; } @@ -73,9 +73,12 @@ class Indexed { void draw(const WindowCtx& ctx, const impl::CameraData& camera) { for (auto& f : front) { - if (!f.state) continue; // skip deleted - if (!f.gl) + if (!f.state) { + continue; // skip deleted + } + if (!f.gl) { f.gl = std::make_unique(*f.state); // init GL for added + } f.gl->draw(ctx, camera, *f.state); } } @@ -89,7 +92,7 @@ class Indexed { // send updated, added or deleted state to the front for (size_t i = 0; i < front.size(); i++) { if (back[i] && front[i].state) { - *front[i].state = *back[i]; + front[i].state->update_from(*back[i]); back[i]->clear(); } else if (back[i] && !front[i].state) { front[i].state = std::make_unique(*back[i]); @@ -185,32 +188,41 @@ PointViz::PointViz(const std::string& name, bool fix_aspect, int window_width, // add user-setable input handlers pimpl->glfw->key_handler = [this](const WindowCtx& ctx, int key, int mods) { for (auto& f : pimpl->key_handlers) - if (!f(ctx, key, mods)) break; + if (!f(ctx, key, mods)) { + break; + } }; pimpl->glfw->mouse_button_handler = [this](const WindowCtx& ctx, int button, int action, int mods) { for (auto& f : pimpl->mouse_button_handlers) if (!f(ctx, ouster::viz::MouseButton(button), ouster::viz::MouseButtonEvent(action), - ouster::viz::EventModifierKeys(mods))) + ouster::viz::EventModifierKeys(mods))) { break; + } }; pimpl->glfw->scroll_handler = [this](const WindowCtx& ctx, double x, double y) { for (auto& f : pimpl->scroll_handlers) - if (!f(ctx, x, y)) break; + if (!f(ctx, x, y)) { + break; + } }; pimpl->glfw->mouse_pos_handler = [this](const WindowCtx& ctx, double x, double y) { for (auto& f : pimpl->mouse_pos_handlers) - if (!f(ctx, x, y)) break; + if (!f(ctx, x, y)) { + break; + } }; // glfwPollEvents blocks during resize on macos. Keep rendering to avoid // artifacts during resize pimpl->glfw->resize_handler = [this](const WindowCtx& ctx) { for (auto& f : pimpl->window_resize_handlers) { - if (!f(ctx)) break; + if (!f(ctx)) { + break; + } } #ifdef __APPLE__ draw(); @@ -240,11 +252,17 @@ void PointViz::running(bool state) { pimpl->glfw->running(state); } void PointViz::visible(bool state) { pimpl->glfw->visible(state); } -bool PointViz::update() { +void PointViz::update() { std::lock_guard guard{pimpl->update_mx}; - // last frame hasn't been drawn yet - if (pimpl->front_changed) return false; + // TWS 20241014: note we used to return here if + // the last frame hasn't been drawn yet. + // However, this can cause problem if the API user calls + // update(), especially if more than once, before + // calling run(). + // We're preserving front_changed in case we want + // to expose this value via the API later on. + // if (pimpl->front_changed) return false; // propagate camera changes pimpl->camera_front = pimpl->camera_back; @@ -256,8 +274,6 @@ bool PointViz::update() { pimpl->rings.update(pimpl->target); pimpl->front_changed = true; - - return true; } int PointViz::viewport_width() const { @@ -353,31 +369,31 @@ double PointViz::fps() const { return pimpl->fps_; } * Input handling */ void PointViz::push_key_handler( - std::function&& f) { + std::function&& callback) { // TODO: not thread safe: called in glfwPollEvents() - pimpl->key_handlers.push_front(std::move(f)); + pimpl->key_handlers.push_front(std::move(callback)); } void PointViz::push_mouse_button_handler( std::function&& f) { - pimpl->mouse_button_handlers.push_front(std::move(f)); + ouster::viz::EventModifierKeys)>&& callback) { + pimpl->mouse_button_handlers.push_front(std::move(callback)); } void PointViz::push_scroll_handler( - std::function&& f) { - pimpl->scroll_handlers.push_front(std::move(f)); + std::function&& callback) { + pimpl->scroll_handlers.push_front(std::move(callback)); } void PointViz::push_mouse_pos_handler( - std::function&& f) { - pimpl->mouse_pos_handlers.push_front(std::move(f)); + std::function&& callback) { + pimpl->mouse_pos_handlers.push_front(std::move(callback)); } void PointViz::push_frame_buffer_handler( - std::function&, int, int)>&& f) { - pimpl->frame_buffer_handlers.push_front(std::move(f)); + std::function&, int, int)>&& callback) { + pimpl->frame_buffer_handlers.push_front(std::move(callback)); } void PointViz::pop_key_handler() { pimpl->key_handlers.pop_front(); } @@ -396,8 +412,8 @@ void PointViz::pop_frame_buffer_handler() { } void PointViz::push_frame_buffer_resize_handler( - std::function&& f) { - pimpl->window_resize_handlers.push_front(std::move(f)); + std::function&& callback) { + pimpl->window_resize_handlers.push_front(std::move(callback)); } void PointViz::pop_frame_buffer_resize_handler() { @@ -459,16 +475,6 @@ Cloud::Cloud(size_t w, size_t h, const mat4d& extrinsic) transform_data_{std::make_shared>(12 * w, 0)}, palette_data_{std::make_shared>( &spezia_palette[0][0], &spezia_palette[0][0] + spezia_n * 3)} { - // set everything to changed so on GLCloud object reuse we properly draw - // everything first time - range_changed_ = true; - key_changed_ = true; - mask_changed_ = true; - xyz_changed_ = true; - offset_changed_ = true; - point_size_changed_ = true; - palette_changed_ = true; - // initialize per-column poses to identity for (size_t v = 0; v < w; v++) { (*transform_data_)[3 * v] = 1; @@ -504,6 +510,30 @@ Cloud::Cloud(size_t w, size_t h, const float* dir, const float* off, set_offset(off); } +void Cloud::update_from(const Cloud& other) { + // TODO[tws] This is ugly. + // VAOs should have some encapsulation that allows better management. + bool range_changed = other.range_changed_ || range_changed_; + bool key_changed = other.key_changed_ || key_changed_; + bool mask_changed = other.mask_changed_ || mask_changed_; + bool pose_changed = other.pose_changed_ || pose_changed_; + bool xyz_changed = other.xyz_changed_ || xyz_changed_; + bool offset_changed = other.offset_changed_ || offset_changed_; + bool transform_changed = other.transform_changed_ || transform_changed_; + bool palette_changed = other.palette_changed_ || palette_changed_; + bool point_size_changed = other.point_size_changed_ || point_size_changed_; + *this = other; + this->range_changed_ = range_changed; + this->key_changed_ = key_changed; + this->mask_changed_ = mask_changed; + this->pose_changed_ = pose_changed; + this->xyz_changed_ = xyz_changed; + this->offset_changed_ = offset_changed; + this->transform_changed_ = transform_changed; + this->palette_changed_ = palette_changed; + this->point_size_changed_ = point_size_changed; +} + void Cloud::clear() { range_changed_ = false; key_changed_ = false; @@ -656,6 +686,18 @@ void Cloud::set_palette(const float* palette, size_t palette_size) { Image::Image() = default; +void Image::update_from(const Image& other) { + bool position_changed = other.position_changed_ || position_changed_; + bool image_changed = other.image_changed_ || image_changed_; + bool mask_changed = other.mask_changed_ || mask_changed_; + bool palette_changed = other.palette_changed_ || palette_changed_; + *this = other; + this->position_changed_ = position_changed; + this->image_changed_ = image_changed; + this->mask_changed_ = mask_changed; + this->palette_changed_ = palette_changed; +} + void Image::clear() { position_changed_ = false; image_changed_ = false; @@ -664,6 +706,12 @@ void Image::clear() { } void Image::set_image(size_t width, size_t height, const float* image_data) { + if (width < 1 || height < 1) { + throw std::invalid_argument("invalid image size"); + } + if (!image_data) { + throw std::invalid_argument("null image data"); + } const size_t n = width * height; image_data_.resize(4 * n); image_width_ = width; @@ -681,6 +729,12 @@ void Image::set_image(size_t width, size_t height, const float* image_data) { void Image::set_image_rgb(size_t width, size_t height, const float* image_data_rgb) { + if (width < 1 || height < 1) { + throw std::invalid_argument("invalid image size"); + } + if (!image_data_rgb) { + throw std::invalid_argument("null image data"); + } const size_t n = width * height; image_data_.resize(4 * n); image_width_ = width; @@ -699,6 +753,12 @@ void Image::set_image_rgb(size_t width, size_t height, void Image::set_image_rgba(size_t width, size_t height, const float* image_data_rgba) { + if (width < 1 || height < 1) { + throw std::invalid_argument("invalid image size"); + } + if (!image_data_rgba) { + throw std::invalid_argument("null image data"); + } const size_t n = width * height; image_data_.resize(4 * n); image_width_ = width; @@ -716,6 +776,12 @@ void Image::set_image_rgba(size_t width, size_t height, } void Image::set_mask(size_t width, size_t height, const float* mask_data) { + if (width < 1 || height < 1) { + throw std::invalid_argument("invalid mask size"); + } + if (!mask_data) { + throw std::invalid_argument("null mask data"); + } size_t n = width * height * 4; mask_data_.resize(n); mask_width_ = width; @@ -735,6 +801,9 @@ void Image::set_hshift(float hshift) { } void Image::set_palette(const float* palette, size_t palette_size) { + if (!palette) { + throw std::invalid_argument("null palette"); + } palette_data_.resize(palette_size * 3); std::copy(palette, palette + (palette_size * 3), palette_data_.begin()); palette_changed_ = true; @@ -747,34 +816,39 @@ void Image::clear_palette() { use_palette_ = false; } -double WindowCtx::aspect_ratio() const { - // prevent potential for divide-by-zero, - // though GLFW prevents sizing the window with zero width or height - constexpr int min_height = 1; - int height = viewport_height; - if (viewport_height < min_height) { - height = min_height; +void WindowCtx::check_invariants() const { + if (window_width < 1 || window_height < 1) { + throw std::logic_error("invalid window size"); } + if (viewport_width < 1 || viewport_height < 1) { + throw std::logic_error("invalid viewport size"); + } +} - return static_cast(viewport_width) / height; +double WindowCtx::aspect_ratio() const { + check_invariants(); + return static_cast(window_width) / window_height; } std::pair WindowCtx::normalized_coordinates(double x, double y) const { - double world_x = 2.0 / viewport_height * x - aspect_ratio(); - double world_y = 2.0 * (1.0 - (y / viewport_height)) - 1.0; + check_invariants(); + double world_x = (2.0 / window_width * x - 1.0) * aspect_ratio(); + double world_y = 2.0 * (1.0 - (y / window_height)) - 1.0; return std::pair(world_x, world_y); } std::pair WindowCtx::window_coordinates( double normalized_x, double normalized_y) const { - double window_x = (normalized_x + aspect_ratio()) * viewport_height / 2.0; - double window_y = viewport_height * (1 - normalized_y) / 2.0; + check_invariants(); + double window_x = (normalized_x + aspect_ratio()) * window_height / 2.0; + double window_y = window_height * (1 - normalized_y) / 2.0; return std::pair(window_x, window_y); } nonstd::optional> Image::window_coordinates_to_image_pixel( const WindowCtx& ctx, double x, double y) const { + ctx.check_invariants(); auto world = ctx.normalized_coordinates(x, y); // compute image pixel coordinates, accounting for hshift @@ -797,6 +871,7 @@ nonstd::optional> Image::window_coordinates_to_image_pixel( std::pair Image::image_pixel_to_window_coordinates( const WindowCtx& ctx, int px, int py) const { + ctx.check_invariants(); double img_rel_x = static_cast(px) / image_width_; double img_rel_y = static_cast(py) / image_height_; @@ -814,6 +889,7 @@ std::pair Image::image_pixel_to_window_coordinates( } std::pair Image::pixel_size(const WindowCtx& ctx) const { + ctx.check_invariants(); auto lower_left = ctx.window_coordinates(position_[0], position_[3]); auto upper_right = ctx.window_coordinates(position_[1], position_[2]); return std::pair( @@ -826,6 +902,14 @@ Cuboid::Cuboid(const mat4d& pose, const std::array& rgba) { set_rgba(rgba); } +void Cuboid::update_from(const Cuboid& other) { + bool transform_changed = other.transform_changed_ || transform_changed_; + bool rgba_changed = other.rgba_changed_ || rgba_changed_; + *this = other; + this->transform_changed_ = transform_changed; + this->rgba_changed_ = rgba_changed; +} + void Cuboid::clear() { transform_changed_ = false; rgba_changed_ = false; @@ -852,6 +936,18 @@ Label::Label(const std::string& text, float x, float y, bool align_right, set_position(x, y, align_right, align_top); } +void Label::update_from(const Label& other) { + bool pos_changed = other.pos_changed_ || pos_changed_; + bool scale_changed = other.scale_changed_ || scale_changed_; + bool text_changed = other.text_changed_ || text_changed_; + bool rgba_changed = other.rgba_changed_ || rgba_changed_; + *this = other; + this->pos_changed_ = pos_changed; + this->scale_changed_ = scale_changed; + this->text_changed_ = text_changed; + this->rgba_changed_ = rgba_changed; +} + void Label::clear() { text_changed_ = false; pos_changed_ = false; diff --git a/python/MANIFEST.in b/python/MANIFEST.in index a0583b6e..bfeff614 100644 --- a/python/MANIFEST.in +++ b/python/MANIFEST.in @@ -8,6 +8,6 @@ prune sdk/python prune sdk/.git prune sdk/artifacts prune sdk/sdk-extensions -prune sdk/Jenkinsfile -prune sdk/clang-linting.sh -prune sdk/build \ No newline at end of file +prune sdk/build +recursive-exclude sdk Jenkinsfile +recursive-exclude sdk clang-linting.sh \ No newline at end of file diff --git a/python/src/cpp/_client.cpp b/python/src/cpp/_client.cpp index 8041e1a1..8b76423f 100644 --- a/python/src/cpp/_client.cpp +++ b/python/src/cpp/_client.cpp @@ -409,18 +409,13 @@ struct set_field { * - W: Number of pose matrices * - 4x4: The transformation matrices * - * @param[in] input_row_major If the param points is stored in row major, then - * it's true. Otherwise it's false. - * * @return A NumPy array of shape (H, W, 3) containing the dewarped 3D points * after applying the corresponding 4x4 transformation matrices to the points. * */ -// TODO Hao remove the input_row_major parameter py::array_t dewarp(const py::array_t& points, - const py::array_t& poses, - bool input_row_major = true) { + const py::array_t& poses) { auto poses_buf = poses.request(); auto points_buf = points.request(); @@ -455,7 +450,7 @@ py::array_t dewarp(const py::array_t& points, Eigen::Map dewarped_points( static_cast(result_buf.ptr), num_rows * num_poses, point_dim); - if (input_row_major) { + if (points.flags() & py::array_t::c_style) { Eigen::Map> points_mat(static_cast(points_buf.ptr), num_rows * num_poses, point_dim); @@ -1249,7 +1244,8 @@ void init_client(py::module& m, py::module&) { py::arg("timestamp_mode") = sensor::timestamp_mode::TIME_FROM_INTERNAL_OSC, py::arg("lidar_port") = 0, py::arg("imu_port") = 0, - py::arg("timeout_sec") = 10, py::arg("persist_config") = false) + py::arg("timeout_sec") = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS, + py::arg("persist_config") = false) .def( "poll", [](const client_shared_ptr& self, @@ -1278,7 +1274,7 @@ void init_client(py::module& m, py::module&) { [](client_shared_ptr& self, int timeout_sec) -> std::string { return sensor::get_metadata(*self, timeout_sec); }, - py::arg("timeout_sec") = DEFAULT_HTTP_REQUEST_TIMEOUT_SECONDS) + py::arg("timeout_sec") = LONG_HTTP_REQUEST_TIMEOUT_SECONDS) .def("shutdown", [](client_shared_ptr& self) { self.reset(); }); // New Client @@ -1303,7 +1299,7 @@ void init_client(py::module& m, py::module&) { [](sensor::Sensor& self, int timeout) { return self.fetch_metadata(timeout); }, - py::arg("timeout") = 40) + py::arg("timeout") = LONG_HTTP_REQUEST_TIMEOUT_SECONDS) .def("http_client", &sensor::Sensor::http_client) .def("desired_config", &sensor::Sensor::desired_config) .def("hostname", &sensor::Sensor::hostname); @@ -1315,7 +1311,8 @@ void init_client(py::module& m, py::module&) { return new sensor::SensorClient(sensors, config_timeout, buffer_time); }), - py::arg("sensors"), py::arg("config_timeout") = 45, + py::arg("sensors"), + py::arg("config_timeout") = LONG_HTTP_REQUEST_TIMEOUT_SECONDS, py::arg("buffer_time") = 0) .def(py::init([](std::vector sensors, std::vector metadata, @@ -1325,7 +1322,8 @@ void init_client(py::module& m, py::module&) { config_timeout, buffer_size); }), py::arg("sensors"), py::arg("metadata"), - py::arg("config_timeout") = 1, py::arg("buffer_time") = 0) + py::arg("config_timeout") = LONG_HTTP_REQUEST_TIMEOUT_SECONDS, + py::arg("buffer_time") = 0) .def("get_sensor_info", [](sensor::SensorClient& self) { return self.get_sensor_info(); }) .def("flush", &sensor::SensorClient::flush) @@ -1349,7 +1347,8 @@ void init_client(py::module& m, py::module&) { return new sensor::SensorScanSource(sensors, timeout, queue_size, soft_id_check); }), - py::arg("sensors"), py::arg("config_timeout") = 45, + py::arg("sensors"), + py::arg("config_timeout") = LONG_HTTP_REQUEST_TIMEOUT_SECONDS, py::arg("queue_size") = 2, py::arg("soft_id_check") = false) .def(py::init([](std::vector sensors, std::vector metadata, @@ -1359,8 +1358,8 @@ void init_client(py::module& m, py::module&) { queue_size, soft_id_check); }), py::arg("sensors"), py::arg("metadata"), - py::arg("config_timeout") = 45, py::arg("queue_size") = 2, - py::arg("soft_id_check") = false) + py::arg("config_timeout") = LONG_HTTP_REQUEST_TIMEOUT_SECONDS, + py::arg("queue_size") = 2, py::arg("soft_id_check") = false) .def(py::init([](std::vector sensors, std::vector metadata, const std::vector>& field_types, @@ -1371,8 +1370,8 @@ void init_client(py::module& m, py::module&) { queue_size, soft_id_check); }), py::arg("sensors"), py::arg("metadata"), py::arg("fields"), - py::arg("config_timeout") = 45, py::arg("queue_size") = 2, - py::arg("soft_id_check") = false) + py::arg("config_timeout") = LONG_HTTP_REQUEST_TIMEOUT_SECONDS, + py::arg("queue_size") = 2, py::arg("soft_id_check") = false) .def("get_sensor_info", [](sensor::SensorScanSource& self) { return self.get_sensor_info(); @@ -1883,39 +1882,44 @@ void init_client(py::module& m, py::module&) { m.def("destagger_double", &ouster::destagger); py::class_(m, "SensorHttp") - .def("metadata", &SensorHttp::metadata, py::arg("timeout_sec") = 1) + .def("metadata", &SensorHttp::metadata, + py::arg("timeout_sec") = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) .def("sensor_info", &SensorHttp::sensor_info, - py::arg("timeout_sec") = 1) + py::arg("timeout_sec") = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) .def("get_config_params", &SensorHttp::get_config_params, - py::arg("active"), py::arg("timeout_sec") = 1) + py::arg("active"), + py::arg("timeout_sec") = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) .def("set_config_param", &SensorHttp::set_config_param, py::arg("key"), - py::arg("value"), py::arg("timeout_sec") = 1) + py::arg("value"), + py::arg("timeout_sec") = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) .def("active_config_params", &SensorHttp::active_config_params, - py::arg("timeout_sec") = 1) + py::arg("timeout_sec") = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) .def("staged_config_params", &SensorHttp::staged_config_params, - py::arg("timeout_sec") = 1) + py::arg("timeout_sec") = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) .def("set_udp_dest_auto", &SensorHttp::set_udp_dest_auto, - py::arg("timeout_sec") = 1) + py::arg("timeout_sec") = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) .def("beam_intrinsics", &SensorHttp::beam_intrinsics, - py::arg("timeout_sec") = 1) + py::arg("timeout_sec") = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) .def("imu_intrinsics", &SensorHttp::imu_intrinsics, - py::arg("timeout_sec") = 1) + py::arg("timeout_sec") = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) .def("lidar_intrinsics", &SensorHttp::lidar_intrinsics, - py::arg("timeout_sec") = 1) + py::arg("timeout_sec") = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) .def("lidar_data_format", &SensorHttp::lidar_data_format, - py::arg("timeout_sec") = 1) + py::arg("timeout_sec") = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) .def("reinitialize", &SensorHttp::reinitialize, - py::arg("timeout_sec") = 1) + py::arg("timeout_sec") = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) .def("save_config_params", &SensorHttp::save_config_params, - py::arg("timeout_sec") = 1) + py::arg("timeout_sec") = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) .def("get_user_data", &SensorHttp::get_user_data, - py::arg("timeout_sec") = 1) + py::arg("timeout_sec") = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) // TODO: get_user_data_and_policy is hard to bind, bind later if needed .def("set_user_data", &SensorHttp::set_user_data, py::arg("data"), py::arg("keep_on_config_delete") = true, - py::arg("timeout_sec") = 1) + py::arg("timeout_sec") = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) .def("delete_user_data", &SensorHttp::delete_user_data, - py::arg("timeout_sec") = 1) + py::arg("timeout_sec") = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) + .def("network", &SensorHttp::network, + py::arg("timeout_sec") = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) .def("firmware_version", [](SensorHttp& self) { return self.firmware_version(); }) .def("hostname", &SensorHttp::hostname) @@ -1924,7 +1928,8 @@ void init_client(py::module& m, py::module&) { [](const std::string& hostname, int timeout_sec) { return SensorHttp::create(hostname, timeout_sec); }, - py::arg("hostname"), py::arg("timeout_sec") = 40); + py::arg("hostname"), + py::arg("timeout_sec") = LONG_HTTP_REQUEST_TIMEOUT_SECONDS); py::class_(m, "ScanBatcher") .def(py::init()) @@ -2158,15 +2163,37 @@ void init_client(py::module& m, py::module&) { m.def("dewarp", py::overload_cast&, - const py::array_t&, bool>(&dewarp), - "Dewarp points with given poses", py::arg("points"), py::arg("poses"), - py::arg("input_row_major") = true); + const py::array_t&>(&dewarp), + R"( + Applies a set of 4x4 pose transformations to a collection of 3D points. + Args: + points: A NumPy array of shape (H, W, 3) representing the 3D points. + poses: A NumPy array of shape (W, 4, 4) representing the 4x4 pose + + Return: + A NumPy array of shape (H, W, 3) containing the dewarped 3D points + )", + py::arg("points"), py::arg("poses")); m.def("transform", py::overload_cast&, const py::array_t&>(&transform), - "Transform points with given a pose matrix", py::arg("points"), - py::arg("pose")); + R"( + Applies a single of 4x4 pose transformations to a collection of 3D points. + Args: + points: A NumPy array of shape (H, W, 3), or (N, 3) + pose: A NumPy array of shape (4, 4) representing the 4x4 pose + + Return: + A NumPy array of shape (H, W, 3) or (N, 3) containing the transformed 3D points + after applying the corresponding 4x4 transformation matrices to the points + )", + py::arg("points"), py::arg("pose")); m.attr("__version__") = ouster::SDK_VERSION; + + m.attr("SHORT_HTTP_REQUEST_TIMEOUT_SECONDS") = + py::int_(SHORT_HTTP_REQUEST_TIMEOUT_SECONDS); + m.attr("LONG_HTTP_REQUEST_TIMEOUT_SECONDS") = + py::int_(LONG_HTTP_REQUEST_TIMEOUT_SECONDS); } diff --git a/python/src/ouster/cli/core/cli_args.py b/python/src/ouster/cli/core/cli_args.py index 34810729..fdc9e338 100644 --- a/python/src/ouster/cli/core/cli_args.py +++ b/python/src/ouster/cli/core/cli_args.py @@ -13,6 +13,7 @@ class CliArgs(Borg): def __init__(self, args=None): super().__init__() if args is not None: + # globals are the root of all evil self.args = args else: if not hasattr(self, 'args'): diff --git a/python/src/ouster/cli/plugins/discover.py b/python/src/ouster/cli/plugins/discover.py index a4845c71..573964af 100644 --- a/python/src/ouster/cli/plugins/discover.py +++ b/python/src/ouster/cli/plugins/discover.py @@ -9,12 +9,12 @@ import json import logging from typing import Optional, Tuple -import requests import time from socket import AddressFamily import asyncio import click from ouster.cli.core import cli +from ouster.sdk.client import SensorHttp from zeroconf import IPVersion, ServiceStateChange, Zeroconf from concurrent.futures import ThreadPoolExecutor, as_completed import zeroconf @@ -173,34 +173,6 @@ def format_hostname_for_url(hostname_str: str) -> str: return hostname_str -def get_sensor_info(hostname_or_address, socket_timeout): - url = f"http://{format_hostname_for_url(hostname_or_address)}/api/v1/sensor/metadata/sensor_info" - response = requests.get(url, timeout=socket_timeout) - return response.json() - - -def get_sensor_config(hostname_or_address, socket_timeout): - url = f"http://{format_hostname_for_url(hostname_or_address)}/api/v1/sensor/cmd/get_config_param?args=active" - response = requests.get(url, timeout=socket_timeout) - if response.status_code != 200: - return None - return response.json() - - -def get_sensor_network(hostname_or_address, socket_timeout): - url = f"http://{format_hostname_for_url(hostname_or_address)}/api/v1/system/network" - response = requests.get(url, timeout=socket_timeout) - return response.json() - - -def get_sensor_user_data(hostname_or_address, socket_timeout): - url = f"http://{format_hostname_for_url(hostname_or_address)}/api/v1/user/data" - response = requests.get(url, timeout=socket_timeout) - if response.status_code != 200: - return None - return response.json() - - def get_output_for_sensor(sensor): undefined_value = '-' unknown = 'UNKNOWN' @@ -274,15 +246,16 @@ def get_all_sensor_info(info, socket_timeout, show_user_data) -> str: warnings = [] for address in addresses: try: + sensor_http = SensorHttp.create(address, socket_timeout) if not sensor_info: - sensor_info = get_sensor_info(address, socket_timeout) + sensor_info = sensor_http.sensor_info() if not config: - config = get_sensor_config(address, socket_timeout) + config = sensor_http.active_config_params() if not network: - network = get_sensor_network(address, socket_timeout) + network = json.loads(sensor_http.network(socket_timeout)) if show_user_data and not user_data: - user_data = get_sensor_user_data(address, socket_timeout) - except OSError as e: + user_data = sensor_http.user_data(socket_timeout) + except (RuntimeError, OSError) as e: warning_prefix = f"Could not connect to {info.server} via {address}" warnings.append( get_text_for_oserror( diff --git a/python/src/ouster/cli/plugins/source.py b/python/src/ouster/cli/plugins/source.py index 7abe87ba..7a3ea20e 100644 --- a/python/src/ouster/cli/plugins/source.py +++ b/python/src/ouster/cli/plugins/source.py @@ -721,16 +721,16 @@ def catch_iter(): ctx.scan_iter = catch_iter() # speed up slicing on indexed OSF if slicing comes first - if command_names[0] == "slice": + global _last_slice + if command_names[0] == "slice" and _last_slice: # we can only speed it up if its indexed and OSF at the moment if isinstance(ctx.scan_source, OsfScanSource) and ctx.scan_source.is_indexed: - global _last_slice # at the moment we can only handle index based slices (where the start is not a float) # TODO: support time based slices if not _last_slice[0] is float: # finally calculate wrap-around start and end indexes assuming cycle is set # TODO: revist when we revist cycle/loop - start_index = _last_slice[0] % len(ctx.scan_source) + start_index = _last_slice[0] end_index = None if _last_slice[1] is not None: end_index = _last_slice[1] - start_index @@ -739,6 +739,7 @@ def catch_iter(): callbacks = callbacks[1:] # remove the callback since we dont need it now # finally apply the rest of the slice (end and interval) ctx.scan_iter = islice(ctx.scan_iter, 0, end_index, _last_slice[2]) + _last_slice = None # globals are the root of all evil try: # Execute multicommand callbacks diff --git a/python/src/ouster/cli/plugins/source_mapping.py b/python/src/ouster/cli/plugins/source_mapping.py index f4fb7dda..c56a4fc1 100644 --- a/python/src/ouster/cli/plugins/source_mapping.py +++ b/python/src/ouster/cli/plugins/source_mapping.py @@ -398,7 +398,7 @@ def pc_status_print(): # to remove out range points valid_row_index = scan.field(ChanField.RANGE) > 0 out_range_row_index = scan.field(ChanField.RANGE) == 0 - dewarped_points = dewarp(points, column_poses, input_row_major=False) + dewarped_points = dewarp(points, column_poses) filtered_points = dewarped_points[valid_row_index] filtered_keys = keys[valid_row_index] diff --git a/python/src/ouster/sdk/_bindings/client.pyi b/python/src/ouster/sdk/_bindings/client.pyi index 994c2014..f4cd3b3f 100644 --- a/python/src/ouster/sdk/_bindings/client.pyi +++ b/python/src/ouster/sdk/_bindings/client.pyi @@ -21,6 +21,10 @@ from typing import (Any, ClassVar, Dict, Iterator, List, Optional, overload, Tup from ouster.sdk.client.data import (BufferT, ColHeader, FieldDType, FieldTypes) +SHORT_HTTP_REQUEST_TIMEOUT_SECONDS: int +LONG_HTTP_REQUEST_TIMEOUT_SECONDS: int + + class PacketValidationFailure: NONE: ClassVar[PacketValidationFailure] ID: ClassVar[PacketValidationFailure] @@ -114,52 +118,56 @@ class SensorConnection: class SensorHttp: - def metadata(self, timeout_sec: int = 1) -> dict: + def metadata(self, timeout_sec: int = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) -> dict: + ... + + def sensor_info(self, timeout_sec: int = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) -> dict: ... - def sensor_info(self, timeout_sec: int = 1) -> dict: + def get_config_params(self, active: bool, timeout_sec: int = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) -> str: ... - def get_config_params(self, active: bool, timeout_sec: int = 1) -> str: + def set_config_params(self, key: str, value: str, timeout_sec: int = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) -> None: ... - def set_config_params(self, key: str, value: str, timeout_sec: int = 1) -> None: + def active_config_params(self, timeout_sec: int = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) -> dict: ... - def active_config_params(self, timeout_sec: int = 1) -> dict: + def staged_config_params(self, timeout_sec: int = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) -> dict: ... - def staged_config_params(self, timeout_sec: int = 1) -> dict: + def set_udp_dest_auto(self, timeout_sec: int = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) -> None: ... - def set_udp_dest_auto(self, timeout_sec: int = 1) -> None: + def beam_intrinsics(self, timeout_sec: int = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) -> dict: ... - def beam_intrinsics(self, timeout_sec: int = 1) -> dict: + def imu_intrinsics(self, timeout_sec: int = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) -> dict: ... - def imu_intrinsics(self, timeout_sec: int = 1) -> dict: + def lidar_intrinsics(self, timeout_sec: int = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) -> dict: ... - def lidar_intrinsics(self, timeout_sec: int = 1) -> dict: + def lidar_data_format(self, timeout_sec: int = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) -> dict: ... - def lidar_data_format(self, timeout_sec: int = 1) -> dict: + def reinitialize(self, timeout_sec: int = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) -> None: ... - def reinitialize(self, timeout_sec: int = 1) -> None: + def save_config_params(self, timeout_sec: int = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) -> None: ... - def save_config_params(self, timeout_sec: int = 1) -> None: + def get_user_data(self, timeout_sec: int = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) -> str: ... - def get_user_data(self, timeout_sec: int = 1) -> str: + def set_user_data(self, user_data: str, keep_on_config_delete: bool, timeout_sec: int = + SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) -> None: ... - def set_user_data(self, user_data: str, keep_on_config_delete: bool, timeout_sec: int = 1) -> None: + def delete_user_data(self, timeout_sec: int = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) -> None: ... - def delete_user_data(self, timeout_sec: int = 1) -> None: + def network(self, timeout_sec: int = SHORT_HTTP_REQUEST_TIMEOUT_SECONDS) -> None: ... def hostname(self) -> str: @@ -169,7 +177,7 @@ class SensorHttp: ... @staticmethod - def create(hostname: str, timeout_sec: int = 40) -> SensorHttp: + def create(hostname: str, timeout_sec: int = LONG_HTTP_REQUEST_TIMEOUT_SECONDS) -> SensorHttp: ... @@ -1293,7 +1301,7 @@ def parse_and_validate_metadata(metadata: str) -> Tuple[SensorInfo, ValidatorIss ... -def dewarp(points: ndarray, poses: ndarray, input_row_major: bool = True) -> ndarray: +def dewarp(points: ndarray, poses: ndarray) -> ndarray: ... def transform(points: ndarray, pose: ndarray) -> ndarray: diff --git a/python/src/ouster/sdk/_bindings/osf.pyi b/python/src/ouster/sdk/_bindings/osf.pyi index 2aca5a5d..a05f7ca3 100644 --- a/python/src/ouster/sdk/_bindings/osf.pyi +++ b/python/src/ouster/sdk/_bindings/osf.pyi @@ -150,7 +150,7 @@ class Writer: @overload def save(self, stream_id: int, scan: LidarScan, ts: int) -> None: ... @overload - def save(self, stream_id: int, scan: List[LidarScan]) -> None: ... + def save(self, scan: List[LidarScan]) -> None: ... def add_sensor(self, info: SensorInfo, fields_to_write: List[str] = ...) -> int: ... def add_metadata(self, arg0: object) -> int: ... def save_message(self, stream_id: int, receive_ts: int, sensor_ts: int, buffer: BufferT) -> int: ... diff --git a/python/src/ouster/sdk/_bindings/viz.pyi b/python/src/ouster/sdk/_bindings/viz.pyi index 458aec37..eab715e3 100644 --- a/python/src/ouster/sdk/_bindings/viz.pyi +++ b/python/src/ouster/sdk/_bindings/viz.pyi @@ -313,7 +313,7 @@ class PointViz: def running(self, state: bool) -> None: ... - def update(self) -> bool: + def update(self) -> None: ... def visible(self, state: bool) -> bool: diff --git a/python/src/ouster/sdk/bag/bag_scan_source.py b/python/src/ouster/sdk/bag/bag_scan_source.py index 0b76baf0..cc864d86 100644 --- a/python/src/ouster/sdk/bag/bag_scan_source.py +++ b/python/src/ouster/sdk/bag/bag_scan_source.py @@ -55,6 +55,7 @@ def __init__( raise # generate the field types per sensor with flags/raw_fields if specified + raw_fields |= (field_names is not None and len(field_names) != 0) field_types = resolve_field_types(self._source.metadata, raw_headers=raw_headers, raw_fields=raw_fields) diff --git a/python/src/ouster/sdk/client/__init__.py b/python/src/ouster/sdk/client/__init__.py index b5e5e424..ee81c250 100644 --- a/python/src/ouster/sdk/client/__init__.py +++ b/python/src/ouster/sdk/client/__init__.py @@ -48,6 +48,7 @@ from ouster.sdk._bindings.client import ScanBatcher from ouster.sdk._bindings.client import dewarp from ouster.sdk._bindings.client import transform +from ouster.sdk._bindings.client import LONG_HTTP_REQUEST_TIMEOUT_SECONDS, SHORT_HTTP_REQUEST_TIMEOUT_SECONDS from .data import BufferT from .data import FieldDType diff --git a/python/src/ouster/sdk/io_type.py b/python/src/ouster/sdk/io_type.py index 084e3178..2f3315f4 100644 --- a/python/src/ouster/sdk/io_type.py +++ b/python/src/ouster/sdk/io_type.py @@ -53,10 +53,9 @@ def io_type(source: str) -> OusterIoType: if os.path.isdir(source) and io_type_from_extension(source) == OusterIoType.BAG: return OusterIoType.BAG try: - if socket.gethostbyname(source): + if socket.getaddrinfo(source, 0): return OusterIoType.SENSOR except Exception: pass - raise ValueError("Source type expected to be a sensor hostname, ip address," " or a .pcap, .osf, or .bag file.") diff --git a/python/src/ouster/sdk/mapping/util.py b/python/src/ouster/sdk/mapping/util.py index 79717026..810a6577 100644 --- a/python/src/ouster/sdk/mapping/util.py +++ b/python/src/ouster/sdk/mapping/util.py @@ -67,10 +67,16 @@ def writeScanColPose(slam_prev_pose, slam_curr_pose, scans: List[Optional[client if slam_frame_diff <= 0: raise ValueError("frame_diff must greater than zero.") - if slam_prev_pose is None or not scans: + if not scans: + # Handle empty scans + return + + if slam_prev_pose is None: # First scan pose in KISS in identity matrix. not enough poses to do # perturbation in every column. Scan col poses will be identity matrix # by default + for scan in scans: + scan.pose[:] = np.eye(4) # type: ignore return diff_log = pu.log_pose(np.dot(slam_curr_pose, diff --git a/python/src/ouster/sdk/open_source.py b/python/src/ouster/sdk/open_source.py index f5ffbb91..0ce6e4ac 100644 --- a/python/src/ouster/sdk/open_source.py +++ b/python/src/ouster/sdk/open_source.py @@ -66,14 +66,14 @@ def open_source(source_url: Union[str, List[str]], sensor_idx: int = 0, *args, if len(source_url) == 0: raise ValueError("No valid source specified") - first_url: str - first_url = source_url[0] if type(source_url) is list else source_url # type: ignore - if type(source_url) is list: source_url = [os.path.expanduser(url) for url in source_url] else: source_url = os.path.expanduser(source_url) # type: ignore + first_url: str + first_url = source_url[0] if type(source_url) is list else source_url # type: ignore + source_type: OusterIoType scan_source: Optional[MultiScanSource] = None try: diff --git a/python/src/ouster/sdk/osf/osf_scan_source.py b/python/src/ouster/sdk/osf/osf_scan_source.py index ae2ad843..a4d21006 100644 --- a/python/src/ouster/sdk/osf/osf_scan_source.py +++ b/python/src/ouster/sdk/osf/osf_scan_source.py @@ -141,6 +141,7 @@ def __init__( def _osf_convert(self, reader: Reader, output: str) -> None: # TODO: figure out how to get the current chunk_size chunk_size = 0 + progressbar(0, 1, "", "indexed") writer = Writer(output, chunk_size) writer.set_metadata_id(reader.metadata_id) for _, m in reader.meta_store.items(): diff --git a/python/src/ouster/sdk/pcap/pcap_multi_packet_reader.py b/python/src/ouster/sdk/pcap/pcap_multi_packet_reader.py index 78580928..41cc0347 100644 --- a/python/src/ouster/sdk/pcap/pcap_multi_packet_reader.py +++ b/python/src/ouster/sdk/pcap/pcap_multi_packet_reader.py @@ -7,6 +7,7 @@ import time import numpy as np +import os from threading import Lock import logging @@ -86,7 +87,8 @@ def __init__(self, if metadatas is None and metadata_paths: metadatas = [] for idx, meta_path in enumerate(metadata_paths): - with open(meta_path) as meta_file: + meta_path_full = os.path.expanduser(meta_path) + with open(meta_path_full) as meta_file: meta_json = meta_file.read() meta_info = SensorInfo(meta_json) self._metadata_json.append(meta_json) diff --git a/python/src/ouster/sdk/pcap/pcap_scan_source.py b/python/src/ouster/sdk/pcap/pcap_scan_source.py index a5e64efb..93915895 100644 --- a/python/src/ouster/sdk/pcap/pcap_scan_source.py +++ b/python/src/ouster/sdk/pcap/pcap_scan_source.py @@ -59,6 +59,7 @@ def __init__( raise # generate the field types per sensor with flags/raw_fields if specified + raw_fields |= (field_names is not None and len(field_names) != 0) field_types = resolve_field_types(self._source.metadata, raw_headers=raw_headers, raw_fields=raw_fields) diff --git a/python/src/ouster/sdk/sensor/sensor_scan_source.py b/python/src/ouster/sdk/sensor/sensor_scan_source.py index 311869d3..cd79558f 100644 --- a/python/src/ouster/sdk/sensor/sensor_scan_source.py +++ b/python/src/ouster/sdk/sensor/sensor_scan_source.py @@ -21,6 +21,8 @@ def __init__( lidar_port: Optional[int] = None, imu_port: Optional[int] = None, complete: bool = False, + raw_headers: bool = False, + raw_fields: bool = False, soft_id_check: bool = False, do_not_reinitialize: bool = False, no_auto_udp_dest: bool = False, @@ -36,6 +38,8 @@ def __init__( max time period at which every new collated scan is released/cut), default is 0.21s complete: set to True to only release complete scans. + raw_headers: if True, include raw headers in decoded LidarScans + raw_fields: if True, include raw fields in decoded LidarScans extrinsics: list of extrinsincs to apply to each sensor field_names: list of fields to decode into a LidarScan, if not provided decodes all fields @@ -58,7 +62,7 @@ def __init__( mode_metadata = [] for hostname in hostnames: print(f"Contacting sensor {hostname}...") - sensor_http = SensorHttp.create(hostname, 10) + sensor_http = SensorHttp.create(hostname, client.LONG_HTTP_REQUEST_TIMEOUT_SECONDS) config = build_sensor_config(sensor_http, lidar_port, imu_port, @@ -72,13 +76,16 @@ def __init__( f"lidar port {config.udp_port_lidar} with udp dest '{config.udp_dest}'...") sensor = _Sensor(hostname, config) - mode_metadata.append(sensor.fetch_metadata(45)) + mode_metadata.append(sensor.fetch_metadata()) s_list.append(_Sensor(hostname, config)) self._field_types = [] self._fields = [] + raw_fields |= (field_names is not None and len(field_names) != 0) for m in mode_metadata: - ft = resolve_field_types(m) + ft = resolve_field_types(m, + raw_headers=raw_headers, + raw_fields=raw_fields) real_ft = [] fnames = [] for f in ft: @@ -94,8 +101,11 @@ def __init__( self._fields.append(fnames) self._field_types.append(real_ft) - self._cli = _SensorScanSource(s_list, [], config_timeout=45, queue_size=2, - soft_id_check=soft_id_check, fields=self._field_types) + self._cli = _SensorScanSource( + s_list, [], + config_timeout=client.LONG_HTTP_REQUEST_TIMEOUT_SECONDS, queue_size=2, + soft_id_check=soft_id_check, fields=self._field_types + ) self._metadata = self._cli.get_sensor_info() self._dt = dt diff --git a/python/src/ouster/sdk/sensor/util.py b/python/src/ouster/sdk/sensor/util.py index 4f3341d1..6e4eaebd 100644 --- a/python/src/ouster/sdk/sensor/util.py +++ b/python/src/ouster/sdk/sensor/util.py @@ -1,6 +1,7 @@ from typing import Optional from copy import copy -import requests +from urllib import request +import json import ouster.sdk.client as client from ouster.sdk.client import (SensorHttp, Version) @@ -21,19 +22,24 @@ def _auto_detected_udp_dest(http_client: SensorHttp, hostname = http_client.hostname() orig_config = current_config or client.SensorConfig(http_client.get_config_params(True)) + # escape ipv6 addresses + if hostname.count(':') >= 2: + hostname = "[" + hostname + "]" + # get what the possible auto udp_dest is config_endpoint = f"http://{hostname}/api/v1/sensor/config" - response = requests.post(config_endpoint, params={'reinit': False, 'persist': False}, - json={'udp_dest': '@auto'}) - response.raise_for_status() + req = request.Request(config_endpoint + "?reinit=False&persist=False", method="POST") + req.add_header('Content-Type', 'application/json') + data = json.dumps({'udp_dest': '@auto'}) + request.urlopen(req, data = data.encode()).read() # get staged config udp_auto_config = client.SensorConfig(http_client.get_config_params(False)) - # set staged config back to original - response = requests.post(config_endpoint, params={'reinit': False, 'persist': False}, - json={'udp_dest': str(orig_config.udp_dest)}) - response.raise_for_status() + req = request.Request(config_endpoint + "?reinit=False&persist=False", method="POST") + req.add_header('Content-Type', 'application/json') + data = json.dumps({'udp_dest': str(orig_config.udp_dest)}) + request.urlopen(req, data = data.encode()).read() return udp_auto_config.udp_dest diff --git a/python/src/ouster/sdk/util/pose_util.py b/python/src/ouster/sdk/util/pose_util.py index 663f88c8..df89b211 100644 --- a/python/src/ouster/sdk/util/pose_util.py +++ b/python/src/ouster/sdk/util/pose_util.py @@ -1,5 +1,5 @@ from typing_extensions import Protocol -from typing import (Union, Tuple, List, Optional, Iterable, Sequence) +from typing import (Union, Tuple, List, Optional, Sequence) import numpy as np @@ -634,54 +634,6 @@ def __getitem__(self, idx): return self._poses[idx] -def dewarp(xyz: np.ndarray, *, scan_pose: Optional[PoseH] = None, - column_poses: Optional[np.ndarray] = None) -> np.ndarray: - """Returns transformed: xyz_return = scan_pose @ column_poses @ xyz - - Args: - xyz: is the return of `client.XYZLut` call, which has (H, W, 3) dimensions - and column major contiguous storage - scan_pose: optional. (4, 4) homogeneous pose of the scan - column_poses: optional. (W, 4, 4) homogeneous poses of the scans column - - Returns: - xyz with applied scan_pose and/or column_poses: - xyz_return = scan_pose @ column_poses @ xyz - binary layout of the returned `xyz` is ensure to match the one that is - returned by call `client.XYZLut` so it can further used with PointViz - and other functions that expect this specific layout. - """ - if xyz.ndim != 3 or xyz.shape[2] != 3: - raise ValueError("Expect xyz to be (H, W, 3) shape") - - if scan_pose is not None and scan_pose.shape != (4, 4): - raise ValueError("Expect scan_pose to be (4, 4) shape") - - if column_poses is not None: - if not (column_poses.shape[0] == xyz.shape[1] - and column_poses.shape[1] == 4 and column_poses.shape[2] == 4): - raise ValueError("Expect column_poses to be (W, 4, 4) shape") - - # Apply transformations - if column_poses is not None: - xyz_poses = np.matmul(scan_pose, column_poses) if scan_pose is not None else column_poses - - xyz_transformed = np.transpose(np.matmul(xyz_poses[:, :3, :3], np.transpose(xyz, axes=(1, 2, 0))), - axes=(2, 0, 1)) + xyz_poses[np.newaxis, :, :3, -1] - elif scan_pose is not None: - xyz_transformed = np.transpose(np.matmul(scan_pose[np.newaxis, :3, :3], - np.transpose(xyz, axes=(1, 2, 0))), - axes=(2, 0, 1)) + scan_pose[np.newaxis, :3, -1] - else: - xyz_transformed = xyz - - return xyz_transformed - - -ScansIterable = Union[Iterable[client.LidarScan], - Iterable[List[Optional[client.LidarScan]]]] - - def get_rot_matrix_to_align_to_gravity(accel_x: float, accel_y: float, accel_z: float): diff --git a/python/src/ouster/sdk/viz/accumulators.py b/python/src/ouster/sdk/viz/accumulators.py index b45fc184..b569e78f 100644 --- a/python/src/ouster/sdk/viz/accumulators.py +++ b/python/src/ouster/sdk/viz/accumulators.py @@ -251,12 +251,10 @@ def _draw(self) -> None: self._draw_osd() # TODO[tws] likely remove; realistically we only need one lock and LidarScanViz should manage it - def draw(self, update: bool = True) -> bool: + def draw(self, update: bool = True) -> None: """Process and draw the latest state to the screen.""" with self._lock: self._draw() if update: - return self._viz.update() - else: - return False + self._viz.update() diff --git a/python/src/ouster/sdk/viz/accumulators_config.py b/python/src/ouster/sdk/viz/accumulators_config.py index 422be6dd..b871b203 100644 --- a/python/src/ouster/sdk/viz/accumulators_config.py +++ b/python/src/ouster/sdk/viz/accumulators_config.py @@ -15,7 +15,7 @@ class LidarScanVizAccumulatorsConfig: def __init__(self, accum_max_num: int = 0, accum_min_dist_meters: float = 0, - accum_min_dist_num: int = 1, # TODO[tws]: this is a *weird* default - change it? Remove it? + accum_min_dist_num: int = 0, map_enabled: bool = False, map_select_ratio: float = MAP_SELECT_RATIO, map_max_points: int = MAP_MAX_POINTS_NUM, diff --git a/python/src/ouster/sdk/viz/core.py b/python/src/ouster/sdk/viz/core.py index ec6fc400..335cb440 100644 --- a/python/src/ouster/sdk/viz/core.py +++ b/python/src/ouster/sdk/viz/core.py @@ -9,7 +9,7 @@ from collections import deque from functools import partial -from enum import Enum +from enum import Enum, auto import os import threading import time @@ -46,6 +46,10 @@ class LidarScanViz: LidarScan. Sets up key bindings to toggle which channel fields and returns are displayed, and change 2D image and point size. """ + class OsdState(Enum): + NONE = auto(), + DEFAULT = auto(), + HELP = auto() class FlagsMode(Enum): NONE = 0 @@ -129,7 +133,8 @@ def __init__( self._viz.target_display.enable_rings(True) # initialize osd - self._osd_enabled = True + self._osd_state = LidarScanViz.OsdState.DEFAULT + self._previous_osd_state = LidarScanViz.OsdState.NONE self._osd = Label("", 0, 1) self._viz.add(self._osd) @@ -155,6 +160,7 @@ def __init__( self._tracked_sensor = 0 self._scan_pose = np.eye(4) + self._scans: List[Optional[LidarScan]] = [] self._scan_num = -1 self._first_frame_ts = None @@ -166,6 +172,11 @@ def metadata(self) -> List[client.SensorInfo]: """Metadatas for the displayed sensors.""" return self._model._metas + @property + def osd_state(self) -> "LidarScanViz.OsdState": + """Returns the state of the on screen display.""" + return self._osd_state + def _setup_controls(self) -> None: # key bindings. will be called from rendering thread, must be synchronized key_bindings: Dict[Tuple[int, int], Callable[[LidarScanViz], None]] = { @@ -191,7 +202,7 @@ def _setup_controls(self) -> None: # TODO[pb]: Extract FlagsMode to custom processor (TBD the whole thing) (ord('C'), 0): LidarScanViz.update_flags_mode, (ord('9'), 0): LidarScanViz.toggle_scan_axis, - (ord('/'), 1): LidarScanViz.print_keys, + (ord('/'), 1): LidarScanViz.toggle_help, } self._key_definitions: Dict[str, str] = { @@ -245,6 +256,10 @@ def cycle_img_mode(self, i: int, *, direction: int = 1) -> None: with self._lock: self._model.cycle_image_mode(i, direction) + # Note, updating the image mode needs the current scan + # because ImageMode.enabled requires it. + self._model.update(self._scans) + def cycle_cloud_mode(self, direction: int = 1) -> None: """Change the coloring mode of the 3D point cloud.""" with self._lock: @@ -352,7 +367,10 @@ def cicle_ring_line_width(self) -> None: def toggle_osd(self, state: Optional[bool] = None) -> None: """Show or hide the on-screen display.""" with self._lock: - self._osd_enabled = not self._osd_enabled if state is None else state + if self._osd_state != LidarScanViz.OsdState.DEFAULT: + self._osd_state = LidarScanViz.OsdState.DEFAULT + else: + self._osd_state = LidarScanViz.OsdState.NONE def toggle_camera_follow(self) -> None: """Toggle the camera follow mode.""" @@ -371,13 +389,18 @@ def update_flags_mode(self, else: self._flags_mode = mode + # if no scan is set yet, skip updating the cloud masks + if not self._scans: + return + # reset the cloud range field (since HIDE_BLOOM will modify it) for scan, sensor in zip(self._scans, self._model._sensors): if not scan: continue for i, range_field in ((0, ChanField.RANGE), (1, ChanField.RANGE2)): - sensor._clouds[i].set_range(scan.field(range_field)) + if range_field in scan.fields: + sensor._clouds[i].set_range(scan.field(range_field)) # TODO[UN]: need to be done as part of the combined lidar mode # set mask on all points in the second cloud, clear other mask @@ -456,7 +479,7 @@ def update(self, if self._scans_accum: self._scans_accum.update(scans, self._scan_num) - def draw(self, update: bool = True) -> bool: + def draw(self, update: bool = True) -> None: """Process and draw the latest state to the screen.""" with self._lock: self._draw() @@ -468,9 +491,7 @@ def draw(self, update: bool = True) -> bool: self._image_size_initialized = True if update: - return self._viz.update() - else: - return False + self._viz.update() def run(self) -> None: """Run the rendering loop of the visualizer. @@ -494,9 +515,15 @@ def _format_version(version: client.Version) -> str: return result def _update_multi_viz_osd(self): - if not self._osd_enabled: + if self._osd_state == LidarScanViz.OsdState.NONE: self._osd.set_text("") return + elif self._osd_state == LidarScanViz.OsdState.HELP: + on_screen_help_text = [] + for key_binding in self._key_definitions: + on_screen_help_text.append(f"{key_binding:^7}: {self._key_definitions[key_binding]}") + self._osd.set_text('\n'.join(on_screen_help_text)) + return enabled_clouds_str = "" for idx, sensor in enumerate(self._model._sensors): @@ -566,12 +593,20 @@ def _draw_update_scan_poses(self) -> None: self._scan_pose = client.first_valid_column_pose(scan) self._viz.camera.set_target(np.linalg.inv(self._scan_pose)) - def print_keys(self) -> None: + def print_key_bindings(self) -> None: + print(">---------------- Key Bindings --------------<") + for key_binding in self._key_definitions: + print(f"{key_binding:^7}: {self._key_definitions[key_binding]}") + print(">--------------------------------------------<") + + def toggle_help(self) -> None: with self._lock: - print(">---------------- Key Bindings --------------<") - for key_binding in self._key_definitions: - print(f"{key_binding:^7}: {self._key_definitions[key_binding]}") - print(">--------------------------------------------<") + if self._osd_state != LidarScanViz.OsdState.HELP: + self._previous_osd_state = self._osd_state + self._osd_state = LidarScanViz.OsdState.HELP + self.print_key_bindings() + else: + self._osd_state = self._previous_osd_state def _first_scan_ts(scans: Union[List[Optional[LidarScan]], LidarScan]): @@ -757,9 +792,10 @@ def __init__(self, self._proc_exit = False # playback status display + # TODO[tws] probably move playback osd to LidarScanViz... + # We've had to define extra key handlers here to support it for no good reason. self._playback_osd = Label("", 1, 1, align_right=True) self._viz.add(self._playback_osd) - self._osd_enabled = True self._update_playback_osd() # continuous screenshots recording @@ -772,14 +808,15 @@ def __init__(self, (ord('.'), 2): partial(SimpleViz.seek_relative, n_frames=10), (ord(' '), 0): SimpleViz.toggle_pause, (ord('O'), 0): SimpleViz.toggle_osd, + (ord('/'), 1): SimpleViz.toggle_help, (ord('X'), 1): SimpleViz.toggle_img_recording, (ord('Z'), 1): SimpleViz.screenshot, } key_definitions: Dict[str, str] = { 'o': "Toggle information overlay", - 'shift+x': "Toggle a continuous saving of screenshots", - 'shift+z': "Take a screenshot!", + 'SHIFT+x': "Toggle a continuous saving of screenshots", + 'SHIFT+z': "Take a screenshot!", ". / ,": "Step forward one frame", "> / <": "Increase/decrease playback rate (during replay)", 'SPACE': "Pause and unpause", @@ -806,8 +843,13 @@ def handle_keys(self: SimpleViz, ctx: WindowCtx, key: int, push_point_viz_handler(self._viz, self, handle_keys) + def toggle_help(self) -> None: + self._scan_viz.toggle_help() + self._update_playback_osd() + self._scan_viz.draw() + def _update_playback_osd(self) -> None: - if not self._osd_enabled: + if self._scan_viz.osd_state != LidarScanViz.OsdState.DEFAULT: self._playback_osd.set_text("") return @@ -850,8 +892,7 @@ def modify_rate(self, amount: int) -> None: def toggle_osd(self, state: Optional[bool] = None) -> None: """Show or hide the on-screen display.""" with self._cv: - self._osd_enabled = not self._osd_enabled if state is None else state - self._scan_viz.toggle_osd(self._osd_enabled) + self._scan_viz.toggle_osd() self._update_playback_osd() self._scan_viz.draw() diff --git a/python/src/ouster/sdk/viz/map_accumulator.py b/python/src/ouster/sdk/viz/map_accumulator.py index 37e00a65..7b9812d3 100644 --- a/python/src/ouster/sdk/viz/map_accumulator.py +++ b/python/src/ouster/sdk/viz/map_accumulator.py @@ -1,7 +1,7 @@ import numpy as np from typing import Dict, Optional, List, Union from ouster.sdk.client import LidarScan, ChanField -import ouster.sdk.util.pose_util as pu +from ouster.sdk.client import dewarp from ouster.sdk._bindings.viz import Cloud, PointViz from ouster.sdk.viz.accum_base import AccumulatorBase from ouster.sdk.viz.model import LidarScanVizModel @@ -126,7 +126,7 @@ def _update_map(self) -> None: replace=False) row_sel, col_sel = nzi[nzc], nzj[nzc] xyz = sensor._xyzlut(range_field) - xyz = pu.dewarp(xyz, column_poses=scan.pose)[row_sel, col_sel] + xyz = dewarp(xyz, scan.pose)[row_sel, col_sel] xyz_num = xyz.shape[0] # TODO[tws] previously protected by "with self._lock" - do we still need this? diff --git a/python/tests/test_cli_osf_slice.py b/python/tests/test_cli_osf_slice.py new file mode 100644 index 00000000..81585ccd --- /dev/null +++ b/python/tests/test_cli_osf_slice.py @@ -0,0 +1,261 @@ +import os +import tempfile +import pytest +from pathlib import Path +from ouster.cli.core import cli # type: ignore +from ouster.cli.core.cli_args import CliArgs +from ouster.cli.plugins import source, source_osf # noqa: F401 +from ouster.sdk import open_source +from click.testing import CliRunner +from tests.conftest import OSFS_DATA_DIR + + +# TODO[tws] these tests should be unit tests, not CliRunner-based tests. +# Refactor the underlying code in the process_commands method to enable this. + +# TODO[tws] these tests are also using the open_source method with sensor_index=-1 +# which enables us to close the file using the close method. +# When using sensor_index > -1, this isn't possible, resulting in an error on Windows +# when attempting to unlink the file in the finally block. This may be confusing +# for API users and may result in bugs. + + +@pytest.fixture +def test_osf_file() -> str: + return str(Path(OSFS_DATA_DIR) / 'OS-1-128_v2.3.0_1024x10_lb_n3.osf') + + +@pytest.fixture +def test_osf_file_new() -> str: + return str(Path(OSFS_DATA_DIR) / 'OS-0-128_v3.0.1_1024x10_20241017_141645.osf') + + +def test_osf_slice(test_osf_file) -> None: + """It should display a warning if slicing out of bounds.""" + num_scans_in_src = 3 + # check precondition + src = open_source(test_osf_file) + assert src.scans_num == num_scans_in_src + src.close() + + try: + with tempfile.NamedTemporaryFile(suffix='.osf', delete=False) as result_osf: + result_osf.close() + runner = CliRunner() + result = runner.invoke(cli, + CliArgs([ + '--traceback', + 'source', test_osf_file, + 'slice', f'{num_scans_in_src}:{num_scans_in_src + 20}', # note - out of bounds + 'save', '--ts', 'lidar', '--overwrite', result_osf.name + ]).args, catch_exceptions=False + ) + print(result.output) + assert result.exit_code == 0 # FIXME? + assert os.path.isfile(result_osf.name) + + result_src = open_source(result_osf.name, -1) + assert result_src.scans_num == [] # FIXME + result_src.close() + finally: + os.unlink(result_osf.name) + + +def test_osf_slice_2(test_osf_file) -> None: + """It should slice by indices.""" + + expected_num_scans = 3 + slice_start = 1 + slice_end = 3 + # check precondition + src = open_source(test_osf_file) + scans = [scan for scan in src] + assert src.scans_num == expected_num_scans + src.close() + + try: + with tempfile.NamedTemporaryFile(suffix='.osf', delete=False) as result_osf: + result_osf.close() + runner = CliRunner() + result = runner.invoke(cli, + CliArgs([ + '--traceback', + 'source', test_osf_file, + 'slice', f'{slice_start}:{slice_end}', # note - out of bounds + 'save', '--ts', 'lidar', '--overwrite', result_osf.name + ]).args, catch_exceptions=False + ) + assert result.exit_code == 0 # FIXME? + assert os.path.isfile(result_osf.name) + + result_src = open_source(result_osf.name, -1) + result_scans = [scan for scan in result_src] + assert result_src.scans_num == [slice_end - slice_start] + assert result_scans[0] == [scans[1]] + assert result_scans[1] == [scans[2]] + result_src.close() + finally: + os.unlink(result_osf.name) + + +def test_osf_slice_time(test_osf_file_new) -> None: + """It should display a warning if slicing out of bounds.""" + + expected_num_scans = 3 + # check precondition + src = open_source(test_osf_file_new) + assert src.scans_num == expected_num_scans + src.close() + + try: + with tempfile.NamedTemporaryFile(suffix='.osf', delete=False) as result_osf: + result_osf.close() + runner = CliRunner() + result = runner.invoke(cli, + CliArgs([ + '--traceback', + 'source', test_osf_file_new, + 'slice', '60s:120s', + 'save', '--overwrite', result_osf.name + ]).args, catch_exceptions=False) + assert result.exit_code == 0 # FIXME? + print(result.output) + assert os.path.isfile(result_osf.name) + + result_src = open_source(result_osf.name, -1) + assert result_src.scans_num == [] + # TODO[tws] figure out how to capture "WARNING: No scans saved." + result_src.close() + finally: + os.unlink(result_osf.name) + + +# FIXME[tws]? +# Slicing by time yields scans when using --ts lidar +# Note - test_osf_file has no packet timestamps +def test_osf_slice_time_2(test_osf_file) -> None: + """It will include all scans if --ts lidar is used.""" + + expected_num_scans = 3 + # check precondition + src = open_source(test_osf_file) + assert src.scans_num == expected_num_scans + src.close() + + try: + with tempfile.NamedTemporaryFile(suffix='.osf', delete=False) as result_osf: + result_osf.close() + runner = CliRunner() + result = runner.invoke(cli, + CliArgs([ + '--traceback', + 'source', test_osf_file, + 'slice', '0ms:00001ms', # <-- note, should probably only result in a single scan + 'save', + '--ts', 'lidar', # <-- NOTE using lidar timestamps + '--overwrite', result_osf.name + ]).args, catch_exceptions=False) + assert result.exit_code == 0 # FIXME? + print(result.output) + assert os.path.isfile(result_osf.name) + + result_src = open_source(result_osf.name, -1) + assert result_src.scans_num == [3] + # TODO[tws] figure out how to capture "WARNING: No scans saved." + result_src.close() + finally: + os.unlink(result_osf.name) + + +# FIXME[tws]? +def test_osf_slice_time_3(test_osf_file_new) -> None: + """It will allow a time slice stop value equal to zero, which evalutes to False.""" + + expected_num_scans = 3 + # check precondition + src = open_source(test_osf_file_new) + assert src.scans_num == expected_num_scans + src.close() + + try: + with tempfile.NamedTemporaryFile(suffix='.osf', delete=False) as result_osf: + result_osf.close() + runner = CliRunner() + result = runner.invoke(cli, + CliArgs([ + '--traceback', + 'source', test_osf_file_new, + 'slice', '0.0000ms:0.0000ms', + 'save', '--overwrite', result_osf.name + ]).args, catch_exceptions=False) + assert result.exit_code == 0 # FIXME? + print(result.output) + assert os.path.isfile(result_osf.name) + + result_src = open_source(result_osf.name, -1) + assert result_src.scans_num == [expected_num_scans] # FIXME? + result_src.close() + finally: + os.unlink(result_osf.name) + + +def test_osf_slice_time_4(test_osf_file_new) -> None: + """It will allow a time slice stop value equal to the start value.""" + + expected_num_scans = 3 + # check precondition + src = open_source(test_osf_file_new) + assert src.scans_num == expected_num_scans + src.close() + + try: + with tempfile.NamedTemporaryFile(suffix='.osf', delete=False) as result_osf: + result_osf.close() + runner = CliRunner() + result = runner.invoke(cli, + CliArgs([ + '--traceback', + 'source', test_osf_file_new, + 'slice', '1.0000ms:1.0000ms', # TODO[tws] should we force the user to specify stop > start? + 'save', '--overwrite', result_osf.name + ]).args, catch_exceptions=False) + assert result.exit_code == 0 # FIXME? + print(result.output) + assert os.path.isfile(result_osf.name) + + result_src = open_source(result_osf.name, -1) + assert result_src.scans_num == [] + result_src.close() + finally: + os.unlink(result_osf.name) + + +def test_osf_slice_time_5(test_osf_file_new) -> None: + """It should slice an OSF file by time.""" + + scans_in_src = 3 + # check precondition + src = open_source(test_osf_file_new) + assert src.scans_num == scans_in_src + src.close() + + try: + with tempfile.NamedTemporaryFile(suffix='.osf', delete=False) as result_osf: + result_osf.close() + runner = CliRunner() + result = runner.invoke(cli, + CliArgs([ + '--traceback', + 'source', test_osf_file_new, + 'slice', '0ms:0.0001ms', # should only return a single scan + 'save', '--overwrite', result_osf.name + ]).args, catch_exceptions=False) + assert result.exit_code == 0 + print(result.output) + assert os.path.isfile(result_osf.name) + + result_src = open_source(result_osf.name, -1) + assert result_src.scans_num == [1] + result_src.close() + finally: + os.unlink(result_osf.name) diff --git a/python/tests/test_pose_util.py b/python/tests/test_pose_util.py index 3c5eb348..77ae8e2d 100644 --- a/python/tests/test_pose_util.py +++ b/python/tests/test_pose_util.py @@ -1,13 +1,8 @@ from typing import List - -import math - import numpy as np - +import math import time import pytest - - import ouster.sdk.util.pose_util as pu from ouster.sdk.client import dewarp, transform @@ -312,15 +307,25 @@ def test_transform_N_M_3(): def test_dewarp(): - poses = np.array([[1, 0, 0, 1, 0, 1, 0, -2, 0, 0, 1, 3, 0, 0, 0, 1] for _ in range(1024)]) - points = np.array([[i - 3, i + 1, i + 2] for i in range(128 * 1024)]) + poses = np.array([[1, 0, 0, 1, 0, 1, 0, -2, 0, 0, 1, 3, 0, 0, 0, 1] for _ in range(4)]) + points = np.array([[i - 3, i + 1, i + 2] for i in range(2 * 4)]) num_poses = poses.shape[0] pts_per_pose = int(points.shape[0] / poses.shape[0]) - # c++ py-binding dewarp poses_reshaped = poses.reshape(num_poses, 4, 4) points_reshaped = points.reshape(pts_per_pose, num_poses, 3) - dewarped_points_c_plus = dewarp(points_reshaped, poses_reshaped) - dewarped_point_py = pu.dewarp(points_reshaped, column_poses=poses_reshaped) - np.testing.assert_allclose(dewarped_points_c_plus, dewarped_point_py, rtol=1e-5, atol=1e-8) + expected_points = np.array([[[-2, -1, 5], + [-1, 0, 6], + [0, 1, 7], + [1, 2, 8]], + + [[2, 3, 9], + [3, 4, 10], + [4, 5, 11], + [5, 6, 12]]]) + + dewarped_points = dewarp(points_reshaped, poses_reshaped) + + np.testing.assert_allclose(dewarped_points.shape, (2, 4, 3)) + np.testing.assert_allclose(dewarped_points, expected_points, rtol=1e-5, atol=1e-8) diff --git a/python/tests/test_viz_core.py b/python/tests/test_viz_core.py index ba23cf0d..1e5d448a 100644 --- a/python/tests/test_viz_core.py +++ b/python/tests/test_viz_core.py @@ -313,3 +313,91 @@ def test_lidarscanviz_update_sets_default_view_mode(): viz = LidarScanViz([meta], MockPointViz()) viz.update([scan]) assert viz._model._cloud_mode_name == ChanField.REFLECTIVITY + + +def test_lidarscanviz_cycle_img_mode_updates_images(): + """ + When paused, cycling the img mode should update the images. + """ + class MockImage: + """ + TWS 20241007: this kind of mock would be unnecessary + if we bind the various properties of the viz::Image class. + """ + def __init__(self): + self.set_image_called = False + + def clear_palette(self, *args, **kwargs): + pass + + def set_image(self, *args, **kwargs): + self.set_image_called = True + + meta = SensorInfo.from_default(LidarMode.MODE_1024x10) + scan = LidarScan(meta) + assert ChanField.REFLECTIVITY in scan.fields # a precondition + viz = LidarScanViz([meta], MockPointViz()) + viz.update([scan]) + + # precondition: the default image modes + assert viz._model._image_mode_names == [ChanField.REFLECTIVITY, ChanField.NEAR_IR] + + viz._model._sensors[0]._images = [MockImage(), MockImage()] + assert not viz._model._sensors[0]._images[0].set_image_called + assert not viz._model._sensors[0]._images[1].set_image_called + + viz.cycle_img_mode(0) + + assert viz._model._image_mode_names == [ChanField.SIGNAL, ChanField.NEAR_IR] + assert viz._model._sensors[0]._images[0].set_image_called + assert viz._model._sensors[0]._images[1].set_image_called + + +def test_lidarscanviz_highlight_second_doesnt_crash_with_no_scan(): + """ + Highlighting the second return should not cause a crash if there is no scan yet. + """ + meta = SensorInfo.from_default(LidarMode.MODE_1024x10) + viz = LidarScanViz([meta], MockPointViz()) + # check preconditions + assert not viz._scans + viz.update_flags_mode(LidarScanViz.FlagsMode.HIGHLIGHT_SECOND) + + +def test_lidarscanviz_highlight_second_doesnt_crash_with_no_second_return(): + """ + Highlighting the second return should not cause a crash if there is no second return. + """ + meta = SensorInfo.from_default(LidarMode.MODE_1024x10) + scan = LidarScan(meta) + # check preconditions + assert ChanField.REFLECTIVITY in scan.fields + assert ChanField.REFLECTIVITY2 not in scan.fields + viz = LidarScanViz([meta], MockPointViz()) + viz.update([scan]) + viz.update_flags_mode(LidarScanViz.FlagsMode.HIGHLIGHT_SECOND) + + +def test_osd_state(): + """Hiding the help should reset the OSD to its previous state.""" + meta = SensorInfo.from_default(LidarMode.MODE_1024x10) + viz = LidarScanViz([meta], MockPointViz()) + assert viz.osd_state == LidarScanViz.OsdState.DEFAULT + viz.toggle_osd() + assert viz.osd_state == LidarScanViz.OsdState.NONE + + # if state is DEFAULT, toggling help twice resets to DEFAULT + viz.toggle_osd() + assert viz.osd_state == LidarScanViz.OsdState.DEFAULT + viz.toggle_help() + assert viz.osd_state == LidarScanViz.OsdState.HELP + viz.toggle_help() + assert viz.osd_state == LidarScanViz.OsdState.DEFAULT + + # if state is NONE, toggling help twice resets to NONE + viz.toggle_osd() + assert viz.osd_state == LidarScanViz.OsdState.NONE + viz.toggle_help() + assert viz.osd_state == LidarScanViz.OsdState.HELP + viz.toggle_help() + assert viz.osd_state == LidarScanViz.OsdState.NONE diff --git a/python/tests/test_viz_utils.py b/python/tests/test_viz_utils.py index c4a14036..1efa9786 100644 --- a/python/tests/test_viz_utils.py +++ b/python/tests/test_viz_utils.py @@ -358,7 +358,7 @@ def test_viz_util_traj_eval_scans_poses(test_data_dir, cloud_scan = viz.Cloud(scan.h * scan.w) xyz = xyzlut(scan.field(client.ChanField.RANGE)) # TODO hao: remove the input_row_major - xyz = dewarp(xyz, scan.pose, input_row_major=False) + xyz = dewarp(xyz, scan.pose) cloud_scan.set_xyz(xyz) cloud_scan.set_key(key) else: diff --git a/tests/osfs/OS-0-128_v3.0.1_1024x10_20241017_141645.osf b/tests/osfs/OS-0-128_v3.0.1_1024x10_20241017_141645.osf new file mode 100644 index 00000000..3cf048b8 Binary files /dev/null and b/tests/osfs/OS-0-128_v3.0.1_1024x10_20241017_141645.osf differ diff --git a/tests/point_viz_test.cpp b/tests/point_viz_test.cpp index 84bb24b9..ab522898 100644 --- a/tests/point_viz_test.cpp +++ b/tests/point_viz_test.cpp @@ -6,8 +6,8 @@ using namespace ouster::viz; TEST(PointViz, window_coordinates_to_world_coordinates) { WindowCtx ctx; - ctx.viewport_width = 600; - ctx.viewport_height = 400; + ctx.window_width = ctx.viewport_width = 600; + ctx.window_height = ctx.viewport_height = 400; auto world = ctx.normalized_coordinates(0, 0); EXPECT_DOUBLE_EQ(world.first, -1.5); EXPECT_DOUBLE_EQ(world.second, 1); @@ -20,8 +20,8 @@ TEST(PointViz, window_coordinates_to_world_coordinates) { TEST(PointViz, window_coordinates_to_image_pixel) { WindowCtx ctx; - ctx.viewport_width = 400; - ctx.viewport_height = 300; + ctx.window_width = ctx.viewport_width = 400; + ctx.window_height = ctx.viewport_height = 300; Image img; auto pixel = img.window_coordinates_to_image_pixel(ctx, 0, 0); EXPECT_FALSE(pixel.has_value()); // by default, images have no width/height @@ -32,7 +32,7 @@ TEST(PointViz, window_coordinates_to_image_pixel) { img.set_image(w, h, img_data); img.set_position(-1.3333333333, 1.3333333333, -1, 1); pixel = - img.window_coordinates_to_image_pixel(ctx, 0, ctx.viewport_height - 1); + img.window_coordinates_to_image_pixel(ctx, 0, ctx.window_height - 1); EXPECT_EQ(pixel->first, 0); EXPECT_EQ(pixel->second, 2); @@ -40,21 +40,20 @@ TEST(PointViz, window_coordinates_to_image_pixel) { EXPECT_EQ(pixel->first, 0); EXPECT_EQ(pixel->second, 0); - pixel = - img.window_coordinates_to_image_pixel(ctx, ctx.viewport_width - 1, 0); + pixel = img.window_coordinates_to_image_pixel(ctx, ctx.window_width - 1, 0); EXPECT_EQ(pixel->first, 3); EXPECT_EQ(pixel->second, 0); - pixel = img.window_coordinates_to_image_pixel(ctx, ctx.viewport_width - 1, - ctx.viewport_height - 1); + pixel = img.window_coordinates_to_image_pixel(ctx, ctx.window_width - 1, + ctx.window_height - 1); EXPECT_EQ(pixel->first, 3); EXPECT_EQ(pixel->second, 2); } TEST(PointViz, image_pixel_to_window_coordinates) { WindowCtx ctx; - ctx.viewport_width = 400; - ctx.viewport_height = 300; + ctx.window_width = ctx.viewport_width = 400; + ctx.window_height = ctx.viewport_height = 300; Image img; constexpr int w = 4; @@ -63,7 +62,7 @@ TEST(PointViz, image_pixel_to_window_coordinates) { img.set_image(w, h, img_data); img.set_position(-1.3333333333, 1.3333333333, -1, 1); auto pixel = - img.window_coordinates_to_image_pixel(ctx, 0, ctx.viewport_height - 1); + img.window_coordinates_to_image_pixel(ctx, 0, ctx.window_height - 1); EXPECT_EQ(pixel->first, 0); EXPECT_EQ(pixel->second, 2);