Source code for QVideo.cameras.Genicam._tree

from typing import Any
import math
from qtpy import QtCore
from QVideo.lib import QCameraTree
from QVideo.cameras.Genicam import QGenicamCamera
from genicam.genapi import (IValue, EAccessMode, EVisibility,
                            ICategory, ICommand, IEnumeration,
                            IBoolean, IInteger, IFloat, IString)
import logging


logger = logging.getLogger(__name__)

_MICROSECOND_UNITS = frozenset({'us', 'µs', 'μs'})

__all__ = ['QGenicamTree']


class QGenicamTree(QCameraTree):

    '''Camera property tree for
    :class:`~QVideo.cameras.Genicam.QGenicamCamera`.

    Builds a :class:`~QVideo.lib.QCameraTree.QCameraTree` from the camera's
    GenICam node map and exposes visibility and per-feature enable/disable
    controls.

    A timer polls the camera periodically so that autonomous camera-side
    changes (e.g. ``Gain`` being adjusted by auto-exposure, ``GainAuto``
    reverting from ``"Once"`` to ``"Off"``) are reflected in the UI.
    ``PyNodeCallback`` is not used because it only fires when the **host**
    writes a node, not when the camera changes a value autonomously.

    Parameters
    ----------
    camera : QGenicamCamera
        Camera instance to use.
    visibility : EVisibility
        Maximum GenICam visibility level to display.
        Default: ``EVisibility.Guru``.
    controls : list of str or None
        If given, only nodes whose names appear in this list are shown;
        all others are hidden.  Default: ``None`` (show all).
    *args :
        Forwarded to :class:`~QVideo.lib.QCameraTree.QCameraTree`.
    **kwargs :
        Forwarded to :class:`~QVideo.lib.QCameraTree.QCameraTree`.
    '''

    def __init__(self, *args,
                 camera: QGenicamCamera,
                 visibility: EVisibility = EVisibility.Guru,
                 controls: list[str] | None = None,
                 **kwargs) -> None:
        self._scaleFactors: dict[str, float] = {}
        self._decParams: set[str] = set()
        description = self.description(camera)
        super().__init__(camera, description, *args, **kwargs)
        self._visibility = visibility
        self.controls = controls
        self.visibility = visibility
        self._updateEnabled()
        self._startTimer()

    def _startTimer(self) -> None:
        '''Start the timer to poll camera-side changes.'''
        self._timer = QtCore.QTimer(self)
        self._timer.setInterval(500)
        self._timer.timeout.connect(self._pollCamera)
        self._timer.start()
        instance = QtCore.QCoreApplication.instance()
        quitting = instance.aboutToQuit
        quitting.connect(self._timer.stop)

