v0.25.0
What changed and why
A sibling project running its own vision stack on real cameras handed us three defect classes it
had just hit, and asked whether the same shapes existed here. They did — in different places, with
different consequences. Cross-checking them turned up a theme that runs through this release:
calls that return as if they worked, while nothing happened. A stop that takes the host process
down. A grab that reports neither a frame nor an error. A restart that is read as "already running".
An exposure value that is accepted and discarded. None of these fail loudly; every one of them is
found later, by the wrong person, from a symptom that points somewhere else.
This release is the fix for all eight, plus the removal promised in 0.24.0.
Two changes you may notice even if nothing was broken for you
- GigE now applies
CamOpt.FlipandCamOpt.Rotation. The acquisition contract in the Imaging
README states that delivered frames already have them applied, but nothing in
CvInspect.Imaging.Gevread either value — onlyVideoCaptureCam(USB/file) andVirtualCamdid.
So swapping backends under the same recipe silently changed the image orientation: teach with a USB
camera, set the flip, move to GigE, and that setting quietly does nothing. That is worse than a
dead feature — someone who verified the behaviour on one backend has no reason to suspect the
other. Nor is it only a dead feature for the person configuring the camera: if your settings UI
exposes these properties — and a UI generated fromCamOptdoes — an operator can set a rotation
on a GigE camera today and watch nothing happen, with no exception and no log line to explain it.
The transform is applied just before publication, in the established order (rotate, then flip),
and both values at their defaultNonereturn the frame unchanged, so an installation that does
not use them pays nothing. If you do set either value for a GigE camera, your image orientation
changes with this release. SetExposureTimeUsnow sticks. It may be called beforeOpen— the implementation holds the
value and applies it when the device opens — and the last value given wins over
CamOpt.ExposureTimeUsand survives a reopen. This is the ruleReconnectingCamalready used when
restoring a reconnected camera, now pushed down into the backends and written into theICam
contract. If you driveGevCamdirectly and apply exposure changes by editingCamOptand doing
Close/Open, you now get the value from your earlierSetExposureTimeUscall instead.
What is remembered is the value you asked for, recorded before the device call — not the value
the device accepted. If applying it fails, or the camera snaps it onto its own grid, the next open
still asks for what you last requested. Remembering only successful values would mean that after a
failed apply, a stale success beats a freshly editedCamOpton the next reconnect: the operator
sees "I saved it and the brightness did not change", and nobody connects that to the one failure
line in the log. The ordering is part of the contract now, not an implementation accident.
Acquisition: silent no-ops and one fatal one
- A stop could take the whole process down.
VideoCaptureCam.StopContinuousCorewaited on the
worker withJoin(100)and then disposed the CTS whether or not the worker had left. The loop
keeps touchingtoken.IsCancellationRequestedandtoken.WaitHandleafterwards, and a disposed
CTS's wait handle throwsObjectDisposedExceptionon an unguarded background thread — an
unhandled exception that ends the process. Exceeding 100 ms is not an exceptional case: a slow
device read, or a file source's own pacing wait (1000/fps — 100 ms at 10 fps), sits right on it.
The loop body is now guarded, and the CTS is released only when the worker is confirmed to have
left. The remaining contract is documented rather than hidden: this call returning does not mean
the loop has ended, and one more frame may be published after it. VirtualCam's stop had no wait at all. The no-argumentTimer.Dispose()does not wait for a
callback in flight, so a frame could be published afterStopContinuousreturned and be picked up
as the answer to the nextGrabOne— whichICam.StopContinuousexplicitly promises against. It
now waits on a drain handle, outside the lock (the thing being waited on is your frame handler,
which may call back into this camera), skips the wait when the publishing thread is the one asking
to stop, and keeps the handle if the wait times out rather than letting a late signal hit a
disposed one.VideoCaptureCam.GrabOneswallowed read failures — it discardedTryReadFrame's result and
returned with no frame, no exception and no log. For a grab a person asked for, that is
indistinguishable from success. The continuous loop already logged the same failure.- A receive pump that died on its own left its reference behind. If the GigE stream closed or a
receive failed,PumpLoopexited without cancellation,IsGrabbingstayedtrueforever, and the
nextStartContinuousread that as "already running" and returned silently — the camera never
restarted and nothing said so. The pump now cleans up after itself and raises the
GrabbingChanged(false)thatICamalready promised for acquisition that stops without being
asked to. Openafter a control loss did nothing. Losing control clearsIsConnectedbut leaves the
device reference, andOpenonly looked at the reference — so it returned as though the camera
were already open while the caller waited for frames that were never coming. It now folds the dead
session and reopens. The disconnect was already announced at control-loss time, so no second
notification is raised.
Display
CvDispCtrlkept subscriptions on shapes it was given. The shape list usually belongs to the
host and outlives the control, so a handler left onCvEditShape.Changedpins a discarded control
— with its back buffer, pixel array and visual tree (measured: attaching 40 controls to one shape
list and dropping them grew the process by 799 MB, against 50 MB for the same 40 with no shapes).
Tab and page switches are exactly the layout that does this. The control now subscribes while it is
loaded and releases on unload, and does not re-subscribe while unloaded — a list swapped in behind
a hidden control would otherwise revive the subscription with noUnloadedleft to undo it. Hosts
need do nothing.
Removal
CvInspGeom.VerifyLandmarkis gone, as announced when it was marked obsolete in 0.24.0.
Deciding whether a landmark is present is the host's policy, not the library's; the geometry
underneath it shipped in 0.24.0 asFixturePoseand theNormalizedWindowoverloads. The only
caller moved its 8 call sites onto those primitives and reported zero remaining calls; the other
two known consumers never called it.
How this was verified
Honest accounting, because it is uneven. The GigE mount transform has a regression that was run
against a deliberately broken build (asymmetric marker, flip/rotate/identity/timestamp). The rest of
the acquisition work is device-dependent and the suite does not cover it — those fixes rest on the
documented contract, on reading the mechanism through, and on reproduction procedures recorded with
each commit. One regression written for the VirtualCam drain was removed rather than kept: it
passed against the unfixed build too, because publication is already complete by the time a handler
can slow it down. A test that passes without proving anything is worse than no test.
Version
0.25.0 — minor. CvInspGeom.VerifyLandmark was removed from the public API.
Checks
-
dotnet buildin the default configuration reports 0 warnings -
mainis an ancestor of this branch (the release-pr-guard job verifies it) - the tag will be created on the merge commit,
mainmerged back intodevafterwards, anddevbumped to the next-devversion