emulator does smth
This commit is contained in:
@@ -0,0 +1,165 @@
|
|||||||
|
Panels:
|
||||||
|
- Class: rviz_common/Displays
|
||||||
|
Help Height: 87
|
||||||
|
Name: Displays
|
||||||
|
Property Tree Widget:
|
||||||
|
Expanded:
|
||||||
|
- /Global Options1
|
||||||
|
- /Status1
|
||||||
|
Splitter Ratio: 0.5
|
||||||
|
Tree Height: 696
|
||||||
|
- Class: rviz_common/Selection
|
||||||
|
Name: Selection
|
||||||
|
- Class: rviz_common/Tool Properties
|
||||||
|
Expanded:
|
||||||
|
- /2D Goal Pose1
|
||||||
|
- /Publish Point1
|
||||||
|
Name: Tool Properties
|
||||||
|
Splitter Ratio: 0.5886790156364441
|
||||||
|
- Class: rviz_common/Views
|
||||||
|
Expanded:
|
||||||
|
- /Current View1
|
||||||
|
Name: Views
|
||||||
|
Splitter Ratio: 0.5
|
||||||
|
- Class: rviz_common/Time
|
||||||
|
Experimental: false
|
||||||
|
Name: Time
|
||||||
|
SyncMode: 0
|
||||||
|
SyncSource: ""
|
||||||
|
Visualization Manager:
|
||||||
|
Class: ""
|
||||||
|
Displays:
|
||||||
|
- Alpha: 0.5
|
||||||
|
Cell Size: 1
|
||||||
|
Class: rviz_default_plugins/Grid
|
||||||
|
Color: 160; 160; 164
|
||||||
|
Enabled: true
|
||||||
|
Line Style:
|
||||||
|
Line Width: 0.029999999329447746
|
||||||
|
Value: Lines
|
||||||
|
Name: Grid
|
||||||
|
Normal Cell Count: 0
|
||||||
|
Offset:
|
||||||
|
X: 0
|
||||||
|
Y: 0
|
||||||
|
Z: 0
|
||||||
|
Plane: XY
|
||||||
|
Plane Cell Count: 10
|
||||||
|
Reference Frame: <Fixed Frame>
|
||||||
|
Value: true
|
||||||
|
- Class: rviz_default_plugins/Image
|
||||||
|
Enabled: true
|
||||||
|
Max Value: 1
|
||||||
|
Median window: 5
|
||||||
|
Min Value: 0
|
||||||
|
Name: RAW
|
||||||
|
Normalize Range: true
|
||||||
|
Topic:
|
||||||
|
Depth: 5
|
||||||
|
Durability Policy: Volatile
|
||||||
|
History Policy: Keep Last
|
||||||
|
Reliability Policy: Reliable
|
||||||
|
Value: /arctic_bot/camera
|
||||||
|
Value: true
|
||||||
|
- Class: rviz_default_plugins/Image
|
||||||
|
Enabled: true
|
||||||
|
Max Value: 1
|
||||||
|
Median window: 5
|
||||||
|
Min Value: 0
|
||||||
|
Name: Crosshair
|
||||||
|
Normalize Range: true
|
||||||
|
Topic:
|
||||||
|
Depth: 5
|
||||||
|
Durability Policy: Volatile
|
||||||
|
History Policy: Keep Last
|
||||||
|
Reliability Policy: Reliable
|
||||||
|
Value: /fire_extinguisher_emulator/fire_extinguisher_image
|
||||||
|
Value: true
|
||||||
|
Enabled: true
|
||||||
|
Global Options:
|
||||||
|
Background Color: 48; 48; 48
|
||||||
|
Fixed Frame: map
|
||||||
|
Frame Rate: 30
|
||||||
|
Name: root
|
||||||
|
Tools:
|
||||||
|
- Class: rviz_default_plugins/Interact
|
||||||
|
Hide Inactive Objects: true
|
||||||
|
- Class: rviz_default_plugins/MoveCamera
|
||||||
|
- Class: rviz_default_plugins/Select
|
||||||
|
- Class: rviz_default_plugins/FocusCamera
|
||||||
|
- Class: rviz_default_plugins/Measure
|
||||||
|
Line color: 128; 128; 0
|
||||||
|
- Class: rviz_default_plugins/SetInitialPose
|
||||||
|
Covariance x: 0.25
|
||||||
|
Covariance y: 0.25
|
||||||
|
Covariance yaw: 0.06853891909122467
|
||||||
|
Topic:
|
||||||
|
Depth: 5
|
||||||
|
Durability Policy: Volatile
|
||||||
|
History Policy: Keep Last
|
||||||
|
Reliability Policy: Reliable
|
||||||
|
Value: /initialpose
|
||||||
|
- Class: rviz_default_plugins/SetGoal
|
||||||
|
Topic:
|
||||||
|
Depth: 5
|
||||||
|
Durability Policy: Volatile
|
||||||
|
History Policy: Keep Last
|
||||||
|
Reliability Policy: Reliable
|
||||||
|
Value: /goal_pose
|
||||||
|
- Class: rviz_default_plugins/PublishPoint
|
||||||
|
Single click: true
|
||||||
|
Topic:
|
||||||
|
Depth: 5
|
||||||
|
Durability Policy: Volatile
|
||||||
|
History Policy: Keep Last
|
||||||
|
Reliability Policy: Reliable
|
||||||
|
Value: /clicked_point
|
||||||
|
Transformation:
|
||||||
|
Current:
|
||||||
|
Class: rviz_default_plugins/TF
|
||||||
|
Value: true
|
||||||
|
Views:
|
||||||
|
Current:
|
||||||
|
Class: rviz_default_plugins/Orbit
|
||||||
|
Distance: 10
|
||||||
|
Enable Stereo Rendering:
|
||||||
|
Stereo Eye Separation: 0.05999999865889549
|
||||||
|
Stereo Focal Distance: 1
|
||||||
|
Swap Stereo Eyes: false
|
||||||
|
Value: false
|
||||||
|
Focal Point:
|
||||||
|
X: 0
|
||||||
|
Y: 0
|
||||||
|
Z: 0
|
||||||
|
Focal Shape Fixed Size: true
|
||||||
|
Focal Shape Size: 0.05000000074505806
|
||||||
|
Invert Z Axis: false
|
||||||
|
Name: Current View
|
||||||
|
Near Clip Distance: 0.009999999776482582
|
||||||
|
Pitch: 0.785398006439209
|
||||||
|
Target Frame: <Fixed Frame>
|
||||||
|
Value: Orbit (rviz)
|
||||||
|
Yaw: 0.785398006439209
|
||||||
|
Saved: ~
|
||||||
|
Window Geometry:
|
||||||
|
Crosshair:
|
||||||
|
collapsed: false
|
||||||
|
Displays:
|
||||||
|
collapsed: false
|
||||||
|
Height: 1052
|
||||||
|
Hide Left Dock: false
|
||||||
|
Hide Right Dock: false
|
||||||
|
QMainWindow State: 000000ff00000000fd00000004000000000000016a0000035cfc0200000008fb0000001200530065006c0065006300740069006f006e00000001e10000009b0000007901000003fb0000001e0054006f006f006c002000500072006f007000650072007400690065007302000001ed000001df00000185000000a3fb000000120056006900650077007300200054006f006f02000001df000002110000018500000122fb000000200054006f006f006c002000500072006f0070006500720074006900650073003203000002880000011d000002210000017afb000000100044006900730070006c006100790073010000004c0000035c000000fd01000003fb0000002000730065006c0065006300740069006f006e00200062007500660066006500720200000138000000aa0000023a00000294fb00000014005700690064006500530074006500720065006f02000000e6000000d2000003ee0000030bfb0000000c004b0069006e0065006300740200000186000001060000030c0000026100000001000002100000035cfc0200000004fb0000001e0054006f006f006c002000500072006f00700065007200740069006500730100000041000000780000000000000000fc0000004c000001b50000002200fffffffa000000000100000002fb000000060052004100570100000000ffffffff0000005c01000003fb0000000a005600690065007700730000000670000001100000011001000003fb0000001200430072006f0073007300680061006900720100000202000001a60000002201000003fb0000001200530065006c0065006300740069006f006e010000025a000000b200000000000000000000000200000490000000a9fc0100000001fb0000000a00560069006500770073030000004e00000080000002e10000019700000003000007800000004cfc0100000002fb0000000800540069006d0065010000000000000780000002bd01000003fb0000000800540069006d00650100000000000004500000000000000000000004040000035c00000004000000040000000800000008fc0000000100000002000000010000000a0054006f006f006c00730100000000ffffffff0000000000000000
|
||||||
|
RAW:
|
||||||
|
collapsed: false
|
||||||
|
Selection:
|
||||||
|
collapsed: false
|
||||||
|
Time:
|
||||||
|
collapsed: false
|
||||||
|
Tool Properties:
|
||||||
|
collapsed: false
|
||||||
|
Views:
|
||||||
|
collapsed: false
|
||||||
|
Width: 1920
|
||||||
|
X: 0
|
||||||
|
Y: 0
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
from launch import LaunchDescription
|
||||||
|
from launch.actions import IncludeLaunchDescription, DeclareLaunchArgument
|
||||||
|
from launch.substitutions import (
|
||||||
|
PathJoinSubstitution,
|
||||||
|
LaunchConfiguration,
|
||||||
|
)
|
||||||
|
from launch.launch_description_sources import PythonLaunchDescriptionSource
|
||||||
|
from launch_ros.substitutions import FindPackageShare
|
||||||
|
from launch.conditions import IfCondition
|
||||||
|
from launch_ros.actions import Node
|
||||||
|
|
||||||
|
|
||||||
|
def generate_launch_description():
|
||||||
|
pkg_unity_robot_controller = FindPackageShare(package="").find("unity_robot_controller")
|
||||||
|
|
||||||
|
# Launch arguments
|
||||||
|
launch_args = [
|
||||||
|
DeclareLaunchArgument(
|
||||||
|
"use_sim_time",
|
||||||
|
description="Use clock from simulation",
|
||||||
|
default_value="True",
|
||||||
|
),
|
||||||
|
DeclareLaunchArgument(
|
||||||
|
"namespace", default_value="arctic_bot", description="Top-level namespace"
|
||||||
|
),
|
||||||
|
DeclareLaunchArgument(
|
||||||
|
"start_rviz", default_value="True", description="Use basic rviz"
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Unity connection
|
||||||
|
unity_client = Node(
|
||||||
|
package="ros_tcp_endpoint",
|
||||||
|
executable="default_server_endpoint",
|
||||||
|
name="ros_tcp_endpoint",
|
||||||
|
# output="screen",
|
||||||
|
arguments=[
|
||||||
|
"--ros-args",
|
||||||
|
"-p",
|
||||||
|
"ROS_IP:=127.0.0.1",
|
||||||
|
"-p",
|
||||||
|
"ROS_TCP_PORT:=10000",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
# joy = IncludeLaunchDescription(
|
||||||
|
# PythonLaunchDescriptionSource(
|
||||||
|
# PathJoinSubstitution([pkg_unity_robot_controller, "launch", "joy.launch.py"])
|
||||||
|
# ),
|
||||||
|
# launch_arguments={"namespace": LaunchConfiguration("namespace")}.items(),
|
||||||
|
# )
|
||||||
|
|
||||||
|
# fire extingusher emulator
|
||||||
|
fe_emulator = Node(
|
||||||
|
package="unity_robot_controller",
|
||||||
|
executable="fire_extinguisher_emulator_node",
|
||||||
|
parameters=[
|
||||||
|
{"use_sim_time": LaunchConfiguration("use_sim_time")},
|
||||||
|
],
|
||||||
|
remappings=[
|
||||||
|
("raw_image", "/arctic_bot/camera"),
|
||||||
|
("camera_info", "/arctic_bot/camera/camera_info")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
rviz = Node(
|
||||||
|
package="rviz2",
|
||||||
|
executable="rviz2",
|
||||||
|
parameters=[
|
||||||
|
{"use_sim_time": LaunchConfiguration("use_sim_time")},
|
||||||
|
],
|
||||||
|
arguments=[
|
||||||
|
"-d",
|
||||||
|
PathJoinSubstitution([pkg_unity_robot_controller, "config", "unity.rviz"]),
|
||||||
|
],
|
||||||
|
condition=IfCondition(LaunchConfiguration('start_rviz'))
|
||||||
|
)
|
||||||
|
|
||||||
|
return LaunchDescription(
|
||||||
|
launch_args
|
||||||
|
+ [
|
||||||
|
unity_client,
|
||||||
|
#joy,
|
||||||
|
fe_emulator,
|
||||||
|
rviz,
|
||||||
|
]
|
||||||
|
)
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
from setuptools import find_packages, setup
|
from setuptools import find_packages, setup
|
||||||
|
import os
|
||||||
|
from glob import glob
|
||||||
|
|
||||||
package_name = 'unity_robot_controller'
|
package_name = 'unity_robot_controller'
|
||||||
|
|
||||||
@@ -10,6 +12,8 @@ setup(
|
|||||||
('share/ament_index/resource_index/packages',
|
('share/ament_index/resource_index/packages',
|
||||||
['resource/' + package_name]),
|
['resource/' + package_name]),
|
||||||
('share/' + package_name, ['package.xml']),
|
('share/' + package_name, ['package.xml']),
|
||||||
|
(os.path.join('share', package_name, 'launch'), [f for f in glob('launch/*') if os.path.isfile(f)]) ,
|
||||||
|
(os.path.join('share', package_name, 'config'), [f for f in glob('config/*') if os.path.isfile(f)]) ,
|
||||||
],
|
],
|
||||||
install_requires=['setuptools'],
|
install_requires=['setuptools'],
|
||||||
zip_safe=True,
|
zip_safe=True,
|
||||||
@@ -24,6 +28,7 @@ setup(
|
|||||||
},
|
},
|
||||||
entry_points={
|
entry_points={
|
||||||
'console_scripts': [
|
'console_scripts': [
|
||||||
|
'fire_extinguisher_emulator_node = unity_robot_controller.fire_extinguisher_emulator:main'
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ import rclpy
|
|||||||
from rclpy.node import Node
|
from rclpy.node import Node
|
||||||
from geometry_msgs.msg import Point, Vector3
|
from geometry_msgs.msg import Point, Vector3
|
||||||
from rclpy.executors import MultiThreadedExecutor
|
from rclpy.executors import MultiThreadedExecutor
|
||||||
from sensors_msgs.msg import Image, CameraInfo
|
from sensor_msgs.msg import Image, CameraInfo
|
||||||
|
from cv_bridge import CvBridge
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
class FireExtinguisherEmulatorNode(Node):
|
class FireExtinguisherEmulatorNode(Node):
|
||||||
@@ -14,42 +17,178 @@ class FireExtinguisherEmulatorNode(Node):
|
|||||||
|
|
||||||
self.declare_parameter('crosshair_alpha', 0.5)
|
self.declare_parameter('crosshair_alpha', 0.5)
|
||||||
self.declare_parameter('crosshair_speed', 1.) # pixel/sec
|
self.declare_parameter('crosshair_speed', 1.) # pixel/sec
|
||||||
|
self.declare_parameter('crosshair_color', [255, 0, 0]) # RGB (red)
|
||||||
|
|
||||||
self.crosshair_alpha = self.get_parameter('crosshair_alpha').value
|
self.crosshair_alpha = self.get_parameter('crosshair_alpha').value
|
||||||
self.crosshair_speed = self.get_parameter('crosshair_speed').value
|
self.crosshair_speed = self.get_parameter('crosshair_speed').value
|
||||||
|
self.crosshair_color = self.get_parameter('crosshair_color').value
|
||||||
|
|
||||||
|
# Publishers
|
||||||
self.fire_ext_image_pub = self.create_publisher(Image, '~/fire_extinguisher_image', 10)
|
self.fire_ext_image_pub = self.create_publisher(Image, '~/fire_extinguisher_image', 10)
|
||||||
self.fire_ext_image_pub = self.create_publisher(Vector3, '~/fire_extinguisher_vector', 10)
|
self.fire_ext_vector_pub = self.create_publisher(Vector3, '~/fire_extinguisher_vector', 10)
|
||||||
|
|
||||||
|
# State variables
|
||||||
self.target_x = 0.0
|
self.target_x = 0.0
|
||||||
self.target_y = 0.0
|
self.target_y = 0.0
|
||||||
|
|
||||||
self.current_x = 0.0
|
self.current_x = 0.0
|
||||||
self.current_y = 0.0
|
self.current_y = 0.0
|
||||||
|
|
||||||
self.target_timer = self.create_timer(0.1, self.target_timer_cb)
|
# Camera info
|
||||||
|
self.camera_info = None
|
||||||
|
self.width = 0
|
||||||
|
self.height = 0
|
||||||
|
self.fx = 0.0
|
||||||
|
self.fy = 0.0
|
||||||
|
self.cx = 0.0
|
||||||
|
self.cy = 0.0
|
||||||
|
|
||||||
|
# Image buffer
|
||||||
|
self.current_image = None
|
||||||
|
self.bridge = CvBridge()
|
||||||
|
|
||||||
|
# Timers and subscriptions
|
||||||
|
self.target_timer = self.create_timer(0.01, self.target_timer_cb) # 100 Hz
|
||||||
self.create_subscription(Point, '~/target_point', self.target_cb, 10)
|
self.create_subscription(Point, '~/target_point', self.target_cb, 10)
|
||||||
self.create_subscription(CameraInfo, 'camera_info', self.cam_info_cb, 10)
|
self.create_subscription(CameraInfo, 'camera_info', self.cam_info_cb, 10)
|
||||||
self.create_subscription(Image, 'raw_image', self.raw_image_cb, 10)
|
self.create_subscription(Image, 'raw_image', self.raw_image_cb, 10)
|
||||||
|
|
||||||
|
# UI control subscription (optional)
|
||||||
|
self.create_subscription(Vector3, '~/ui_control', self.ui_control_cb, 10)
|
||||||
|
|
||||||
def target_cb(self, msg):
|
def target_cb(self, msg):
|
||||||
self.target_x = min( max(-1., msg.x), 1.)
|
"""Received target point in normalized coordinates [-1, 1]"""
|
||||||
self.target_y = min( max(-1., msg.y), 1.)
|
self.target_x = min(max(-1.0, msg.x), 1.0)
|
||||||
|
self.target_y = min(max(-1.0, msg.y), 1.0)
|
||||||
|
|
||||||
|
def ui_control_cb(self, msg):
|
||||||
|
"""Manual UI control for crosshair (normalized coordinates)"""
|
||||||
|
self.target_x = min(max(-1.0, msg.x), 1.0)
|
||||||
|
self.target_y = min(max(-1.0, msg.y), 1.0)
|
||||||
|
|
||||||
def cam_info_cb(self, msg):
|
def cam_info_cb(self, msg):
|
||||||
# save info and shutdown that subscription
|
"""Save camera info and shutdown subscription"""
|
||||||
|
if self.camera_info is None:
|
||||||
|
self.camera_info = msg
|
||||||
|
self.width = msg.width
|
||||||
|
self.height = msg.height
|
||||||
|
self.fx = msg.k[0] # fx
|
||||||
|
self.fy = msg.k[4] # fy
|
||||||
|
self.cx = msg.k[2] # cx
|
||||||
|
self.cy = msg.k[5] # cy
|
||||||
|
|
||||||
|
self.get_logger().info(f"Camera info received: {self.width}x{self.height}, fx={self.fx}, fy={self.fy}")
|
||||||
|
|
||||||
|
# Remove the subscription after getting camera info
|
||||||
|
#self.sub_camera_info = self.sub_camera_info.__class__() # Disable subscription
|
||||||
|
if hasattr(self, 'sub_camera_info'):
|
||||||
|
self.destroy_subscription(self.sub_camera_info)
|
||||||
|
|
||||||
def target_timer_cb(self):
|
def target_timer_cb(self):
|
||||||
if self.current_x != self.target_x or self.current_y != self.target_x:
|
"""Smooth movement of crosshair towards target with speed limit"""
|
||||||
# do target smooth movement to target assumint crosshair_speed limits
|
if self.camera_info is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Calculate target pixels
|
||||||
|
target_pixel_x = (self.target_x + 1.0) * 0.5 * self.width
|
||||||
|
target_pixel_y = (self.target_y + 1.0) * 0.5 * self.height
|
||||||
|
|
||||||
|
# Calculate distance to target
|
||||||
|
dx = target_pixel_x - self.current_x
|
||||||
|
dy = target_pixel_y - self.current_y
|
||||||
|
distance = np.sqrt(dx**2 + dy**2)
|
||||||
|
|
||||||
|
# Move towards target with speed limit
|
||||||
|
if distance > 0.1:
|
||||||
|
speed_pixels = self.crosshair_speed # pixels per second
|
||||||
|
dt = 0.01 # 100 Hz timer
|
||||||
|
max_step = speed_pixels * dt
|
||||||
|
|
||||||
|
if distance > max_step:
|
||||||
|
step_x = max_step * dx / distance
|
||||||
|
step_y = max_step * dy / distance
|
||||||
|
else:
|
||||||
|
step_x = dx
|
||||||
|
step_y = dy
|
||||||
|
|
||||||
|
self.current_x += step_x
|
||||||
|
self.current_y += step_y
|
||||||
|
|
||||||
def raw_image_cb(self, raw_msg):
|
def raw_image_cb(self, raw_msg):
|
||||||
# draw some kind of simple crosshair from current x and y
|
"""Draw crosshair on image and publish 3D vector"""
|
||||||
|
if self.camera_info is None:
|
||||||
|
return
|
||||||
|
|
||||||
# using saved info calculate 3d unit vector from camera frame and publish it
|
try:
|
||||||
|
# Convert ROS Image to OpenCV image
|
||||||
|
cv_image = self.bridge.imgmsg_to_cv2(raw_msg, desired_encoding='bgr8')
|
||||||
|
except Exception as e:
|
||||||
|
self.get_logger().error(f"Failed to convert image: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Draw crosshair
|
||||||
|
crosshair_size = 20
|
||||||
|
crosshair_thickness = 2
|
||||||
|
color = tuple(self.crosshair_color) # RGB -> tuple for OpenCV
|
||||||
|
|
||||||
|
# Horizontal line
|
||||||
|
x1 = int(self.current_x - crosshair_size)
|
||||||
|
x2 = int(self.current_x + crosshair_size)
|
||||||
|
y1 = int(self.current_y)
|
||||||
|
y2 = int(self.current_y)
|
||||||
|
cv2.line(cv_image, (x1, y1), (x2, y2), color, crosshair_thickness)
|
||||||
|
|
||||||
|
# Vertical line
|
||||||
|
x1 = int(self.current_x)
|
||||||
|
x2 = int(self.current_x)
|
||||||
|
y1 = int(self.current_y - crosshair_size)
|
||||||
|
y2 = int(self.current_y + crosshair_size)
|
||||||
|
cv2.line(cv_image, (x1, y1), (x2, y2), color, crosshair_thickness)
|
||||||
|
|
||||||
|
# Circle at center
|
||||||
|
cv2.circle(cv_image, (int(self.current_x), int(self.current_y)), 5, color, -1)
|
||||||
|
|
||||||
|
# Add alpha transparency effect (simple blend with background)
|
||||||
|
if self.crosshair_alpha < 1.0:
|
||||||
|
overlay = cv_image.copy()
|
||||||
|
cv_image = cv2.addWeighted(cv_image, 1.0, overlay, self.crosshair_alpha, 0)
|
||||||
|
|
||||||
|
# Convert back to ROS Image
|
||||||
|
try:
|
||||||
|
ros_image = self.bridge.cv2_to_imgmsg(cv_image, encoding='bgr8')
|
||||||
|
ros_image.header = raw_msg.header
|
||||||
|
self.fire_ext_image_pub.publish(ros_image)
|
||||||
|
except Exception as e:
|
||||||
|
self.get_logger().error(f"Failed to publish image: {e}")
|
||||||
|
|
||||||
|
# Calculate 3D unit vector from camera frame
|
||||||
|
if self.fx > 0 and self.fy > 0:
|
||||||
|
# Pixel coordinates to normalized coordinates
|
||||||
|
px = self.current_x
|
||||||
|
py = self.current_y
|
||||||
|
|
||||||
|
# Inverse projection: (px, py) -> (x, y, z) in camera frame
|
||||||
|
x = (px - self.cx) / self.fx
|
||||||
|
y = (py - self.cy) / self.fy
|
||||||
|
z = 1.0
|
||||||
|
|
||||||
|
# Normalize to unit vector
|
||||||
|
norm = np.sqrt(x**2 + y**2 + z**2)
|
||||||
|
if norm > 0:
|
||||||
|
x /= norm
|
||||||
|
y /= norm
|
||||||
|
z /= norm
|
||||||
|
|
||||||
|
vector_msg = Vector3()
|
||||||
|
vector_msg.x = x
|
||||||
|
vector_msg.y = y
|
||||||
|
vector_msg.z = z
|
||||||
|
self.fire_ext_vector_pub.publish(vector_msg)
|
||||||
|
|
||||||
|
def destroy_node(self):
|
||||||
|
"""Clean up subscriptions"""
|
||||||
|
if hasattr(self, 'sub_camera_info'):
|
||||||
|
self.sub_camera_info.close()
|
||||||
|
super().destroy_node()
|
||||||
|
|
||||||
|
|
||||||
def main(args=None):
|
def main(args=None):
|
||||||
@@ -65,7 +204,6 @@ def main(args=None):
|
|||||||
node.destroy_node()
|
node.destroy_node()
|
||||||
rclpy.shutdown()
|
rclpy.shutdown()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user