HDevGuidelines
The HDevGuidelines are a collection of proven guidelines, rules, and best practices for coding in the HDevelop language for HALCON. The name and structure are loosely inspired by the CppCoreGuidelines.
The HALCON Core Guidelines are a result of many years of using MVTec HALCON at Heindl Solutions to develop demanding machine vision applications.
Concatenation of tuples#
Tuples are concatenated and entries are appended at the end of a tuple with the []
syntax.
* Initialize empty tuple 'SomeTuple' SomeTuple := [] * Append an entry to 'SomeTuple' SomeTuple := [SomeTuple, 'someEntry'] * Append another entry to 'SomeTuple' SomeTuple := [SomeTuple, 'someOtherEntry']
Example#
read_image (Images, ['fabrik','monkey']) count_obj (Images, NumberImages) Features := [] for Index0 := 0 to NumberImages-1 by 1 select_obj (Images, Image, Index0+1) FindSomeFeatureValue (Image, Value) Features := [Features, Value] endfor
Concatenation of objects#
Objects are concatenated with concat_obj
.
* Initialize empty object 'ManyObjects' gen_empty_obj (ManyObjects) * Append an object to 'ManyObjects' concat_obj (ManyObjects, SomeOtherObject, ManyObjects) * Append another object to 'ManyObjects' concat_obj (ManyObjects, SomeOtherObject2, ManyObjects)
Example#
gen_empty_obj (Images) for Index := 0 to 4 by 1 GrabSomeImage (Image1) concat_obj (Images, Image1, Images) endfor
True/false variants#
Iterate over two variants of the same code that once uses 'true'
and in the second iteration uses 'false'
.
IndexTrueFalse := 0 TrueFalseTuple := ['false','true'][IndexTrueFalse]
Example#
for IndexTrueFalse := 0 to 1 by 1 affine_trans_image (Image, ImageAffinTrans, HomMat2DRotate, 'constant', ['false','true'][IndexTrueFalse]) * ... endfor
Hint#
Although uncommon in most programming languages, HALCON frequently uses the strings 'true'
and 'false'
for many—though not all—operator calls.
Find matches#
Use the operator tuple_find
or the inline operation find
together with the check find(Tuple, ToFind) > -1
to determine whether an element is contained in a container tuple.
Contains := (find(Tuple, ToFind) > -1)
Reason#
In using the check find(Tuple, ToFind) > -1
instead of e.g. find(Tuple, ToFind) != -1
care is taken of the case where the first argument is the empty tuple. In this case, we want the check for existence (for any entry in ToFind
) to return false
. But a comparision with != -1
would return true
, as the result of find(Tuple, ToFind)
is the empty tuple.
Example 1#
IndexA := find([], 'needle') // IndexA = [] IndexB := find(['hay', 'stack'], 'needle') // IndexB = -1
Example 2#
In this example the if
branch in the following example is not taken, as expected:
Files := 'blister/blister_0'+[1:6] read_image (Images, Files) * ImagesToIgnore := [0,2,3] ImagesToIgnore := [] for Index0 := 0 to |Files|-1 by 1 if (find(ImagesToIgnore, Index0) > -1) continue endif select_obj (Images, Image, Index0+1) dev_display (Image) * ... endfor
Sorting, Selecting entries in tuples or objects using indices#
Entries of a tuple can be selected by index via the []
operator.
Text := ['Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consetetur', 'sadipscing'] Indices := [0, 1, 5] SelectedText := Text[Indices] * SelectedText = ['Lorem', 'ipsum', 'consetetur']
Reason#
After e.g. sorting according to a specific criteria in one tuple, entries of this and other tuples and objects are selected via the []
operator (for tuples) or the select_obj
operator (for objects).
Example#
gen_random_region (RegionRandom, 128, 128) get_region_runs (RegionRandom, Row, ColumnBegin, ColumnEnd) * Sort all chord variables by chord length Indices := sort_index (ColumnEnd - ColumnBegin + 1) RowSort := Row[Indices] ColumnBeginSort := ColumnBegin[Indices] ColumnEndSort := ColumnEnd[Indices] * Get entry with maximum chord length MaxIndex := sort_index (ColumnEnd - ColumnBegin + 1) MaxIndex := MaxIndex[|MaxIndex|-1] // index of maximum element MaxRow := Row[MaxIndex] MaxColumnBegin := ColumnBegin[MaxIndex] MaxColumnEnd := ColumnEnd[MaxIndex] * Get entry with minimum chord length MinIndex := sort_index (ColumnEnd - ColumnBegin + 1) MinIndex := MinIndex[0] // index of minimum element MinRow := Row[MinIndex] MinColumnBegin := ColumnBegin[MinIndex] MinColumnEnd := ColumnEnd[MinIndex] * Select objects read_image (Images, ['fabrik', 'monkey', 'alpha1']) Indices0 := [0,2] select_obj (Images, ImagesSelected, Indices0+1) // fabrik, alpha1
Hint#
If you know that all values are non-negative, finding the index of the maximum element can be expressed more concisely:
MaxIndex := sort_index (Length) MaxIndex := MaxIndex[|MaxIndex|-1] * more concisely: MaxIndex := sort_index (-Length) MaxIndex := MaxIndex[0] // index of maximum element
Selecting entries in multiple tuples or objects using a mask#
Entries of a tuple are selected by a mask via the select_mask
operator or the select_obj
operator in conjunction with the select_mask
operator.
Text := ['Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consetetur', 'sadipscing'] SelectMask := [1, 1, 0, 0, 0, 1, 0] SelectedText := select_mask(Text, SelectMask) * SelectedText = ['Lorem', 'ipsum', 'consetetur']
Reason#
After selecting according to a specific criteria in one tuple, entries of this and other tuples and objects are selected via the select_mask
operator (for tuples) or the select_mask_obj
helper procedure (for objects).
Example#
Filenames := ['monkey', 'mreut', 'fabrik', 'alpha1', 'alpha2'] read_image (Images, Filenames) threshold (Images, Region, 128, 255) area_center (Region, Area, __, __) Mask := Area [>] 200000 * Mask := [0,1,0,1,1] SelectedFilenames := select_mask(Filenames,Mask) * SelectedFilenames = ['mreut', 'alpha1', 'alpha2'] select_mask_obj (Images, ObjectSelected, Mask) * ObjectSelected = <image 'mreut'>, <image 'alpha1'>, <image 'alpha2'>
Always use 0-based indices#
Many object-related operators use 1-based indices, in contrast to tuples and vectors, which use 0-based indices. However, 0-based indices should be used consistently until one of these 1-based operators is called. The most notable of these are select_obj
, copy_obj
, and access_channel
.
select_obj (Image, ObjectSelected, Indices0 + 1)
Reason#
Reasoning about indexing becomes significantly easier when a consistent index base is used throughout the HALCON script. Since most programming languages use 0-based indexing, it is recommended to adopt this convention for both tuples and objects in HALCON code. To maintain compatibility with HALCON’s 1-based object indexing, simply add 1 in the relevant operator calls—only where necessary.
Example#
list_image_files ('bin_switch', 'default', [], ImageFiles) read_image (Images, ImageFiles) * Train with 60% of the images TrainIndices0 := [0:int(0.60*|ImageFiles|)] TrainFiles := ImageFiles[TrainIndices0] select_obj (Images, TrainImages, TrainIndices0 + 1) * Test with 20% of the images TestIndices0 := [int(0.60*|ImageFiles|)+1:0.80*|ImageFiles|] TestFiles := ImageFiles[TestIndices0] select_obj (Images, TestImages, TestIndices0 + 1) * Cross validate with another 20% of the images CvIndices0 := [int(0.80*|ImageFiles|)+1:|ImageFiles|-1] CvFiles := ImageFiles[CvIndices0] select_obj (Images, CvImages, CvIndices0 + 1) for Index0 := 0 to |TestFiles|-1 by 1 Filename := TestFiles[Index0] select_obj (TestImages, Image, Index0 + 1) * ... endfor
Alternating sequences#
Alternating sequences come in handy to start with an algorithm in the middle and then making your way towards the lower and upper bound.
* Generate an alternating sequence e.g. [2, 3, 1, 4, 0] for NumberImages = 5 NumberImages := 5 Sequence := [1 : NumberImages] AltSeq := int(pow(-1,Sequence)) * (Sequence/2) + ((NumberImages-1)/2) * AltSeq = [2, 3, 1, 4, 0] * This construct also handles the edge case with 0 elements NumberImages := 0 Sequence := [1 : NumberImages] AltSeq := int(pow(-1,Sequence)) * (Sequence/2) + ((NumberImages-1)/2) * AltSeq = []
Example#
Filenames := 'ampoules/ampoules_' + [1:8]$'.2' + '.png' read_image (Image, Filenames) Sequence := [1 : |Filenames|] AltSeq := int(pow(-1,Sequence)) * (Sequence/2) + ((|Filenames|-1)/2) * Starting from the center, get the match with a (dummy) distance 0 BestIndex0 := [] for Index0 := 0 to |AltSeq|-1 by 1 CurrentIndex0 := AltSeq[Index0] get_distance (CurrentIndex0, Distance) // output: Distance if (Distance = 0) BestIndex0 := CurrentIndex0 break endif endfor
How to stay in procedure during debugging#
To inspect intermediate results, it is often helpful to stay in a procedure without returning. A breakpoint alone may not be sufficient, as it can be unintentionally skipped by repeatedly pressing F5. To resume execution after an infinite loop, manually set the program counter (PC) by clicking the line number of the statement you wish to execute next.
while (true) stop () endwhile
Example#
* complicated_procedure (: : : Result) Debug := false * Set Debug to true only when in HDevelop (not in HDevEngine nor in exported code) SystemInformations := [] dev_get_system ('engine_environment', SystemInformations) if (SystemInformations = 'HDevelop') Debug := true endif * ... complicated_algorithm1 (IntermedResult1) if (Debug) * dev_display (...) stop () endif complicated_algorithm2 (IntermedResult2) if (Debug) * dev_display (...) stop () endif complicated_algorithm3 (IntermedResult3) if (Debug) * dev_display (...) stop () endif Result := [IntermedResult1, IntermedResult2, IntermedResult3] * Inspect IntermedResult1, or IntermedResult2, or IntermedResult3 if (Debug) while (true) stop () endwhile endif return ()
Hint#
IF the procedure did not work as expected or you want to check some details, you can reset the procedure execution with the shortcut Shift+F2. All input arguments to the procedure call will be reset and the next execution will start at the beginning of the procedure.
How to generate left-hand zeros (e.g. 003)#
Convert numbers to strings with left-hand zeros where needed using the $'.DIGITS'
syntax (where DIGITS has to be replaced by the intended number of digits).
Indices := [1, 2, 3, 10, 999] IndicesStr := Indices$'.2' * IndicesStr = ['01', '02', '03', '10', '999']
Example#
Filenames := 'ampoules/ampoules_' + [1:22]$'.2' + '.png' * Filenames = ['ampoules/ampoules_01.png', 'ampoules/ampoules_02.png', ...] read_image (Image, Filenames)
Convert strings with optional leading zeros to numbers#
When converting strings to numbers, be cautious if the numbers may contain leading zeros — for example, '025'
. For a robust conversion, it's important to trim any optional whitespace from the beginning or end of the string and to remove any leading zeros. Otherwise, the leading zero may cause tuple_number
to interpret the value as an octal number. This can be particularly problematic, as the code may appear to work correctly in most cases, but fail in others.
Num := number('001') // = 1 ✅ Num := number('002') // = 2 ✅ Num := number('009') // = '009' ❌ (kept as string) Num := number('010') // = 8 (!) ❌ (wrong number)
For a robust conversion of strings to numbers, use:
Number := number(regexp_replace(NumberStr,'^\\s*0*(.+?)\\s*$', '$1'))
Example#
ImageFiles := ['cool_001deg', 'room_025deg', 'redhot_666deg', 'whitehot_1333deg'] TemperatureStr := regexp_match(ImageFiles, '^\\w+_(.*)deg') * TemperatureStr = ['001', '025', '666', '1333'] TemperatureERROR := number(TemperatureStr) * TemperatureERROR := [1, 21, 666, 1333] * ^^^^ error: expected: 25 * correct: TemperatureDeg := number(regexp_replace(TemperatureStr,'^\\s*0*(.+?)\\s*$', '$1')) TemperatureKelvin := TemperatureDeg + 273
Clear output parameters of procedures#
It is often good practice to explicitly clear output parameters in HDevelop or HDevEngine procedures. Without this step, output parameters may behave inconsistently between scripted HDevelop code and exported code (e.g., C++, C#, etc.).
procedure TestWithClear (: : : MyTuple, MyVector)
MyTuple := []
MyVector.clear()
* ...
Reason#
HDevelop (tested with 12.0.2) clears output tuples and output vectors on first write access. If there is no write access, the original value of the output variable will not be modified. If there is a write operation, in HDevelop the tuple/vector is cleared before the first write operation, while in exported code the referenced output tuple/vector is used without clearing. So the behavior of exported and non-exported code is different. To avoid confusion (and hard to find bugs), clear your output tuples/vectors in the first lines of your procedure.
Example#
Local procedures#
procedure TestWithoutClear (: : : MyTuple, MyVector) MyTuple[0] := 'zero' MyTuple[1] := 'one' * MyVector.at(0) := 'zero' MyVector.at(1) := 'one' return () procedure TestWithClear (: : : MyTuple, MyVector) MyTuple := [] MyVector.clear() * MyTuple[0] := 'zero' MyTuple[1] := 'one' * MyVector.at(0) := 'zero' MyVector.at(1) := 'one' MyVector.at(0) := 'zero' MyVector.at(1) := 'one' return ()
Main procedure#
MyTuple0 := [0,1,2] MyVector0 := {0,1,2} TestWithoutClear (MyTuple0, MyVector0) # std::cout << "after TestWithoutClear, MyTuple0=" << hv_MyTuple0.ToString().Text() << std::endl; # std::cout << "after TestWithoutClear, MyVector0=" << hvec_MyVector0.ToString().Text() << std::endl; * MyVector0 in HDevelop: {['zero'],['one']} * MyVector0 in C++: {['zero'],['one'], [2]} !! * MyTuple1 := [0,1,2] MyVector1 := {0,1,2} TestWithClear (MyTuple1, MyVector1) # std::cout << "after TestWithClear, MyTuple1=" << hv_MyTuple1.ToString().Text() << std::endl; # std::cout << "after TestWithClear, MyVector1=" << hvec_MyVector1.ToString().Text() << std::endl; * MyVector1 in HDevelop: {['zero'],['one']} * MyVector1 in C++: {['zero'],['one']}
Select the largest region#
Given a number of regions, select the region with the largest area:
connection (Region, ConnectedRegions) area_center (ConnectedRegions, Area, Row, Column) * potentially unintended behaviour: might return more than a single region: select_shape (ConnectedRegions, SelectedRegions, 'area', 'and', max(Area), 'max') * better (only a single region): IndexMaxArea0 := sort_index(-Area)[0] select_obj (ConnectedRegions, SelectedRegions, IndexMaxArea0 + 1) * best (you knew this operator existed, did you?): select_shape_std (ConnectedRegions, SelectedRegions, 'max_area', -1)
String split and join#
A string can be split into substrings using the tuple_split
operator, which supports a single separator symbol or multiple possible separators. Similarly, multiple strings can be joined into one, inserting a separator between them, using the tuple_join
operator.
Split := split('Aaa#Bbb#Ccc', '#') * Split = ['Aaa', 'Bbb', 'Ccc'] * split also works with multiple possible separator symbols: Split := split('Aaa#Bbb,Ccc', ',#') * Split = ['Aaa', 'Bbb', 'Ccc'] * join (the inverse operation of split): * pre-HALCON 22.05: Names := ['Aaa', 'Bbc', 'Ccc'] JoinedNames := regexp_replace(sum(['',sum(','+Names)]),'^,','') // 'Aaa,Bbc,Ccc' * operator 'tuple_join' and inline operation 'join' is new in HALCON 22.05: JoinedNames := join(Names, ',')
All product and company names are trademarks™ or registered® trademarks of their respective holders. Use of them does not imply any affiliation with or endorsement by them.