Skip to content

Commit 9ac674f

Browse files
authored
Support empty AvailableData structs (#138)
Closes #137. - Change version string from 0.3.0 to 0.3.0-rc1. - Updates acquire-core-libs and acquire-video-runtime submodules. - Updates acquire-driver-zarr driver to v0.1.8 (bugfix release). - Create an `AvailableData` struct on every call to `runtime.get_available_data()`. It's no longer `Optional`, i.e., will never be `None`. 🚨🚨 Potentially controversial decisions below 🚨🚨 1. Remove the unused `_store` field from `VideoFrame` struct. This was causing `test_setup()` to fail when I did not explicitly delete frames returned by the video frame iterator. 2. As a consequence of (1), the `store` field of `VideoFrameIteratorInner` struct was unused, so I removed this as well. 3. As a consequence of both (1) and (2), the only place `RawAvailableData` is used is as a member `inner` of `AvailableData`, so I made `AvailableData.inner` an `Option<RawAvailableData>`, where it was previously an `Option<Arc<RawAvailableData>>`.
1 parent 1c80d86 commit 9ac674f

File tree

10 files changed

+175
-148
lines changed

10 files changed

+175
-148
lines changed

.github/workflows/test_pr.yml

Lines changed: 47 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -103,53 +103,53 @@ jobs:
103103
- name: Test
104104
run: |
105105
python -m pytest -k test_dcam
106-
107-
egrabber:
108-
name: Python ${{ matrix.python }} (eGrabber)
109-
runs-on:
110-
- self-hosted
111-
- egrabber
112-
- VC-151MX-M6H00
113-
timeout-minutes: 20
114-
strategy:
115-
fail-fast: false
116-
matrix:
117-
python: [ "3.8", "3.9", "3.10" ]
118-
119-
permissions:
120-
actions: write
121-
env:
122-
GH_TOKEN: ${{ github.token }}
123-
steps:
124-
- name: Cancel Previous Runs
125-
uses: styfle/[email protected]
126-
with:
127-
access_token: ${{ github.token }}
128-
129-
- uses: actions/checkout@v3
130-
with:
131-
submodules: true
132-
ref: ${{ github.event.pull_request.head.sha }}
133-
134-
- name: Get CMake 3.24
135-
uses: lukka/get-cmake@latest
136-
with:
137-
cmakeVersion: 3.24.3
138-
139-
- name: Set up Python ${{ matrix.python }}
140-
uses: actions/setup-python@v4
141-
with:
142-
python-version: ${{ matrix.python }}
143-
144-
- name: Install
145-
run: |
146-
pip install --upgrade pip
147-
pip install -e .[testing]
148-
149-
- name: Test
150-
run: |
151-
python -m pytest -k test_egrabber
152-
106+
107+
# TODO (aliddell): uncomment when we get an eGrabber runner up again
108+
# egrabber:
109+
# name: Python ${{ matrix.python }} (eGrabber)
110+
# runs-on:
111+
# - self-hosted
112+
# - egrabber
113+
# - VC-151MX-M6H00
114+
# timeout-minutes: 20
115+
# strategy:
116+
# fail-fast: false
117+
# matrix:
118+
# python: [ "3.8", "3.9", "3.10" ]
119+
120+
# permissions:
121+
# actions: write
122+
# env:
123+
# GH_TOKEN: ${{ github.token }}
124+
# steps:
125+
# - name: Cancel Previous Runs
126+
# uses: styfle/[email protected]
127+
# with:
128+
# access_token: ${{ github.token }}
129+
130+
# - uses: actions/checkout@v3
131+
# with:
132+
# submodules: true
133+
# ref: ${{ github.event.pull_request.head.sha }}
134+
135+
# - name: Get CMake 3.24
136+
# uses: lukka/get-cmake@latest
137+
# with:
138+
# cmakeVersion: 3.24.3
139+
140+
# - name: Set up Python ${{ matrix.python }}
141+
# uses: actions/setup-python@v4
142+
# with:
143+
# python-version: ${{ matrix.python }}
144+
145+
# - name: Install
146+
# run: |
147+
# pip install --upgrade pip
148+
# pip install -e .[testing]
149+
150+
# - name: Test
151+
# run: |
152+
# python -m pytest -k test_egrabber
153153

154154
spinnaker:
155155
name: Python ${{ matrix.python }} (Spinnaker)

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "acquire-imaging"
33
authors = ["Nathan Clack <[email protected]>"]
4-
version = "0.3.0"
4+
version = "0.3.0-rc1"
55
edition = "2021"
66

77
[lib]

drivers.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"acquire-driver-common": "0.1.6",
3-
"acquire-driver-zarr": "0.1.7",
3+
"acquire-driver-zarr": "0.1.8",
44
"acquire-driver-egrabber": "0.1.5",
55
"acquire-driver-hdcam": "0.1.7",
66
"acquire-driver-spinnaker": "0.1.1"

python/acquire/__init__.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -190,17 +190,16 @@ def next_frame() -> Optional[npt.NDArray[Any]]:
190190
"""Get the next frame from the current stream."""
191191
if nframes[stream_id] < p.video[stream_id].max_frame_count:
192192
with runtime.get_available_data(stream_id) as packet:
193-
if packet:
194-
n = packet.get_frame_count()
195-
nframes[stream_id] += n
196-
logging.info(
197-
f"[stream {stream_id}] frame count: {nframes}"
198-
)
199-
f = next(packet.frames())
200-
logging.debug(
201-
f"stream {stream_id} frame {f.metadata().frame_id}"
202-
)
203-
return f.data().squeeze().copy()
193+
n = packet.get_frame_count()
194+
nframes[stream_id] += n
195+
logging.info(
196+
f"[stream {stream_id}] frame count: {nframes}"
197+
)
198+
f = next(packet.frames())
199+
logging.debug(
200+
f"stream {stream_id} frame {f.metadata().frame_id}"
201+
)
202+
return f.data().squeeze().copy()
204203
return None
205204

206205
while is_not_done(): # runtime.get_state()==DeviceState.Running:

python/acquire/acquire.pyi

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ from numpy.typing import NDArray
1414

1515
@final
1616
class AvailableDataContext:
17-
def __enter__(self) -> Optional[AvailableData]: ...
17+
def __enter__(self) -> AvailableData: ...
1818
def __exit__(
1919
self, exc_type: Any, exc_value: Any, traceback: Any
2020
) -> None: ...
@@ -23,7 +23,6 @@ class AvailableDataContext:
2323
class AvailableData:
2424
def frames(self) -> Iterator[VideoFrame]: ...
2525
def get_frame_count(self) -> int: ...
26-
def invalidate(self) -> None: ...
2726
def __iter__(self) -> Iterator[VideoFrame]: ...
2827

2928
@final

src/runtime.rs

Lines changed: 55 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use numpy::{
55
Ix4, ToPyArray,
66
};
77
use parking_lot::Mutex;
8+
use pyo3::exceptions::PyRuntimeError;
89
use pyo3::prelude::*;
910
use serde::{Deserialize, Serialize};
1011
use std::{
@@ -196,7 +197,14 @@ impl Runtime {
196197
Ok(AvailableDataContext {
197198
inner: self.inner.clone(),
198199
stream_id,
199-
available_data: None,
200+
available_data: Python::with_gil(|py| {
201+
Py::new(
202+
py,
203+
AvailableData {
204+
inner: Arc::new(Mutex::new(None)),
205+
},
206+
)
207+
})?,
200208
})
201209
}
202210
}
@@ -273,13 +281,13 @@ impl Drop for RawAvailableData {
273281

274282
#[pyclass]
275283
pub(crate) struct AvailableData {
276-
inner: Option<Arc<RawAvailableData>>,
284+
inner: Arc<Mutex<Option<RawAvailableData>>>,
277285
}
278286

279287
#[pymethods]
280288
impl AvailableData {
281289
fn get_frame_count(&self) -> usize {
282-
if let Some(inner) = &self.inner {
290+
if let Some(inner) = &*self.inner.lock() {
283291
inner.get_frame_count()
284292
} else {
285293
0
@@ -288,9 +296,9 @@ impl AvailableData {
288296

289297
fn frames(&self) -> VideoFrameIterator {
290298
VideoFrameIterator {
291-
inner: if let Some(frames) = &self.inner {
299+
inner: if let Some(frames) = &*self.inner.lock() {
292300
Some(VideoFrameIteratorInner {
293-
store: frames.clone(),
301+
store: self.inner.clone(),
294302
cur: Mutex::new(frames.beg),
295303
end: frames.end,
296304
})
@@ -303,24 +311,26 @@ impl AvailableData {
303311
fn __iter__(slf: PyRef<'_, Self>) -> PyResult<Py<VideoFrameIterator>> {
304312
Py::new(slf.py(), slf.frames())
305313
}
314+
}
306315

316+
impl AvailableData {
307317
fn invalidate(&mut self) {
308318
// Will drop the RawAvailableData and cause Available data to act like
309319
// an empty iterator.
310-
self.inner = None;
320+
*self.inner.lock() = None;
311321
}
312322
}
313323

314324
#[pyclass]
315325
pub(crate) struct AvailableDataContext {
316326
inner: Arc<RawRuntime>,
317327
stream_id: u32,
318-
available_data: Option<Py<AvailableData>>,
328+
available_data: Py<AvailableData>,
319329
}
320330

321331
#[pymethods]
322332
impl AvailableDataContext {
323-
fn __enter__(&mut self) -> PyResult<Option<Py<AvailableData>>> {
333+
fn __enter__(&mut self) -> PyResult<Py<AvailableData>> {
324334
let AvailableDataContext {
325335
inner,
326336
stream_id,
@@ -329,45 +339,40 @@ impl AvailableDataContext {
329339
let stream_id = *stream_id;
330340
let (beg, end) = inner.map_read(stream_id)?;
331341
let nbytes = unsafe { byte_offset_from(beg, end) };
332-
if nbytes > 0 {
333-
log::trace!(
334-
"[stream {}] ACQUIRED {:p}-{:p}:{} bytes",
335-
stream_id,
336-
beg,
337-
end,
338-
nbytes
342+
343+
log::trace!(
344+
"[stream {}] ACQUIRED {:p}-{:p}:{} bytes",
345+
stream_id,
346+
beg,
347+
end,
348+
nbytes
349+
);
350+
*available_data = Python::with_gil(|py| {
351+
Py::new(
352+
py,
353+
AvailableData {
354+
inner: Arc::new(Mutex::new(Some(RawAvailableData {
355+
runtime: self.inner.clone(),
356+
beg: NonNull::new(beg).ok_or(anyhow!("Expected non-null buffer"))?,
357+
end: NonNull::new(end).ok_or(anyhow!("Expected non-null buffer"))?,
358+
stream_id,
359+
consumed_bytes: None,
360+
}))),
361+
},
339362
)
340-
};
341-
if nbytes > 0 {
342-
*available_data = Some(Python::with_gil(|py| {
343-
Py::new(
344-
py,
345-
AvailableData {
346-
inner: Some(Arc::new(RawAvailableData {
347-
runtime: self.inner.clone(),
348-
beg: NonNull::new(beg).ok_or(anyhow!("Expected non-null buffer"))?,
349-
end: NonNull::new(end).ok_or(anyhow!("Expected non-null buffer"))?,
350-
stream_id,
351-
consumed_bytes: None,
352-
})),
353-
},
354-
)
355-
})?);
356-
}
363+
})?;
357364
return Ok(self.available_data.clone());
358365
}
359366

360367
fn __exit__(&mut self, _exc_type: &PyAny, _exc_value: &PyAny, _traceback: &PyAny) {
361368
Python::with_gil(|py| {
362-
if let Some(a) = &self.available_data {
363-
a.as_ref(py).borrow_mut().invalidate()
364-
};
369+
(&self.available_data).as_ref(py).borrow_mut().invalidate();
365370
});
366371
}
367372
}
368373

369374
struct VideoFrameIteratorInner {
370-
store: Arc<RawAvailableData>,
375+
store: Arc<Mutex<Option<RawAvailableData>>>,
371376
cur: Mutex<NonNull<capi::VideoFrame>>,
372377
end: NonNull<capi::VideoFrame>,
373378
}
@@ -379,7 +384,7 @@ impl Iterator for VideoFrameIteratorInner {
379384

380385
fn next(&mut self) -> Option<Self::Item> {
381386
let mut cur = self.cur.lock();
382-
if *cur < self.end {
387+
if (*self.store.lock()).is_some() && *cur < self.end {
383388
let out = VideoFrame {
384389
_store: self.store.clone(),
385390
cur: *cur,
@@ -503,7 +508,7 @@ impl IntoDimension for capi::ImageShape {
503508

504509
#[pyclass]
505510
pub(crate) struct VideoFrame {
506-
_store: Arc<RawAvailableData>,
511+
_store: Arc<Mutex<Option<RawAvailableData>>>,
507512
cur: NonNull<capi::VideoFrame>,
508513
}
509514

@@ -512,6 +517,11 @@ unsafe impl Send for VideoFrame {}
512517
#[pymethods]
513518
impl VideoFrame {
514519
fn metadata(slf: PyRef<'_, Self>) -> PyResult<VideoFrameMetadata> {
520+
if (*slf._store.lock()).is_none() {
521+
return Err(PyRuntimeError::new_err(
522+
"VideoFrame is not valid outside of context",
523+
));
524+
}
515525
let cur = slf.cur.as_ptr();
516526
let meta = unsafe {
517527
VideoFrameMetadata {
@@ -522,7 +532,12 @@ impl VideoFrame {
522532
Ok(meta)
523533
}
524534

525-
fn data<'py>(&self, py: Python<'py>) -> Py<PyAny> {
535+
fn data<'py>(&self, py: Python<'py>) -> PyResult<Py<PyAny>> {
536+
if (*self._store.lock()).is_none() {
537+
return Err(PyRuntimeError::new_err(
538+
"VideoFrame is not valid outside of context",
539+
));
540+
}
526541
let cur = self.cur.as_ptr();
527542

528543
macro_rules! gen_match {
@@ -556,7 +571,7 @@ impl VideoFrame {
556571
}
557572
.unwrap();
558573

559-
array.to_pyobject(py)
574+
Ok(array.to_pyobject(py))
560575
}
561576
}
562577

0 commit comments

Comments
 (0)