- Published on
TwinCAT Vision Part 1
- Authors
- Name
- Kassym Dorsel
First steps into Beckhoff's newly released TC3 Vision. Setting up basic metrology using the provided functions and simplifying them using custom structs, functions and FBs.
Since Beckhoff first started talking about having vision capabilities in TwinCAT a few years ago, I've been waiting in anticipation to see what it would bring. Most of the camera inspections I work with are simple due to the geometry and also the physical setup. Currently this is achieved using smart cameras that are interfaced over some sort of industrial protocol.
Having a separate smart camera requires yet another engineering software tool to learn and use and have available and the added complexity of interfacing yet another third party device. Bringing the engineering setup into the same tool as the rest of the PLC development would streamline things.
The TwinCAT vision is API based versus having a GUI. The provided API has many filter functions (Gaussian, Laplace, Scharr, Sobel, Bilateral, etc) and can be quite powerful if your application needs it and you know your image processing. They do include basic measurement functions that should be sufficient for most basic metrology needs. These functions are LocatedEdge
, LocateEllipse
, LocationCircularEdge
, MeasureEdgeDistance
, MeasureAngleBetweenEdges
, ClosestPointBF
. They will require some basic parameterization, and doing this will likely take a little longer than having a GUI for point and click definitions.
You can find the vision documentation pdf here.
Background
Let's start with the final image with all found edges displayed. The red O is the start of the search line and the red X the end. The green lines are the best fit line while the blue X are the matched points. The original is 2500x2000px hence the small markups.
In a typical smart camera setup this would be very quick and easy to setup. Pretty much by dragging and dropping three separate measurement tools onto the image and sizing them correctly.
Initial Implementation
This initial implementation is based on Beckhoff's locate edge example code. The aforementioned PDF has step by step example on getting started on page 23. After playing around with the major parameters I was able to get all edges to be found and measured. THe documentation is very complete and it does into details in explaining the parameters using in all edge finding algorithms.
The search direction is between
aStartPoint
andaEndPoint
and the search width isnSearchLines*fSearchLineDist
px wide.
All code in this post has been tested and works. If you grab all the parts it will compile, however the project configuration and any potential variable linking will be missing.
PROGRAM MAIN
VAR
hr : HRESULT;
hrFunc : HRESULT;
fbCamera : FB_VN_SimpleCameraControl;
eState : ETcVnCameraState;
ipImageIn : ITcVnImage;
ipImageInDisp : ITcVnDisplayableImage;
ipImageRes : ITcVnImage;
ipImageResDisp : ITcVnDisplayableImage;
// result
edge209Start : ITcVnContainer;
edge209Stop : ITcVnContainer;
d209LineStart : TcVnVector4_LREAL;
d209LineStop : TcVnVector4_LREAL;
d209Dist : REAL;
edge222Start : ITcVnContainer;
edge222Stop : ITcVnContainer;
d222LineStart : TcVnVector4_LREAL;
d222LineStop : TcVnVector4_LREAL;
d222Dist : REAL;
edge201Start : ITcVnContainer;
edge201Stop : ITcVnContainer;
d201LineStart : TcVnVector4_LREAL;
d201LineStop : TcVnVector4_LREAL;
d201Dist : REAL;
// parameters
d209Start : TcVnPoint2_REAL := [245, 795];
d209Stop : TcVnPoint2_REAL := [245, 1770];
d222Start : TcVnPoint2_REAL := [760, 875];
d222Stop : TcVnPoint2_REAL := [760, 1700];
d201Start : TcVnPoint2_REAL := [115, 1290];
d201Stop : TcVnPoint2_REAL := [640, 1290];
eDirection : ETcVnEdgeDirection := TCVN_ED_LIGHT_TO_DARK;
fMinStrength : REAL := 50;
nSearchLines : UDINT := 95;
fSearchLineDist : REAL := 2;
nMaxThickness : UDINT := 10;
fSearchGap : REAL := 0;
fApproxPrecision : REAL := 0.01;
nSubpixIter : UDINT := 10;
eAlgorithm : ETcVnEdgeDetectionAlgorithm := TCVN_EDA_INTERPOLATION;
nSearchLines201 : UDINT := 181;
fSearchLineDist201 : REAL := 5;
nMaxThickness201 : UDINT := 10;
nSearchLines222 : UDINT := 85;
fSearchLineDist222 : REAL := 2;
// Watchdog
hrWD : HRESULT;
tStop : DINT := 15000; // Task time set to 20ms
tRest : DINT;
nFraction : UDINT;
// drawing
aColorGreen : TcVnVector4_LREAL := [0, 255, 0];
aColorBlue : TcVnVector4_LREAL := [0, 0, 255];
aColorRed : TcVnVector4_LREAL := [255, 0, 0];
END_VAR
// Implementation
eState := fbCamera.GetState();
CASE eState OF
TCVN_CS_INITIAL, TCVN_CS_OPENING, TCVN_CS_OPENED, TCVN_CS_STARTACQUISITION:
hr := fbCamera.StartAcquisition();
TCVN_CS_ACQUIRING:
hr := fbCamera.GetCurrentImage(ipImageIn);
IF SUCCEEDED(hr) AND ipImageIn <> 0 THEN
hrWD := F_VN_StartRelWatchdog(tStop, hr);
hrFunc := F_VN_MeasureEdgeDistanceExp(
ipSrcImage:= ipImageIn,
fAvgDistance:= d209Dist,
aStartPoint:= d209Start,
aEndPoint:= d209Stop,
eEdgeDirection:= eDirection,
fMinStrength:= fMinStrength,
nSearchLines:= nSearchLines,
fSearchLineDist:= fSearchLineDist,
nMaxThickness:= nMaxThickness,
bInvertSearchDirection:= FALSE,
fSearchGap:= fSearchGap,
nSubpixelsIterations:= nSubpixIter,
fApproxPrecision:= fApproxPrecision,
eAlgorithm:= eAlgorithm,
ipEdgePoints1:= edge209Start,
ipEdgePoints2:= edge209Stop,
ipDistances:= 0,
hrPrev:=hr
);
hrFunc := F_VN_MeasureEdgeDistanceExp(
ipSrcImage:= ipImageIn,
fAvgDistance:= d201Dist,
aStartPoint:= d201Start,
aEndPoint:= d201Stop,
eEdgeDirection:= eDirection,
fMinStrength:= fMinStrength,
nSearchLines:= nSearchLines201,
fSearchLineDist:= fSearchLineDist201,
nMaxThickness:= nMaxThickness201,
bInvertSearchDirection:= FALSE,
fSearchGap:= fSearchGap,
nSubpixelsIterations:= nSubpixIter,
fApproxPrecision:= fApproxPrecision,
eAlgorithm:= eAlgorithm,
ipEdgePoints1:= edge201Start,
ipEdgePoints2:= edge201Stop,
ipDistances:= 0,
hrPrev:=hr
);
hrFunc := F_VN_MeasureEdgeDistanceExp(
ipSrcImage:= ipImageIn,
fAvgDistance:= d222Dist,
aStartPoint:= d222Start,
aEndPoint:= d222Stop,
eEdgeDirection:= eDirection,
fMinStrength:= fMinStrength,
nSearchLines:= nSearchLines222,
fSearchLineDist:= fSearchLineDist222,
nMaxThickness:= nMaxThickness,
bInvertSearchDirection:= FALSE,
fSearchGap:= fSearchGap,
nSubpixelsIterations:= nSubpixIter,
fApproxPrecision:= fApproxPrecision,
eAlgorithm:= eAlgorithm,
ipEdgePoints1:= edge222Start,
ipEdgePoints2:= edge222Stop,
ipDistances:= 0,
hrPrev:=hr
);
hrWD := F_VN_StopWatchdog(hrWD, nFractionProcessed=>nFraction, tRest=>tRest);
// Draw result for visualization
hr := F_VN_ConvertColorSpace(ipImageIn, ipImageRes, TCVN_CST_GRAY_TO_RGB, hr);
hr := F_VN_DrawPoint(REAL_TO_UDINT(d209Start[0]), REAL_TO_UDINT(d209Start[1]), ipImageRes, TCVN_DS_CIRCLE, aColorRed, hr);
hr := F_VN_DrawPoint(REAL_TO_UDINT(d209Stop[0]), REAL_TO_UDINT(d209Stop[1]), ipImageRes, TCVN_DS_X, aColorRed, hr);
hr := F_VN_FitLine(edge209Start, d209LineStart, hr);
hr := F_VN_DrawLine_TcVnVector4_LREAL(d209LineStart, ipImageRes, aColorGreen, 1, hr);
hr := F_VN_DrawPointsExp(edge209Start, ipImageRes, TCVN_DS_PLUS, aColorBlue, 1, 1, TCVN_LT_8_CONNECTED, hr);
hr := F_VN_FitLine(edge209Stop, d209LineStop, hr);
hr := F_VN_DrawLine_TcVnVector4_LREAL(d209LineStop, ipImageRes, aColorGreen, 1, hr);
hr := F_VN_DrawPointsExp(edge209Stop, ipImageRes, TCVN_DS_PLUS, aColorBlue, 1, 1, TCVN_LT_8_CONNECTED, hr);
hr := F_VN_DrawPoint(REAL_TO_UDINT(d222Start[0]), REAL_TO_UDINT(d222Start[1]), ipImageRes, TCVN_DS_CIRCLE, aColorRed, hr);
hr := F_VN_DrawPoint(REAL_TO_UDINT(d222Stop[0]), REAL_TO_UDINT(d222Stop[1]), ipImageRes, TCVN_DS_X, aColorRed, hr);
hr := F_VN_FitLine(edge222Start, d222LineStart, hr);
hr := F_VN_DrawLine_TcVnVector4_LREAL(d222LineStart, ipImageRes, aColorGreen, 1, hr);
hr := F_VN_DrawPointsExp(edge222Start, ipImageRes, TCVN_DS_PLUS, aColorBlue, 1, 1, TCVN_LT_8_CONNECTED, hr);
hr := F_VN_FitLine(edge222Stop, d222LineStop, hr);
hr := F_VN_DrawLine_TcVnVector4_LREAL(d222LineStop, ipImageRes, aColorGreen, 1, hr);
hr := F_VN_DrawPointsExp(edge222Stop, ipImageRes, TCVN_DS_PLUS, aColorBlue, 1, 1, TCVN_LT_8_CONNECTED, hr);
hr := F_VN_DrawPoint(REAL_TO_UDINT(d201Start[0]), REAL_TO_UDINT(d201Start[1]), ipImageRes, TCVN_DS_CIRCLE, aColorRed, hr);
hr := F_VN_DrawPoint(REAL_TO_UDINT(d201Stop[0]), REAL_TO_UDINT(d201Stop[1]), ipImageRes, TCVN_DS_X, aColorRed, hr);
hr := F_VN_FitLine(edge201Start, d201LineStart, hr);
hr := F_VN_DrawLine_TcVnVector4_LREAL(d201LineStart, ipImageRes, aColorGreen, 1, hr);
hr := F_VN_DrawPointsExp(edge201Start, ipImageRes, TCVN_DS_PLUS, aColorBlue, 1, 1, TCVN_LT_8_CONNECTED, hr);
hr := F_VN_FitLine(edge201Stop, d201LineStop, hr);
hr := F_VN_DrawLine_TcVnVector4_LREAL(d201LineStop, ipImageRes, aColorGreen, 1, hr);
hr := F_VN_DrawPointsExp(edge201Stop, ipImageRes, TCVN_DS_PLUS, aColorBlue, 1, 1, TCVN_LT_8_CONNECTED, hr);
// Display source and result image
hr := F_VN_TransformIntoDisplayableImage(ipImageIn, ipImageInDisp, S_OK);
hr := F_VN_TransformIntoDisplayableImage(ipImageRes, ipImageResDisp, S_OK);
END_IF
TCVN_CS_ERROR:
hr := fbCamera.Reset();
END_CASE
Make it DRY
Given that Structured Text does not allow overloading function calls or defining default input parameters (like a FB), the above example is very heavy and convoluted. Mostly when most of the inputs we pass are actually just the recommended default values.
We can rectify this by using structs with default values and pass them to helper functions by reference. This struct will hold all input parameters and also all return values. It should be considered a full single representation of a measurement to be done.
TYPE ST_DimBase :
STRUCT
// Config
name : STRING(255);
// Must be set. Default values will return errors
pFrom : TcVnPoint2_REAL := [-1, -1];
pTo1 : TcVnPoint2_REAL := [-1, -1];
pTo2 : TcVnPoint2_REAL := [-1, -1]; // Used for angle
lines : UDINT := 0;
// Can use defaults
dir : ETcVnEdgeDirection := TCVN_ED_LIGHT_TO_DARK;
minStr : REAL := 50;
lineDist : REAL := 1;
maxThick : UDINT := 10;
gap : REAL := 0;
precision : REAL := 0.001; // 0.01 - 0.0001
subpix : UDINT := 10; // 5-10 for INTER, 50-100 for ERF/Gaussian
invDir : BOOL := FALSE;
algorithm : ETcVnEdgeDetectionAlgorithm := TCVN_EDA_INTERPOLATION;
degrees : BOOL := TRUE; // Used for angle
// Results
result : REAL;
results : ITcVnContainer; // Used for distance
edgeFrom : ITcVnContainer;
edgeTo : ITcVnContainer;
lineFrom : TcVnVector4_LREAL;
lineTo : TcVnVector4_LREAL;
END_STRUCT
END_TYPE
Now to make calling the underlying F_VN_MeasureEdgeDistanceExp
function easily by creating a higher level helper function. This function can also set the gap
parameter automatically if the input gap value is less than zero. The gap parameter reduces processing time by skipping all the pixels in between the start and end points. Notice that is uses a REFERENCE TO ST_DimBase
as input. This will allow to pass the variable directly, but will act as pass by reference and allow the function to set the return values inside the struct.
FUNCTION F_Dist : HRESULT
VAR_INPUT
dim : REFERENCE TO ST_DimBase;
img : ITcVnImage;
END_VAR
// Implementation
IF dim.gap < 0 THEN
// If gap less than zero auto calculate based on largest diff between x/y and set to 90%
dim.gap := MAX(ABS(dim.pTo1[0] - dim.pFrom[0]), ABS(dim.pTo1[1] - dim.pFrom[1])) * 0.9;
END_IF
F_Dist := F_VN_MeasureEdgeDistanceExp(
ipSrcImage:=img,
fAvgDistance:=dim.result,
aStartPoint:=dim.pFrom,
aEndPoint:=dim.pTo1,
eEdgeDirection:=dim.dir,
fMinStrength:=dim.minStr,
nSearchLines:=dim.lines,
fSearchLineDist:=dim.lineDist,
nMaxThickness:=dim.maxThick,
bInvertSearchDirection:=dim.invDir,
fSearchGap:=dim.gap,
nSubpixelsIterations:=dim.subpix,
fApproxPrecision:=dim.precision,
eAlgorithm:=dim.algorithm,
ipEdgePoints1:=dim.edgeFrom,
ipEdgePoints2:=dim.edgeTo,
ipDistances:=dim.results,
hrPrev:=S_OK
);
Now to draw the results onto the image. Again the same ST_DimBase
structure as input. This function will try and draw everything it can even if data is missing/incorrect.
FUNCTION F_DimDraw : HRESULT
VAR_INPUT
dim : REFERENCE TO ST_DimBase;
img : ITcVnImage;
END_VAR
VAR
hr : HRESULT;
green : TcVnVector4_LREAL := [0, 255, 0, 0];
blue : TcVnVector4_LREAL := [0, 0, 255, 0];
red : TcVnVector4_LREAL := [255, 0, 0, 0];
END_VAR
// Implementation
IF dim.pFrom[0] >= 0 AND_THEN dim.pFrom[1] >= 0 THEN
hr := F_VN_DrawPoint(TO_UDINT(dim.pFrom[0]), TO_UDINT(dim.pFrom[1]), img, TCVN_DS_CIRCLE, red, hr);
END_IF
IF dim.pTo1[0] >= 0 AND_THEN dim.pTo1[1] >= 0 THEN
hr := F_VN_DrawPoint(TO_UDINT(dim.pTo1[0]), TO_UDINT(dim.pTo1[1]), img, TCVN_DS_X, red, hr);
END_IF
IF dim.pTo2[0] >= 0 AND_THEN dim.pTo2[1] >= 0 THEN
hr := F_VN_DrawPoint(TO_UDINT(dim.pTo2[0]), TO_UDINT(dim.pTo2[1]), img, TCVN_DS_X, red, hr);
END_IF
IF F_VN_CheckIfEmpty(dim.edgeFrom, S_OK) = S_FALSE THEN
hr := F_VN_DrawPointsExp(dim.edgeFrom, img, TCVN_DS_PLUS, blue, 1, 1, TCVN_LT_8_CONNECTED, hr);
hr := F_VN_FitLine(dim.edgeFrom, dim.lineFrom, hr);
hr := F_VN_DrawLine_TcVnVector4_LREAL(dim.lineFrom, img, green, 1, hr);
END_IF
IF F_VN_CheckIfEmpty(dim.edgeTo, S_OK) = S_FALSE THEN
hr := F_VN_DrawPointsExp(dim.edgeTo, img, TCVN_DS_PLUS, blue, 1, 1, TCVN_LT_8_CONNECTED, hr);
hr := F_VN_FitLine(dim.edgeTo, dim.lineTo, hr);
hr := F_VN_DrawLine_TcVnVector4_LREAL(dim.lineTo, img, green, 1, hr);
END_IF
F_DimDraw := hr;
Now if we use these new blocks the previous MAIN
can be rewritten into something more concise by covering up all the unnecessary configuration options and repetition. It is important to note that there is no loss of functionality. All inputs into the function are accessible if desired.
PROGRAM MAIN
VAR
hr : HRESULT;
hrFunc : HRESULT;
fbCamera : FB_VN_SimpleCameraControl;
eState : ETcVnCameraState;
ipImageIn : ITcVnImage;
ipImageInDisp : ITcVnDisplayableImage;
ipImageRes : ITcVnImage;
ipImageResDisp : ITcVnDisplayableImage;
// Watchdog
hrWD : HRESULT;
tStop : DINT := 15000;
tRest : DINT;
nFraction : UDINT;
// parameters
dimDist : ARRAY [0..2] OF ST_DimBase := [
(name:='d201', pFrom:=[115, 1290], pTo1:=[640, 1290], lines:=181, lineDist:=5),
(name:='d209', pFrom:=[245, 795], pTo1:=[245, 1770], lines:=95, lineDist:=2),
(name:='d222', pFrom:=[760, 875], pTo1:=[760, 1700], lines:=85, lineDist:=2)
];
END_VAR
// Implementation
eState := fbCamera.GetState();
CASE eState OF
TCVN_CS_INITIAL, TCVN_CS_OPENING, TCVN_CS_OPENED, TCVN_CS_STARTACQUISITION:
hr := fbCamera.StartAcquisition();
TCVN_CS_ACQUIRING:
hr := fbCamera.GetCurrentImage(ipImageIn);
IF SUCCEEDED(hr) AND ipImageIn <> 0 THEN
hrWD := F_VN_StartRelWatchdog(tStop, hr);
hrFunc := F_Dist(dimDist[0], ipImageIn);
hrFunc := F_Dist(dimDist[1], ipImageIn);
hrFunc := F_Dist(dimDist[2], ipImageIn);
hrWD := F_VN_StopWatchdog(hrWD, nFractionProcessed=>nFraction, tRest=>tRest);
hr := F_VN_ConvertColorSpace(ipImageIn, ipImageRes, TCVN_CST_GRAY_TO_RGB, hr);
F_DimDraw(dimDist[0], ipImageRes);
F_DimDraw(dimDist[1], ipImageRes);
F_DimDraw(dimDist[2], ipImageRes);
// Display source and result image
hr := F_VN_TransformIntoDisplayableImage(ipImageIn, ipImageInDisp, S_OK);
hr := F_VN_TransformIntoDisplayableImage(ipImageRes, ipImageResDisp, S_OK);
END_IF
TCVN_CS_ERROR:
hr := fbCamera.Reset();
END_CASE
Turn it into a FB
Now this is already a lot better and much more manageable, however there are still a few pain points to this.
- A lot of setup and control code around the measurements and camera
- Still need to manually optimize how many vision functions to run in a single cycle (Never allow using more than the cycle time)
These two points can be fixed by creating a higher level function block to hide the inner workings. This FB still uses the struct and helper functions defined earlier.
FUNCTION_BLOCK FB_Camera
VAR_IN_OUT
dimDist : ARRAY [*] OF ST_DimBase;
END_VAR
VAR_OUTPUT
END_VAR
VAR
hr : HRESULT;
hrFunc : HRESULT;
hrWD : HRESULT;
camera : FB_VN_SimpleCameraControl;
camState : ETcVnCameraState;
imgIn : ITcVnImage;
imgInDisp : ITcVnDisplayableImage;
imgOut : ITcVnImage;
imgOutDisp : ITcVnDisplayableImage;
state : INT;
preempt : DINT;
remain : DINT;
complete : UDINT;
ii : DINT;
upper : DINT;
lower : DINT;
END_VAR
// Implementation
camState := camera.GetState();
CASE state OF
0:
lower := LOWER_BOUND(dimDist, 1);
upper := UPPER_BOUND(dimDist, 1);
// Allow up to 80% of the cycle time to be used
preempt := TO_DINT(0.08 * TO_LREAL(_TaskInfo[GETCURTASKINDEXEX()].CycleTime));
state := 10;
10:
CASE camState OF
TCVN_CS_INITIAL, TCVN_CS_OPENING, TCVN_CS_OPENED, TCVN_CS_STARTACQUISITION:
hr := camera.StartAcquisition();
TCVN_CS_ACQUIRING:
hr := camera.GetCurrentImage(imgIn);
IF SUCCEEDED(hr) AND_THEN imgIn <> 0 THEN
ii := lower;
state := 110;
END_IF
END_CASE
110:
FOR ii := ii TO upper DO
hrWD := F_VN_StartAbsWatchdog(preempt, S_OK);
hrFunc := F_Dist(dimDist[ii], imgIn);
hrWD := F_VN_StopWatchdog(hrWD, nFractionProcessed=>complete, tRest=>remain);
IF FAILED(hrFunc) OR_ELSE FAILED(hrWD) THEN
// Send message
END_IF
IF complete <> 100 AND_THEN SUCCEEDED(hrFunc) THEN
EXIT;
ELSIF ii = upper THEN
state := 200;
EXIT;
ELSIF remain < 1000 THEN
// Less than 1ms left in task, if starting a new function will likely not complete
ii := ii + 1;
EXIT;
END_IF
END_FOR
200:
hr := F_VN_ConvertColorSpace(imgIn, imgOut, TCVN_CST_GRAY_TO_RGB, hr);
FOR ii := lower TO upper DO
F_DimDraw(dimDist[ii], imgOut);
END_FOR
hr := F_VN_TransformIntoDisplayableImage(imgIn, imgInDisp, S_OK);
hr := F_VN_TransformIntoDisplayableImage(imgOut, imgOutDisp, S_OK);
state := 300;
300:
// Wait to measure again. Go to state 10
END_CASE
IF camState = TCVN_CS_ERROR THEN
hr := camera.Reset();
END_IF
Now the MAIN
becomes ridiculously simple. Obviously this is a little contrived since there is functionality missing - start, stop, done, reset, etc.
PROGRAM MAIN
VAR
camera : FB_Camera;
dimDist : ARRAY [0..2] OF ST_DimBase := [
(name:='d201', pFrom:=[115, 1290], pTo1:=[640, 1290], gap:=-1, lines:=181, lineDist:=5),
(name:='d209', pFrom:=[245, 795], pTo1:=[245, 1770], gap:=-1, lines:=95, lineDist:=2),
(name:='d222', pFrom:=[760, 875], pTo1:=[760, 1700], gap:=-1, lines:=85, lineDist:=2)
];
END_VAR
// Implementation
camera(dimDist := dimDist);
The beauty of this function block is that it allows you to easily configure your measurements as an array of structs of variable length and passing it to the camera instance. It will also automatically manage running the vision functions inside the alloted task cycle time and taking the necessary number of cycles to finish to 100%. To do it it finds the current task cycle time and allows starting new vision functions up to 80% of that cycle, it then allows the PLC to finish the cycle on the next cycle it continues with where it had left off.
Conclusion
Beckhoff Vision is definitely a feature that I will continue playing with and look forward to trying out in a future project which requires some simple inline metrology. At this time however it is still fairly convoluted and opaque giving it a potentially steep learning curve. This is mostly due to not having a GUI configuration tool and being solely API based (except for some basic camera setup).
Pros:
- Directly integrated into the TwinCAT development environment
- No need for yet another field bus connection (or even DIO) to a smart camera
- Free development and testing using the 7-day trial licenses
- High level of fine control and functionality built in allowing advanced use
- Allows the creation of custom image processors and filters for the ever more advanced user
- I'm assuming cheaper to get a dumb camera and license than a smart camera, mostly if using more than one camera
Cons:
- Potentially frustrating and steep learning curve for users who are used to having a GUI (a la smart camera)
- Longer development time. It is much quicker to draw a box around an item then figure out pixel counts
- Limited basic feature set making it hard to use unless you're an image processing expert or doing something fairly simple
- Convoluted syntax (although as seen here, there are workarounds)
Continue reading about TwinCAT Vision with these two blog posts.