From c8f994f408c09b07e1b0aac9702016a6cf40e3a3 Mon Sep 17 00:00:00 2001 From: wangpeng2-mechmind Date: Wed, 24 Jun 2026 10:53:12 +0800 Subject: [PATCH] samples: update mecheye_python_samples to SDK 2.6.0 --- area_scan_3d_camera/README.md | 9 +- .../advanced/capture_depth_source_2d_image.py | 44 ++++ .../calibration/hand_eye_calibration.py | 10 + .../set_point_cloud_processing_parameters.py | 48 +++++ .../util/set_scanning_parameters.py | 102 ++++++++- profiler/Advanced/transform_point_cloud.py | 13 +- ...igger_multiple_profilers_simultaneously.py | 194 ++++++++++++------ ...rigger_with_external_device_and_encoder.py | 8 +- ...ger_with_external_device_and_fixed_rate.py | 3 + .../trigger_with_software_and_encoder.py | 8 +- .../trigger_with_software_and_fixed_rate.py | 7 +- profiler/README.md | 14 +- .../Util/handle_nan_and_negative_in_depth.py | 5 +- 13 files changed, 380 insertions(+), 85 deletions(-) create mode 100644 area_scan_3d_camera/advanced/capture_depth_source_2d_image.py diff --git a/area_scan_3d_camera/README.md b/area_scan_3d_camera/README.md index 560f2ae..20ea41a 100644 --- a/area_scan_3d_camera/README.md +++ b/area_scan_3d_camera/README.md @@ -29,7 +29,12 @@ The samples marked with `(OpenCV)` require [OpenCV](https://pypi.org/project/ope Set multiple exposure times, and then obtain and save the untextured and textured point clouds. * [capture_point_cloud_with_normals](https://github.com/MechMindRobotics/mecheye_python_samples/tree/master/area_scan_3d_camera/basic/capture_point_cloud_with_normals.py) Calculate normals and save the untextured and textured point clouds with normals. + * [save_virtual_device](https://github.com/MechMindRobotics/mecheye_python_samples/tree/master/area_scan_3d_camera/basic/save_virtual_device.py) + Save the data acquired by the camera as a virtual device file. * **advanced** + * [capture_depth_source_2d_image](https://github.com/MechMindRobotics/mecheye_python_samples/tree/master/area_scan_3d_camera/advanced/capture_depth_source_2d_image.py) `(OpenCV)` + Obtain and save the 2D image from the depth source camera. + > Note: This sample is only applicable to camera models that provide the depth-source 2D image, such as Mech-Eye ULTRA M. * [convert_depth_map_to_point_cloud](https://github.com/MechMindRobotics/mecheye_python_samples/tree/master/area_scan_3d_camera/advanced/convert_depth_map_to_point_cloud.py) Generate a point cloud from the depth map and save the point cloud. * [multiple_cameras_capture_sequentially](https://github.com/MechMindRobotics/mecheye_python_samples/tree/master/area_scan_3d_camera/advanced/multiple_cameras_capture_sequentially.py) `(OpenCV)` @@ -38,11 +43,13 @@ The samples marked with `(OpenCV)` require [OpenCV](https://pypi.org/project/ope Obtain and save 2D images, depth maps, and point clouds simultaneously from multiple cameras. * [capture_periodically](https://github.com/MechMindRobotics/mecheye_python_samples/tree/master/area_scan_3d_camera/advanced/capture_periodically.py) `(OpenCV)` Obtain and save 2D images, depth maps, and point clouds periodically for the specified duration from a camera. + * [warm_up](https://github.com/MechMindRobotics/mecheye_python_samples/tree/master/area_scan_3d_camera/advanced/warm_up.py) `(OpenCV)` + Warm up the device. * [mapping_2d_image_to_depth_map](https://github.com/MechMindRobotics/mecheye_python_samples/tree/master/area_scan_3d_camera/advanced/mapping_2d_image_to_depth_map.py) Generate untextured and textured point clouds from a masked 2D image and a depth map. * [render_depth_map](https://github.com/MechMindRobotics/mecheye_python_samples/tree/master/area_scan_3d_camera/advanced/render_depth_map.py) `(OpenCV)` Obtain and save the depth map rendered with the jet color scheme. - * [transform_point_cloud](https://github.com/MechMindRobotics/mecheye_python_samples/tree/master/area_scan_3d_camera/Advanced/transform_point_cloud.py) + * [transform_point_cloud](https://github.com/MechMindRobotics/mecheye_python_samples/tree/master/area_scan_3d_camera/advanced/transform_point_cloud.py) Obtain and save the point clouds in the custom reference frame. * [set_parameters_of_laser_cameras](https://github.com/MechMindRobotics/mecheye_python_samples/tree/master/area_scan_3d_camera/advanced/set_parameters_of_laser_cameras.py) Set the parameters specific to laser cameras. diff --git a/area_scan_3d_camera/advanced/capture_depth_source_2d_image.py b/area_scan_3d_camera/advanced/capture_depth_source_2d_image.py new file mode 100644 index 0000000..cf552ef --- /dev/null +++ b/area_scan_3d_camera/advanced/capture_depth_source_2d_image.py @@ -0,0 +1,44 @@ +# With this sample, you can obtain and save the depth-source 2D image. + +import cv2 + +from mecheye.shared import * +from mecheye.area_scan_3d_camera import * +from mecheye.area_scan_3d_camera_utils import find_and_connect + + +class CaptureDepthSource2DImage(object): + def __init__(self): + self.camera = Camera() + + def capture_depth_source_2d_image(self): + frame_2d = Frame2D() + error = self.camera.capture_depth_source_2d(frame_2d) + if not error.is_ok(): + show_error(error) + return + + if frame_2d.color_type() == ColorTypeOf2DCamera_Monochrome: + image_2d = frame_2d.get_gray_scale_image() + file_name = "DepthSourceGrayScale2DImage.png" + print("Capture and save the depth-source gray scale 2D image: {}".format(file_name)) + elif frame_2d.color_type() == ColorTypeOf2DCamera_Color: + image_2d = frame_2d.get_color_image() + file_name = "DepthSourceColor2DImage.png" + print("Capture and save the depth-source color 2D image: {}".format(file_name)) + else: + print("The acquired depth-source 2D image has an unsupported color type.") + return + + cv2.imwrite(file_name, image_2d.data()) + + def main(self): + if find_and_connect(self.camera): + self.capture_depth_source_2d_image() + self.camera.disconnect() + print("Disconnected from the camera successfully.") + + +if __name__ == '__main__': + a = CaptureDepthSource2DImage() + a.main() \ No newline at end of file diff --git a/area_scan_3d_camera/calibration/hand_eye_calibration.py b/area_scan_3d_camera/calibration/hand_eye_calibration.py index 71b799c..1a6986c 100644 --- a/area_scan_3d_camera/calibration/hand_eye_calibration.py +++ b/area_scan_3d_camera/calibration/hand_eye_calibration.py @@ -120,6 +120,7 @@ def input_command(self): print("T: obtain the 2D image with feature recognition result") print("A: enter the current robot pose") print("C: calculate extrinsic parameters") + print("F: obtain the first corner of current image") while True: user_input = input() if user_input == "P" or user_input == "p": @@ -130,6 +131,8 @@ def input_command(self): return "A" elif user_input == "C" or user_input == "c": return "C" + elif user_input == "F" or user_input == "f": + return "F" else: print("Unknown command, please enter correct command type") @@ -289,6 +292,13 @@ def calibrate(self): print("The extrinsic parameters are:") print(camera_to_base.to_string()) save_extrinsic_parameters(camera_to_base.to_string()) + elif command == "F": + corner = PointXYZ() + error_status = self.calibration.extract_current_image_first_corner( + self.camera, corner) + show_error(error_status) + if error_status.is_ok(): + print(f"The first corner is: {corner.x:.3f}, {corner.y:.3f}, {corner.z:.3f}") def main(self): if not find_and_connect(self.camera): diff --git a/area_scan_3d_camera/util/set_point_cloud_processing_parameters.py b/area_scan_3d_camera/util/set_point_cloud_processing_parameters.py index a5da6aa..5ded4bf 100644 --- a/area_scan_3d_camera/util/set_point_cloud_processing_parameters.py +++ b/area_scan_3d_camera/util/set_point_cloud_processing_parameters.py @@ -25,6 +25,24 @@ def set_point_cloud_processing_parameters(self): error = current_user_set.set_enum_value(PointCloudNoiseRemoval.name, PointCloudNoiseRemoval.Value_Normal) show_error(error) + error = current_user_set.set_enum_value(PointCloudDepthSmooth.name, + PointCloudDepthSmooth.Value_Normal) + show_error(error) + error = current_user_set.set_enum_value(PointCloudDepthHoleFilling.name, + PointCloudDepthHoleFilling.Value_Normal) + show_error(error) + error = current_user_set.set_enum_value(PointCloudDepthSurfaceNoiseRemoval.name, + PointCloudDepthSurfaceNoiseRemoval.Value_Normal) + show_error(error) + error = current_user_set.set_enum_value(PointCloudPhaseClusterOutlierRemoval.name, + PointCloudPhaseClusterOutlierRemoval.Value_L5) + show_error(error) + error = current_user_set.set_enum_value(PointCloudSpuriousPhaseRemoval.name, + PointCloudSpuriousPhaseRemoval.Value_Normal) + show_error(error) + error = current_user_set.set_enum_value(PointCloudLargeGradNoiseRemoval.name, + PointCloudLargeGradNoiseRemoval.Value_Normal) + show_error(error) error = current_user_set.set_enum_value(PointCloudOutlierRemoval.name, PointCloudOutlierRemoval.Value_Normal) show_error(error) @@ -38,6 +56,24 @@ def set_point_cloud_processing_parameters(self): error, noise_removal = current_user_set.get_enum_value_string( PointCloudNoiseRemoval.name) show_error(error) + error, depth_smooth = current_user_set.get_enum_value_string( + PointCloudDepthSmooth.name) + show_error(error) + error, depth_hole_filling = current_user_set.get_enum_value_string( + PointCloudDepthHoleFilling.name) + show_error(error) + error, depth_surface_noise_removal = current_user_set.get_enum_value_string( + PointCloudDepthSurfaceNoiseRemoval.name) + show_error(error) + error, phase_cluster_outlier_removal = current_user_set.get_enum_value_string( + PointCloudPhaseClusterOutlierRemoval.name) + show_error(error) + error, spurious_phase_removal = current_user_set.get_enum_value_string( + PointCloudSpuriousPhaseRemoval.name) + show_error(error) + error, large_grad_noise_removal = current_user_set.get_enum_value_string( + PointCloudLargeGradNoiseRemoval.name) + show_error(error) error, outlier_removal = current_user_set.get_enum_value_string( PointCloudOutlierRemoval.name) show_error(error) @@ -49,6 +85,18 @@ def set_point_cloud_processing_parameters(self): "(0: Off, 1: Weak, 2: Normal, 3: Strong)") print("Point Cloud Noise Removal:", noise_removal, "(0: Off, 1: Weak, 2: Normal, 3: Strong)") + print("Depth Smooth:", depth_smooth, + "(0: Off, 1: Weak, 2: Normal, 3: Strong)") + print("Depth Hole Filling:", depth_hole_filling, + "(0: Off, 1: Weak, 2: Normal, 3: Strong)") + print("Depth Surface Noise Removal:", depth_surface_noise_removal, + "(0: Off, 1: Weak, 2: Normal, 3: Strong)") + print("Phase Cluster Outlier Removal:", phase_cluster_outlier_removal, + "(0: Off, 1: L1, 2: L2, 3: L3, 4: L4, 5: L5, 6: L6, 7: L7, 8: L8, 9: L9, 10: L10)") + print("Spurious Phase Removal:", spurious_phase_removal, + "(0: Off, 1: Weak, 2: Normal, 3: Strong)") + print("Large Gradient Noise Removal:", large_grad_noise_removal, + "(0: Off, 1: Weak, 2: Normal, 3: Strong)") print("Point Cloud Outlier Removal:", outlier_removal, "(0: Off, 1: Weak, 2: Normal, 3: Strong)") print("Point Cloud Edge Preservation:", edge_preservation, diff --git a/area_scan_3d_camera/util/set_scanning_parameters.py b/area_scan_3d_camera/util/set_scanning_parameters.py index ab62c83..d4c48f9 100644 --- a/area_scan_3d_camera/util/set_scanning_parameters.py +++ b/area_scan_3d_camera/util/set_scanning_parameters.py @@ -21,8 +21,17 @@ def set_scanning_parameters(self): error, user_set_name = current_user_set.get_name() show_error(error) print("\ncurrent_user_set: " + user_set_name) + error, available_params = current_user_set.get_available_parameter_names() + show_error(error) # Set the exposure times for acquiring depth information. + if Scanning3DExposureCount.name in available_params: + error = current_user_set.set_int_value(Scanning3DExposureCount.name, 1) + show_error(error) + error, exposure_count = current_user_set.get_int_value( + Scanning3DExposureCount.name) + show_error(error) + print("\n3D scanning exposure count: {}".format(exposure_count)) error = current_user_set.set_float_array_value( Scanning3DExposureSequence.name, [5]) # error = current_user_set.set_float_array_value( @@ -36,6 +45,64 @@ def set_scanning_parameters(self): for i in exposure_sequence: print("3D scanning exposure time: {}".format(i)) + # Some models provide exposure group parameters. Use "GroupExposureSelector" to select the + # target group, and then set the group exposure time, gain, and power level. + if (Scanning3DGroupExposureSelector.name in available_params and + Scanning3DGroupExposureTime.name in available_params and + Scanning3DGroupGain.name in available_params and + Scanning3DGroupDlpPowerLevel.name in available_params): + error = current_user_set.set_enum_value( + Scanning3DGroupExposureSelector.name, + Scanning3DGroupExposureSelector.Value_Exposure1) + show_error(error) + error = current_user_set.set_float_value( + Scanning3DGroupExposureTime.name, 10) + show_error(error) + error = current_user_set.set_float_value( + Scanning3DGroupGain.name, 2.0) + show_error(error) + error = current_user_set.set_int_value( + Scanning3DGroupDlpPowerLevel.name, 80) + show_error(error) + + error, group_exposure_time = current_user_set.get_float_value( + Scanning3DGroupExposureTime.name) + show_error(error) + error, group_gain = current_user_set.get_float_value( + Scanning3DGroupGain.name) + show_error(error) + error, group_dlp_power_level = current_user_set.get_int_value( + Scanning3DGroupDlpPowerLevel.name) + show_error(error) + print("Group Exposure1: exposure time {}, gain {}, DLP power level {}.".format( + group_exposure_time, group_gain, group_dlp_power_level)) + + error = current_user_set.set_enum_value( + Scanning3DGroupExposureSelector.name, + Scanning3DGroupExposureSelector.Value_Exposure2) + show_error(error) + error = current_user_set.set_float_value( + Scanning3DGroupExposureTime.name, 5) + show_error(error) + error = current_user_set.set_float_value( + Scanning3DGroupGain.name, 0.0) + show_error(error) + error = current_user_set.set_int_value( + Scanning3DGroupDlpPowerLevel.name, 60) + show_error(error) + + error, group_exposure_time = current_user_set.get_float_value( + Scanning3DGroupExposureTime.name) + show_error(error) + error, group_gain = current_user_set.get_float_value( + Scanning3DGroupGain.name) + show_error(error) + error, group_dlp_power_level = current_user_set.get_int_value( + Scanning3DGroupDlpPowerLevel.name) + show_error(error) + print("Group Exposure2: exposure time {}, gain {}, DLP power level {}.".format( + group_exposure_time, group_gain, group_dlp_power_level)) + # Set the ROI for the depth map and point cloud, and then obtain the parameter values for checking. roi = ROI(0, 0, 500, 500) error = current_user_set.set_roi_value(Scanning3DROI.name, roi) @@ -81,7 +148,7 @@ def set_scanning_parameters(self): # show_error(error) # The following models also provide a "FlashAcquisitionMode" when using the flash exposure - # mode: DEEP, DEEP-GL, LSR S/L/XL, LSR S-GL/L-GL/XL-GL, PRO XS/S/M, PRX XS-GL/S-GL/M-GL, NANO, NANO-GL, NANO ULTRA, NANO ULTRA-GL. Uncomment the following lines to set + # mode: DEEP, DEEP-GL, LSR S/L/XL, LSR S-GL/L-GL/XL-GL, PRO XS/S/M, PRO XS-GL/S-GL/M-GL, NANO, NANO-GL, NANO ULTRA, NANO ULTRA-GL. Uncomment the following lines to set # the "FlashAcquisitionMode" parameter to "Responsive". # flash_acquisition_mode_2d=Scanning2DFlashAcquisitionMode.Value_Responsive # error = current_user_set.set_enum_value( @@ -112,11 +179,38 @@ def set_scanning_parameters(self): # setting the "Scan2DGain" parameter when the exposure mode is set to fixed exposure, auto # exposure, HDR, or flash mode, and the flash acquisition mode is set to # responsive. - # print("\n2D image gain: ", scan2DGain)value(Scanning2DGain.name,2.0)) - # show_error(error)et_float_value(Scanning2DGain.name) + # error = current_user_set.set_float_value(Scanning2DGain.name, 2.0) + # show_error(error) # error, scan2DGain = current_user_set.get_float_value(Scanning2DGain.name) - # show_error(current_user_set.set_float_value(Scanning2DGain.name,2.6)) + # show_error(error) + # print("\n2D image gain: ", scan2DGain) + + # The following parameters are only available on some models. Uncomment to set and read values. + # error = current_user_set.set_float_value( + # Scanning2DPatternRoleGain.name, 2.0) + # show_error(error) + # error, pattern_role_gain = current_user_set.get_float_value( + # Scanning2DPatternRoleGain.name) + # show_error(error) + # print("\n2D pattern role gain: {}".format(pattern_role_gain)) + # + # error = current_user_set.set_float_value( + # Scanning2DFlashGain.name, 2.0) + # show_error(error) + # error, flash_gain = current_user_set.get_float_value( + # Scanning2DFlashGain.name) + # show_error(error) + # print("\n2D flash gain: {}".format(flash_gain)) + # + # error = current_user_set.set_int_value( + # Scanning2DFlashPowerLevel.name, 80) + # show_error(error) + # error, flash_power_level = current_user_set.get_int_value( + # Scanning2DFlashPowerLevel.name) + # show_error(error) + # print("\n2D flash power level: {} %".format(flash_power_level)) + error, exposure_mode_2d = current_user_set.get_enum_value_string( Scanning2DExposureMode.name) show_error(error) diff --git a/profiler/Advanced/transform_point_cloud.py b/profiler/Advanced/transform_point_cloud.py index bddd8f2..81e419e 100644 --- a/profiler/Advanced/transform_point_cloud.py +++ b/profiler/Advanced/transform_point_cloud.py @@ -25,6 +25,14 @@ def convert_batch_to_point_cloud_with_transformation(profile_batch: ProfileBatch if not error.is_ok(): show_error(error) return + + error, scan_distance = user_set.get_float_value(ScanDistance.name) + if not error.is_ok(): + show_error(error) + return + print( + f"Current Y-axis resolution: {y_resolution} um, scan distance: {scan_distance} um.") + # # Uncomment the following line for custom Y Unit # y_resolution = get_trigger_interval_distance() @@ -64,8 +72,11 @@ def set_parameters(self): show_error(self.user_set.set_float_value( SoftwareTriggerRate.name, 1000)) - # Set the "Scan Line Count" parameter (the number of lines to be scanned) to 1600 + # Set the "Scan Line Count" parameter (the number of lines to be scanned) to 1600 show_error(self.user_set.set_int_value(ScanLineCount.name, 1600)) + # Set the "Travel Speed" parameter to 100 mm/s. This value is used to calculate the + # Y-axis resolution and scan distance when line scan is triggered at a fixed rate. + show_error(self.user_set.set_float_value(TravelSpeed.name, 100.0)) error, self.data_width = self.user_set.get_int_value( DataPointsPerProfile.name) diff --git a/profiler/Advanced/trigger_multiple_profilers_simultaneously.py b/profiler/Advanced/trigger_multiple_profilers_simultaneously.py index 1834f60..380da6e 100644 --- a/profiler/Advanced/trigger_multiple_profilers_simultaneously.py +++ b/profiler/Advanced/trigger_multiple_profilers_simultaneously.py @@ -12,57 +12,65 @@ def set_timed_exposure(user_set: UserSet, exposure_time: int): # Set the exposure mode to timed - show_error(user_set.set_enum_value( - ExposureMode.name, ExposureMode.Value_Timed)) + show_error(user_set.set_enum_value(ExposureMode.name, ExposureMode.Value_Timed)) # Set the exposure time to {exposure_time} μs - show_error(user_set.set_int_value( - ExposureTime.name, exposure_time)) + show_error(user_set.set_int_value(ExposureTime.name, exposure_time)) -def set_hdr_exposure(user_set: UserSet, exposure_time: int, proportion1: float, proportion2: float, first_threshold: float, second_threshold: float): +def set_hdr_exposure( + user_set: UserSet, + exposure_time: int, + proportion1: float, + proportion2: float, + first_threshold: float, + second_threshold: float, +): # Set the "Exposure Mode" parameter to "HDR" - show_error(user_set.set_enum_value( - ExposureMode.name, ExposureMode.Value_HDR)) + show_error(user_set.set_enum_value(ExposureMode.name, ExposureMode.Value_HDR)) # Set the total exposure time to {exposure_time} μs - show_error(user_set.set_int_value( - ExposureTime.name, exposure_time)) + show_error(user_set.set_int_value(ExposureTime.name, exposure_time)) # Set the proportion of the first exposure phase to {proportion1}% - show_error(user_set.set_float_value( - HdrExposureTimeProportion1.name, proportion1)) + show_error(user_set.set_float_value(HdrExposureTimeProportion1.name, proportion1)) # Set the proportion of the first + second exposure phases to {proportion2}% (that is, the # second exposure phase occupies {proportion2 - proportion1}%, and the third exposure phase # occupies {100 - proportion2}% of the total exposure time) - show_error(user_set.set_float_value( - HdrExposureTimeProportion2.name, proportion2)) + show_error(user_set.set_float_value(HdrExposureTimeProportion2.name, proportion2)) # Set the first threshold to {first_threshold}. This limits the maximum grayscale value to # {first_threshold} after the first exposure phase is completed. - show_error(user_set.set_float_value( - HdrFirstThreshold.name, first_threshold)) + show_error(user_set.set_float_value(HdrFirstThreshold.name, first_threshold)) # Set the second threshold to {second_threshold}. This limits the maximum grayscale value to # {second_threshold} after the second exposure phase is completed. - show_error(user_set.set_float_value( - HdrSecondThreshold.name, second_threshold)) + show_error(user_set.set_float_value(HdrSecondThreshold.name, second_threshold)) -def set_encoder_trigger(user_set: UserSet, trigger_direction: int, trigger_signal_counting_mode: int, trigger_interval: int): +def set_encoder_trigger( + user_set: UserSet, + trigger_direction: int, + trigger_signal_counting_mode: int, + trigger_interval: int, +): # Set the trigger source to Encoder - show_error(user_set.set_enum_value( - LineScanTriggerSource.name, LineScanTriggerSource.Value_Encoder)) + show_error( + user_set.set_enum_value( + LineScanTriggerSource.name, LineScanTriggerSource.Value_Encoder + ) + ) # Set the encoder trigger direction to {trigger_direction} - show_error(user_set.set_enum_value( - EncoderTriggerDirection.name, trigger_direction)) + show_error(user_set.set_enum_value(EncoderTriggerDirection.name, trigger_direction)) # Set the encoder signal counting mode to be {trigger_signal_counting_mode} - show_error(user_set.set_enum_value( - EncoderTriggerSignalCountingMode.name, trigger_signal_counting_mode)) + show_error( + user_set.set_enum_value( + EncoderTriggerSignalCountingMode.name, trigger_signal_counting_mode + ) + ) # Set the encoder trigger interval to {trigger_interval} - show_error(user_set.set_int_value( - EncoderTriggerInterval.name, trigger_interval)) + show_error(user_set.set_int_value(EncoderTriggerInterval.name, trigger_interval)) def set_parameters(profiler: Profiler): @@ -92,19 +100,25 @@ def set_parameters(profiler: Profiler): # set_hdr_exposure(user_set, 100, 40, 80, 10, 60) # Set the "Data Acquisition Trigger Source" parameter to "Software" - show_error(user_set.set_enum_value( - DataAcquisitionTriggerSource.name, DataAcquisitionTriggerSource.Value_Software)) + show_error( + user_set.set_enum_value( + DataAcquisitionTriggerSource.name, + DataAcquisitionTriggerSource.Value_Software, + ) + ) # # Set the "Data Acquisition Trigger Source" parameter to "External" # show_error(user_set.set_enum_value( # DataAcquisitionTriggerSource.name, DataAcquisitionTriggerSource.Value_External)) # Set the "Line Scan Trigger Source" parameter to "Fixed rate" - show_error(user_set.set_enum_value( - LineScanTriggerSource.name, LineScanTriggerSource.Value_FixedRate)) + show_error( + user_set.set_enum_value( + LineScanTriggerSource.name, LineScanTriggerSource.Value_FixedRate + ) + ) # Set the " Software Trigger Rate" to 1000 Hz - show_error(user_set.set_float_value( - SoftwareTriggerRate.name, 1000)) + show_error(user_set.set_float_value(SoftwareTriggerRate.name, 1000)) # # Set the "Line Scan Trigger Source" parameter to "Encoder" # # Set the (encoder) "Trigger Direction" parameter to "Both" @@ -115,12 +129,14 @@ def set_parameters(profiler: Profiler): # Set the "Scan Line Count" parameter (the number of lines to be scanned) to 1600 show_error(user_set.set_int_value(ScanLineCount.name, 1600)) + # Set the "Travel Speed" parameter to 100 mm/s. This value is used to calculate the + # Y-axis resolution and scan distance when line scan is triggered at a fixed rate. + show_error(user_set.set_float_value(TravelSpeed.name, 100.0)) # Set the "Laser Power" parameter to 100 show_error(user_set.set_int_value(LaserPower.name, 100)) # Set the "Analog Gain" parameter to "Gain_2" - show_error(user_set.set_enum_value( - AnalogGain.name, AnalogGain.Value_Gain_2)) + show_error(user_set.set_enum_value(AnalogGain.name, AnalogGain.Value_Gain_2)) # Set the "Digital Gain" parameter to 0 show_error(user_set.set_int_value(DigitalGain.name, 0)) @@ -131,8 +147,9 @@ def set_parameters(profiler: Profiler): # Set the "Maximum Laser Line Width" parameter to 20 show_error(user_set.set_int_value(MaxLaserLineWidth.name, 20)) # Set the "Spot Selection" parameter to "Strongest" - show_error(user_set.set_enum_value( - SpotSelection.name, SpotSelection.Value_Strongest)) + show_error( + user_set.set_enum_value(SpotSelection.name, SpotSelection.Value_Strongest) + ) # This parameter is only effective for firmware 2.2.1 and below. For firmware 2.3.0 and above, # adjustment of this parameter does not take effect. @@ -155,31 +172,43 @@ def set_parameters(profiler: Profiler): set to "Median", the "Median Filter Window Size" parameter needs to be set. This parameter controls the window size of median filter. """ - show_error(user_set.set_enum_value( - Filter.name, Filter.Value_Mean)) + show_error(user_set.set_enum_value(Filter.name, Filter.Value_Mean)) # Set the "Mean Filter Window Size" parameter to 2 - show_error(user_set.set_enum_value( - MeanFilterWindowSize.name, MeanFilterWindowSize.Value_WindowSize_2)) + show_error( + user_set.set_enum_value( + MeanFilterWindowSize.name, MeanFilterWindowSize.Value_WindowSize_2 + ) + ) - error, data_width = user_set.get_int_value( - DataPointsPerProfile.name) + error, data_width = user_set.get_int_value(DataPointsPerProfile.name) show_error(error) - error, capture_line_count = user_set.get_int_value( - ScanLineCount.name) + error, capture_line_count = user_set.get_int_value(ScanLineCount.name) show_error(error) error, data_acquisition_trigger_source = user_set.get_enum_value( - DataAcquisitionTriggerSource.name) + DataAcquisitionTriggerSource.name + ) show_error(error) - is_software_trigger = data_acquisition_trigger_source == DataAcquisitionTriggerSource.Value_Software + is_software_trigger = ( + data_acquisition_trigger_source == DataAcquisitionTriggerSource.Value_Software + ) return data_width, capture_line_count, is_software_trigger -def acquire_profile_data(profiler: Profiler, profile_batch: ProfileBatch, is_software_trigger: bool, capture_line_count: int, data_width: int) -> bool: +def acquire_profile_data( + profiler: Profiler, + profile_batch: ProfileBatch, + is_software_trigger: bool, + capture_line_count: int, + data_width: int, +) -> bool: """ Call start_acquisition() to enter the laser profiler into the acquisition ready status, and then call trigger_software() to start the data acquisition (triggered by software). + + For external trigger mode, a retrieval timeout mechanism is used to prevent the sample + from getting stuck in an infinite loop when the external trigger signal stops. """ print("Start data acquisition.") status = profiler.start_acquisition() @@ -189,23 +218,52 @@ def acquire_profile_data(profiler: Profiler, profile_batch: ProfileBatch, is_sof if is_software_trigger: status = profiler.trigger_software() - if (not status.is_ok()): + if not status.is_ok(): show_error(status) return False profile_batch.clear() profile_batch.reserve(capture_line_count) + # Maximum number of consecutive retrieval timeouts before giving up (external trigger + # may stop at any time). Each timeout cycle sleeps 200 ms, so 15 retries ≈ 3 s. + MAX_EMPTY_RETRIEVAL_COUNT = 15 + empty_retrieval_count = 0 + while profile_batch.height() < capture_line_count: # Retrieve the profile data batch = ProfileBatch(data_width) status = profiler.retrieve_batch_data(batch) if status.is_ok(): - profile_batch.append(batch) + if batch.height() > 0: + profile_batch.append(batch) + empty_retrieval_count = 0 + else: + empty_retrieval_count += 1 sleep(0.2) else: - show_error(status) - return False + # In external trigger mode, a retrieval timeout is expected when the trigger + # signal stops. Only treat timeout as an empty retrieval; report all other + # errors (e.g., device disconnect, parameter error) immediately. + if ( + not is_software_trigger + and status.error_code == ErrorStatus.MMIND_STATUS_TIMEOUT_ERROR + ): + empty_retrieval_count += 1 + sleep(0.2) + else: + show_error(status) + return False + + if ( + not is_software_trigger + and empty_retrieval_count >= MAX_EMPTY_RETRIEVAL_COUNT + ): + print( + "No more data received from the external trigger source. " + "Stopping acquisition with the data obtained so far." + ) + break print("Stop data acquisition.") status = profiler.stop_acquisition() @@ -214,12 +272,12 @@ def acquire_profile_data(profiler: Profiler, profile_batch: ProfileBatch, is_sof return status.is_ok() -def save_depth_and_intensity(profile_batch: ProfileBatch, depth_file_name: str, intensity_file_name: str): - cv2.imwrite(depth_file_name, - profile_batch.get_depth_map().data()) +def save_depth_and_intensity( + profile_batch: ProfileBatch, depth_file_name: str, intensity_file_name: str +): + cv2.imwrite(depth_file_name, profile_batch.get_depth_map().data()) print(f"Saved the depth map to {depth_file_name}") - cv2.imwrite(intensity_file_name, - profile_batch.get_intensity_image().data()) + cv2.imwrite(intensity_file_name, profile_batch.get_intensity_image().data()) print(f"Saved the intensity image to {intensity_file_name}") @@ -229,22 +287,26 @@ def capture_task(ip_address: str, sensor_sn: str): return # Set the parameters of the profiler and get necessary parameters - data_width, capture_line_count, is_software_trigger = set_parameters( - profiler) + data_width, capture_line_count, is_software_trigger = set_parameters(profiler) # Create a ProfileBatch object to store the profile data profile_batch = ProfileBatch(data_width) - if not acquire_profile_data(profiler, profile_batch, is_software_trigger, capture_line_count, data_width): + if not acquire_profile_data( + profiler, profile_batch, is_software_trigger, capture_line_count, data_width + ): return # Check if the batch's data is complete if profile_batch.check_flag(ProfileBatch.BatchFlag_Incomplete): - print(f"Part of the batch's data of profiler {sensor_sn} is lost, the number of valid profiles is:", - profile_batch.valid_height()) + print( + f"Part of the batch's data of profiler {sensor_sn} is lost, the number of valid profiles is:", + profile_batch.valid_height(), + ) # Save the depth map and intensity image save_depth_and_intensity( - profile_batch, f"depth_{sensor_sn}.png", f"intensity_{sensor_sn}.png") + profile_batch, f"depth_{sensor_sn}.png", f"intensity_{sensor_sn}.png" + ) profiler.disconnect() print("Disconnected from the Mech-Eye Profiler successfully") @@ -268,7 +330,9 @@ def connect_device_and_capture(self): profiler_info = ProfilerInfo() show_error(profiler.get_profiler_info(profiler_info)) process = multiprocessing.Process( - target=capture_task, args=(profiler_info.ip_address, profiler_info.sensor_sn)) + target=capture_task, + args=(profiler_info.ip_address, profiler_info.sensor_sn), + ) processes.append(process) process.start() @@ -279,7 +343,7 @@ def main(self): self.connect_device_and_capture() -if __name__ == '__main__': - multiprocessing.set_start_method('spawn') +if __name__ == "__main__": + multiprocessing.set_start_method("spawn") a = TriggerMultipleProfilersSimultaneously() a.main() diff --git a/profiler/Basic/trigger_with_external_device_and_encoder.py b/profiler/Basic/trigger_with_external_device_and_encoder.py index 9289862..0613386 100644 --- a/profiler/Basic/trigger_with_external_device_and_encoder.py +++ b/profiler/Basic/trigger_with_external_device_and_encoder.py @@ -65,7 +65,7 @@ def set_hdr_exposure(self, exposure_time: int, proportion1: float, proportion2: show_error(self.user_set.set_float_value( HdrSecondThreshold.name, second_threshold)) - def set_encoder_trigger(self, trigger_direction: int, trigger_signal_counting_mode: int, trigger_interval: int): + def set_encoder_trigger(self, trigger_direction: int, trigger_signal_counting_mode: int, trigger_interval: int, encoder_resolution: float): # Set the trigger source to Encoder show_error(self.user_set.set_enum_value( LineScanTriggerSource.name, LineScanTriggerSource.Value_Encoder)) @@ -78,6 +78,9 @@ def set_encoder_trigger(self, trigger_direction: int, trigger_signal_counting_mo # Set the encoder trigger interval to {trigger_interval} show_error(self.user_set.set_int_value( EncoderTriggerInterval.name, trigger_interval)) + # Set the encoder resolution (unit: μm) + show_error(self.user_set.set_float_value( + EncoderResolution.name, encoder_resolution)) def set_parameters(self): self.user_set = self.profiler.current_user_set() @@ -119,8 +122,9 @@ def set_parameters(self): # Set the (encoder) "Trigger Direction" parameter to "Both" # Set the (encoder) "Trigger Signal Counting Mode" parameter to "1×" # Set the (encoder) "Trigger Interval" parameter to 10 + # Set the "Encoder Resolution" parameter to 5 μm self.set_encoder_trigger(EncoderTriggerDirection.Value_Both, - EncoderTriggerSignalCountingMode.Value_Multiple_1, 10) + EncoderTriggerSignalCountingMode.Value_Multiple_1, 10, 5) # Set the "Scan Line Count" parameter (the number of lines to be scanned) to 1600 show_error(self.user_set.set_int_value(ScanLineCount.name, 1600)) diff --git a/profiler/Basic/trigger_with_external_device_and_fixed_rate.py b/profiler/Basic/trigger_with_external_device_and_fixed_rate.py index f591a50..b8b8d97 100644 --- a/profiler/Basic/trigger_with_external_device_and_fixed_rate.py +++ b/profiler/Basic/trigger_with_external_device_and_fixed_rate.py @@ -110,6 +110,9 @@ def set_parameters(self): # Set the "Scan Line Count" parameter (the number of lines to be scanned) to 1600 show_error(self.user_set.set_int_value(ScanLineCount.name, 1600)) + # Set the "Travel Speed" parameter to 100 mm/s. This value is used to calculate the + # Y-axis resolution and scan distance when line scan is triggered at a fixed rate. + show_error(self.user_set.set_float_value(TravelSpeed.name, 100.0)) # Set the "Laser Power" parameter to 100 show_error(self.user_set.set_int_value(LaserPower.name, 100)) diff --git a/profiler/Basic/trigger_with_software_and_encoder.py b/profiler/Basic/trigger_with_software_and_encoder.py index b3d8a31..45499c0 100644 --- a/profiler/Basic/trigger_with_software_and_encoder.py +++ b/profiler/Basic/trigger_with_software_and_encoder.py @@ -65,7 +65,7 @@ def set_hdr_exposure(self, exposure_time: int, proportion1: float, proportion2: show_error(self.user_set.set_float_value( HdrSecondThreshold.name, second_threshold)) - def set_encoder_trigger(self, trigger_direction: int, trigger_signal_counting_mode: int, trigger_interval: int): + def set_encoder_trigger(self, trigger_direction: int, trigger_signal_counting_mode: int, trigger_interval: int, encoder_resolution: float): # Set the trigger source to Encoder show_error(self.user_set.set_enum_value( LineScanTriggerSource.name, LineScanTriggerSource.Value_Encoder)) @@ -78,6 +78,9 @@ def set_encoder_trigger(self, trigger_direction: int, trigger_signal_counting_mo # Set the encoder trigger interval to {trigger_interval} show_error(self.user_set.set_int_value( EncoderTriggerInterval.name, trigger_interval)) + # Set the encoder resolution (unit: μm) + show_error(self.user_set.set_float_value( + EncoderResolution.name, encoder_resolution)) def set_parameters(self): self.user_set = self.profiler.current_user_set() @@ -119,8 +122,9 @@ def set_parameters(self): # Set the (encoder) "Trigger Direction" parameter to "Both" # Set the (encoder) "Trigger Signal Counting Mode" parameter to "1×" # Set the (encoder) "Trigger Interval" parameter to 10 + # Set the "Encoder Resolution" parameter to 5 μm self.set_encoder_trigger(EncoderTriggerDirection.Value_Both, - EncoderTriggerSignalCountingMode.Value_Multiple_1, 10) + EncoderTriggerSignalCountingMode.Value_Multiple_1, 10, 5) # Set the "Scan Line Count" parameter (the number of lines to be scanned) to 1600 show_error(self.user_set.set_int_value(ScanLineCount.name, 1600)) diff --git a/profiler/Basic/trigger_with_software_and_fixed_rate.py b/profiler/Basic/trigger_with_software_and_fixed_rate.py index 0a09d3d..6aff6f4 100644 --- a/profiler/Basic/trigger_with_software_and_fixed_rate.py +++ b/profiler/Basic/trigger_with_software_and_fixed_rate.py @@ -108,8 +108,11 @@ def set_parameters(self): show_error(self.user_set.set_float_value( SoftwareTriggerRate.name, 1000)) - # Set the "Scan Line Count" parameter (the number of lines to be scanned) to 1600 + # Set the "Scan Line Count" parameter (the number of lines to be scanned) to 1600 show_error(self.user_set.set_int_value(ScanLineCount.name, 1600)) + # Set the "Travel Speed" parameter to 100 mm/s. This value is used to calculate the + # Y-axis resolution and scan distance when line scan is triggered at a fixed rate. + show_error(self.user_set.set_float_value(TravelSpeed.name, 100.0)) # Set the "Laser Power" parameter to 100 show_error(self.user_set.set_int_value(LaserPower.name, 100)) @@ -213,7 +216,7 @@ def acquire_profile_data_using_callback(self) -> bool: show_error(self.user_set.set_int_value( CallbackRetrievalTimeout.name, 60000)) - self.callback = CustomAcquisitionCallback(self.data_width) + self.callback = CustomAcquisitionCallback(self.data_width).__disown__() # Register the callback function status = self.profiler.register_acquisition_callback( diff --git a/profiler/README.md b/profiler/README.md index fd85929..8189f50 100644 --- a/profiler/README.md +++ b/profiler/README.md @@ -17,18 +17,18 @@ The samples marked with `(OpenCV)` require [OpenCV](https://pypi.org/project/ope * **Basic** * [trigger_with_software_and_fixed_rate](https://github.com/MechMindRobotics/mecheye_python_samples/tree/master/profiler/Basic/trigger_with_software_and_fixed_rate.py) `(OpenCV)` - Trigger data acquisition with signals input from software, trigger line scans at a fixed rate, and then retrieve and save the acquired data. + Trigger data acquisition with the software + fixed rate method, and then retrieve and save the acquired data. * [trigger_with_external_device_and_fixed_rate](https://github.com/MechMindRobotics/mecheye_python_samples/tree/master/profiler/Basic/trigger_with_external_device_and_fixed_rate.py) `(OpenCV)` - Trigger data acquisition with signals input from the external device, trigger line scans at a fixed rate, and then retrieve and save the acquired data. + Trigger data acquisition with the external + fixed rate method, and then retrieve and save the acquired data. * [trigger_with_software_and_encoder](https://github.com/MechMindRobotics/mecheye_python_samples/tree/master/profiler/Basic/trigger_with_software_and_encoder.py) `(OpenCV)` - Trigger data acquisition with signals input from software, trigger line scans with signals input from the encoder, and then retrieve and save the acquired data. + Trigger data acquisition with the software + encoder method, and then retrieve and save the acquired data. * [trigger_with_external_device_and_encoder](https://github.com/MechMindRobotics/mecheye_python_samples/tree/master/profiler/Basic/trigger_with_external_device_and_encoder.py) `(OpenCV)` - Trigger data acquisition with signals input from the external device, trigger line scans with signals input from the encoder, and then retrieve and save the acquired data. + Trigger data acquisition with the external + encoder method, and then retrieve and save the acquired data. * [trigger_non_stop_acquisition](https://github.com/MechMindRobotics/mecheye_csharp_samples/tree/master/profiler/Basic/trigger_non_stop_acquisition.py) `(OpenCV)` - Trigger non-stop acquisition, and then retrieve and save the acquired data. + Trigger a continuous scan of the target object, and then retrieve and save the acquired data. * **Advanced** * [trigger_multiple_profilers_simultaneously](https://github.com/MechMindRobotics/mecheye_python_samples/tree/master/profiler/Advanced/trigger_multiple_profilers_simultaneously.py) `(OpenCV)` - Trigger multiple laser profilers to acquire data asynchronously and retrieve the acquired data. + Trigger multiple laser profilers to acquire data asynchronously, and then retrieve and save the acquired data. * [blind_spot_filtering](https://github.com/MechMindRobotics/mecheye_python_samples/tree/master/profiler/Advanced/blind_spot_filtering.py) `(OpenCV)` Detect and remove the false data caused by blind spots and obtain the filtered profile data. * [noise_removal](https://github.com/MechMindRobotics/mecheye_python_samples/tree/master/profiler/Advanced/noise_removal.py) `(OpenCV)` @@ -49,7 +49,7 @@ The samples marked with `(OpenCV)` require [OpenCV](https://pypi.org/project/ope * [print_profiler_status](https://github.com/MechMindRobotics/mecheye_python_samples/tree/master/profiler/Util/print_profiler_status.py) Obtain and print the laser profiler's information, such as model, serial number, firmware version, and temperatures. * [handle_nan_and_negative_in_depth](https://github.com/MechMindRobotics/mecheye_python_samples/tree/master/profiler/Util/handle_nan_and_negative_in_depth.py) `(OpenCV)` - Trigger data acquisition and handle NaN and negative values in depth data. + Detect and process NaN and negative values in the depth data, and then generate and save the intensity image and depth map. ## Run the Samples diff --git a/profiler/Util/handle_nan_and_negative_in_depth.py b/profiler/Util/handle_nan_and_negative_in_depth.py index fcbb83c..487bb65 100644 --- a/profiler/Util/handle_nan_and_negative_in_depth.py +++ b/profiler/Util/handle_nan_and_negative_in_depth.py @@ -110,8 +110,11 @@ def set_parameters(self): show_error(self.user_set.set_float_value( SoftwareTriggerRate.name, 1000)) - # Set the "Scan Line Count" parameter (the number of lines to be scanned) to 1600 + # Set the "Scan Line Count" parameter (the number of lines to be scanned) to 1600 show_error(self.user_set.set_int_value(ScanLineCount.name, 1600)) + # Set the "Travel Speed" parameter to 100 mm/s. This value is used to calculate the + # Y-axis resolution and scan distance when line scan is triggered at a fixed rate. + show_error(self.user_set.set_float_value(TravelSpeed.name, 100.0)) # Set the "Laser Power" parameter to 100 show_error(self.user_set.set_int_value(LaserPower.name, 100))