|
|
|
@ -3,7 +3,7 @@
|
|
|
|
import rclpy
|
|
|
|
import rclpy
|
|
|
|
from rclpy.node import Node
|
|
|
|
from rclpy.node import Node
|
|
|
|
from rclpy.executors import SingleThreadedExecutor, MultiThreadedExecutor
|
|
|
|
from rclpy.executors import SingleThreadedExecutor, MultiThreadedExecutor
|
|
|
|
from geometry_msgs.msg import Point, Vector3
|
|
|
|
from geometry_msgs.msg import Point, Vector3, PoseStamped
|
|
|
|
from sensor_msgs.msg import Image, CameraInfo
|
|
|
|
from sensor_msgs.msg import Image, CameraInfo
|
|
|
|
from cv_bridge import CvBridge
|
|
|
|
from cv_bridge import CvBridge
|
|
|
|
import cv2
|
|
|
|
import cv2
|
|
|
|
@ -12,6 +12,9 @@ from std_srvs.srv import Trigger
|
|
|
|
from tf2_ros import Buffer, TransformListener
|
|
|
|
from tf2_ros import Buffer, TransformListener
|
|
|
|
from gen_locations_on_mesh_msgs.msg import LocationsOnMesh
|
|
|
|
from gen_locations_on_mesh_msgs.msg import LocationsOnMesh
|
|
|
|
from gen_locations_on_mesh_msgs.srv import RemoveLocation
|
|
|
|
from gen_locations_on_mesh_msgs.srv import RemoveLocation
|
|
|
|
|
|
|
|
from scipy.spatial.transform import Rotation
|
|
|
|
|
|
|
|
from visualization_msgs.msg import MarkerArray
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FireExtinguisherEmulatorNode(Node):
|
|
|
|
class FireExtinguisherEmulatorNode(Node):
|
|
|
|
|
|
|
|
|
|
|
|
@ -27,7 +30,7 @@ class FireExtinguisherEmulatorNode(Node):
|
|
|
|
|
|
|
|
|
|
|
|
self.declare_parameter('show_target', True)
|
|
|
|
self.declare_parameter('show_target', True)
|
|
|
|
|
|
|
|
|
|
|
|
self.declare_parameter('sof_r', 0.5)
|
|
|
|
self.declare_parameter('sof_cos_sim', 0.5)
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
@ -37,14 +40,14 @@ class FireExtinguisherEmulatorNode(Node):
|
|
|
|
self.robot_frame = self.get_parameter('robot_frame').value
|
|
|
|
self.robot_frame = self.get_parameter('robot_frame').value
|
|
|
|
|
|
|
|
|
|
|
|
self.show_target = self.get_parameter('show_target').value
|
|
|
|
self.show_target = self.get_parameter('show_target').value
|
|
|
|
self.sof_r = self.get_parameter('sof_r').value
|
|
|
|
self.sof_cos_sim = self.get_parameter('sof_cos_sim').value
|
|
|
|
|
|
|
|
|
|
|
|
# Sources of Fire positions in (id, x, y, z, r)
|
|
|
|
# Sources of Fire positions in (id, x, y, z, r)
|
|
|
|
self.sof_poisitions = []
|
|
|
|
self.sof_poisitions = []
|
|
|
|
|
|
|
|
|
|
|
|
# Publishers
|
|
|
|
# Publishers
|
|
|
|
self.fire_ext_image_pub = self.create_publisher(Image, 'crosshair_image', 10)
|
|
|
|
self.fire_ext_image_pub = self.create_publisher(Image, 'crosshair_image', 10)
|
|
|
|
self.fire_ext_vector_pub = self.create_publisher(Vector3, 'crosshair_vector', 10)
|
|
|
|
self.fire_ext_vector_pub = self.create_publisher(PoseStamped, 'crosshair_vector', 10)
|
|
|
|
|
|
|
|
|
|
|
|
# State variables
|
|
|
|
# State variables
|
|
|
|
self.target_x = 0.0
|
|
|
|
self.target_x = 0.0
|
|
|
|
@ -71,7 +74,9 @@ class FireExtinguisherEmulatorNode(Node):
|
|
|
|
self.tf_listener = TransformListener(self.tf_buffer, self)
|
|
|
|
self.tf_listener = TransformListener(self.tf_buffer, self)
|
|
|
|
|
|
|
|
|
|
|
|
# remove SoF
|
|
|
|
# remove SoF
|
|
|
|
self.remove_sof_srv = self.create_client(RemoveLocation, 'remove_location')
|
|
|
|
self.remove_sof_srv = self.create_client(RemoveLocation, '/remove_location')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.fe_vector = None
|
|
|
|
|
|
|
|
|
|
|
|
# Timers and subscriptions
|
|
|
|
# Timers and subscriptions
|
|
|
|
self.target_timer = self.create_timer(0.1, self.target_timer_cb) # 10 Hz
|
|
|
|
self.target_timer = self.create_timer(0.1, self.target_timer_cb) # 10 Hz
|
|
|
|
@ -80,25 +85,35 @@ class FireExtinguisherEmulatorNode(Node):
|
|
|
|
self.create_subscription(Point, 'target_point', self.target_cb, 10)
|
|
|
|
self.create_subscription(Point, 'target_point', self.target_cb, 10)
|
|
|
|
self.sub_camera_info = self.create_subscription(CameraInfo, 'camera_info', self.cam_info_cb, 10)
|
|
|
|
self.sub_camera_info = 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)
|
|
|
|
self.create_subscription(LocationsOnMesh, 'locations', self.locations_cb, 10)
|
|
|
|
self.create_subscription(MarkerArray, '/locations_markers', self.locations_cb, 10)
|
|
|
|
|
|
|
|
|
|
|
|
self.create_service(Trigger, 'fe_burst', self.fe_burst_cb)
|
|
|
|
self.create_service(Trigger, 'fe_burst', self.fe_burst_cb)
|
|
|
|
|
|
|
|
|
|
|
|
def locations_cb(self, msg):
|
|
|
|
def locations_cb(self, msg):
|
|
|
|
for idx, x, y, z in zip(msg.location_id, msg.x, msg.y, msg.z):
|
|
|
|
#for idx, x, y, z in zip(msg.location_id, msg.x, msg.y, msg.z):
|
|
|
|
self.sof_poisitions.append((idx, x, y, z, self.sof_r))
|
|
|
|
self.sof_poisitions = []
|
|
|
|
|
|
|
|
for marker in msg.markers:
|
|
|
|
|
|
|
|
self.sof_poisitions.append((marker.id,
|
|
|
|
|
|
|
|
marker.pose.position.x,
|
|
|
|
|
|
|
|
marker.pose.position.y,
|
|
|
|
|
|
|
|
marker.pose.position.z,
|
|
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
def fe_burst_cb(self, req, res):
|
|
|
|
def fe_burst_cb(self, req, res):
|
|
|
|
|
|
|
|
|
|
|
|
intersected_spheres = find_intersected_spheres(
|
|
|
|
intersected_spheres = find_intersected_spheres(
|
|
|
|
spheres=self.sof_poisitions,
|
|
|
|
spheres=self.sof_poisitions,
|
|
|
|
camera_vector=self.vector_msg, # ваш Vector3 с направлением
|
|
|
|
camera_vector=self.fe_vector, # ваш Vector3 с направлением
|
|
|
|
camera_vector_length=self.fe_distance, # длина луча
|
|
|
|
camera_vector_length=self.fe_distance, # длина луча
|
|
|
|
tf_buffer=self.tf_buffer,
|
|
|
|
tf_buffer=self.tf_buffer,
|
|
|
|
camera_frame=self.camera_info.header.frame_id, # из CameraInfo
|
|
|
|
camera_frame=self.camera_info.header.frame_id, # из CameraInfo
|
|
|
|
world_frame="map"
|
|
|
|
world_frame="map",
|
|
|
|
|
|
|
|
sof_cos_sim=self.sof_cos_sim,
|
|
|
|
|
|
|
|
print_f = self.get_logger().info
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.get_logger().info(f"{intersected_spheres}")
|
|
|
|
|
|
|
|
|
|
|
|
if len(intersected_spheres):
|
|
|
|
if len(intersected_spheres):
|
|
|
|
res.success = True
|
|
|
|
res.success = True
|
|
|
|
res.message = str(intersected_spheres[0][0]) # add ID of source of fire if success
|
|
|
|
res.message = str(intersected_spheres[0][0]) # add ID of source of fire if success
|
|
|
|
@ -184,8 +199,6 @@ class FireExtinguisherEmulatorNode(Node):
|
|
|
|
self.current_y += step_y
|
|
|
|
self.current_y += step_y
|
|
|
|
|
|
|
|
|
|
|
|
#self.get_logger().info(f"Target {self.target_x} {self.target_y} current {self.current_x} {self.current_y}")
|
|
|
|
#self.get_logger().info(f"Target {self.target_x} {self.target_y} current {self.current_x} {self.current_y}")
|
|
|
|
|
|
|
|
|
|
|
|
# Calculate 3D unit vector from camera frame
|
|
|
|
|
|
|
|
if self.fx > 0 and self.fy > 0:
|
|
|
|
if self.fx > 0 and self.fy > 0:
|
|
|
|
px = self.current_x
|
|
|
|
px = self.current_x
|
|
|
|
py = self.current_y
|
|
|
|
py = self.current_y
|
|
|
|
@ -200,11 +213,20 @@ class FireExtinguisherEmulatorNode(Node):
|
|
|
|
y /= norm
|
|
|
|
y /= norm
|
|
|
|
z /= norm
|
|
|
|
z /= norm
|
|
|
|
|
|
|
|
|
|
|
|
self.vector_msg = Vector3()
|
|
|
|
self.fe_vector = (x, y, z)
|
|
|
|
self.vector_msg.x = x
|
|
|
|
|
|
|
|
self.vector_msg.y = y
|
|
|
|
msg = PoseStamped()
|
|
|
|
self.vector_msg.z = z
|
|
|
|
msg.header.frame_id = self.camera_info.header.frame_id
|
|
|
|
self.fire_ext_vector_pub.publish(self.vector_msg)
|
|
|
|
msg.header.stamp = self.get_clock().now().to_msg()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
q = quaternion_from_two_vectors([1.0, 0.0, 0.0], self.fe_vector)
|
|
|
|
|
|
|
|
msg.pose.orientation.x = float(q[0])
|
|
|
|
|
|
|
|
msg.pose.orientation.y = float(q[1])
|
|
|
|
|
|
|
|
msg.pose.orientation.z = float(q[2])
|
|
|
|
|
|
|
|
msg.pose.orientation.w = float(q[3])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.fire_ext_vector_pub.publish(msg)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def raw_image_cb(self, raw_msg):
|
|
|
|
def raw_image_cb(self, raw_msg):
|
|
|
|
"""Draw crosshair on image and publish 3D vector"""
|
|
|
|
"""Draw crosshair on image and publish 3D vector"""
|
|
|
|
@ -260,12 +282,14 @@ class FireExtinguisherEmulatorNode(Node):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def find_intersected_spheres(
|
|
|
|
def find_intersected_spheres(
|
|
|
|
spheres: list[tuple[int, float, float, float, float]], # [(id, x, y, z, r)]
|
|
|
|
spheres: list[tuple[int, float, float, float]], # [(id, x, y, z)]
|
|
|
|
camera_vector: Vector3, # единичный вектор в координатах камеры
|
|
|
|
camera_vector: (float, float, float), # единичный вектор в координатах камеры
|
|
|
|
camera_vector_length: float, # длина луча
|
|
|
|
camera_vector_length: float, # длина луча
|
|
|
|
tf_buffer: Buffer,
|
|
|
|
tf_buffer: Buffer,
|
|
|
|
camera_frame: str,
|
|
|
|
camera_frame: str,
|
|
|
|
world_frame: str = "world"
|
|
|
|
world_frame: str,
|
|
|
|
|
|
|
|
sof_cos_sim,
|
|
|
|
|
|
|
|
print_f = print
|
|
|
|
) -> list[tuple[int, float, float]]:
|
|
|
|
) -> list[tuple[int, float, float]]:
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
Находит все сферы, пересекаемые лучом.
|
|
|
|
Находит все сферы, пересекаемые лучом.
|
|
|
|
@ -275,8 +299,8 @@ def find_intersected_spheres(
|
|
|
|
|
|
|
|
|
|
|
|
# 1. Получаем преобразование камеры -> мир
|
|
|
|
# 1. Получаем преобразование камеры -> мир
|
|
|
|
transform = tf_buffer.lookup_transform(
|
|
|
|
transform = tf_buffer.lookup_transform(
|
|
|
|
world_frame, # target frame (мир)
|
|
|
|
world_frame, # target frame
|
|
|
|
camera_frame, # source frame (камера)
|
|
|
|
camera_frame, # source frame
|
|
|
|
rclpy.time.Time() # latest transform
|
|
|
|
rclpy.time.Time() # latest transform
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@ -286,10 +310,11 @@ def find_intersected_spheres(
|
|
|
|
transform.transform.translation.y,
|
|
|
|
transform.transform.translation.y,
|
|
|
|
transform.transform.translation.z
|
|
|
|
transform.transform.translation.z
|
|
|
|
])
|
|
|
|
])
|
|
|
|
|
|
|
|
print_f(f"{p0}")
|
|
|
|
|
|
|
|
|
|
|
|
# 3. Преобразуем вектор камеры в координаты мира
|
|
|
|
# 3. Преобразуем вектор камеры в координаты мира
|
|
|
|
# Вектор как точка (с нулевой координатой w)
|
|
|
|
# Вектор как точка (с нулевой координатой w)
|
|
|
|
camera_vec = np.array([camera_vector.x, camera_vector.y, camera_vector.z])
|
|
|
|
camera_vec = np.array(camera_vector)
|
|
|
|
|
|
|
|
|
|
|
|
# Матрица вращения из transform (кватернион -> матрица)
|
|
|
|
# Матрица вращения из transform (кватернион -> матрица)
|
|
|
|
q = transform.transform.rotation
|
|
|
|
q = transform.transform.rotation
|
|
|
|
@ -306,31 +331,80 @@ def find_intersected_spheres(
|
|
|
|
# 4. Проходим по всем сферам
|
|
|
|
# 4. Проходим по всем сферам
|
|
|
|
intersected = []
|
|
|
|
intersected = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
l2 = camera_vector_length**2
|
|
|
|
for sphere in spheres:
|
|
|
|
for sphere in spheres:
|
|
|
|
idx, xs, ys, zs, r = sphere
|
|
|
|
idx, xs, ys, zs = sphere
|
|
|
|
p_s = np.array([xs, ys, zs]) # центр сферы в мире
|
|
|
|
|
|
|
|
|
|
|
|
dx = p0[0] - xs
|
|
|
|
# k = p₀ - pₛ (вектор от сферы до камеры)
|
|
|
|
dy = p0[1] - ys
|
|
|
|
k = p0 - p_s
|
|
|
|
D2 = (dx)**2 + (dy)**2
|
|
|
|
|
|
|
|
|
|
|
|
# Коэффициенты квадратного уравнения
|
|
|
|
if D2 <= l2:
|
|
|
|
# t² + 2*b*t + c = 0, где a = 1 (у нас u единичный)
|
|
|
|
dz = p0[2] - zs
|
|
|
|
b = np.dot(k, u)
|
|
|
|
|
|
|
|
c = np.dot(k, k) - r**2
|
|
|
|
def cos_sim(a, b):
|
|
|
|
|
|
|
|
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
|
|
|
|
# Дискриминант
|
|
|
|
|
|
|
|
D = b**2 - c
|
|
|
|
sim = cos_sim(u, -np.array((dx, dy, dz)))
|
|
|
|
|
|
|
|
|
|
|
|
if D >= 0: # есть пересечение (D=0 — касание, D>0 — 2 точки)
|
|
|
|
if sim >= sof_cos_sim:
|
|
|
|
sqrt_D = np.sqrt(D)
|
|
|
|
intersected.append((idx, np.sqrt(D2), sim))
|
|
|
|
t1 = -b - sqrt_D
|
|
|
|
|
|
|
|
t2 = -b + sqrt_D
|
|
|
|
return sorted(intersected, key=lambda x: x[1])
|
|
|
|
|
|
|
|
|
|
|
|
# Проверяем, что хотя бы одна точка пересечения на луче [0, length]
|
|
|
|
|
|
|
|
if (0 <= t1 <= camera_vector_length) or (0 <= t2 <= camera_vector_length):
|
|
|
|
|
|
|
|
intersected.append((idx, t1, t2))
|
|
|
|
# for sphere in spheres:
|
|
|
|
|
|
|
|
# idx, xs, ys, zs, r = sphere
|
|
|
|
return intersected
|
|
|
|
# p_s = np.array([xs, ys, zs]) # центр сферы в мире
|
|
|
|
|
|
|
|
#
|
|
|
|
|
|
|
|
# # k = p₀ - pₛ (вектор от сферы до камеры)
|
|
|
|
|
|
|
|
# k = p0 - p_s
|
|
|
|
|
|
|
|
#
|
|
|
|
|
|
|
|
# # Коэффициенты квадратного уравнения
|
|
|
|
|
|
|
|
# # t² + 2*b*t + c = 0, где a = 1 (у нас u единичный)
|
|
|
|
|
|
|
|
# b = np.dot(k, u)
|
|
|
|
|
|
|
|
# c = np.dot(k, k) - r**2
|
|
|
|
|
|
|
|
#
|
|
|
|
|
|
|
|
# # Дискриминант
|
|
|
|
|
|
|
|
# D = b**2 - c
|
|
|
|
|
|
|
|
#
|
|
|
|
|
|
|
|
# if D >= 0: # есть пересечение (D=0 — касание, D>0 — 2 точки)
|
|
|
|
|
|
|
|
# sqrt_D = np.sqrt(D)
|
|
|
|
|
|
|
|
# t1 = -b - sqrt_D
|
|
|
|
|
|
|
|
# t2 = -b + sqrt_D
|
|
|
|
|
|
|
|
#
|
|
|
|
|
|
|
|
# # Проверяем, что хотя бы одна точка пересечения на луче [0, length]
|
|
|
|
|
|
|
|
# if (0 <= t1 <= camera_vector_length) or (0 <= t2 <= camera_vector_length):
|
|
|
|
|
|
|
|
# intersected.append((idx, t1, t2))
|
|
|
|
|
|
|
|
#
|
|
|
|
|
|
|
|
# return intersected
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def quaternion_from_two_vectors(a, b):
|
|
|
|
|
|
|
|
a = np.array(a, dtype=float)
|
|
|
|
|
|
|
|
b = np.array(b, dtype=float)
|
|
|
|
|
|
|
|
a /= np.linalg.norm(a)
|
|
|
|
|
|
|
|
b /= np.linalg.norm(b)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
v = np.cross(a, b)
|
|
|
|
|
|
|
|
c = np.dot(a, b)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if c < -0.999999:
|
|
|
|
|
|
|
|
axis = np.array([1.0, 0.0, 0.0])
|
|
|
|
|
|
|
|
if abs(a[0]) > 0.9:
|
|
|
|
|
|
|
|
axis = np.array([0.0, 1.0, 0.0])
|
|
|
|
|
|
|
|
v = np.cross(a, axis)
|
|
|
|
|
|
|
|
v /= np.linalg.norm(v)
|
|
|
|
|
|
|
|
q = np.array([v[0], v[1], v[2], 0.0])
|
|
|
|
|
|
|
|
return q
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
s = np.sqrt((1.0 + c) * 2.0)
|
|
|
|
|
|
|
|
invs = 1.0 / s
|
|
|
|
|
|
|
|
qx = v[0] * invs
|
|
|
|
|
|
|
|
qy = v[1] * invs
|
|
|
|
|
|
|
|
qz = v[2] * invs
|
|
|
|
|
|
|
|
qw = 0.5 * s
|
|
|
|
|
|
|
|
return np.array([qx, qy, qz, qw], dtype=float)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main(args=None):
|
|
|
|
def main(args=None):
|
|
|
|
|