419 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
本模块包含了坐标系转换相关的功能类和函数,主要实现以下功能:
1. 像素坐标系到机械臂基座坐标系的转换
2. 相机坐标系到机械臂末端坐标系的转换
3. 机械臂末端坐标系到机械臂基座坐标系的转换
4. 坐标系转换过程中的深度处理和验证
"""
import copy
import cv2
import numpy as np
import math
import yaml
from pathlib import Path
from scipy.spatial.transform import Rotation as R
from scipy.interpolate import CubicSpline
from tools.log import CustomLogger
try:
from .vector import SimpleNormalVector
except:
from vector import SimpleNormalVector
class CoordinateTransformer:
def __init__(self, intrinsics, color_image, depth_image, position, quaternion, vtxdb=None):
"""
初始化坐标转换器
Args:
intrinsics: 相机内参
color_image: 彩色图像
depth_image: 深度图像
position: 机械臂位置
quaternion: 机械臂姿态四元数
"""
# 基础参数初始化
self.logger = CustomLogger(propagate=True)
self.intrinsics = intrinsics
self.color_image = color_image
self.depth_image = depth_image
self.color_visualize = None
self.position = position
self.quaternion = quaternion
self.insert_queue = False
# 坐标范围初始化
self.x_range = (-0.3, 0.5)
self.y_range = (-0.5, 1.7)
self.z_range = (0.65, 1.0)
# 相机坐标系手动偏置初始化
self.camera_offset_x = 0 # 毫米
self.camera_offset_y = 0 # 毫米
self.camera_offset_z = 0 # 毫米
self.vtxdb = vtxdb
# 初始化相机参数
self._init_camera_params()
# 初始化PoseVector
self._init_pose_vector()
self.camera_matrix = None
self.camera_trans = None
self.camera_matrix = np.array(self.vtxdb.get("robot_config","camera.camera_matrix"))
self.camera_trans = np.array(self.vtxdb.get("robot_config","camera.camera_trans"))
camera_offset = self.vtxdb.get("robot_config","camera.camera_offset")
if camera_offset:
self.set_camera_offset(camera_offset[0],camera_offset[1],camera_offset[2])
def _init_camera_params(self):
"""初始化相机参数,从配置文件读取"""
file_path = Path('/home/jsfb/jsfb_ws/global_config/camera_config.yaml')
# 设置默认参数
default_params = {
'bias_x': 20,
'bias_y': 20,
'camera_Upper_limit': 55,
'camera_Lower_limit': 404
}
if not file_path.exists():
self._set_camera_params(default_params)
self.logger.log_warning("相机配置文件不存在,使用默认参数")
return
try:
with open(file_path, 'r') as file:
camera_config = yaml.safe_load(file)
if camera_config.get('camera') == 'new':
params = {
'bias_x': 20,
'bias_y': 0,
'camera_Upper_limit': 30,
'camera_Lower_limit': 375
}
self._set_camera_params(params)
self.logger.log_info("使用新相机参数配置")
elif camera_config.get('camera') == 'old':
self._set_camera_params(default_params)
self.logger.log_info("使用旧相机参数配置")
else:
self._set_camera_params(default_params)
self.logger.log_warning("未知相机类型,使用默认参数")
except yaml.YAMLError as e:
self._set_camera_params(default_params)
self.logger.log_error(f"读取相机配置文件出错: {e}")
def _set_camera_params(self, params):
"""设置相机参数"""
self.bias_x = params['bias_x']
self.bias_y = params['bias_y']
self.camera_Upper_limit = params['camera_Upper_limit']
self.camera_Lower_limit = params['camera_Lower_limit']
self.logger.log_info(f"相机参数设置完成: bias_x={self.bias_x}, bias_y={self.bias_y}")
self.logger.log_info(f"相机限制设置完成: Upper={self.camera_Upper_limit}, Lower={self.camera_Lower_limit}")
def _init_pose_vector(self):
# """初始化PoseVector"""
# # 保存深度图像为npy文件
# output_npy_path = 'aucpuncture2point/configs/using_img/result_depth.npy'
# np.save(output_npy_path, self.depth_image)
# self.vector = PoseVector(
# output_npy_path,
# self.intrinsics,
# self.x_range,
# self.y_range,
# self.z_range,
# 'aucpuncture2point/configs/using_img/color.png',
# is_human_mask=False
# )
self.vector = SimpleNormalVector('aucpuncture2point/configs/using_img/depth.png', 'aucpuncture2point/configs/using_img/color.png', self.intrinsics)
def pixel_to_robotarm(self, target_pixel_points_list, number):
"""
将像素坐标转换为机械臂基座坐标系坐标
Args:
target_pixel_points_list: 目标像素点列表
number: 图像序号,用于保存可视化结果
Returns:
final_to_arm_point: 转换后的机械臂坐标点列表如果验证失败返回None
"""
# 验证输入点的有效性
if not self._validate_points(target_pixel_points_list):
print("target pixel is not valid")
return None
# 处理每个像素点
points, points_fxl, processed_image = self._process_pixel_points(target_pixel_points_list)
if self.insert_queue == True:
return None
print(f"-------------{number}")
# 保存可视化结果
self._save_visualization(processed_image, number)
# 计算最终坐标
return self._calculate_final_coordinates(points, points_fxl)
def _validate_points(self, points):
"""验证输入点的有效性"""
if points[0][1] <= self.camera_Upper_limit:
self.logger.log_error(f"传入第一个点在y像素{self.camera_Upper_limit}以下,没有深度,提示用户躺下一点")
return False
# 限制y坐标范围
for point in points:
if point[1] > self.camera_Lower_limit:
point[1] = self.camera_Lower_limit
return True
# def _process_pixel_points(self, target_pixel_points_list):
# """处理像素点列表"""
# color_image = copy.deepcopy(self.color_image)
# depth_image = copy.deepcopy(self.depth_image)
# points = []
# points_fxl = []
# last_depth = 800
# for coordinate in target_pixel_points_list:
# x, y = int(round(coordinate[0])), int(round(coordinate[1]))
# depth = self._get_depth(x, y, depth_image, last_depth)
# # 在图像上绘制处理点
# cv2.circle(color_image, (x*2, y*2), 3, (51,102,255), 4)
# # 坐标转换
# camera_coordinate = self.deproject_pixel_to_point(self.intrinsics, (x, y), depth)
# arm_coordinate = self.calculate_arm_coords(camera_coordinate)
# points.append({"coordinates": arm_coordinate, "label": ''})
# points_fxl.append({"coordinates": camera_coordinate/1000.0, "label": ''})
# return points, points_fxl, color_image
def smooth_curve_with_spline(self,points):
"""使用三次样条插值平滑曲线."""
points = np.array(points)
x = points[:, 0]
y = points[:, 1]
t = np.linspace(0, 1, len(points))
cs_x = CubicSpline(t, x)
cs_y = CubicSpline(t, y)
t_dense = np.linspace(0, 1, 300)
x_smooth = cs_x(t_dense)
y_smooth = cs_y(t_dense)
smooth_points = np.array([x_smooth, y_smooth]).T.astype(np.int32)
return smooth_points
def _process_pixel_points(self, target_pixel_points_list):
"""处理像素点列表"""
color_image = copy.deepcopy(self.color_image)
depth_image = copy.deepcopy(self.depth_image)
if self.color_visualize is not None:
color_visualize = copy.deepcopy(self.color_visualize)
else:
color_visualize = color_image
points = []
points_fxl = []
last_depth = 800
# 获取原始图像和可视化图像的尺寸
h_orig, w_orig = color_image.shape[:2]
h_visual, w_visual = color_visualize.shape[:2] # 假设 color_visualize 是类属性
# 计算缩放比例
scale_x = w_visual / w_orig
scale_y = h_visual / h_orig
draw_points = []
for coordinate in target_pixel_points_list:
if self.insert_queue == True:
return None
x, y = int(round(coordinate[0])), int(round(coordinate[1]))
depth = self._get_depth(x, y, depth_image, last_depth)
# 在 color_visualize 上绘制点(调整缩放倍数)
x_scaled = int(round(x * scale_x))
y_scaled = int(round(y * scale_y))
if len(target_pixel_points_list) < 2: #只有一个点 直接绘制
cv2.circle(color_visualize, (x_scaled, y_scaled), 3, (47,255,173), 4) #BGR 173 255 47
else:
draw_points.append([x_scaled,y_scaled])
# print(f"draw points----{(x_scaled,y_scaled)}")
# 坐标转换
camera_coordinate = self.deproject_pixel_to_point(self.intrinsics, (x, y), depth)
offset_camera_coordinate = copy.deepcopy(camera_coordinate)
# 应用相机坐标系手动偏置
offset_camera_coordinate[0] += self.camera_offset_x # X轴偏置
offset_camera_coordinate[1] += self.camera_offset_y # Y轴偏置
offset_camera_coordinate[2] += self.camera_offset_z # Z轴偏置
arm_coordinate = self.calculate_arm_coords(offset_camera_coordinate)
points.append({"coordinates": arm_coordinate, "label": ''})
points_fxl.append({"coordinates": camera_coordinate/1000.0, "label": ''})
# 绘制样条插值曲线的图像 (保持不变)
if len(draw_points) > 0:
# image_curve_spline = np.zeros((h_visual, h_visual, 3), dtype=np.uint8)
smooth_points_spline = self.smooth_curve_with_spline(draw_points)
points_array_spline = smooth_points_spline.reshape((-1, 1, 2))
cv2.polylines(color_visualize, [points_array_spline], isClosed=False, color=(47, 255, 173), thickness=2, lineType=cv2.LINE_AA)
return points, points_fxl, color_visualize # 返回可视化后的图像
def _save_visualization(self, image, number):
"""保存可视化结果"""
# cv2.imwrite('aucpuncture2point/configs/using_img/Pixel_Visualization.jpg', image)
cv2.imwrite(f"aucpuncture2point/configs/using_img/Pixel_Visualization_{number}.jpg", image)
def _calculate_final_coordinates(self, points, points_fxl):
"""计算最终坐标"""
# 计算法向量
normals_data = self.vector.run(points_fxl, visualize=True)
# 合并结果
combined_results = []
for i, data in enumerate(normals_data):
normal_arm = self.calculate_arm_normal(data)
normals_position = points[i]["coordinates"] + normal_arm["transformed_normal"]
combined_results.append({
"label": points[i]["label"],
"normals_position": normals_position
})
# 计算最终的机械臂坐标
point = self.calculate_target_Euler(combined_results)
return [item['position'] for item in point]
def _get_depth(self, x, y, depth_image, last_depth):
"""获取深度值,处理无效深度的情况"""
if depth_image[y, x] != 0:
return depth_image[y, x]
non_zero_depths = depth_image[max(0, y-5):y+5, max(0, x-5):x+5]
non_zero_depths = non_zero_depths[non_zero_depths != 0]
return np.median(non_zero_depths) if non_zero_depths.size > 0 else last_depth
def deproject_pixel_to_point(self, intrinsics, pixel, depth):
"""将像素坐标和深度值转换为相机坐标系下的3D坐标"""
x, y = pixel
fx, fy = intrinsics['fx'], intrinsics['fy']
cx, cy = intrinsics['cx'], intrinsics['cy']
X = (x - cx) * depth / fx
Y = (y - cy) * depth / fy
Z = depth
return np.array([X, Y, Z])
# def calculate_arm_coords(self, camera_coordinate):
# """将相机坐标系转换为机械臂坐标系"""
# # 相机坐标系到机械臂末端坐标系
# arm_coordinate = [
# (camera_coordinate[1] - self.bias_y) / 1000, # x
# (camera_coordinate[0] - self.bias_x) / 1000, # y
# (-camera_coordinate[2] - 172) / 1000 # z
# ]
# # 机械臂末端坐标系到基座坐标系
# return (R.from_quat(self.quaternion).as_matrix() @ arm_coordinate + self.position).tolist()
def calculate_arm_coords(self, camera_coordinate):
"""
将相机坐标系转换为机械臂坐标系。
:param camera_coordinate: list, 相机坐标系中的坐标
:return: list, 机械臂坐标系中的坐标
"""
camera_coordinate = np.array(camera_coordinate)
# 将相机坐标系转换到机械臂末端坐标系
arm_coordinate = [0, 0, 0]
if self.camera_matrix is not None and self.camera_trans is not None:
camera_matrix = copy.deepcopy(self.camera_matrix)
camera_trans = copy.deepcopy(self.camera_trans)
else:
camera_matrix = np.array([[-0.04169144,0.99888175,-0.02229508],[0.99912694,0.04174075,0.00175093],[0.00267958,-0.02220262,-0.9997499]])
camera_trans = np.array([-0.00209053,-0.01021561,-0.16877656])
arm_coordinate = camera_matrix @ (camera_coordinate/1000) + camera_trans
# 将机械臂末端坐标系转为机械臂基座坐标系
targetPosition = R.from_quat(self.quaternion).as_matrix() @ arm_coordinate + self.position
return targetPosition.tolist()
def calculate_arm_normal(self, data):
"""将相机坐标系中的法向量转换为机械臂基座坐标系中的法向量"""
normal_vector = data['normal']
# 相机坐标系到机械臂末端坐标系
arm_normal = [normal_vector[1], normal_vector[0], -normal_vector[2]]
# 机械臂末端坐标系到基座坐标系
transformed_normal = R.from_quat(self.quaternion).as_matrix() @ arm_normal
return {
'label': data['label'],
'transformed_normal': transformed_normal.tolist()
}
def calculate_target_Euler(self, points):
"""计算目标欧拉角"""
converted_points = []
for point_data in points:
point = np.array(point_data['normals_position'], dtype=np.float64).reshape(1, -1)
temp_point = np.zeros((1, 6), dtype=np.float64)
# 复制位置坐标
temp_point[:, :3] = point[:, :3]
# 计算欧拉角
# temp_point[0][3] = -math.asin(point[0][4])
# temp_point[0][4] = -math.atan2(-point[0][3], point[0][5])
# temp_point[0][5] = 0.0
temp_point[0][3] = -math.asin(point[0][4])
temp_point[0][4] = math.atan2(point[0][3], point[0][5])
temp_point[0][5] = -np.pi/2
# 确保角度在[-pi, pi]范围内
if temp_point[0][5] > np.pi:
temp_point[0][5] -= 2*np.pi
converted_points.append({
'label': point_data['label'],
'position': temp_point[0].tolist()
})
return converted_points
def set_camera_offset(self, offset_x=0, offset_y=0, offset_z=0):
"""
设置相机坐标系下的手动偏置限制在±50mm范围内
Args:
offset_x: X轴偏置(毫米)范围±50mm
offset_y: Y轴偏置(毫米)范围±50mm
offset_z: Z轴偏置(毫米)范围±50mm
"""
# 限制偏置值在±50mm范围内
offset_x = max(-50, min(50, offset_x))
offset_y = max(-50, min(50, offset_y))
offset_z = max(-50, min(50, offset_z))
self.camera_offset_x = offset_x
self.camera_offset_y = offset_y
self.camera_offset_z = offset_z
self.logger.log_info(f"设置相机坐标系偏置: X={offset_x}mm, Y={offset_y}mm, Z={offset_z}mm")