Skip to content

v0.25.0

Choose a tag to compare

@github-actions github-actions released this 16 Sep 23:34
· 111 commits to main since this release
1d960cc

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.Flip and CamOpt.Rotation. The acquisition contract in the Imaging
    README states that delivered frames already have them applied, but nothing in
    CvInspect.Imaging.Gev read either value — only VideoCaptureCam (USB/file) and VirtualCam did.
    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 from CamOpt does — 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 default None return 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.
  • SetExposureTimeUs now sticks. It may be called before Open — the implementation holds the
    value and applies it when the device opens — and the last value given wins over
    CamOpt.ExposureTimeUs and survives a reopen. This is the rule ReconnectingCam already used when
    restoring a reconnected camera, now pushed down into the backends and written into the ICam
    contract. If you drive GevCam directly and apply exposure changes by editing CamOpt and doing
    Close/Open, you now get the value from your earlier SetExposureTimeUs call 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 edited CamOpt on 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.StopContinuousCore waited on the
    worker with Join(100) and then disposed the CTS whether or not the worker had left. The loop
    keeps touching token.IsCancellationRequested and token.WaitHandle afterwards, and a disposed
    CTS's wait handle throws ObjectDisposedException on 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-argument Timer.Dispose() does not wait for a
    callback in flight, so a frame could be published after StopContinuous returned and be picked up
    as the answer to the next GrabOne — which ICam.StopContinuous explicitly 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.GrabOne swallowed read failures — it discarded TryReadFrame'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, PumpLoop exited without cancellation, IsGrabbing stayed true forever, and the
    next StartContinuous read 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) that ICam already promised for acquisition that stops without being
    asked to.
  • Open after a control loss did nothing. Losing control clears IsConnected but leaves the
    device reference, and Open only 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

  • CvDispCtrl kept subscriptions on shapes it was given. The shape list usually belongs to the
    host and outlives the control, so a handler left on CvEditShape.Changed pins 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 no Unloaded left to undo it. Hosts
    need do nothing.

Removal

  • CvInspGeom.VerifyLandmark is 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 as FixturePose and the NormalizedWindow overloads. 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 build in the default configuration reports 0 warnings
  • main is an ancestor of this branch (the release-pr-guard job verifies it)
  • the tag will be created on the merge commit, main merged back into dev afterwards, and dev bumped to the next -dev version