11"""GenTL backend implemented using the Harvesters library."""
22
3+ # dlclivegui/cameras/backends/gentl_backend.py
34from __future__ import annotations
45
56import glob
67import logging
78import os
89import time
910from collections .abc import Iterable
11+ from typing import ClassVar
1012
1113import cv2
1214import numpy as np
1315
14- from ..base import CameraBackend , register_backend
16+ from ..base import CameraBackend , SupportLevel , register_backend
1517
1618LOG = logging .getLogger (__name__ )
1719
3133class GenTLCameraBackend (CameraBackend ):
3234 """Capture frames from GenTL-compatible devices via Harvesters."""
3335
36+ OPTIONS_KEY : ClassVar [str ] = "gentl"
3437 _DEFAULT_CTI_PATTERNS : tuple [str , ...] = (
3538 r"C:\\Program Files\\The Imaging Source Europe GmbH\\IC4 GenTL Driver for USB3Vision Devices *\\bin\\*.cti" ,
3639 r"C:\\Program Files\\The Imaging Source Europe GmbH\\TIS Grabber\\bin\\win64_x64\\*.cti" ,
@@ -40,28 +43,67 @@ class GenTLCameraBackend(CameraBackend):
4043
4144 def __init__ (self , settings ):
4245 super ().__init__ (settings )
43- props = settings .properties
44- self ._cti_file : str | None = props .get ("cti_file" )
45- self ._serial_number : str | None = props .get ("serial_number" ) or props .get ("serial" )
46- self ._pixel_format : str = props .get ("pixel_format" , "Mono8" )
47- self ._rotate : int = int (props .get ("rotate" , 0 )) % 360
48- self ._crop : tuple [int , int , int , int ] | None = self ._parse_crop (props .get ("crop" ))
49- # Check settings first (from config), then properties (for backward compatibility)
50- self ._exposure : float | None = settings .exposure if settings .exposure else props .get ("exposure" )
51- self ._gain : float | None = settings .gain if settings .gain else props .get ("gain" )
52- self ._timeout : float = float (props .get ("timeout" , 2.0 ))
53- self ._cti_search_paths : tuple [str , ...] = self ._parse_cti_paths (props .get ("cti_search_paths" ))
54- # Parse resolution (width, height) with defaults
55- self ._resolution : tuple [int , int ] | None = self ._parse_resolution (props .get ("resolution" ))
46+
47+ props = settings .properties if isinstance (settings .properties , dict ) else {}
48+ ns = props .get (self .OPTIONS_KEY , {})
49+ if not isinstance (ns , dict ):
50+ ns = {}
51+
52+ self ._cti_file : str | None = ns .get ("cti_file" ) or props .get ("cti_file" )
53+ self ._serial_number : str | None = (
54+ ns .get ("serial_number" ) or ns .get ("serial" ) or props .get ("serial_number" ) or props .get ("serial" )
55+ )
56+ self ._pixel_format : str = ns .get ("pixel_format" ) or props .get ("pixel_format" , "Mono8" )
57+ self ._rotate : int = int (ns .get ("rotate" , props .get ("rotate" , 0 ))) % 360
58+ self ._crop : tuple [int , int , int , int ] | None = self ._parse_crop (ns .get ("crop" , props .get ("crop" )))
59+
60+ self ._exposure : float | None = (
61+ settings .exposure if settings .exposure else ns .get ("exposure" , props .get ("exposure" ))
62+ )
63+ self ._gain : float | None = settings .gain if settings .gain else ns .get ("gain" , props .get ("gain" ))
64+
65+ self ._timeout : float = float (ns .get ("timeout" , props .get ("timeout" , 2.0 )))
66+ self ._cti_search_paths : tuple [str , ...] = self ._parse_cti_paths (
67+ ns .get ("cti_search_paths" , props .get ("cti_search_paths" ))
68+ )
69+
70+ # Resolution request (None = device default)
71+ self ._requested_resolution : tuple [int , int ] | None = self ._get_requested_resolution_or_none ()
72+
73+ # Actuals for GUI
74+ self ._actual_width : int | None = None
75+ self ._actual_height : int | None = None
76+ self ._actual_fps : float | None = None
5677
5778 self ._harvester = None
5879 self ._acquirer = None
5980 self ._device_label : str | None = None
6081
82+ @property
83+ def actual_resolution (self ) -> tuple [int , int ] | None :
84+ if self ._actual_width and self ._actual_height :
85+ return (self ._actual_width , self ._actual_height )
86+ return None
87+
88+ @property
89+ def actual_fps (self ) -> float | None :
90+ return self ._actual_fps
91+
6192 @classmethod
6293 def is_available (cls ) -> bool :
6394 return Harvester is not None
6495
96+ @classmethod
97+ def static_capabilities (cls ) -> dict [str , SupportLevel ]:
98+ return {
99+ "set_resolution" : SupportLevel .SUPPORTED ,
100+ "set_fps" : SupportLevel .SUPPORTED ,
101+ "set_exposure" : SupportLevel .SUPPORTED ,
102+ "set_gain" : SupportLevel .SUPPORTED ,
103+ "device_discovery" : SupportLevel .SUPPORTED ,
104+ "stable_identity" : SupportLevel .SUPPORTED ,
105+ }
106+
65107 @classmethod
66108 def get_device_count (cls ) -> int :
67109 """Get the actual number of GenTL devices detected by Harvester.
@@ -158,6 +200,19 @@ def open(self) -> None:
158200 self ._configure_gain (node_map )
159201 self ._configure_frame_rate (node_map )
160202
203+ # Capture actual resolution even when using defaults
204+ try :
205+ self ._actual_width = int (node_map .Width .value )
206+ self ._actual_height = int (node_map .Height .value )
207+ except Exception :
208+ pass
209+
210+ # Capture actual FPS if available
211+ try :
212+ self ._actual_fps = float (node_map .ResultingFrameRate .value )
213+ except Exception :
214+ self ._actual_fps = None
215+
161216 self ._acquirer .start ()
162217
163218 def read (self ) -> tuple [np .ndarray , float ]:
@@ -184,6 +239,12 @@ def read(self) -> tuple[np.ndarray, float]:
184239
185240 frame = self ._convert_frame (frame )
186241 timestamp = time .time ()
242+
243+ if self ._actual_width is None or self ._actual_height is None :
244+ h , w = frame .shape [:2 ]
245+ self ._actual_width = int (w )
246+ self ._actual_height = int (h )
247+
187248 return frame , timestamp
188249
189250 def stop (self ) -> None :
@@ -232,26 +293,77 @@ def _parse_crop(self, crop) -> tuple[int, int, int, int] | None:
232293 return tuple (int (v ) for v in crop )
233294 return None
234295
235- def _parse_resolution (self , resolution ) -> tuple [int , int ] | None :
236- """Parse resolution setting.
296+ def _get_requested_resolution_or_none (self ) -> tuple [int , int ] | None :
297+ """
298+ Return (w, h) if user explicitly requested a resolution.
299+ Return None to keep device defaults.
300+ """
301+ props = self .settings .properties if isinstance (self .settings .properties , dict ) else {}
302+
303+ legacy = props .get ("resolution" )
304+ if isinstance (legacy , (list , tuple )) and len (legacy ) == 2 :
305+ try :
306+ w , h = int (legacy [0 ]), int (legacy [1 ])
307+ if w > 0 and h > 0 :
308+ return (w , h )
309+ except Exception :
310+ pass
311+
312+ try :
313+ w = int (getattr (self .settings , "width" , 0 ) or 0 )
314+ h = int (getattr (self .settings , "height" , 0 ) or 0 )
315+ if w > 0 and h > 0 :
316+ return (w , h )
317+ except Exception :
318+ pass
237319
238- Args:
239- resolution: Can be a tuple/list [width, height], or None
320+ return None
240321
241- Returns:
242- Tuple of (width, height) or None if not specified
243- Default is (720, 540) if parsing fails but value is provided
322+ def _configure_resolution (self , node_map ) -> None :
323+ """
324+ Configure camera resolution only if explicitly requested.
325+ If None, keep device defaults.
244326 """
245- if resolution is None :
246- return (720 , 540 ) # Default resolution
327+ req = self ._requested_resolution
328+ if req is None :
329+ LOG .info ("Resolution: using device default." )
330+ return
247331
248- if isinstance (resolution , (list , tuple )) and len (resolution ) == 2 :
249- try :
250- return (int (resolution [0 ]), int (resolution [1 ]))
251- except (ValueError , TypeError ):
252- return (720 , 540 )
332+ requested_width , requested_height = req
333+ actual_width , actual_height = None , None
334+
335+ # Width
336+ try :
337+ node = node_map .Width
338+ min_w , max_w = node .min , node .max
339+ inc_w = getattr (node , "inc" , 1 )
340+ width = self ._adjust_to_increment (requested_width , min_w , max_w , inc_w )
341+ node .value = int (width )
342+ actual_width = node .value
343+ except Exception as e :
344+ LOG .warning (f"Failed to set width: { e } " )
253345
254- return (720 , 540 )
346+ # Height
347+ try :
348+ node = node_map .Height
349+ min_h , max_h = node .min , node .max
350+ inc_h = getattr (node , "inc" , 1 )
351+ height = self ._adjust_to_increment (requested_height , min_h , max_h , inc_h )
352+ node .value = int (height )
353+ actual_height = node .value
354+ except Exception as e :
355+ LOG .warning (f"Failed to set height: { e } " )
356+
357+ if actual_width is not None and actual_height is not None :
358+ self ._actual_width = int (actual_width )
359+ self ._actual_height = int (actual_height )
360+ if (actual_width , actual_height ) != (requested_width , requested_height ):
361+ LOG .warning (
362+ f"Resolution mismatch: requested { requested_width } x{ requested_height } , "
363+ f"got { actual_width } x{ actual_height } "
364+ )
365+ else :
366+ LOG .info (f"Resolution set to { actual_width } x{ actual_height } " )
255367
256368 @staticmethod
257369 def _search_cti_file (patterns : tuple [str , ...]) -> str | None :
0 commit comments