diff --git a/src/hik_camera_panel/README.md b/src/hik_camera_panel/README.md new file mode 100644 index 0000000..8f58098 --- /dev/null +++ b/src/hik_camera_panel/README.md @@ -0,0 +1,47 @@ +# hik_camera_panel + +`hik_camera_ros2_driver`(cam1/cam2/cam3)의 노출·게인·ROI 파라미터를 슬라이더 또는 직접 숫자 +입력으로 실시간 조절하는 패널. 카메라별 탭 안에 실시간 이미지 미리보기가 있고, ROI(측광 제외 +상단 비율)를 조절하면 그 경계선이 이미지 위에 바로 그려진다. + +## 실행 + +카메라 노드(`hik_camera_cam1`/`hik_camera_cam2`/`hik_camera_cam3`)가 먼저 떠 있어야 한다. + +```bash +ros2 run hik_camera_panel panel_node +``` + +## 구성 + +- 탭 3개(cam1/cam2/cam3), 각 탭은 `/hik_camera_camN/get_parameters`·`/hik_camera_camN/set_parameters` + 서비스를 직접 호출해서 값을 읽고 쓴다. +- 그룹: 노출 / 게인 / 소프트웨어 AE·ROI / 동기화. +- 숫자 파라미터는 슬라이더+스핀박스를 같이 제공한다. 슬라이더를 드래그하는 동안은 로컬 + 미리보기(ROI 오버레이)만 갱신되고, 손을 뗀 시점에 실제로 카메라에 값을 적용한다. 스핀박스는 + 값을 직접 입력하고 포커스를 벗어나면(또는 Enter) 바로 적용된다. +- 노출/노출상한/노출하한은 범위가 15µs~100ms로 넓어서 슬라이더를 로그 스케일로 매핑했다 + (`param_spec.py`의 `log_scale=True`). +- ROI 미리보기: 카메라의 `//image` 토픽을 구독해서 QImage로 바로 그린다 + (cv_bridge/OpenCV 의존성 없음 — 드라이버가 항상 `rgb8`로 발행하므로 `QImage.Format_RGB888`로 + 직접 변환 가능). + +## 알아둘 것 + +1. **`sync_role`/`sync_master_camera_ns`는 조회만 되고 편집은 막혀 있다.** 드라이버가 publisher/ + subscriber를 노드 시작 시 `initSync()`에서 한 번만 만들기 때문에, 런타임에 이 값을 바꿔도 + 반영되지 않는다 (`hik_camera_node.cpp`의 `dynamicParametersCallback()`도 이 두 파라미터는 + 처리하지 않음 — 시도하면 "Unknown parameter"로 거부됨). 마스터/슬레이브 역할을 바꾸려면 + yaml을 고치고 노드를 재시작해야 한다. +2. **나머지 파라미터는 이번에 `hik_camera_node.cpp`의 `dynamicParametersCallback()`을 확장해서 + 전부 런타임에 반영되도록 만들었다** (`gain_auto_max_db`, `ae_roi_top_ratio`, + `ae_target_percentile`, `ae_target_dn`, `ae_saturation_percentile`, `ae_saturation_dn`, + `ae_step_gain`). 이 패널이 슬라이더로 값을 바꿨을 때 실제로 카메라에 반영되려면 드라이버 + 쪽도 이 커밋 이후 버전이어야 한다. +3. **개발 PC(macOS)에는 ROS 2와 `python_qt_binding`이 없어서 실행 검증을 못 했다.** Python + 문법 검사(`python3 -m py_compile`)와 슬라이더↔값 변환 로직의 라운드트립 테스트만 순수 + Python으로 돌려봤고, 실제 rclpy 파라미터 서비스 호출·Qt 위젯 동작·이미지 렌더링은 로봇 + PC에서 `ros2 run hik_camera_panel panel_node`로 직접 확인해야 한다. +4. 카메라 노드/토픽 이름이 `camera_params_cam{1,2,3}.yaml` 및 + `hik_camera_triple_launch.py`와 다르면 `panel_node.py`의 `DEFAULT_CAMERAS` 목록을 맞춰 + 수정할 것. diff --git a/src/hik_camera_panel/hik_camera_panel/__init__.py b/src/hik_camera_panel/hik_camera_panel/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/hik_camera_panel/hik_camera_panel/panel_node.py b/src/hik_camera_panel/hik_camera_panel/panel_node.py new file mode 100644 index 0000000..eecf9c6 --- /dev/null +++ b/src/hik_camera_panel/hik_camera_panel/panel_node.py @@ -0,0 +1,440 @@ +"""hik_camera_ros2_driver 노출/게인/ROI 파라미터 조절 패널. + +카메라 노드(hik_camera_cam1/cam2/cam3)의 ROS 2 파라미터 서비스(get_parameters/ +set_parameters)를 직접 호출해서 슬라이더/스핀박스로 값을 읽고 쓴다. ROI(측광 제외 +상단 비율)는 카메라의 실시간 이미지 위에 경계선을 오버레이해서 눈으로 보면서 +조절할 수 있게 했다. + +실행: + ros2 run hik_camera_panel panel_node + +전제: + - hik_camera_cam1/cam2/cam3 노드가 이미 떠 있어야 한다 (안 떠 있으면 "새로고침"/ + 슬라이더 조작 시 상태바에 실패 메시지가 뜬다). + - sync_role / sync_master_camera_ns는 조회만 가능하고 편집은 막아뒀다 — 드라이버가 + publisher/subscriber를 노드 시작 시 한 번만 만들기 때문에 런타임에 값을 바꿔도 + 반영되지 않는다 (초기화 흐름을 다시 태우려면 노드 재시작 필요). +""" + +import sys +import threading + +import rclpy +from rclpy.node import Node +from rclpy.qos import qos_profile_sensor_data + +from rcl_interfaces.msg import Parameter, ParameterType, ParameterValue +from rcl_interfaces.srv import GetParameters, SetParameters + +from sensor_msgs.msg import Image + +from python_qt_binding.QtCore import QObject, Qt, pyqtSignal +from python_qt_binding.QtGui import QColor, QImage, QPainter, QPen, QPixmap +from python_qt_binding.QtWidgets import ( + QApplication, + QCheckBox, + QDoubleSpinBox, + QFormLayout, + QGroupBox, + QHBoxLayout, + QLabel, + QLineEdit, + QMainWindow, + QPushButton, + QSlider, + QSpinBox, + QStatusBar, + QTabWidget, + QVBoxLayout, + QWidget, +) + +from hik_camera_panel.param_spec import PARAM_SPEC_BY_NAME, PARAM_SPECS, SLIDER_STEPS, \ + slider_to_value, value_to_slider + +# (node_name, camera_name) — node_name은 파라미터 서비스(//set_parameters)용, +# camera_name은 이미지 토픽(//image)용. hik_camera_triple_launch.py / +# camera_params_cam{1,2,3}.yaml과 일치해야 한다. +DEFAULT_CAMERAS = [ + ('hik_camera_cam1', 'cam1'), + ('hik_camera_cam2', 'cam2'), + ('hik_camera_cam3', 'cam3'), +] + + +def make_parameter_msg(name, type_str, value): + pv = ParameterValue() + if type_str == 'bool': + pv.type = ParameterType.PARAMETER_BOOL + pv.bool_value = bool(value) + elif type_str == 'int': + pv.type = ParameterType.PARAMETER_INTEGER + pv.integer_value = int(round(value)) + elif type_str == 'double': + pv.type = ParameterType.PARAMETER_DOUBLE + pv.double_value = float(value) + else: + pv.type = ParameterType.PARAMETER_STRING + pv.string_value = str(value) + msg = Parameter() + msg.name = name + msg.value = pv + return msg + + +def parameter_value_to_python(pv): + if pv.type == ParameterType.PARAMETER_BOOL: + return pv.bool_value + if pv.type == ParameterType.PARAMETER_INTEGER: + return pv.integer_value + if pv.type == ParameterType.PARAMETER_DOUBLE: + return pv.double_value + if pv.type == ParameterType.PARAMETER_STRING: + return pv.string_value + return None + + +class RosBridge(QObject): + """rclpy 노드를 백그라운드 스레드에서 spin하고, 결과는 Qt 시그널로 GUI 스레드에 넘긴다. + + 파라미터 서비스 호출은 항상 call_async + add_done_callback으로 비동기 처리한다. + add_done_callback은 spin 스레드에서 실행되므로, 그 안에서 위젯을 직접 건드리지 않고 + 시그널만 emit한다 (Qt가 큐잉해서 GUI 스레드에서 슬롯을 실행해준다). + """ + + params_fetched = pyqtSignal(str, dict) # node_name, {param_name: value} + param_set_result = pyqtSignal(str, str, bool, str) # node_name, param_name, ok, reason + image_received = pyqtSignal(str, QImage) # camera_name, image + + def __init__(self): + super().__init__() + self._node = Node('hik_camera_panel') + self._set_clients = {} + self._get_clients = {} + self._image_subs = {} + self._spin_thread = threading.Thread(target=self._spin, daemon=True) + self._spin_thread.start() + + def _spin(self): + rclpy.spin(self._node) + + # -- 서비스 클라이언트 -------------------------------------------------- + + def _set_client(self, node_name): + if node_name not in self._set_clients: + self._set_clients[node_name] = self._node.create_client( + SetParameters, f'/{node_name}/set_parameters') + return self._set_clients[node_name] + + def _get_client(self, node_name): + if node_name not in self._get_clients: + self._get_clients[node_name] = self._node.create_client( + GetParameters, f'/{node_name}/get_parameters') + return self._get_clients[node_name] + + def fetch_parameters(self, node_name, specs): + client = self._get_client(node_name) + if not client.service_is_ready(): + self.param_set_result.emit( + node_name, '(새로고침)', False, + '파라미터 서비스에 연결할 수 없음 — 노드가 실행 중인지 확인하세요') + return + + req = GetParameters.Request() + req.names = [s['name'] for s in specs] + future = client.call_async(req) + + def _done(fut): + try: + resp = fut.result() + except Exception as exc: # noqa: BLE001 - 서비스 호출 자체 실패를 그대로 보고 + self.param_set_result.emit(node_name, '(새로고침)', False, str(exc)) + return + values = {} + for spec, pv in zip(specs, resp.values): + values[spec['name']] = parameter_value_to_python(pv) + self.params_fetched.emit(node_name, values) + + future.add_done_callback(_done) + + def set_parameter(self, node_name, name, type_str, value): + client = self._set_client(node_name) + if not client.service_is_ready(): + self.param_set_result.emit( + node_name, name, False, + '파라미터 서비스에 연결할 수 없음 — 노드가 실행 중인지 확인하세요') + return + + req = SetParameters.Request() + req.parameters = [make_parameter_msg(name, type_str, value)] + future = client.call_async(req) + + def _done(fut): + try: + resp = fut.result() + except Exception as exc: # noqa: BLE001 + self.param_set_result.emit(node_name, name, False, str(exc)) + return + result = resp.results[0] + self.param_set_result.emit(node_name, name, result.successful, result.reason) + + future.add_done_callback(_done) + + # -- 이미지 구독 ---------------------------------------------------------- + + def subscribe_image(self, camera_name): + if camera_name in self._image_subs: + return + + def _cb(msg): + if msg.encoding != 'rgb8': + return + qimg = QImage( + bytes(msg.data), msg.width, msg.height, msg.step, QImage.Format_RGB888).copy() + self.image_received.emit(camera_name, qimg) + + self._image_subs[camera_name] = self._node.create_subscription( + Image, f'/{camera_name}/image', _cb, qos_profile_sensor_data) + + def shutdown(self): + rclpy.shutdown() + + +class ParamRow(QWidget): + """파라미터 하나를 표시하는 한 줄: bool=체크박스, string=텍스트박스(읽기전용 가능), + 나머지(int/double)=슬라이더+스핀박스 동시 제공. + + - previewChanged: 슬라이더 드래그 중(아직 손 안 뗌) 매번 emit — 로컬 미리보기(ROI 오버레이 + 등)만 갱신하고 아직 카메라에는 보내지 않음. + - valueEdited: 슬라이더에서 손을 떼거나, 스핀박스 편집을 마치거나, 체크박스를 토글했을 때 + emit — 이때 실제로 ros2 파라미터를 설정한다. + """ + + valueEdited = pyqtSignal(str, object) + previewChanged = pyqtSignal(str, object) + + def __init__(self, spec, parent=None): + super().__init__(parent) + self.spec = spec + self._suppress = False + layout = QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + + self.checkbox = None + self.line_edit = None + self.slider = None + self.spin = None + + if spec['type'] == 'bool': + self.checkbox = QCheckBox() + self.checkbox.toggled.connect(self._on_bool_changed) + layout.addWidget(self.checkbox) + elif spec['type'] == 'string': + self.line_edit = QLineEdit() + self.line_edit.setReadOnly(spec.get('readonly', False)) + if spec.get('readonly'): + self.line_edit.setToolTip('런타임 변경 미지원 — 노드 재시작 필요') + layout.addWidget(self.line_edit) + else: + self.slider = QSlider(Qt.Horizontal) + self.slider.setRange(0, SLIDER_STEPS) + if spec['type'] == 'int': + self.spin = QSpinBox() + self.spin.setRange(int(spec['min']), int(spec['max'])) + else: + self.spin = QDoubleSpinBox() + self.spin.setRange(spec['min'], spec['max']) + decimals = spec.get('decimals', 2) + self.spin.setDecimals(decimals) + self.spin.setSingleStep(10 ** (-decimals) if decimals > 0 else 1.0) + layout.addWidget(self.slider, 3) + layout.addWidget(self.spin, 1) + + self.slider.sliderMoved.connect(self._on_slider_moved) + self.slider.sliderReleased.connect(self._on_slider_released) + self.spin.editingFinished.connect(self._on_spin_edited) + + def set_value(self, value): + self._suppress = True + try: + if self.checkbox is not None: + self.checkbox.setChecked(bool(value)) + elif self.line_edit is not None: + self.line_edit.setText(str(value)) + else: + self.spin.setValue(value) + self.slider.setValue(value_to_slider( + value, self.spec['min'], self.spec['max'], self.spec.get('log_scale', False))) + finally: + self._suppress = False + + def _on_bool_changed(self, checked): + if not self._suppress: + self.valueEdited.emit(self.spec['name'], checked) + + def _on_slider_moved(self, pos): + value = slider_to_value( + pos, self.spec['min'], self.spec['max'], self.spec.get('log_scale', False)) + self._suppress = True + self.spin.setValue(value) + self._suppress = False + self.previewChanged.emit(self.spec['name'], value) + + def _on_slider_released(self): + value = slider_to_value( + self.slider.value(), self.spec['min'], self.spec['max'], + self.spec.get('log_scale', False)) + self.valueEdited.emit(self.spec['name'], value) + + def _on_spin_edited(self): + if self._suppress: + return + value = self.spin.value() + self._suppress = True + self.slider.setValue(value_to_slider( + value, self.spec['min'], self.spec['max'], self.spec.get('log_scale', False))) + self._suppress = False + self.valueEdited.emit(self.spec['name'], value) + + +class CameraTab(QWidget): + def __init__(self, node_name, camera_name, bridge, parent=None): + super().__init__(parent) + self.node_name = node_name + self.camera_name = camera_name + self.bridge = bridge + self.rows = {} + self._latest_image = None + self._roi_ratio = 0.0 + + outer = QHBoxLayout(self) + + left_layout = QVBoxLayout() + groups = {} + for spec in PARAM_SPECS: + group_name = spec['group'] + if group_name not in groups: + box = QGroupBox(group_name) + box.setLayout(QFormLayout()) + groups[group_name] = box + left_layout.addWidget(box) + row = ParamRow(spec) + row.valueEdited.connect(self._on_value_edited) + row.previewChanged.connect(self._on_preview_changed) + groups[group_name].layout().addRow(spec['label'], row) + self.rows[spec['name']] = row + + refresh_btn = QPushButton('새로고침') + refresh_btn.clicked.connect(self.refresh) + left_layout.addWidget(refresh_btn) + left_layout.addStretch(1) + + left_widget = QWidget() + left_widget.setLayout(left_layout) + + self.preview = QLabel('이미지 대기 중...') + self.preview.setMinimumSize(480, 360) + self.preview.setAlignment(Qt.AlignCenter) + self.preview.setStyleSheet('background-color: #202020; color: #aaaaaa;') + + outer.addWidget(left_widget, 2) + outer.addWidget(self.preview, 3) + + self.bridge.params_fetched.connect(self._on_params_fetched) + self.bridge.image_received.connect(self._on_image_received) + self.bridge.subscribe_image(camera_name) + + self.refresh() + + def refresh(self): + self.bridge.fetch_parameters(self.node_name, PARAM_SPECS) + + def _on_params_fetched(self, node_name, values): + if node_name != self.node_name: + return + for name, value in values.items(): + if name in self.rows: + self.rows[name].set_value(value) + if 'ae_roi_top_ratio' in values: + self._roi_ratio = values['ae_roi_top_ratio'] + self._redraw_preview() + + def _on_value_edited(self, name, value): + spec = PARAM_SPEC_BY_NAME[name] + if spec.get('readonly'): + return + self.bridge.set_parameter(self.node_name, name, spec['type'], value) + if name == 'ae_roi_top_ratio': + self._roi_ratio = value + self._redraw_preview() + + def _on_preview_changed(self, name, value): + if name == 'ae_roi_top_ratio': + self._roi_ratio = value + self._redraw_preview() + + def _on_image_received(self, camera_name, qimg): + if camera_name != self.camera_name: + return + self._latest_image = qimg + self._redraw_preview() + + def _redraw_preview(self): + if self._latest_image is None: + return + target_width = self.preview.width() if self.preview.width() > 0 else 480 + pixmap = QPixmap.fromImage(self._latest_image).scaledToWidth( + target_width, Qt.SmoothTransformation) + + painter = QPainter(pixmap) + w, h = pixmap.width(), pixmap.height() + boundary_y = int(h * self._roi_ratio) + if boundary_y > 0: + painter.fillRect(0, 0, w, boundary_y, QColor(0, 0, 0, 120)) + pen = QPen(QColor(255, 60, 60)) + pen.setWidth(2) + painter.setPen(pen) + painter.drawLine(0, boundary_y, w, boundary_y) + painter.end() + + self.preview.setPixmap(pixmap) + + +class MainWindow(QMainWindow): + def __init__(self, bridge, cameras): + super().__init__() + self.setWindowTitle('hik_camera 노출 / 게인 / ROI 패널') + self.bridge = bridge + + tabs = QTabWidget() + for node_name, camera_name in cameras: + tabs.addTab(CameraTab(node_name, camera_name, bridge), camera_name) + self.setCentralWidget(tabs) + + self.setStatusBar(QStatusBar()) + bridge.param_set_result.connect(self._on_param_set_result) + + self.resize(1280, 720) + + def _on_param_set_result(self, node_name, name, ok, reason): + if ok: + self.statusBar().showMessage(f'[{node_name}] {name} 적용됨', 2000) + else: + self.statusBar().showMessage(f'[{node_name}] {name} 실패: {reason}', 6000) + + +def main(args=None): + rclpy.init(args=args) + + app = QApplication(sys.argv) + bridge = RosBridge() + window = MainWindow(bridge, DEFAULT_CAMERAS) + window.show() + + exit_code = app.exec_() + bridge.shutdown() + sys.exit(exit_code) + + +if __name__ == '__main__': + main() diff --git a/src/hik_camera_panel/hik_camera_panel/param_spec.py b/src/hik_camera_panel/hik_camera_panel/param_spec.py new file mode 100644 index 0000000..19c0997 --- /dev/null +++ b/src/hik_camera_panel/hik_camera_panel/param_spec.py @@ -0,0 +1,81 @@ +"""hik_camera_ros2_driver가 선언하는 파라미터 스펙. + +이름/타입/범위는 hik_camera_node.cpp의 declareParameters()와 dynamicParametersCallback()에 +선언/처리되는 것과 반드시 일치해야 한다 (그쪽을 바꾸면 여기도 같이 바꿀 것). + +sync_role / sync_master_camera_ns는 런타임에 값을 바꿔도 드라이버가 무시한다 +(subscription/publisher가 노드 시작 시 initSync()에서 한 번만 만들어짐) — 그래서 +readonly=True로 표시해 패널에서는 조회만 하고 편집은 막는다. +""" + +import math + +SLIDER_STEPS = 1000 + + +PARAM_SPECS = [ + # --- 노출 --- + dict(name='exposure_auto', type='bool', group='노출', label='자동 노출'), + dict(name='exposure_time', type='int', group='노출', label='수동 노출시간 [us]', + min=15, max=100000, log_scale=True), + dict(name='exposure_auto_target_brightness', type='int', group='노출', + label='목표 밝기 (온보드 AE)', min=0, max=255), + dict(name='exposure_auto_min', type='double', group='노출', label='자동 노출 하한 [us]', + min=15.0, max=100000.0, log_scale=True, decimals=0), + dict(name='exposure_auto_max', type='double', group='노출', label='자동 노출 상한 [us]', + min=15.0, max=100000.0, log_scale=True, decimals=0), + + # --- 게인 --- + dict(name='gain', type='double', group='게인', label='수동 게인 [dB]', + min=0.0, max=17.0, decimals=1), + dict(name='gain_auto', type='bool', group='게인', label='자동 게인'), + dict(name='gain_auto_max_db', type='double', group='게인', label='자동 게인 상한 [dB]', + min=0.0, max=17.0, decimals=1), + + # --- 소프트웨어 AE / ROI --- + dict(name='use_software_ae', type='bool', group='소프트웨어 AE / ROI', + label='소프트웨어 AE 사용'), + dict(name='ae_roi_top_ratio', type='double', group='소프트웨어 AE / ROI', + label='측광 제외 상단 비율 (0.5=하단 절반만)', min=0.0, max=0.95, decimals=2), + dict(name='ae_target_percentile', type='double', group='소프트웨어 AE / ROI', + label='목표 퍼센타일', min=0.0, max=100.0, decimals=1), + dict(name='ae_target_dn', type='int', group='소프트웨어 AE / ROI', + label='목표 DN', min=0, max=255), + dict(name='ae_saturation_percentile', type='double', group='소프트웨어 AE / ROI', + label='포화 방지 퍼센타일', min=0.0, max=100.0, decimals=1), + dict(name='ae_saturation_dn', type='int', group='소프트웨어 AE / ROI', + label='포화 방지 DN', min=0, max=255), + dict(name='ae_step_gain', type='double', group='소프트웨어 AE / ROI', + label='보정 댐핑 (0~1)', min=0.0, max=1.0, decimals=2), + + # --- 동기화 (재시작 필요 — 아래 readonly 참고) --- + dict(name='sync_role', type='string', group='동기화 (재시작 필요)', label='역할', + readonly=True), + dict(name='sync_master_camera_ns', type='string', group='동기화 (재시작 필요)', + label='마스터 camera_name', readonly=True), +] + +PARAM_SPEC_BY_NAME = {spec['name']: spec for spec in PARAM_SPECS} + + +def value_to_slider(value, vmin, vmax, log_scale): + """실제 값을 0~SLIDER_STEPS 정수 슬라이더 위치로 변환.""" + value = min(max(value, vmin), vmax) + if log_scale: + vmin_eff = max(vmin, 1e-9) + value_eff = max(value, vmin_eff) + lo, hi = math.log(vmin_eff), math.log(vmax) + frac = (math.log(value_eff) - lo) / (hi - lo) if hi > lo else 0.0 + else: + frac = (value - vmin) / (vmax - vmin) if vmax > vmin else 0.0 + return int(round(frac * SLIDER_STEPS)) + + +def slider_to_value(pos, vmin, vmax, log_scale): + """슬라이더 위치(0~SLIDER_STEPS)를 실제 값으로 변환.""" + frac = min(max(pos, 0), SLIDER_STEPS) / SLIDER_STEPS + if log_scale: + vmin_eff = max(vmin, 1e-9) + lo, hi = math.log(vmin_eff), math.log(vmax) + return math.exp(lo + frac * (hi - lo)) + return vmin + frac * (vmax - vmin) diff --git a/src/hik_camera_panel/package.xml b/src/hik_camera_panel/package.xml new file mode 100644 index 0000000..1361faf --- /dev/null +++ b/src/hik_camera_panel/package.xml @@ -0,0 +1,29 @@ + + + + hik_camera_panel + 1.0.0 + + hik_camera_ros2_driver의 노출/게인/ROI 파라미터를 슬라이더 또는 직접 입력으로 + 실시간 조절하는 PyQt(python_qt_binding) 기반 패널. 카메라 3대(cam1/cam2/cam3) 탭과 + ROI 경계가 오버레이된 실시간 이미지 미리보기를 제공한다. + + khj + Apache-2.0 + + ament_python + + rclpy + rcl_interfaces + sensor_msgs + python_qt_binding + + ament_copyright + ament_flake8 + ament_pep257 + python3-pytest + + + ament_python + + diff --git a/src/hik_camera_panel/resource/hik_camera_panel b/src/hik_camera_panel/resource/hik_camera_panel new file mode 100644 index 0000000..e69de29 diff --git a/src/hik_camera_panel/setup.cfg b/src/hik_camera_panel/setup.cfg new file mode 100644 index 0000000..c6111e4 --- /dev/null +++ b/src/hik_camera_panel/setup.cfg @@ -0,0 +1,4 @@ +[develop] +script_dir=$base/lib/hik_camera_panel +[install] +install_scripts=$base/lib/hik_camera_panel diff --git a/src/hik_camera_panel/setup.py b/src/hik_camera_panel/setup.py new file mode 100644 index 0000000..881a75f --- /dev/null +++ b/src/hik_camera_panel/setup.py @@ -0,0 +1,28 @@ +from setuptools import find_packages, setup + +package_name = 'hik_camera_panel' + +setup( + name=package_name, + version='1.0.0', + packages=find_packages(exclude=['test']), + data_files=[ + ('share/ament_index/resource_index/packages', ['resource/' + package_name]), + ('share/' + package_name, ['package.xml']), + ], + install_requires=['setuptools'], + zip_safe=True, + maintainer='khj', + maintainer_email='khj@example.com', + description=( + 'hik_camera_ros2_driver의 노출/게인/ROI 파라미터를 슬라이더/직접입력으로 ' + '실시간 조절하는 패널' + ), + license='Apache-2.0', + tests_require=['pytest'], + entry_points={ + 'console_scripts': [ + 'panel_node = hik_camera_panel.panel_node:main', + ], + }, +) diff --git a/src/hik_camera_ros2_driver/src/hik_camera_node.cpp b/src/hik_camera_ros2_driver/src/hik_camera_node.cpp index 33a8f39..0a98186 100644 --- a/src/hik_camera_ros2_driver/src/hik_camera_node.cpp +++ b/src/hik_camera_ros2_driver/src/hik_camera_node.cpp @@ -886,6 +886,20 @@ private: status = MV_CC_SetIntValue(camera_handle_, "AutoExposureTimeUpperLimit", static_cast(exposure_auto_max_)); } + } else if (name == "gain_auto_max_db") { + gain_auto_max_db_ = param.as_double(); + if (gain_auto_ && !use_software_ae_) { + status = MV_CC_SetFloatValue( + camera_handle_, "AutoGainUpperLimit", static_cast(gain_auto_max_db_)); + } + } else if (name == "ae_roi_top_ratio") { + ae_roi_top_ratio_ = param.as_double(); + } else if (name == "ae_target_percentile") { + ae_target_percentile_ = param.as_double(); + } else if (name == "ae_saturation_percentile") { + ae_saturation_percentile_ = param.as_double(); + } else if (name == "ae_step_gain") { + ae_step_gain_ = param.as_double(); } else { result.successful = false; result.reason = "Unknown parameter: " + name; @@ -904,6 +918,10 @@ private: status = MV_CC_SetIntValue(camera_handle_, "AutoTargetBrightness", static_cast(exposure_auto_target_brightness_)); } + } else if (name == "ae_target_dn") { + ae_target_dn_ = static_cast(param.as_int()); + } else if (name == "ae_saturation_dn") { + ae_saturation_dn_ = static_cast(param.as_int()); } else { result.successful = false; result.reason = "Unknown parameter: " + name;