|
|
|
|
|
import numpy as np
|
|
|
|
|
|
from PIL import Image
|
|
|
|
|
|
import math
|
|
|
|
|
|
|
|
|
|
|
|
import config
|
|
|
|
|
|
from cat import clip_and_rotate_point_cloud, merge_point_clouds
|
|
|
|
|
|
from image import detect_large_holes
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def tiff_depth_to_point_clouds(tiff_paths, sn, dedup=True):
|
|
|
|
|
|
"""
|
|
|
|
|
|
将多个 TIFF 深度图转换为点云,并拼接在一起后生成 PCD 文件
|
|
|
|
|
|
|
|
|
|
|
|
:param tiff_paths: list of str, TIFF 深度图路径列表
|
|
|
|
|
|
:param sn: str, 设备序列号,用于查找配置参数
|
|
|
|
|
|
:param dedup: bool, 是否根据 (x, y) 去重,默认开启
|
|
|
|
|
|
:return: list of [x, y, z], 合并后的点云数据
|
|
|
|
|
|
"""
|
|
|
|
|
|
merged_points = []
|
|
|
|
|
|
|
|
|
|
|
|
# Step 1: 遍历每个 TIFF 文件,生成对应的点云
|
|
|
|
|
|
for tiff_path in tiff_paths:
|
|
|
|
|
|
points = tiff_depth_to_point_cloud(tiff_path+".tiff", sn, dedup)
|
|
|
|
|
|
merged_points.extend([points])
|
|
|
|
|
|
|
|
|
|
|
|
# Step 2: 可选:使用 merge_point_clouds 对点云进行 x 轴拼接(若需要)
|
|
|
|
|
|
# 注意:如果每个点云已经带有偏移,则不需要再次拼接
|
|
|
|
|
|
has_voids = True
|
|
|
|
|
|
if hasattr(config, 'CUT_CONFIG_MAP') and isinstance(config.CUT_CONFIG_MAP, dict) and (sn in config.CUT_CONFIG_MAP):
|
|
|
|
|
|
merged_points, xrange = merge_point_clouds(merged_points, config.CUT_CONFIG_MAP[sn],sn)
|
|
|
|
|
|
|
|
|
|
|
|
real_holes = detect_large_holes(merged_points, sn, xrange)
|
|
|
|
|
|
if real_holes.__len__()==0:
|
|
|
|
|
|
has_voids=False
|
|
|
|
|
|
# Step 3: 写入最终的 PCD 文件
|
|
|
|
|
|
output_pcd_path = config.save_path("pcd", f"{sn}_merged.pcd")
|
|
|
|
|
|
write_pcd(output_pcd_path, merged_points)
|
|
|
|
|
|
# print(f"Merged and saved to {output_pcd_path}")
|
|
|
|
|
|
return has_voids
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def tiff_depth_to_point_cloud(tiff_path,sn, dedup=True):
|
|
|
|
|
|
"""
|
|
|
|
|
|
将 TIFF 深度图转换为点云,基于给定的视场角计算相机内参
|
|
|
|
|
|
|
|
|
|
|
|
:param tiff_path: str, TIFF 深度图路径
|
|
|
|
|
|
:param hfov_degrees: float or None, 水平视场角(单位:度)
|
|
|
|
|
|
:param vfov_degrees: float or None, 垂直视场角(单位:度)
|
|
|
|
|
|
:param output_ply_path: str or None, 输出 Pcd 文件路径(可选)
|
|
|
|
|
|
:param dedup: bool, 是否根据 (x, y) 去重,默认开启
|
|
|
|
|
|
:return: list of [x, y, z] 点云数据
|
|
|
|
|
|
"""
|
|
|
|
|
|
hfov_degrees = config.CAMERA_CONFIG_MAP[sn].get("x_angle")
|
|
|
|
|
|
max_z = config.CAMERA_CONFIG_MAP[sn].get("max_z")
|
|
|
|
|
|
vfov_degrees = config.CAMERA_CONFIG_MAP[sn].get("y_angle")
|
|
|
|
|
|
# plane_scaling_ratio = config.CAMERA_CONFIG_MAP[sn].get("plane_scaling_ratio")
|
|
|
|
|
|
# 加载 TIFF 图像
|
|
|
|
|
|
depth_image = Image.open(tiff_path)
|
|
|
|
|
|
depth_array = np.array(depth_image, dtype=np.float32)
|
|
|
|
|
|
|
|
|
|
|
|
height, width = depth_array.shape
|
|
|
|
|
|
|
|
|
|
|
|
# 根据提供的 FOV 计算 fx/fy
|
|
|
|
|
|
if hfov_degrees is not None:
|
|
|
|
|
|
hfov_rad = math.radians(hfov_degrees)
|
|
|
|
|
|
fx = width / (2 * math.tan(hfov_rad / 2))
|
|
|
|
|
|
fy = fx * height / width # 保持像素方形比例
|
|
|
|
|
|
else:
|
|
|
|
|
|
vfov_rad = math.radians(vfov_degrees)
|
|
|
|
|
|
fy = height / (2 * math.tan(vfov_rad / 2))
|
|
|
|
|
|
fx = fy * width / height # 保持像素方形比例
|
|
|
|
|
|
|
|
|
|
|
|
cx = width / 2
|
|
|
|
|
|
cy = height / 2
|
|
|
|
|
|
|
|
|
|
|
|
print(f"Calculated intrinsics: fx={fx:.2f}, fy={fy:.2f}, cx={cx:.2f}, cy={cy:.2f}")
|
|
|
|
|
|
|
|
|
|
|
|
points = []
|
|
|
|
|
|
seen_xy = set() # 用于记录已添加的 (x, y)
|
|
|
|
|
|
|
|
|
|
|
|
# 获取裁剪配置
|
|
|
|
|
|
clip_config = config.CUT_CONFIG_MAP.get(sn)
|
|
|
|
|
|
if clip_config:
|
|
|
|
|
|
min_pt = np.array(clip_config.get("min_pt", [-np.inf] * 3))
|
|
|
|
|
|
max_pt = np.array(clip_config.get("max_pt", [np.inf] * 3))
|
|
|
|
|
|
rotation = np.array(clip_config.get("rotation", [1,0,0,0,1,0,0,0,1])).reshape(3, 3)
|
|
|
|
|
|
else:
|
|
|
|
|
|
min_pt = max_pt = rotation = None
|
|
|
|
|
|
|
|
|
|
|
|
for v in range(height):
|
|
|
|
|
|
for u in range(width):
|
|
|
|
|
|
z = depth_array[v, u] # mm -> m
|
|
|
|
|
|
if z <= 0 or z > max_z:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
x = (u - cx) * z / fx
|
|
|
|
|
|
y = (v - cy) * z / fy
|
|
|
|
|
|
|
|
|
|
|
|
# 构造点并旋转(保留浮点精度)
|
|
|
|
|
|
point = np.array([x, y, z])
|
|
|
|
|
|
if clip_config:
|
|
|
|
|
|
rotated_point = (rotation @ point.T).T
|
|
|
|
|
|
if not np.all(rotated_point >= min_pt) or not np.all(rotated_point <= max_pt):
|
|
|
|
|
|
continue
|
|
|
|
|
|
x_final, y_final, z_final = rotated_point
|
|
|
|
|
|
else:
|
|
|
|
|
|
x_final, y_final, z_final = point
|
|
|
|
|
|
|
|
|
|
|
|
# 到这里才进行去重判断
|
|
|
|
|
|
if dedup:
|
|
|
|
|
|
x_int = int(round(x_final))
|
|
|
|
|
|
y_int = int(round(y_final))
|
|
|
|
|
|
z_int = int(round(z_final))
|
|
|
|
|
|
if (x_int, y_int) in seen_xy:
|
|
|
|
|
|
continue
|
|
|
|
|
|
seen_xy.add((x_int, y_int))
|
|
|
|
|
|
# 保留浮点精度写入结果
|
|
|
|
|
|
points.append([x_int, y_int, z_int])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 可选输出PLY文件用于可视化
|
|
|
|
|
|
if config.CAMERA_CONFIG_MAP[sn].get("save_pcd"):
|
|
|
|
|
|
output_ply_path = config.save_path("pcd",sn+".pcd")
|
|
|
|
|
|
write_pcd(output_ply_path, points)
|
|
|
|
|
|
|
|
|
|
|
|
return points
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 新增点云数据转换函数
|
|
|
|
|
|
def convert_sdk_points(sValue, width, height):
|
|
|
|
|
|
"""
|
|
|
|
|
|
将 SDK 原始点云数据转换为 NumPy 点云数组
|
|
|
|
|
|
|
|
|
|
|
|
:param sValue: tuple(float), SDK 输出的扁平化点云数据
|
|
|
|
|
|
:param width: int, 点云宽度
|
|
|
|
|
|
:param height: int, 点云高度
|
|
|
|
|
|
:return: np.ndarray(shape=(N,3)), 三维点云数组
|
|
|
|
|
|
"""
|
|
|
|
|
|
points = np.array(sValue).reshape(-1, 3) # 形状转换 (N,3)
|
|
|
|
|
|
# 剔除无效点(X/Y/Z 任一为 NaN 或 Inf)
|
|
|
|
|
|
mask = np.all(np.isfinite(points), axis=1)
|
|
|
|
|
|
return points[mask]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def process_point_cloud(points, sn, dedup=True):
|
|
|
|
|
|
"""
|
|
|
|
|
|
对原始点云应用旋转、裁剪和去重
|
|
|
|
|
|
|
|
|
|
|
|
:param points: np.ndarray(shape=(N,3)), 原始点云数据
|
|
|
|
|
|
:param sn: str, 设备序列号(用于加载配置)
|
|
|
|
|
|
:param dedup: bool, 是否启用去重
|
|
|
|
|
|
:return: list of [x, y, z], 处理后的点云列表
|
|
|
|
|
|
"""
|
|
|
|
|
|
# 加载配置参数
|
|
|
|
|
|
clip_config = config.CUT_CONFIG_MAP.get(sn)
|
|
|
|
|
|
if clip_config:
|
|
|
|
|
|
min_pt = np.array(clip_config.get("min_pt", [-np.inf] * 3))
|
|
|
|
|
|
max_pt = np.array(clip_config.get("max_pt", [np.inf] * 3))
|
|
|
|
|
|
rotation = np.array(clip_config.get("rotation", [1, 0, 0, 0, 1, 0, 0, 0, 1])).reshape(3, 3)
|
|
|
|
|
|
else:
|
|
|
|
|
|
min_pt = max_pt = rotation = None
|
|
|
|
|
|
|
|
|
|
|
|
processed_points = []
|
|
|
|
|
|
seen_xy = set()
|
|
|
|
|
|
|
|
|
|
|
|
for point in points:
|
|
|
|
|
|
x, y, z = point
|
|
|
|
|
|
|
|
|
|
|
|
# 无效点过滤(Z ≤ 0 或超出最大距离)
|
|
|
|
|
|
if z <= 0 or z > config.CAMERA_CONFIG_MAP[sn].get("max_z", np.inf):
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# 应用旋转矩阵
|
|
|
|
|
|
if clip_config:
|
|
|
|
|
|
rotated = rotation @ point
|
|
|
|
|
|
if not np.all(rotated >= min_pt) or not np.all(rotated <= max_pt):
|
|
|
|
|
|
continue
|
|
|
|
|
|
x_final, y_final, z_final = rotated
|
|
|
|
|
|
else:
|
|
|
|
|
|
x_final, y_final, z_final = x, y, z
|
|
|
|
|
|
|
|
|
|
|
|
# 去重逻辑(保留浮点精度)
|
|
|
|
|
|
if dedup:
|
|
|
|
|
|
# 使用浮点哈希避免离散化损失
|
|
|
|
|
|
key = (round(x_final, 3), round(y_final, 3))
|
|
|
|
|
|
if key in seen_xy:
|
|
|
|
|
|
continue
|
|
|
|
|
|
seen_xy.add(key)
|
|
|
|
|
|
|
|
|
|
|
|
processed_points.append([x_final, y_final, z_final])
|
|
|
|
|
|
|
|
|
|
|
|
return processed_points
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def sValue_to_pcd(sValue,stPointCloudImage,sn):
|
|
|
|
|
|
# 数据格式转换
|
|
|
|
|
|
points = convert_sdk_points(sValue, stPointCloudImage.nWidth, stPointCloudImage.nHeight)
|
|
|
|
|
|
|
|
|
|
|
|
# 应用旋转、裁剪、去重(假设设备序列号已知)
|
|
|
|
|
|
processed_points = process_point_cloud(points, sn, dedup=True)
|
|
|
|
|
|
|
|
|
|
|
|
output_ply_path = config.save_path("pcd", sn + ".pcd")
|
|
|
|
|
|
# 保存结果
|
|
|
|
|
|
write_pcd(output_ply_path, processed_points)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def write_pcd(filename, points):
|
|
|
|
|
|
"""
|
|
|
|
|
|
将点云写入 PCD 文件
|
|
|
|
|
|
|
|
|
|
|
|
:param filename: str, 输出 PCD 文件路径
|
|
|
|
|
|
:param points: list of [x, y, z], 点云数据
|
|
|
|
|
|
"""
|
|
|
|
|
|
with open(filename, 'w') as f:
|
|
|
|
|
|
# 写入 PCD 文件头
|
|
|
|
|
|
f.write("# .PCD v0.7 - Point Cloud Data file format\n")
|
|
|
|
|
|
f.write(f"VERSION 0.7\n")
|
|
|
|
|
|
f.write("FIELDS x y z\n")
|
|
|
|
|
|
f.write("SIZE 4 4 4\n") # float 类型的大小是 4 字节
|
|
|
|
|
|
f.write("TYPE F F F\n") # 每个字段的数据类型
|
|
|
|
|
|
f.write(f"COUNT 1 1 1\n") # 每个字段的数量
|
|
|
|
|
|
f.write(f"WIDTH {len(points)}\n") # 点数量
|
|
|
|
|
|
f.write("HEIGHT 1\n") # 单行表示无结构点云
|
|
|
|
|
|
f.write("VIEWPOINT 0 0 0 1 0 0 0\n") # 默认视角参数
|
|
|
|
|
|
f.write(f"POINTS {len(points)}\n") # 总点数
|
|
|
|
|
|
f.write("DATA ascii\n") # 数据部分以 ASCII 形式存储
|
|
|
|
|
|
|
|
|
|
|
|
# 写入点数据
|
|
|
|
|
|
for point in points:
|
|
|
|
|
|
f.write(f"{point[0]} {point[1]} {point[2]}\n")
|
|
|
|
|
|
|
|
|
|
|
|
print(f"Saved point cloud to {filename}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
|
|
tiff_depth_to_point_cloud("D:/git/test/hik3d-python/image/2025-06-26/depth/191330147_-Depth.tiff", "00DA6823936")
|