11"""Aravis backend for GenICam cameras."""
22
3+ # dlclivegui/cameras/backends/aravis_backend.py
34from __future__ import annotations
45
56import logging
67import time
8+ from typing import ClassVar
79
810import cv2
911import numpy as np
1012
11- from ..base import CameraBackend , register_backend
13+ from ..base import CameraBackend , SupportLevel , register_backend
1214
1315LOG = logging .getLogger (__name__ )
1416
2830class AravisCameraBackend (CameraBackend ):
2931 """Capture frames from GenICam-compatible devices via Aravis."""
3032
33+ OPTIONS_KEY : ClassVar [str ] = "aravis"
34+
3135 def __init__ (self , settings ):
3236 super ().__init__ (settings )
33- props = settings .properties
34- self ._camera_id : str | None = props .get ("camera_id" )
35- self ._pixel_format : str = props .get ("pixel_format" , "Mono8" )
36- self ._timeout : int = int (props .get ("timeout" , 2000000 )) # microseconds
37- self ._n_buffers : int = int (props .get ("n_buffers" , 10 ))
37+
38+ props = settings .properties if isinstance (settings .properties , dict ) else {}
39+ ns = props .get (self .OPTIONS_KEY , {})
40+ if not isinstance (ns , dict ):
41+ ns = {}
42+
43+ self ._camera_id : str | None = ns .get ("camera_id" ) or props .get ("camera_id" )
44+ self ._pixel_format : str = ns .get ("pixel_format" ) or props .get ("pixel_format" , "Mono8" )
45+ self ._timeout : int = int (ns .get ("timeout" , props .get ("timeout" , 2_000_000 )))
46+ self ._n_buffers : int = int (ns .get ("n_buffers" , props .get ("n_buffers" , 10 )))
47+
48+ # Resolution handling
49+ self ._requested_resolution : tuple [int , int ] | None = self ._get_requested_resolution_or_none ()
50+ self ._actual_width : int | None = None
51+ self ._actual_height : int | None = None
52+ self ._actual_fps : float | None = None
3853
3954 self ._camera = None
4055 self ._stream = None
4156 self ._device_label : str | None = None
4257
58+ @property
59+ def actual_resolution (self ) -> tuple [int , int ] | None :
60+ """Return the actual resolution of the camera after opening."""
61+ if self ._actual_width is not None and self ._actual_height is not None :
62+ return (self ._actual_width , self ._actual_height )
63+ return None
64+
65+ @property
66+ def actual_fps (self ) -> float | None :
67+ """Return the actual frame rate of the camera after opening."""
68+ return self ._actual_fps
69+
4370 @classmethod
4471 def is_available (cls ) -> bool :
4572 """Check if Aravis is available on this system."""
4673 return ARAVIS_AVAILABLE
4774
75+ @classmethod
76+ def static_capabilities (cls ) -> dict [str , SupportLevel ]:
77+ """Return a dict describing supported features for UI purposes."""
78+ caps = super ().static_capabilities ()
79+ caps .update (
80+ {
81+ "set_resolution" : SupportLevel .SUPPORTED ,
82+ "set_fps" : SupportLevel .SUPPORTED ,
83+ "set_exposure" : SupportLevel .SUPPORTED ,
84+ "set_gain" : SupportLevel .SUPPORTED ,
85+ "device_discovery" : SupportLevel .SUPPORTED ,
86+ "stable_identity" : SupportLevel .SUPPORTED ,
87+ }
88+ )
89+ return caps
90+
4891 @classmethod
4992 def get_device_count (cls ) -> int :
5093 """Get the actual number of Aravis devices detected.
@@ -61,54 +104,54 @@ def get_device_count(cls) -> int:
61104 return - 1
62105
63106 def open (self ) -> None :
64- """Open the Aravis camera device."""
65- if not ARAVIS_AVAILABLE : # pragma: no cover - optional dependency
66- raise RuntimeError (
67- "The 'aravis' library is required for the Aravis backend. "
68- "Install it via your system package manager (e.g., 'sudo apt install gir1.2-aravis-0.8' on Ubuntu)."
69- )
70-
71- # Update device list
107+ if not ARAVIS_AVAILABLE :
108+ raise RuntimeError ("Aravis library not available" )
109+
72110 Aravis .update_device_list ()
73111 n_devices = Aravis .get_n_devices ()
74-
75112 if n_devices == 0 :
76113 raise RuntimeError ("No Aravis cameras detected" )
77114
78- # Open camera by ID or index
79115 if self ._camera_id :
80116 self ._camera = Aravis .Camera .new (self ._camera_id )
81- if self ._camera is None :
82- raise RuntimeError (f"Failed to open camera with ID '{ self ._camera_id } '" )
83117 else :
84118 index = int (self .settings .index or 0 )
85119 if index < 0 or index >= n_devices :
86120 raise RuntimeError (f"Camera index { index } out of range for { n_devices } Aravis device(s)" )
87121 camera_id = Aravis .get_device_id (index )
88122 self ._camera = Aravis .Camera .new (camera_id )
89- if self ._camera is None :
90- raise RuntimeError (f"Failed to open camera at index { index } " )
91123
92- # Get device information for label
124+ if self ._camera is None :
125+ raise RuntimeError ("Failed to open Aravis camera" )
126+
93127 self ._device_label = self ._resolve_device_label ()
94128
95- # Configure camera
96129 self ._configure_pixel_format ()
130+ self ._configure_resolution ()
97131 self ._configure_exposure ()
98132 self ._configure_gain ()
99133 self ._configure_frame_rate ()
100134
101- # Create stream
135+ # Capture actual resolution even when using defaults
136+ try :
137+ self ._actual_width = int (self ._camera .get_integer ("Width" ))
138+ self ._actual_height = int (self ._camera .get_integer ("Height" ))
139+ except Exception :
140+ pass
141+
142+ try :
143+ self ._actual_fps = float (self ._camera .get_float ("AcquisitionFrameRate" ))
144+ except Exception :
145+ self ._actual_fps = None
146+
102147 self ._stream = self ._camera .create_stream (None , None )
103148 if self ._stream is None :
104149 raise RuntimeError ("Failed to create Aravis stream" )
105150
106- # Push buffers to stream
107151 payload_size = self ._camera .get_payload ()
108152 for _ in range (self ._n_buffers ):
109153 self ._stream .push_buffer (Aravis .Buffer .new_allocate (payload_size ))
110154
111- # Start acquisition
112155 self ._camera .start_acquisition ()
113156
114157 def read (self ) -> tuple [np .ndarray , float ]:
@@ -136,6 +179,10 @@ def read(self) -> tuple[np.ndarray, float]:
136179 height = buffer .get_image_height ()
137180 pixel_format = buffer .get_image_pixel_format ()
138181
182+ if self ._actual_width is None or self ._actual_height is None :
183+ self ._actual_width = int (width )
184+ self ._actual_height = int (height )
185+
139186 # Convert to numpy array
140187 if pixel_format == Aravis .PIXEL_FORMAT_MONO_8 :
141188 frame = np .frombuffer (data , dtype = np .uint8 ).reshape ((height , width ))
@@ -214,6 +261,61 @@ def device_name(self) -> str:
214261 # ------------------------------------------------------------------
215262 # Configuration helpers
216263 # ------------------------------------------------------------------
264+ def _get_requested_resolution_or_none (self ) -> tuple [int , int ] | None :
265+ """
266+ Return (w, h) if user explicitly requested a resolution.
267+ Return None to keep device defaults.
268+ """
269+ props = self .settings .properties if isinstance (self .settings .properties , dict ) else {}
270+
271+ legacy = props .get ("resolution" )
272+ if isinstance (legacy , (list , tuple )) and len (legacy ) == 2 :
273+ try :
274+ w , h = int (legacy [0 ]), int (legacy [1 ])
275+ if w > 0 and h > 0 :
276+ return (w , h )
277+ except Exception :
278+ pass
279+
280+ try :
281+ w = int (getattr (self .settings , "width" , 0 ) or 0 )
282+ h = int (getattr (self .settings , "height" , 0 ) or 0 )
283+ if w > 0 and h > 0 :
284+ return (w , h )
285+ except Exception :
286+ pass
287+
288+ return None
289+
290+ def _configure_resolution (self ) -> None :
291+ """
292+ Apply width/height only if explicitly requested.
293+ If None, keep device defaults.
294+ """
295+ if self ._camera is None :
296+ return
297+
298+ req = self ._requested_resolution
299+ if req is None :
300+ LOG .info ("Resolution: using device default." )
301+ return
302+
303+ req_w , req_h = req
304+ try :
305+ self ._camera .set_integer ("Width" , int (req_w ))
306+ self ._camera .set_integer ("Height" , int (req_h ))
307+
308+ aw = int (self ._camera .get_integer ("Width" ))
309+ ah = int (self ._camera .get_integer ("Height" ))
310+ self ._actual_width = aw
311+ self ._actual_height = ah
312+
313+ if (aw , ah ) != (req_w , req_h ):
314+ LOG .warning (f"Resolution mismatch: requested { req_w } x{ req_h } , got { aw } x{ ah } " )
315+ else :
316+ LOG .info (f"Resolution set to { aw } x{ ah } " )
317+ except Exception as exc :
318+ LOG .warning (f"Failed to set resolution to { req_w } x{ req_h } : { exc } " )
217319
218320 def _configure_pixel_format (self ) -> None :
219321 """Configure the camera pixel format."""
0 commit comments