20 KiB
Recognition Control System
The recognition control system manages the image processing lifecycle: acquiring images, running them through a processing pipeline, and emitting results as events. Each camera gets its own IRecognitionControl instance, registered in the per-camera Ninject child kernel.
Architecture Overview
IRecognitionControl (interface)
^
|
BaseRecognitionControl (abstract base - lifecycle, threading, events)
^
|--- HawkeyeRecognitionControl (workflow-based pipeline processing)
|--- CandyboxImageProcessingControl (histogram-based processing)
|--- TestImageProcessingControl (minimal test stub)
|--- [Your custom implementation]
BaseRecognitionControlSettings (abstract base - shared config)
^
|--- HawkeyeRecognitionSettings
|--- CandyboxRecognitionControlSettings
|--- TestRecognitionControlSettings
|--- [Your custom settings]
IRecognitionControl Interface
Defined in VisionBuilder.UI.Common/Processing/IRecognitionControl.cs.
public interface IRecognitionControl
{
bool IsRunning { get; }
List<RecipeData> GetRecipesData();
void SetRecipe(RecipeData recipe);
void Start();
void Stop();
void Pause();
void Resume();
event Action<ImageProcessedEvent> ImageProcessed;
event Action<SessionStartedEvent> SessionStarted;
event Action<SessionEndedEvent> SessionEnded;
event Action<ErrorsInSequenceEvent> ErrorsInSequenceAlarm;
void HotReload();
event Action RecipeFileChanged;
}
| Member | Description |
|---|---|
IsRunning |
Whether the processing loop is active. |
GetRecipesData() |
Returns available recipes (name + optional preview image). |
SetRecipe(recipe) |
Selects a recipe. Throws if IsRunning is true. |
Start() |
Initializes the recipe, warms up, then starts the processing loop on a background task. |
Stop() |
Cancels the processing loop, waits for it to finish, runs cleanup. |
Pause() / Resume() |
Temporarily suspends/resumes image processing without stopping the session. |
HotReload() |
Reloads recipe parameters without stopping the session. |
ImageProcessed |
Fired after every processed image with full result data. |
SessionStarted / SessionEnded |
Fired when a session starts/stops. |
ErrorsInSequenceAlarm |
Fired when consecutive errors reach the configured threshold. |
RecipeFileChanged |
Fired when the recipe file on disk changes (for hot-reload UI). |
BaseRecognitionControl
Defined in VisionBuilder.UI.Common/Processing/BaseRecognitionControl.cs. This abstract class implements all of IRecognitionControl and provides the processing loop, threading, pause logic, error-in-sequence tracking, and event plumbing. Subclasses only need to implement four methods.
Abstract Methods to Implement
// Load/prepare resources for the given recipe
protected abstract void Initialize(RecipeData currentRecipe);
// Run a throwaway processing pass to warm up caches, JIT, models, etc.
protected abstract void WarmUp();
// Acquire and process one image. Return null to stop the loop.
protected abstract (
Mat originalImage,
Mat analysisImage,
TimeSpan processingTime,
TimeSpan acquisitionTime,
string[] errorNames
)? ProcessImage(CancellationToken token);
// Return the list of available recipes
public abstract List<RecipeData> GetRecipesData();
Virtual Methods (Optional Overrides)
// Release resources when the session stops (called from Stop())
protected virtual void Cleanup() { }
// Reload recipe parameters without restarting the session
public virtual void HotReload() { }
Protected Helper
// Call this to raise the RecipeFileChanged event (e.g. from a file watcher)
protected void RaiseRecipeFileChanged();
Processing Loop Behavior
The Start() method:
- Validates that a recipe is set and the control is not already running.
- Shows a loading indicator via
ILoadingService. - Calls
Initialize(currentRecipe). - Calls
WarmUp()(errors are logged, not thrown). - Launches
Loop()onTask.Runwith aCancellationToken. - Fires
SessionStarted. - Hides the loading indicator.
The Loop() runs continuously until cancellation:
- If paused, sleeps 100ms and continues.
- Calls
ProcessImage(token). If it returnsnull, the loop exits. - If processing time is below
MinimumProcessingTime, sleeps the difference. - Tracks consecutive errors. If they reach
ErrorsInSequenceAlarm, fires the alarm event. - Builds an
ImageProcessedEventand fires it.
The Stop() method:
- Cancels the token, waits for the loop task to finish.
- Calls
Cleanup(). - Fires
SessionEnded.
Thread Safety
All public lifecycle methods (Start, Stop, Pause, Resume, SetRecipe) are protected by a shared lock. The processing loop runs on a background Task. ProcessImage receives a CancellationToken and should respect it for timely shutdown.
BaseRecognitionControlSettings
Defined in VisionBuilder.UI.Common/Processing/BaseRecognitionControlSettings.cs. Provides the shared configuration for all recognition controls.
public abstract class BaseRecognitionControlSettings(string cameraName) : ISettings
{
public string CameraName { get; } // Injected camera identifier
public int MinimumProcessingTime { get; set; } // Floor for loop iteration (ms)
public string RecipeNameFilter { get; set; } = "*"; // Glob filter for recipe discovery
public int ErrorsInSequenceAlarm { get; set; } = 5; // Consecutive errors before alarm
}
All properties are registered in the Inspectron.Settings system under {CameraName}/Recognition by the RegisterSettings method. Override it to add custom settings (always call base.RegisterSettings first).
Events
All event classes are in VisionBuilder.UI.Common/Commands/.
ImageProcessedEvent
Fired after every image. Contains everything needed to update the UI and record statistics.
public class ImageProcessedEvent : IEvent
{
public DateTime SessionStart { get; set; }
public string ImageSource { get; set; } // Camera name
public string RecipeName { get; set; }
public Mat ImageOriginal { get; set; } // Raw camera image
public Mat ImageAnalysis { get; set; } // Annotated/processed image
public bool HasError { get; set; }
public List<string> ErrorNames { get; set; } // Names of failed operations
public TimeSpan AnalysisTime { get; set; } // Processing minus acquisition
public TimeSpan AcquisitionTime { get; set; } // Camera capture time
public TimeSpan SleepTime { get; set; } // MinimumProcessingTime padding
}
SessionStartedEvent / SessionEndedEvent
public class SessionStartedEvent : IEvent
{
public string RecipeName { get; set; }
public DateTime SessionStarted { get; set; }
}
public class SessionEndedEvent : IEvent
{
public DateTime SessionEnded { get; set; }
}
ErrorsInSequenceEvent
public class ErrorsInSequenceEvent
{
public int Errors { get; set; } // Current consecutive error count
}
RecipeData
Defined in VisionBuilder.UI.Common/ViewModel/Classes/RecipeData.cs.
public class RecipeData
{
public Mat? Image { get; set; } // Optional preview thumbnail
public string RecipeName { get; set; } // Recipe identifier (typically filename without extension)
}
ILoadingService
Defined in VisionBuilder.UI.Common/Processing/ILoadingService.cs. Called by BaseRecognitionControl during Start() and Stop() to show/hide a loading overlay in the UI.
public interface ILoadingService
{
void StartLoading(string title);
void StopLoading(string title);
}
The loading service is injected into BaseRecognitionControl via the constructor and exposed as the LoadingService protected property, so subclasses can use it in GetRecipesData() or elsewhere.
How to Implement a Custom Recognition Control
Step 1: Create Settings
Extend BaseRecognitionControlSettings with any additional configuration your processing needs.
using Inspectron.Settings;
using VisionBuilder.UI.Common.Processing;
public class MyRecognitionSettings(string cameraName) : BaseRecognitionControlSettings(cameraName)
{
public double Threshold { get; set; } = 0.5;
public override void RegisterSettings(InspectronSettings settings)
{
base.RegisterSettings(settings);
settings.RegisterSimple(this, () => Threshold,
CameraName + "/Recognition", nameof(Threshold));
}
}
The RegisterSettings call makes the property editable in the application's settings UI. Always call base.RegisterSettings(settings) first to register the base properties (MinimumProcessingTime, RecipeNameFilter, ErrorsInSequenceAlarm).
Step 2: Create the Recognition Control
Extend BaseRecognitionControl and implement the four abstract members.
using System.Diagnostics;
using OpenCvSharp;
using VisionBuilder.UI.Common.Processing;
using VisionBuilder.UI.Common.ViewModel.Classes;
public class MyRecognitionControl : BaseRecognitionControl
{
private readonly MyRecognitionSettings _settings;
private readonly IImageSource _imageSource;
private MyProcessor _processor; // your processing logic
public MyRecognitionControl(
MyRecognitionSettings settings,
IImageSource imageSource,
ILoadingService loadingService)
: base(settings, loadingService)
{
_settings = settings;
_imageSource = imageSource;
}
public override List<RecipeData> GetRecipesData()
{
// Return available recipes. Use _settings.RecipeNameFilter if applicable.
var recipes = new List<RecipeData>();
foreach (var file in Directory.GetFiles(@"..\Data\Recipes", "*.myrecipe"))
{
recipes.Add(new RecipeData
{
RecipeName = Path.GetFileNameWithoutExtension(file),
Image = null // or load a preview thumbnail
});
}
return recipes;
}
protected override void Initialize(RecipeData currentRecipe)
{
// Load the recipe and prepare your processing pipeline.
// Called once when Start() is invoked.
var path = Path.Combine(@"..\Data\Recipes", currentRecipe.RecipeName + ".myrecipe");
_processor = MyProcessor.Load(path, _settings.Threshold);
}
protected override void WarmUp()
{
// Optional: run a throwaway pass to warm up JIT, GPU, ONNX models, etc.
// Errors here are caught and logged, they won't prevent startup.
var dummy = new Mat(100, 100, MatType.CV_8UC3, Scalar.All(0));
_processor.Process(dummy);
dummy.Dispose();
}
protected override (Mat originalImage, Mat analysisImage,
TimeSpan processingTime, TimeSpan acquisitionTime,
string[] errorNames)?
ProcessImage(CancellationToken token)
{
// 1. Acquire image
var swTotal = Stopwatch.StartNew();
var swAcquire = Stopwatch.StartNew();
var image = _imageSource.GetImage(token).Result;
swAcquire.Stop();
if (image == null)
return null; // null signals "stop the loop"
// 2. Process
var result = _processor.Process(image);
swTotal.Stop();
// 3. Build annotated image
var annotated = image.Clone();
// ... draw overlays on annotated ...
// 4. Collect error names (empty array = pass)
string[] errors = result.Defects
.Select(d => d.Name)
.ToArray();
return (image, annotated, swTotal.Elapsed, swAcquire.Elapsed, errors);
}
protected override void Cleanup()
{
// Release resources when the session stops.
_processor?.Dispose();
_processor = null;
}
}
Key implementation notes for ProcessImage
- Return
nullto signal the loop should exit (e.g. when the image source is exhausted or cancelled). - Respect the
CancellationToken— pass it to blocking calls likeGetImage(). CatchOperationCanceledExceptionif you need cleanup before returning null. errorNamesdrives the pass/fail logic. An empty array means the image passed. Each string becomes a named error in statistics and UI.originalImageis the raw camera image;analysisImageis the annotated version shown in the preview UI.processingTimeis the total wall-clock time including acquisition;acquisitionTimeis just the camera capture. The base class computesAnalysisTime = processingTime - acquisitionTimefor the event.
Step 3: Register via DI
Register your settings and control in the per-camera Ninject child kernel. This is typically done in a plugin's RegisterCameraModules or in an extension method.
Option A: In a plugin
public class MyPlugin : IPlugin
{
public void RegisterGlobalModules(IKernel kernel) { }
public void RegisterCameraModules(IKernel kernel, string cameraName)
{
kernel.Bind<MyRecognitionSettings, BaseRecognitionControlSettings, ISettings>()
.ToConstant(new MyRecognitionSettings(cameraName));
kernel.Rebind<IRecognitionControl>()
.To<MyRecognitionControl>()
.InSingletonScope();
}
}
Option B: As an extension method (like HawkeyeRecipe)
public static class ModuleExtensions
{
public static IChildKernel UseMyRecipes(this IChildKernel self, string cameraName)
{
self.Bind<MyRecognitionSettings, BaseRecognitionControlSettings, ISettings>()
.ToConstant(new MyRecognitionSettings(cameraName));
self.Bind<IRecognitionControl>()
.To<MyRecognitionControl>()
.InSingletonScope();
return self;
}
}
Important DI details:
- Bind your settings class to three types: itself,
BaseRecognitionControlSettings, andISettings. TheISettingsbinding makesRegisterSettingsget called by the settings system. TheBaseRecognitionControlSettingsbinding lets the base class constructor receive it. - Use
Rebind<IRecognitionControl>(notBind) if replacing an existing binding from another recipe provider. - Always use
InSingletonScope()— there should be exactly one recognition control per camera.
Step 4: Hot Reload (Optional)
To support live recipe parameter updates without stopping:
- Set up a
FileSystemWatcherinInitialize()to monitor the recipe file. - When the file changes, call
RaiseRecipeFileChanged()to notify the UI. - Override
HotReload()to apply the new parameters on the next loop iteration.
Example from HawkeyeRecognitionControl:
private FileSystemWatcher? _fileWatcher;
private System.Timers.Timer? _debounceTimer;
private string? _pendingHotReloadFile;
private string? _currentRecipeFilePath;
protected override void Initialize(RecipeData currentRecipe)
{
_currentRecipeFilePath = Path.GetFullPath(/* recipe path */);
_workflow = LoadRecipe(_currentRecipeFilePath);
StartFileWatcher(_currentRecipeFilePath);
}
private void StartFileWatcher(string filePath)
{
var directory = Path.GetDirectoryName(filePath)!;
var fileName = Path.GetFileName(filePath);
_debounceTimer = new System.Timers.Timer(500) { AutoReset = false };
_debounceTimer.Elapsed += (_, _) => RaiseRecipeFileChanged();
_fileWatcher = new FileSystemWatcher(directory, fileName)
{
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size,
EnableRaisingEvents = true
};
_fileWatcher.Changed += (_, _) =>
{
_debounceTimer.Stop();
_debounceTimer.Start(); // debounce to 500ms
};
}
public override void HotReload()
{
// Store path; ProcessImage picks it up on the next iteration
Interlocked.Exchange(ref _pendingHotReloadFile, _currentRecipeFilePath);
}
protected override void Cleanup()
{
_fileWatcher?.Dispose();
_debounceTimer?.Dispose();
}
The UI flow: RecipeFileChanged event -> SingleCameraVM sets IsHotReloadAvailable = true -> user clicks hot-reload button -> calls HotReload() -> next ProcessImage iteration applies new parameters.
How the UI Consumes IRecognitionControl
SingleCameraVM (in VisionBuilder.UI.Common/ViewModel/SingleCameraVM.cs) is the primary consumer:
Commands:
// Start/Stop run on background tasks to avoid blocking the UI
await Task.Run(() => _recognitionControl.Start());
await Task.Run(() => _recognitionControl.Stop());
// Pause/Resume are synchronous (just flip a flag)
_recognitionControl.Pause();
_recognitionControl.Resume();
// Recipe selection
_recognitionControl.SetRecipe(selectedRecipe);
// Hot reload
_recognitionControl.HotReload();
Event subscriptions (in constructor):
recognitionControl.ImageProcessed += Handle; // -> PreviewVm, StatisticsVm, ErrorsVm
recognitionControl.SessionStarted += StatisticsVm.Handle;
recognitionControl.SessionStarted += ErrorsVm.Handle;
recognitionControl.RecipeFileChanged += () =>
SynchronizationContext?.Post(_ => IsHotReloadAvailable = true, null);
Other modules subscribing to events:
VisionBuilderStatistics— writes CSV logs per session, tracks pass/fail counts.VisionBuilderRingbuffer— saves images to ringbuffers (separate good/bad).IOCommanderCameraModule— emits GPIO signals on session start/end, image result, and error alarms.- Plugin modules (e.g.
PralinenModule) — custom PLC integration based on results.
Existing Implementations Reference
HawkeyeRecognitionControl
Project: VisionBuilder.UI.Recipes.HawkeyeRecipe
The primary implementation for workflow-based image processing.
- GetRecipesData: Loads
.jhrcpfiles fromData/Recipes/, deserializes each to extract recipe name and preview image. - Initialize: Loads the workflow JSON file, starts a file watcher for hot-reload.
- WarmUp: Executes the workflow once with an
EmptyImageSource. - ProcessImage: Runs the full workflow pipeline, collects errors from operations where
Result == false, draws graphics overlays on the analysis image usingOpenCVCanvas. - HotReload: Uses
Interlocked.Exchangeto pass the file path;ProcessImagechecks for pending reload via_workflow.HotReloadParameters(). - Cleanup: Disposes the file watcher and debounce timer.
- Settings:
HawkeyeRecognitionSettingsaddsAlwaysError(bool) for testing.
CandyboxImageProcessingControl
Project: Plugins/CandyboxPlugin
Histogram-based image processing for candy box quality control.
- GetRecipesData: Loads
.jsonfiles fromData/Recipes/, finds matching sample.bmpimages for previews. - Initialize: Creates a
HistoRecipe, applies camera width settings to HawkEye hardware. - WarmUp: No-op.
- ProcessImage: Acquires image, processes via
HistoRecipe, collects errors fromErrorPointsandErrorReason. - Settings:
CandyboxRecognitionControlSettingsaddsAlwaysErrorwith[SettingDescription]attribute. - DI: Plugin replaces the existing
BaseRecognitionControlSettingsbinding before adding its own.
TestImageProcessingControl
Project: Plugins/TestPlugin
Minimal stub for testing and development.
- GetRecipesData: Returns two hardcoded recipes ("Hello", "World") with red 100x100 Mat images.
- Initialize / WarmUp: No-op.
- ProcessImage: Returns a fixed red image with
"TestError". - Settings:
TestRecognitionControlSettingswith no extra properties.
Checklist for New Implementations
- Create a settings class extending
BaseRecognitionControlSettings. - Override
RegisterSettingsand callbase.RegisterSettings(settings)first. - Create a recognition control class extending
BaseRecognitionControl. - Implement
GetRecipesData(),Initialize(),WarmUp(),ProcessImage(). - Override
Cleanup()to release resources. - (Optional) Override
HotReload()and set up file watching withRaiseRecipeFileChanged(). - Register both classes in the DI container (bind settings to 3 types, bind control as singleton).
- Ensure
ProcessImagerespectsCancellationTokenand returnsnullwhen cancelled/exhausted.