[docs] def closeEvent(self, event) -> None: self._timer.stop() super().closeEvent(event)
[docs] def description(self, camera: QGenicamCamera) -> list[dict]: '''Return a list of dicts describing the node map''' root = camera.node('Root') if root is None: return [] description = self.describe(root) return description.get('children', [])
[docs] def describe(self, feature: IValue) -> dict[str, Any]: '''Return a dictionary describing a feature''' this = dict(name=feature.node.name, title=feature.node.display_name, visibility=feature.node.visibility) mode = feature.node.get_access_mode() if mode == EAccessMode.NI: return this if isinstance(feature, ICategory): this['type'] = 'group' this['children'] = [self.describe(f) for f in feature.features] return this if isinstance(feature, ICommand): this['type'] = 'action' return this if mode not in (EAccessMode.RW, EAccessMode.RO): return this if isinstance(feature, IEnumeration): this['type'] = 'list' this['value'] = this['default'] = feature.to_string() this['limits'] = [v.symbolic for v in feature.entries] elif isinstance(feature, IBoolean): this['type'] = 'bool' this['value'] = this['default'] = feature.value elif isinstance(feature, IInteger): this['type'] = 'int' this['value'] = this['default'] = feature.value this['min'] = feature.min this['max'] = feature.max this['step'] = feature.inc elif isinstance(feature, IFloat): this['type'] = 'float' unit = feature.unit if unit in _MICROSECOND_UNITS: scale = 1e-6 self._scaleFactors[feature.node.name] = scale this['siPrefix'] = True unit = 's' else: scale = 1.0 this['value'] = this['default'] = feature.value * scale lo = feature.min * scale hi = feature.max * scale this['min'] = lo this['max'] = hi this['units'] = unit if feature.has_inc(): this['step'] = feature.inc * scale if lo > 0 and hi / lo > 100: self._decParams.add(feature.node.name) v = feature.value * scale if v > 0: this['step'] = 10 ** (math.floor(math.log10(v)) - 1) elif isinstance(feature, IString): this['type'] = 'str' this['value'] = this['default'] = feature.value else: # FIXME: Support for IRegister nodes logger.debug( f'Unsupported node type: {feature.node.name}: {type(feature)}') return this
def _connectSignals(self) -> None: super()._connectSignals() for item in self.listAllItems(): p = item.param p.sigValueChanged.connect(self._handleItemChanges) @QtCore.Slot(object, object) def _sync(self, root, changes) -> None: if self._ignoreSync: return for param, change, value in changes: if change == 'value': key = param.name() if key in self._scaleFactors: value = value / self._scaleFactors[key] # type: ignore self.camera.set(key, value) self._ignoreSync = True for key, value in self.camera.settings.items(): if key in self._scaleFactors: value = value * self._scaleFactors[key] # type: ignore self.set(key, value) self._ignoreSync = False @property def controls(self) -> list[str] | None: return self._controls @controls.setter def controls(self, controls: list[str] | None) -> None: self._controls = controls for item in self.listAllItems()[1:]: p = item.param name = p.opts['name'] node = self.camera.node(name) visible = controls is None or name in controls p.opts['visibility'] = (node.node.visibility if node is not None and visible else EVisibility.Invisible) self._updateVisible() @property def visibility(self) -> EVisibility: return self._visibility @visibility.setter def visibility(self, visibility: EVisibility) -> None: self._visibility = visibility self._updateVisible() def _handleItemChanges(self) -> None: if self._ignoreSync: return logger.debug('Handling item changes') self._updateVisible() self._updateEnabled() self._updateLimits() self._updateValues() def _updateVisible(self) -> None: for item in self.listAllItems()[1:]: p = item.param p.setOpts(visible=self.visible(p))
[docs] def visible(self, param) -> bool: ptype = param.opts['type'] if ptype in ('action', None): return False if ptype == 'group': return any(self.visible(c) for c in param.children()) get = param.opts.get visibility = get('visibility', EVisibility.Invisible) return visibility <= self.visibility
def _updateEnabled(self) -> None: for item in self.listAllItems()[1:]: p = item.param if p.opts.get('visible', False): name = p.opts['name'] if self.camera.has_node(name): p.setOpts(enabled=self.camera.is_readwrite(name)) def _updateLimits(self) -> None: '''Refresh Parameter constraints from the live GenICam node values. Called after every property change so that dependent nodes (e.g. ``OffsetX`` range after a ``Width`` change) reflect the current hardware state in the UI. Only visible leaf parameters are updated. ''' for item in self.listAllItems()[1:]: p = item.param if not p.opts.get('visible', False): continue name = p.opts.get('name') if not self.camera.has_node(name): continue node = self.camera.node(name) if isinstance(node, IInteger): p.setOpts(limits=(node.min, node.max), step=node.inc) elif isinstance(node, IFloat): scale = self._scaleFactors.get(name, 1.0) opts = {'limits': (node.min * scale, node.max * scale)} if name in self._decParams: value = p.value() if value > 0: step = 10 ** (math.floor(math.log10(value)) - 1) if node.has_inc(): step = max(step, node.inc * scale) opts['step'] = step elif node.has_inc(): opts['step'] = node.inc * scale p.setOpts(**opts) elif isinstance(node, IEnumeration): p.setOpts(limits=[v.symbolic for v in node.entries]) def _nodeValue(self, name: str, node: IValue) -> object: '''Read the current value of a node, applying any scale factor.''' if isinstance(node, IEnumeration): value = node.to_string() elif isinstance(node, (IBoolean, IInteger, IFloat, IString)): value = node.value else: return None if name in self._scaleFactors: value *= self._scaleFactors[name] return value def _updateValues(self) -> None: '''Refresh read-only Parameter values from the live GenICam node map. Called after every property change so that derived read-only nodes (e.g. ``AcquisitionResultingFrameRate``) reflect the current hardware state in the UI. Only visible read-only leaf parameters are updated. Signals are blocked during the update to prevent re-entrant calls. ''' for item in self.listAllItems()[1:]: p = item.param if not p.opts.get('visible', False): continue name = p.opts.get('name') if not self.camera.has_node(name): continue node = self.camera.node(name) if node.node.get_access_mode() != EAccessMode.RO: continue value = self._nodeValue(name, node) if value is None: continue p.blockSignals(True) try: p.setValue(value) finally: p.blockSignals(False) def _pollCamera(self) -> None: '''Refresh all readable Parameter values and enabled states. Called by the poll timer to pick up autonomous camera-side changes that do not generate host-side notifications — e.g. ``Gain`` being adjusted during auto-exposure, or ``GainAuto`` reverting from ``"Once"`` to ``"Off"`` after the sweep completes. Returns immediately if the camera is no longer open (e.g. during application shutdown) to prevent accessing freed C++ genapi objects. Signals are not blocked so that the visual widgets update and :meth:`_handleItemChanges` fires when a value changes. :attr:`_ignoreSync` is set for the duration so that the resulting ``sigTreeStateChanged`` emissions do not send values back to the camera. :meth:`_updateEnabled` is called unconditionally so that access-mode changes (e.g. ``ExposureTime`` becoming writable again after the sweep) are reflected even when the controlling node value has not changed. ''' if not self.camera.isOpen(): return self._ignoreSync = True try: self._updateLimits() for item in self.listAllItems()[1:]: p = item.param if not p.opts.get('visible', False): continue name = p.opts.get('name') if not self.camera.has_node(name): continue node = self.camera.node(name) mode = node.node.get_access_mode() if mode not in (EAccessMode.RO, EAccessMode.RW): continue value = self._nodeValue(name, node) if value is None: continue p.setValue(value) self._updateEnabled() finally: self._ignoreSync = False if __name__ == '__main__': # pragma: no cover QGenicamTree.example()