diff --git a/README.md b/README.md index 7230082e..93be62a0 100644 --- a/README.md +++ b/README.md @@ -65,57 +65,72 @@ this option is considered experimental/unofficial. ### Point cloud capture To quickly capture a point cloud using default settings, run the following code: - - import zivid - app = zivid.Application() - camera = app.connect_camera() - settings = zivid.Settings(acquisitions=[zivid.Settings.Acquisition()]) - frame = camera.capture(settings) - frame.save("result.zdf") +```python +import zivid +app = zivid.Application() +camera = app.connect_camera() +settings = zivid.Settings( + acquisitions=[zivid.Settings.Acquisition()], + color=zivid.Settings2D(acquisitions=[zivid.Settings2D.Acquisition()]), +) +frame = camera.capture_2d_3d(settings) +frame.save("result.zdf") +``` Instead of using the API to define capture settings, it is also possible to load them from YML files that have been exported from [Zivid Studio][zivid-studio-guide-url] or downloaded from the Zivid Knowledge Base [settings library][zivid-two-standard-settings-url]. This can be done by providing the filesystem path to such a file, for example: - settings = Settings.load("ZividTwo_Settings_2xHDR_Normal.yml") - frame = camera.capture(settings) +```python +settings = Settings.load("ZividTwo_Settings_2xHDR_Normal.yml") +frame = camera.capture_2d_3d(settings) +``` ### Point cloud data access Data can easily be accessed in the form of Numpy arrays: - import zivid - app = zivid.Application() - camera = app.connect_camera() - settings = zivid.Settings(acquisitions=[zivid.Settings.Acquisition()]) - frame = camera.capture(settings) - xyz = frame.point_cloud().copy_data("xyz") # Get point coordinates as [Height,Width,3] float array - rgba = frame.point_cloud().copy_data("rgba") # Get point colors as [Height,Width,4] uint8 array - bgra = frame.point_cloud().copy_data("bgra") # Get point colors as [Height,Width,4] uint8 array +```python +import zivid +app = zivid.Application() +camera = app.connect_camera() +settings = zivid.Settings( + acquisitions=[zivid.Settings.Acquisition()], + color=zivid.Settings2D(acquisitions=[zivid.Settings2D.Acquisition()]), +) +frame = camera.capture_2d_3d(settings) +xyz = frame.point_cloud().copy_data("xyz") # Get point coordinates as [Height,Width,3] float array +rgba = frame.point_cloud().copy_data("rgba") # Get point colors as [Height,Width,4] uint8 array +bgra = frame.point_cloud().copy_data("bgra") # Get point colors as [Height,Width,4] uint8 array +``` ### Capture Assistant Instead of manually adjusting settings, the Capture Assistant may be used to find the optimal settings for your scene: - import zivid - app = zivid.Application() - camera = app.connect_camera() - capture_assistant_params = zivid.capture_assistant.SuggestSettingsParameters() - settings = zivid.capture_assistant.suggest_settings(camera, capture_assistant_params) - frame = camera.capture(settings) - frame.save("result.zdf") +```python +import zivid +app = zivid.Application() +camera = app.connect_camera() +capture_assistant_params = zivid.capture_assistant.SuggestSettingsParameters() +settings = zivid.capture_assistant.suggest_settings(camera, capture_assistant_params) +frame = camera.capture_2d_3d(settings) +frame.save("result.zdf") +``` ### Using camera emulation If you do not have a camera, you can use the `FileCameraZivid2M70.zfc` file in the [Sample Data][zivid-download-sampledata-url] to emulate a camera. - import zivid - app = zivid.Application() - camera = app.create_file_camera("path/to/FileCameraZivid2M70.zfc") - settings = zivid.Settings(acquisitions=[zivid.Settings.Acquisition()]) - frame = camera.capture(settings) - frame.save("result.zdf") +```python +import zivid +app = zivid.Application() +camera = app.create_file_camera("path/to/FileCameraZivid2M70.zfc") +settings = zivid.Settings(acquisitions=[zivid.Settings.Acquisition()]) +frame = camera.capture_3d(settings) +frame.save("result.zdf") +``` ## Examples diff --git a/modules/zivid/camera.py b/modules/zivid/camera.py index 44117bfa..08003f90 100644 --- a/modules/zivid/camera.py +++ b/modules/zivid/camera.py @@ -48,9 +48,125 @@ def __str__(self): def __eq__(self, other): return self.__impl == other._Camera__impl + def capture_2d_3d(self, settings): + """Capture a 2D+3D frame. + + This method captures both a 3D point cloud and a 2D color image. Use this method when you want to capture + colored point clouds. This method will throw if `Settings.color` is not set. Use `capture_3d` for capturing a 3D + point cloud without a 2D color image. + + These remarks below apply for all capture functions: + + This method returns right after the acquisition of the images is complete, and the camera has stopped projecting + patterns. Therefore, after this method has returned, the camera can be moved, or objects in the scene can be + moved, or a capture from another camera with overlapping field of view can be triggered, without affecting the + point cloud. + + When this method returns, there is still remaining data to transfer from the camera to the PC, and the + processing of the final point cloud is not completed. Transfer and processing of the point cloud will continue + in the background. When you call a method on the returned `Frame` object that requires the capture to be + finished, for example `Frame.point_cloud`, that method will block until the processing is finished and the point + cloud is available. If an exception occurs after the acquisition of images is complete (during transfer or + processing of the capture), then that exception is instead thrown when you access the `Frame` object. + + The capture functions can be invoked back-to-back, for doing rapid back-to-back acquisition of multiple (2D or + 3D) captures on the same camera. This is for example useful if you want to do one high-resolution 2D capture + followed by a lower-resolution 3D capture. The acquisition of the next capture will begin quickly after + acquisition of the previous capture completed, even when there is remaining transfer and processing for the + first capture. This allows pipelining several 2D and/or 3D captures, by doing acquisition in parallel with data + transfer and processing. + + Note: There can be maximum of two in-progress uncompleted 3D (or 2D+3D) captures simultaneously per Zivid + camera. If you invoke `capture_2d_3d` or `capture_3d` when there are two uncompleted 3D captures in-progress, + then the capture will not start until the first of the in-progress 3D captures has finished all transfer and + processing. There is a similar limit of maximum two in-process 2D captures per camera. + + Capture functions can also be called on multiple cameras simultaneously. However, if the cameras have + overlapping field-of-view then you need to take consideration and sequence the capture calls to avoid the + captures interfering with each other. + + Args: + settings: Settings to use for the capture. + + Returns: + A frame containing a 3D point cloud, a 2D color image, and metadata. + + Raises: + TypeError: If the settings argument is not a Settings instance. + """ + if not isinstance(settings, Settings): + raise TypeError( + "Unsupported type for argument settings. Got {}, expected {}.".format( + type(settings), Settings.__name__ + ) + ) + return Frame(self.__impl.capture_2d_3d(_to_internal_settings(settings))) + + def capture_3d(self, settings): + """Capture a single 3D frame. + + This method is used to capture a 3D frame without a 2D color image. It ignores all color settings in the input + settings. See `capture_2d_3d` for capturing a 2D+3D frame. + + This method returns right after the acquisition of the images is complete, and the camera has stopped projecting + patterns. For more information, see the remarks section of `capture_2d_3d` above. Those remarks apply for both + 2D, 3D, and 2D+3D captures. + + Args: + settings: Settings to use for the capture. + + Returns: + A frame containing a 3D point cloud and metadata. + + Raises: + TypeError: If the settings argument is not a Settings + """ + if not isinstance(settings, Settings): + raise TypeError( + "Unsupported type for argument settings. Got {}, expected {}.".format( + type(settings), Settings.__name__ + ) + ) + return Frame(self.__impl.capture_3d(_to_internal_settings(settings))) + + def capture_2d(self, settings): + """Capture a single 2D frame. + + This method returns right after the acquisition of the images is complete, and the camera has stopped projecting + patterns. For more information, see the remarks section of `capture_2d_3d` above. Those remarks apply for both + 2D, 3D, and 2D+3D captures. + + Args: + settings: Settings to use for the capture. Can be either a Settings2D instance or a Settings instance. + If a Settings instance is provided, only the Settings.color part is used. An exception is thrown + if the Settings.color part is not set. + + Returns: + A Frame2D containing a 2D image and metadata + + Raises: + TypeError: If the settings argument is not a Settings2D or a Settings. + """ + if isinstance(settings, Settings2D): + return Frame2D(self.__impl.capture_2d(_to_internal_settings2d(settings))) + if isinstance(settings, Settings): + return Frame2D(self.__impl.capture_2d(_to_internal_settings(settings))) + raise TypeError( + "Unsupported settings type, expected: {expected_types}, got: {value_type}".format( + expected_types=" or ".join([Settings.__name__, Settings2D.__name__]), + value_type=type(settings), + ) + ) + def capture(self, settings): """Capture a single frame or a single 2D frame. + This method is deprecated as of SDK 2.14, and will be removed in the next SDK major version (3.0). Use + `capture_2d_3d` instead for capturing 2D+3D frames, use `capture_3d` for capturing 3D frames without a 2D color + image, or use `capture_2d` for capturing a 2D color image only. + + This method shares the common remarks about capture functions as found under `capture_2d_3d`. + Args: settings: Settings to be used to capture. Can be either a Settings or Settings2D instance @@ -64,7 +180,12 @@ def capture(self, settings): return Frame(self.__impl.capture(_to_internal_settings(settings))) if isinstance(settings, Settings2D): return Frame2D(self.__impl.capture(_to_internal_settings2d(settings))) - raise TypeError("Unsupported settings type: {}".format(type(settings))) + raise TypeError( + "Unsupported settings type, expected: {expected_types}, got: {value_type}".format( + expected_types=" or ".join([Settings.__name__, Settings2D.__name__]), + value_type=type(settings), + ) + ) @property def info(self): diff --git a/samples/sample_calibrate_eye_to_hand.py b/samples/sample_calibrate_eye_to_hand.py index fb57df41..84937381 100644 --- a/samples/sample_calibrate_eye_to_hand.py +++ b/samples/sample_calibrate_eye_to_hand.py @@ -19,7 +19,7 @@ def _acquire_checkerboard_frame(camera): settings = zivid.capture_assistant.suggest_settings( camera, suggest_settings_parameters ) - return camera.capture(settings) + return camera.capture_2d_3d(settings) def _enter_robot_pose(index): diff --git a/samples/sample_capture.py b/samples/sample_capture.py index 16366283..bb5abe5a 100644 --- a/samples/sample_capture.py +++ b/samples/sample_capture.py @@ -1,7 +1,7 @@ """Capture sample.""" import datetime -from zivid import Application, Settings +from zivid import Application, Settings, Settings2D def _main(): @@ -14,7 +14,14 @@ def _main(): settings.processing.filters.outlier.removal.enabled = True settings.processing.filters.outlier.removal.threshold = 5.0 - with camera.capture(settings) as frame: + settings.color = Settings2D() + settings.color.acquisitions.append(Settings2D.Acquisition()) + settings.color.acquisitions[0].aperture = 5.6 + settings.color.acquisitions[0].exposure_time = datetime.timedelta( + microseconds=8333 + ) + + with camera.capture_2d_3d(settings) as frame: frame.save("result.zdf") diff --git a/samples/sample_capture_2d.py b/samples/sample_capture_2d.py index 6e94c37c..2142e30d 100644 --- a/samples/sample_capture_2d.py +++ b/samples/sample_capture_2d.py @@ -14,7 +14,7 @@ def _main(): microseconds=10000 ) - with camera.capture(settings_2d) as frame_2d: + with camera.capture_2d(settings_2d) as frame_2d: image = frame_2d.image_rgba() image.save("result.png") diff --git a/samples/sample_capture_assistant.py b/samples/sample_capture_assistant.py index 17f9c3ad..8bcd9f15 100644 --- a/samples/sample_capture_assistant.py +++ b/samples/sample_capture_assistant.py @@ -17,7 +17,7 @@ def _main(): camera, suggest_settings_parameters ) - with camera.capture(settings) as frame: + with camera.capture_2d_3d(settings) as frame: frame.save("result.zdf") diff --git a/samples/sample_capture_async.py b/samples/sample_capture_async.py index c60f74e1..765f7415 100644 --- a/samples/sample_capture_async.py +++ b/samples/sample_capture_async.py @@ -9,7 +9,7 @@ def _capture_sync(cameras: list[zivid.Camera]) -> list[zivid.Frame]: return [ - camera.capture( + camera.capture_3d( zivid.Settings( acquisitions=[ zivid.Settings.Acquisition( diff --git a/samples/sample_capture_from_file.py b/samples/sample_capture_from_file.py index 4c47e325..344587fe 100644 --- a/samples/sample_capture_from_file.py +++ b/samples/sample_capture_from_file.py @@ -1,14 +1,17 @@ """File camera capture sample.""" -from zivid import Application, Settings +from zivid import Application, Settings, Settings2D def _main(): app = Application() with app.create_file_camera("FileCameraZivid2M70.zfc") as camera: - settings = Settings(acquisitions=[Settings.Acquisition()]) + settings = Settings( + acquisitions=[Settings.Acquisition()], + color=Settings2D(acquisitions=[Settings2D.Acquisition()]), + ) - with camera.capture(settings) as frame: + with camera.capture_2d_3d(settings) as frame: frame.save("result.zdf") diff --git a/samples/sample_capture_hdr.py b/samples/sample_capture_hdr.py index e9ff7500..820819a1 100644 --- a/samples/sample_capture_hdr.py +++ b/samples/sample_capture_hdr.py @@ -1,6 +1,8 @@ """HDR capture sample.""" -from zivid import Application, Settings +import datetime + +from zivid import Application, Settings, Settings2D def _main(): @@ -10,7 +12,15 @@ def _main(): acquisitions=[ Settings.Acquisition(aperture=aperture) for aperture in (10.90, 5.80, 2.83) - ] + ], + color=Settings2D( + acquisitions=[ + Settings2D.Acquisition( + exposure_time=datetime.timedelta(microseconds=exposure_time) + ) + for exposure_time in (1677, 5000, 10000) + ] + ), ) with camera.capture(settings) as hdr_frame: hdr_frame.save("result.zdf") diff --git a/samples/sample_presets.py b/samples/sample_presets.py index e829d332..82fd6384 100644 --- a/samples/sample_presets.py +++ b/samples/sample_presets.py @@ -28,7 +28,7 @@ def _main(): ] print("Capturing point cloud with preset '{}' ...".format(chosen_preset.name)) - with camera.capture(chosen_preset.settings) as frame: + with camera.capture_2d_3d(chosen_preset.settings) as frame: frame.save("result.zdf") settings_file = chosen_preset.name + ".yml" diff --git a/samples/sample_project_on_checkerboard.py b/samples/sample_project_on_checkerboard.py index aea83b13..835036c6 100644 --- a/samples/sample_project_on_checkerboard.py +++ b/samples/sample_project_on_checkerboard.py @@ -17,7 +17,9 @@ def _detect_checkerboard(camera): print("Detecting checkerboard...") settings = Settings() settings.acquisitions.append(Settings.Acquisition()) - with camera.capture(settings) as frame: + settings.color = Settings2D() + settings.color.acquisitions.append(Settings2D.Acquisition()) + with camera.capture_2d_3d(settings) as frame: detection_result = detect_feature_points(frame.point_cloud()) if not detection_result.valid(): raise RuntimeError("Failed to detect checkerboard") diff --git a/src/ReleasableCamera.cpp b/src/ReleasableCamera.cpp index 00cb9d87..57f447ee 100644 --- a/src/ReleasableCamera.cpp +++ b/src/ReleasableCamera.cpp @@ -17,6 +17,14 @@ namespace ZividPython .def(py::self != py::self) // NOLINT .def("disconnect", &ReleasableCamera::disconnect) .def("connect", &ReleasableCamera::connect) + .def("capture_2d_3d", &ReleasableCamera::capture2D3D) + .def("capture_3d", &ReleasableCamera::capture3D) + .def("capture_2d", + py::overload_cast(&ReleasableCamera::capture2D), + py::arg("settings_2d")) + .def("capture_2d", + py::overload_cast(&ReleasableCamera::capture2D), + py::arg("settings")) .def("capture", py::overload_cast(&ReleasableCamera::capture), py::arg("settings")) .def("capture", py::overload_cast(&ReleasableCamera::capture), diff --git a/src/include/ZividPython/ReleasableCamera.h b/src/include/ZividPython/ReleasableCamera.h index 8346a05f..5c746976 100644 --- a/src/include/ZividPython/ReleasableCamera.h +++ b/src/include/ZividPython/ReleasableCamera.h @@ -20,6 +20,10 @@ namespace ZividPython ZIVID_PYTHON_ADD_COMPARE(!=) ZIVID_PYTHON_FORWARD_0_ARGS_WRAP_RETURN(ReleasableCamera, connect) ZIVID_PYTHON_FORWARD_0_ARGS(disconnect) + ZIVID_PYTHON_FORWARD_1_ARGS_WRAP_RETURN(ReleasableFrame, capture2D3D, const Zivid::Settings &, settings) + ZIVID_PYTHON_FORWARD_1_ARGS_WRAP_RETURN(ReleasableFrame, capture3D, const Zivid::Settings &, settings) + ZIVID_PYTHON_FORWARD_1_ARGS_WRAP_RETURN(ReleasableFrame2D, capture2D, const Zivid::Settings2D &, settings2D) + ZIVID_PYTHON_FORWARD_1_ARGS_WRAP_RETURN(ReleasableFrame2D, capture2D, const Zivid::Settings &, settings) ZIVID_PYTHON_FORWARD_1_ARGS_WRAP_RETURN(ReleasableFrame, capture, const Zivid::Settings &, settings) ZIVID_PYTHON_FORWARD_1_ARGS_WRAP_RETURN(ReleasableFrame2D, capture, const Zivid::Settings2D &, settings2D) ZIVID_PYTHON_FORWARD_0_ARGS(state) diff --git a/test/test_camera_capture.py b/test/test_camera_capture.py index 8be699e1..6a1e6706 100644 --- a/test/test_camera_capture.py +++ b/test/test_camera_capture.py @@ -1,6 +1,108 @@ import pytest +def test_capture_2d_3d_one_2d_and_one_3d(shared_file_camera): + import zivid + + acquisitions3d = [zivid.Settings.Acquisition()] + acquisitions2d = [zivid.Settings2D.Acquisition()] + settings = zivid.Settings( + acquisitions=acquisitions3d, color=zivid.Settings2D(acquisitions=acquisitions2d) + ) + + with shared_file_camera.capture_2d_3d(settings) as frame: + assert frame + assert isinstance(frame, zivid.frame.Frame) + assert len(frame.settings.acquisitions) == 1 + assert frame.settings.color + assert len(frame.settings.color.acquisitions) == 1 + + +def test_capture_2d_3d_two_2d_and_one_3d(shared_file_camera): + import zivid + + acquisitions3d = [zivid.Settings.Acquisition()] + acquisitions2d = [zivid.Settings2D.Acquisition(), zivid.Settings2D.Acquisition()] + settings = zivid.Settings( + acquisitions=acquisitions3d, color=zivid.Settings2D(acquisitions=acquisitions2d) + ) + + with shared_file_camera.capture_2d_3d(settings) as frame: + assert frame + assert isinstance(frame, zivid.frame.Frame) + assert len(frame.settings.acquisitions) == 1 + assert frame.settings.color + assert len(frame.settings.color.acquisitions) == 2 + + +def test_capture_2d_3d_one_2d_and_two_3d(shared_file_camera): + import zivid + + acquisitions3d = [zivid.Settings.Acquisition(), zivid.Settings.Acquisition()] + acquisitions2d = [zivid.Settings2D.Acquisition()] + settings = zivid.Settings( + acquisitions=acquisitions3d, color=zivid.Settings2D(acquisitions=acquisitions2d) + ) + + with shared_file_camera.capture_2d_3d(settings) as frame: + assert frame + assert isinstance(frame, zivid.frame.Frame) + assert len(frame.settings.acquisitions) == 2 + assert frame.settings.color + assert len(frame.settings.color.acquisitions) == 1 + + +def test_capture_2d_3d_two_2d_and_two_3d(shared_file_camera): + import zivid + + acquisitions3d = [zivid.Settings.Acquisition(), zivid.Settings.Acquisition()] + acquisitions2d = [zivid.Settings2D.Acquisition(), zivid.Settings2D.Acquisition()] + settings = zivid.Settings( + acquisitions=acquisitions3d, color=zivid.Settings2D(acquisitions=acquisitions2d) + ) + + with shared_file_camera.capture_2d_3d(settings) as frame: + assert frame + assert isinstance(frame, zivid.frame.Frame) + assert len(frame.settings.acquisitions) == 2 + assert frame.settings.color + assert len(frame.settings.color.acquisitions) == 2 + + +def test_capture_3d_one_acquisition(shared_file_camera): + import zivid + + acquisitions = [zivid.Settings.Acquisition()] + settings = zivid.Settings(acquisitions=acquisitions) + with shared_file_camera.capture_3d(settings) as frame: + assert frame + assert isinstance(frame, zivid.frame.Frame) + assert len(frame.settings.acquisitions) == 1 + assert frame.settings.color is None + + +def test_capture_2d_with_settings_2d(shared_file_camera): + import zivid + + acquisitions = [zivid.Settings2D.Acquisition()] + settings = zivid.Settings2D(acquisitions=acquisitions) + with shared_file_camera.capture_2d(settings) as frame: + assert frame + assert isinstance(frame, zivid.Frame2D) + assert len(frame.settings.acquisitions) == 1 + + +def test_capture_2d_with_settings(shared_file_camera): + import zivid + + acquisitions = [zivid.Settings2D.Acquisition()] + settings = zivid.Settings(color=zivid.Settings2D(acquisitions=acquisitions)) + with shared_file_camera.capture_2d(settings) as frame: + assert frame + assert isinstance(frame, zivid.Frame2D) + assert len(frame.settings.acquisitions) == 1 + + def test_one_acquisition_in_list(shared_file_camera): import zivid diff --git a/test/test_settings.py b/test/test_settings.py index 5908c3e1..9a38037d 100644 --- a/test/test_settings.py +++ b/test/test_settings.py @@ -131,30 +131,49 @@ def test_default_settings(application): def test_set_color_settings(): + # pylint: disable=protected-access + from zivid import Settings, Settings2D + from zivid.settings import _to_settings, _to_internal_settings settings = Settings() assert settings.color is None + to_cpp_and_back = _to_settings(_to_internal_settings(settings)) + assert to_cpp_and_back.color is None + assert to_cpp_and_back == settings + settings.color = Settings2D() assert settings.color is not None assert isinstance(settings.color, Settings2D) assert settings.color == Settings2D() + to_cpp_and_back = _to_settings(_to_internal_settings(settings)) + assert to_cpp_and_back.color is not None + assert to_cpp_and_back == settings settings = Settings(color=Settings2D()) assert settings.color is not None assert isinstance(settings.color, Settings2D) assert settings.color == Settings2D() + to_cpp_and_back = _to_settings(_to_internal_settings(settings)) + assert to_cpp_and_back.color is not None + assert to_cpp_and_back == settings settings = Settings(color=Settings2D(acquisitions=(Settings2D.Acquisition(),))) assert settings.color is not None assert isinstance(settings.color, Settings2D) assert len(settings.color.acquisitions) == 1 + to_cpp_and_back = _to_settings(_to_internal_settings(settings)) + assert to_cpp_and_back.color is not None + assert to_cpp_and_back == settings settings = Settings() settings.color = Settings2D() settings.color.acquisitions.append(Settings2D.Acquisition()) assert len(settings.color.acquisitions) == 1 + to_cpp_and_back = _to_settings(_to_internal_settings(settings)) + assert len(to_cpp_and_back.color.acquisitions) == 1 + assert to_cpp_and_back == settings def test_set_acquisition_list():