The in-process C ABI is the smallest, lowest-friction way to put your own algorithm into the VisionLab runtime: you build a DLL that exports three functions, and your algorithm then behaves like a built-in one — it appears in the algorithm tree with an auto-generated parameter form, and can be used as a recipe step whose measurements feed the constraint/aggregate engine.
There are two integration models. This document covers the in-process C ABI (your algorithm runs inside the runtime). The other model — your app drives the runtime as a black box over shared-memory IPC — is covered in
PLUGIN_INTEGRATION_GUIDE.mdandRECIPE_IPC_PROTOCOL.md.
A complete, compilable example is at
shared/plugin_sdk/algo_plugin/examples/pin_hole/. Build it standalone in three minutes with
PLUGIN_SDK_QUICK_START.md.
- Pure C ABI + JSON contract. No C++ objects cross the DLL boundary (
cv::Mat,QJsonObject, STL containers are an ABI disaster across DLLs). - Schema reuse. The
schemayou declare is exactly the field format the runtime'sParamFormWidgetconsumes → the parameter panel is generated with zero UI changes. - Result reuse. Your
overlaymaps onto the runtime's existingDetectionOutcomefields → drawn by the existing overlay renderer. - Recipe reuse. A plugin algorithm is just "algorithm key + JSON parameters", so recipe serialization and the constraint engine work unchanged (§7).
- The host allocates buffers; the plugin only fills them. Never allocate/free across the DLL boundary.
The single header you ship to customers: shared/plugin_sdk/algo_plugin/circle_qt_plugin.h.
#define CIRCLE_QT_PLUGIN_API_VERSION 1
#ifdef _WIN32
# ifdef CIRCLE_QT_PLUGIN_BUILD
# define CQ_EXPORT __declspec(dllexport)
# else
# define CQ_EXPORT __declspec(dllimport)
# endif
#else
# define CQ_EXPORT __attribute__((visibility("default")))
#endif
/* Image: row-major. channels = 1 (gray) or 3 (BGR).
Input `data` is host-owned and read-only; output `outDisplayBgr->data`
is a host-allocated writable buffer (size it for rows*cols*3). */
typedef struct {
unsigned char* data;
int rows;
int cols;
int channels; /* 1 or 3 */
int step; /* bytes per row (>= cols*channels; stride allowed) */
} CQImage;
#ifdef __cplusplus
extern "C" {
#endif
/* Version handshake: called first after load; mismatch => refuse to load. */
CQ_EXPORT int circle_qt_plugin_api_version(void);
/* Register: write the metadata JSON of all your algorithms into outJson.
0 = ok; <0 = error; -1 if outBufSize is too small. */
CQ_EXPORT int circle_qt_plugin_register(const char* appVersion,
char* outJson, int outBufSize);
/* Run: one common entry point; `algo` selects the algorithm.
0 = ran (an NG verdict is data in the result's `ok` field);
<0 = hard error (bad input / internal exception), reason in errBuf. */
CQ_EXPORT int circle_qt_plugin_run(const char* algo,
const CQImage* img,
const char* paramsJson,
char* outResultJson, int outResultBufSize,
CQImage* outDisplayBgr, /* nullable */
char* errBuf, int errBufSize);
#ifdef __cplusplus
}
#endifConventions: all strings are UTF-8; the host allocates every buffer; the plugin never returns a pointer it allocated.
{
"api_version": 1,
"algorithms": [
{
"name": "customer_pin_hole",
"label": "Pin hole (customer)",
"category": "measure",
"roi_type": "none",
"schema": [
{"name": "thresh_value", "label": "Threshold", "type": "int",
"min": 1, "max": 255, "default": 100, "slider": true},
{"name": "min_radius", "label": "Min radius", "type": "double",
"min": 1, "max": 2000, "step": 1, "default": 5, "slider": true}
],
"defaults": { "thresh_value": 100, "min_radius": 5 }
}
]
}| Field | Meaning |
|---|---|
name |
Unique algorithm key (referenced by recipes) |
label |
UI display name |
category |
Metadata: measure / defect / other (informational) |
roi_type |
Fixed enum circle / ellipse / line / rect / tm / none → maps to the runtime's ROIType; none or empty hides the ROI editor |
schema |
The ParamFormWidget field format (name/label/type/min/max/step/default/slider) — the host calls setSchema directly |
defaults |
Default parameters (nested JSON) |
schema field types: int / double / bool / enum (with an options array) / string
— the same set as the built-in algorithms.
{
"ok": true,
"status": "Pin hole diameter OK",
"ms": 1.2,
"measurements": {
"radius_px": 88.5,
"diameter_px": 177.0
},
"overlay": {
"circles": [{ "cx": 320.0, "cy": 240.0, "radius": 88.5 }],
"lines": [{ "x1": 0, "y1": 0, "x2": 10, "y2": 10 }],
"rects": [{ "cx": 0, "cy": 0, "width": 100, "height": 80, "angle": 0 }]
}
}| Field | Meaning |
|---|---|
ok |
Algorithm verdict (OK / NG); status is readable text |
ms |
Elapsed time |
measurements |
Key/value pairs consumed by the constraint engine and the result panel; points and line_x1/y1/x2/y2 feed cross-shot aggregation (§7.3) |
overlay.circles/lines/rects |
Mapped onto DetectionOutcome's hasCircle/hasLine/rectResults → drawn with the standard overlay |
outDisplayBgr (nullable) |
The plugin paints a full BGR image → the host shows it (replaceDisplayImage). Use this for custom shapes / debug overlays |
overlay → DetectionOutcome mapping
| Plugin overlay | DetectionOutcome | Display |
|---|---|---|
circles[] |
hasCircle / cx,cy,radius |
circle + crosshair |
lines[] |
hasLine / linePt1,linePt2 |
line |
rects[] |
rectResults |
rotated rectangle |
outDisplayBgr |
displayImg + replaceDisplayImage |
full-frame replace |
v1 supports circles / lines / rectangles + the full-image fallback. Custom point overlays
(overlay.points) are planned for v1.1.
At startup the runtime scans <exe>/plugins/*.dll|*.so, and for each plugin:
- loads it, resolves the three exports, and checks
circle_qt_plugin_api_version(); - parses the
registermetadata, rejecting name collisions with existing algorithms; - adds each algorithm to the tree under a top-level Plugins group, with a
schema-driven parameter form and (per
roi_type) the appropriate ROI editor; - routes execution to
circle_qt_plugin_run()for both single-image detection and recipe steps. In a recipe, the plugin'smeasurementsare passed through unchanged, so constraints and aggregation consume them exactly like built-in steps.
The runtime passes the full working image (gray/BGR, no ROI crop — built-in algorithms crop internally). If your algorithm needs a ROI, put its coordinates in your parameter JSON and crop inside the plugin.
A plugin algorithm can be used in recipes for step execution + constraints + cross-shot
aggregation — because a plugin step is just {algorithm, parameters, emit, ...} JSON, and
its measurements are passed through untouched.
| Recipe capability | Status |
|---|---|
| Step execution + OK/NG | ✅ |
Constraint judging (measurements) |
✅ (zero changes) |
| Cross-shot aggregate point pool | ✅ mechanism ready — return points (or line_*) in measurements and tag the step with emit in the recipe; coordinates are in original-image pixels, matching the aggregate pool |
To feed aggregation, include points in your result:
{
"ok": true,
"measurements": {
"radius_px": 88.5,
"points": [ { "x": 318.1, "y": 241.0 }, { "x": 322.0, "y": 239.5 } ]
}
}and give that recipe step an emit tag — no new top-level field or ABI version needed.
- Frozen ABI.
CQImageand the three export signatures: once released, any change bumpsCIRCLE_QT_PLUGIN_API_VERSION(the host rejects a mismatch). The parameter/result JSON may gain fields (forward compatible) but must not remove or change the meaning of existing ones. - Trust model. A plugin is in-process native code; a crash takes the host down. v1 targets your own SDK / contracted customers. Untrusted plugins would need an out-of-process model.
- Memory discipline. Write only into host-provided buffers; never return plugin-allocated memory.
- Image ownership. Input
CQImage.datais host-owned and read-only;outDisplayBgr->datais a host-allocated writable buffer (sizedrows*cols*3); fill it and set rows/cols/channels/step.
overlay.points— plugin-defined point clouds (host adds apluginPointsoverlay).- ROI-cropped images passed to plugins.
- A HALCON backend switch at the runner layer.
- An embedded-Python path with the same register/run contract.