.
Basler cameras and MVTec HALCON make for a good combination for industrial machine vision applications.
Imagine the following hardware setup: Two cameras, one on each side of a conveyor belt. There are some objects which have to be inspected from the left and from the right side simultaneously. The inspection is triggered by a GPIO of each of the cameras:
Let's start with a single camera first. This task should be straightforward to complete:
acquire_basler_00.hdev
* acquire_basler_00.hdev dev_update_off () dev_open_window (0, 0, 512, 512, 'black', WindowHandleLeft) InterfaceName := 'GigEVision2' Device := 'default' open_framegrabber (InterfaceName, 0, 0, 0, 0, 0, 0, 'default', -1, 'default', -1, 'false', 'default', Device, 0, -1, AcqHandleLeft) while (true) grab_image (ImageLeft, AcqHandleLeft) ⚡ dev_display (ImageLeft) ProcessLeft (ImageLeft, ResultLeft) dev_disp_text ('Left: ' + ResultLeft, 'window', 'top', 'left', 'black', [], []) wait_seconds (0.200) endwhile close_framegrabber (AcqHandleLeft)
Patience you must have, my young Padawan. This program works, but can be improved:
'default'
arguments of open_framegrabber
. The separate definition of InterfaceName
and Device
makes the code a bit more readable. Nevertheless, this has a major drawback in this special case: If the interface driver name is not written directly as string argument for open_framegrabber
HDevelop will not be able to provide helpful suggestions for exactly the GigEVision2
interface and a keypress of F1 will not show the specific documentation for the GigEVision2
interface. We will thus move the InterfaceName
directly into the call to open_framegrabber
.'default'
works as long as there is only a single camera attached to the interface. But in general it is a bad idea to count on this simplification: Using 'default'
, the script depends on the fact that no other device can be found, but this will break as soon as a further camera becomes somehow visible to this machine.grab_image
is almost never a good idea unless you do not care about performance at all. We will switch to grab_image_async
later.open_framegrabber (InterfaceName, …)to
open_framegrabber ('GigEVision2', …)
we can use HDevelop's autocompletion to specifically select a certain camera. Autocompleting single arguments in a complex operator call like open_framegrabber
works best in the Operator Window. This window was visible by default up to HDevelop 20.05 but nowadays this window has to be shown manually using the menu entry Window ➡ Open Qperator Window. We can now change the Device
:
* acquire_basler_01.hdev ⋮ open_framegrabber ('GigEVision2', 0, 0, 0, 0, 0, 0, 'default', -1, 'default', -1, 'false', 'default', \ ' | device:00306347d7ab_Basler_acA160020gc | unique_name:00306347d7ab_Basler_acA160020gc | interface:Esen_ITF_80fa5b400f14d0a7003effffff00 | producer:Esen', \ 0, -1, AcqHandleLeft) ⋮
We obviously didn't want to write the device name by hand here!
An appropriate next step would be to set a custom name for the camera. This can be done using Basler's "pylon Viewer". Go to pylon Viewer ➡ menu Tools ➡ pylon IP Configurator and select your camera. Then enter a Device User ID
and click on Save
. Make sure to close the camera in pylon Viewer when switching back to HDevelop.
set_framegrabber_param (AcqHandle, 'DeviceUserID', 'GigE_Cam1').
open_framegrabber ('GigEVision2', 0, 0, 0, 0, 0, 0, 'default', -1, 'default', -1, 'false', 'default', \
'GigE_Cam1', 0, -1, AcqHandleLeft)
Sometimes a more complicated device name will be suggested like' | device:GigE_Cam1 | unique_name:00306347d7ab_Basler_acA160020gc | user_name:GigE_Cam1 | interface:Esen_ITF_80fa5b400f14d0a7003effffff00 | producer:Esen'
which we can abbreviate manually to 'GigE_Cam1'
.
We do not use the 'pylon'
interface. It is outdated. The latest version is "pylon Interface for Basler Cameras Rev. 13.0.4 (x64-win64) (Release date: 2019-08-26) ".
Also remember to use 'GigEVision2'
: 'GigEVision'
is deprecated/outdated as well.
If 'GigEVision2'
is not available in your HALCON installation, you can download it from MVTec. Don't be surprised if the version is 20.11.xx: This version denotes the lowest HALCON version still supported. So for example an interface 20.11.xx is perfectly fine for HALCON version 22.11.
* acquire_basler_01.hdev dev_update_off () dev_open_window (0, 0, 512, 512, 'black', WindowHandleLeft) open_framegrabber ('GigEVision2', 0, 0, 0, 0, 0, 0, 'default', -1, 'default', -1, 'false', 'default', \ 'GigE_Cam1', 0, -1, AcqHandleLeft) set_framegrabber_param (AcqHandleLeft, 'ExposureTimeAbs', 50000) // microseconds while (true) grab_image (ImageLeft, AcqHandleLeft) ⚡ // better use grab_image_async, see comment in acquire_basler_00.hdev dev_display (ImageLeft) ProcessLeft (ImageLeft, ResultLeft) dev_disp_text ('Left: ' + ResultLeft, 'window', 'top', 'left', 'black', [], []) endwhile close_framegrabber (AcqHandleLeft)
This might work for now and the resulting acquired images are good:
Until someday the algorithm fails and the investigation shows that the acquired image now looks like:
What happened? By setting only certain parameters (like ExposureTimeAbs
in this example) there will be lots of other parameters persisted on the camera until the next power cycle. There might be another script (a legacy or test implementation) still laying around on this machine and this will sooner or later be executed by someone: This can very well change some persisted parameters on the camera! Or settings are changed directly in Pylon Viewer! Thus suddenly the Gain (for example) will be at some other random value. To make our application more robust, we recommend to always reset the camera to factory defaults directly after opening and then change the appropriate settings manually (Software Persistence):
* acquire_basler_02.hdev dev_update_off () dev_open_window (0, 0, 512, 512, 'black', WindowHandleLeft) open_framegrabber ('GigEVision2', 0, 0, 0, 0, 0, 0, 'default', -1, 'default', -1, 'false', 'default', \ 'GigE_Cam1', 0, -1, AcqHandleLeft) * reset the camera to factory settings set_framegrabber_param (AcqHandleLeft, 'UserSetSelector', 'Default') set_framegrabber_param (AcqHandleLeft, 'UserSetLoad','') * set specific parameters needed for our application set_framegrabber_param (AcqHandleLeft, 'ExposureTimeAbs', 50000) // microseconds while (true) grab_image (ImageLeft, AcqHandleLeft) ⚡ // better use grab_image_async dev_display (ImageLeft) ProcessLeft (ImageLeft, ResultLeft) dev_disp_text ('Left: ' + ResultLeft, 'window', 'top', 'left', 'black', [], []) wait_seconds (0.200) endwhile close_framegrabber (AcqHandleLeft)
We do not like alternatives like persisting settings in User Sets on the camera that much: This hides essential knowledge from the script developer, makes version control harder and causes problems if a camera has to be exchanged.
Note: It seems that a 'UserSetLoad'
does not reset all parameters. At least for our camera, after a power cycle the default for 'AcquisitionMode'
is set to 'Continuous'
, but this won't be reset with a call to 'UserSetLoad'
!
Let's have a look at the timing so far:
acquire_basler_timing.hdev
⋮ while (true) * Assume trigger signal here count_seconds (TriggerTime) grab_image (ImageLeft, AcqHandleLeft) count_seconds (Now) DurationMillis := 1000.0 * (Now - TriggerTime) dev_display (ImageLeft) dev_disp_text ('Duration: ' + DurationMillis + ' ms', 'window', 'bottom', 'left', 'black', [], []) ⋮ endwhile close_framegrabber (AcqHandleLeft)
With my test setup the duration between enter and finish of grab_image is approximately 330 milliseconds. Using Data Chunks we can see that multiple images were acquired internally by the camera. In my case the 'ChunkFramecounter'
returns 3 most of the time. This is caused by the setting 'AcquisitionMode'
which is 'Continuous'
by default.
'Continuous'
to 'SingleFrame'
:
set_framegrabber_param (AcqHandle, 'AcquisitionMode', 'SingleFrame').
but this still leaves as with an execution time of 300 milliseconds for the grab_image
operator call.
To fix this and to decrease the time that we have to wait for new images to arrive and in anticipation that we want to add a 2nd camera later grab_image
should be replaced by a combination of grab_image_start
/ grab_image_async
. This way we have a chance to execute two acquisitions simultaneously in a later step: acquire_basler_async1.hdev
⋮ grab_image_start (AcqHandleLeft, -1) while (true) * Assume trigger signal here count_seconds (TriggerTime) * ATTENTION: Do not use! Returns an outdated image! grab_image_async (ImageLeft, AcqHandleLeft, -1) ⚡ count_seconds (Now) DurationMillis := 1000.0 * (Now - TriggerTime) dev_display (ImageLeft) dev_disp_text ('Duration: ' + DurationMillis + ' ms', 'window', 'bottom', 'left', 'black', [], []) ⋮ endwhile close_framegrabber (AcqHandleLeft)
In using grab_image_async
this way, we will receive the previously taken image (in most cases)! Of course for our application, we need to take an image of the current scene as fast as possible as soon as our software event triggers the acquisition part of the script!
acquire_basler_with_softtrigger.hdev
⋮ * set specific parameters needed for our application set_framegrabber_param (AcqHandleLeft, 'ExposureTimeAbs', 50000) // microseconds * enable software triggering set_framegrabber_param (AcqHandleLeft, 'TriggerMode', 'On') set_framegrabber_param (AcqHandleLeft, 'TriggerSelector', 'FrameStart') set_framegrabber_param (AcqHandleLeft, 'TriggerSource', 'Software') dev_get_system ('engine_environment', EngineEnvironment) if (EngineEnvironment == 'HDevelop') * during development an infinite timeout would be * very inconvenient set_framegrabber_param (AcqHandleLeft, 'grab_timeout', 1000) // milliseconds endif grab_image_start (AcqHandleLeft, -1) while (true) * Assume trigger signal here count_seconds (TriggerTime) set_framegrabber_param (AcqHandleLeft, 'TriggerSoftware', '') grab_image_async (ImageLeft, AcqHandleLeft, -1) count_seconds (Now) DurationMillis := 1000.0 * (Now - TriggerTime) dev_display (ImageLeft) dev_disp_text ('Duration: ' + DurationMillis + ' ms', 'window', 'bottom', 'left', 'black', [], []) ⋮ endwhile close_framegrabber (AcqHandleLeft)
Now the time between a trigger signal and the finish of the acquisition decreases to approximately 100 milliseconds which seems to stem from 50 milliseconds exposure time and some 50 milliseconds other overhead (maybe data transfer).
grab_image_async
will return with an error. This is only activated during a development session in HDevelop.''
is ignored.grab_image_async
it is not important which of the following two acquisition modes is active:set_framegrabber_param (AcqHandle, 'AcquisitionMode', 'SingleFrame')or
set_framegrabber_param (AcqHandle, 'AcquisitionMode', 'Continuous')
Beware of some potential issues if instead of GigEVision you want to utilize USB3 cameras. Probably PylonViewer and HALCON are installed on the same machine. PylonViewer installs its own driver which appears as USB Composite Device / Basler ace USB3 Vision Camera
in Windows' Device Manager. This way, the camera can be used within Pylon Viewer.
We want to use the camera from HALCON as well. If you start up HDevelop's Image Acquisition Assistant it will detect 'USB3Vision'
devices and give you a warning like
We do NOT recommend to follow this suggestion! This would install the HALCON USB3Vision driver. Afterwards HDevelop/HALCON would be able to use the camera (good), but PylonViewer would be no more (bad).
The solution is to use the 'GenICamTL'
acquisition interface instead! Now HALCON as well as the PylonSDK can use the camera.
If you want to use the Pylon Driver but the libusbx driver is already installed (Windows):
C:\Program Files\Basler\pylon 6\Applications\Win32\share\Tools\x64
Afterwards you can use
open_framegrabber ('GenICamTL', …)
If you really want to use the libusbx driver but the Pylon Driver is already installed (Windows) you can do the following:
Afterwards you can use
open_framegrabber ('USB3Vision', …)
acquire_basler_2cam.hdev
dev_update_off () open_framegrabber ('GenICamTL', 0, 0, 0, 0, 0, 0, 'default', -1, 'default', -1, 'false', 'default', 'BaslerUSB3A', 0, -1, AcqHandleLeft) open_framegrabber ('GenICamTL', 0, 0, 0, 0, 0, 0, 'default', -1, 'default', -1, 'false', 'default', 'Basler_USB3_AceU', 0, -1, AcqHandleRight) AcqHandles := [AcqHandleLeft, AcqHandleRight] * setup all open cameras for IndexCam0 := 0 to |AcqHandles|-1 by 1 AcqH := AcqHandles[IndexCam0] dev_open_window (0, IndexCam0 * 520, 512, 512, 'black', WindowHandle1) WindowHandles[IndexCam0] := WindowHandle1 * reset the camera to factory settings set_framegrabber_param (AcqH, 'UserSetSelector', 'Default') set_framegrabber_param (AcqH, 'UserSetLoad','') * set specific parameters needed for our application set_framegrabber_param (AcqH, 'ExposureTime', 50000) // microseconds * enable software triggering set_framegrabber_param (AcqH, 'TriggerMode', 'On') set_framegrabber_param (AcqH, 'TriggerSelector', 'FrameStart') set_framegrabber_param (AcqH, 'TriggerSource', 'Software') dev_get_system ('engine_environment', EngineEnvironment) if (EngineEnvironment == 'HDevelop') * during development an infinite timeout would be * very inconvenient set_framegrabber_param (AcqH, 'grab_timeout', 1000) // milliseconds endif grab_image_start (AcqH, -1) endfor while (true) * Assume trigger signal here count_seconds (TriggerTime) for IndexCam0 := 0 to |AcqHandles|-1 by 1 AcqH := AcqHandles[IndexCam0] set_framegrabber_param (AcqH, 'TriggerSoftware', '') endfor grab_image_async (ImageLeft, AcqHandleLeft, -1) grab_image_async (ImageRight, AcqHandleRight, -1) count_seconds (Now) DurationMillis := 1000.0 * (Now - TriggerTime)endwhile close_framegrabber (AcqHandleLeft)…
dev_set_window (WindowHandles[0]) dev_display (ImageLeft) dev_set_window (WindowHandles[1]) dev_display (ImageRight) dev_set_window (WindowHandles[0]) dev_disp_text ('Duration: ' + DurationMillis + ' ms', 'window', 'bottom', 'left', 'black', [], []) ProcessLeft (ImageLeft, ResultLeft) dev_disp_text ('Left: ' + ResultLeft, 'window', 'top', 'left', 'black', [], []) dev_set_window (WindowHandles[1]) dev_disp_text ('Duration: ' + DurationMillis + ' ms', 'window', 'bottom', 'left', 'black', [], []) ProcessRight (ImageRight, ResultRight) dev_disp_text ('Right: ' + ResultRight, 'window', 'top', 'left', 'black', [], []) wait_seconds (0.200)
ExposureTimeAbs
has to be changed to ExposureTime
for our USB3Vision cameras as ExposureTimeAbs
is not supported.Software triggers do not have the strict timing properties that are often required. Software triggering introduces additional processing and communication delays, leading to variable trigger-to-exposure times. This variability can result in inconsistent image acquisition timing, especially in high-speed applications where precise synchronization is crucial. Therefore after initial experiments using the software triggering one often wants to switch to hardware-triggering mechanisms. Many industrial cameras have GPIO pins which can be used for that. In our example, we use the pin 'Line1' to trigger acquisitions:
acquire_basler_hardwaretrigger.hdev
⋮ * enable H/W trigger on Line1 input set_framegrabber_param (AcqHandle, 'TriggerMode', 'On') set_framegrabber_param (AcqHandle, 'LineSelector', 'Line1') set_framegrabber_param (AcqHandle, 'TriggerActivation', 'FallingEdge') set_framegrabber_param (AcqHandle, 'grab_timeout', 1000) // milliseconds ⋮
We have shown various typical tasks and sometimes necessary hacks for using Basler GigEVision and Basler USB3 cameras in HALCON.
Please feel free to contact us if you have any questions or need help with your project.