|
|
import numpy as np
|
|
|
import cv2
|
|
|
import matplotlib.pyplot as plt
|
|
|
|
|
|
from scipy.ndimage import label
|
|
|
|
|
|
import config
|
|
|
|
|
|
|
|
|
def point_cloud_to_2d_image(points, resolution=1.0, x_range=(17, 275), y_range=(-129, -1227)):
|
|
|
"""
|
|
|
将点云转换为 2D 图像(X-Y 平面),每个像素值表示对应位置的 Z 值平均值
|
|
|
"""
|
|
|
if not isinstance(points, np.ndarray):
|
|
|
points = np.array(points)
|
|
|
|
|
|
if len(points) == 0:
|
|
|
raise ValueError("点云为空")
|
|
|
|
|
|
x_min, x_max = x_range
|
|
|
y_min, y_max = y_range
|
|
|
|
|
|
width = int((x_max - x_min) / resolution) + 1
|
|
|
height = int((y_max - y_min) / resolution) + 1
|
|
|
|
|
|
image = np.zeros((height, width), dtype=np.float32)
|
|
|
count_map = np.zeros((height, width), dtype=np.int32)
|
|
|
|
|
|
for x, y, z in points:
|
|
|
xi = int((x - x_min) / resolution)
|
|
|
yi = int((y - y_min) / resolution)
|
|
|
|
|
|
if 0 <= xi < width and 0 <= yi < height:
|
|
|
image[yi, xi] += z
|
|
|
count_map[yi, xi] += 1
|
|
|
|
|
|
# 防止除零错误
|
|
|
count_map[count_map == 0] = 1
|
|
|
image /= count_map
|
|
|
|
|
|
return image, (x_min, y_min)
|
|
|
|
|
|
|
|
|
def detect_holes_by_density(density_map, density_threshold_ratio=0.5, min_area=100):
|
|
|
"""
|
|
|
基于点云密度图识别空洞区域
|
|
|
|
|
|
:param density_map: 2D numpy array, 每个像素表示该位置点云密度
|
|
|
:param density_threshold_ratio: 密度低于均值的 ratio 倍时视为空洞候选
|
|
|
:param min_area: 最小空洞面积(像素数)
|
|
|
:return: list of ((cx, cy), area),空洞中心和面积(图像坐标)
|
|
|
"""
|
|
|
# 计算邻域平均密度(3x3窗口)
|
|
|
avg_density = np.zeros_like(density_map)
|
|
|
for i in range(density_map.shape[0]):
|
|
|
for j in range(density_map.shape[1]):
|
|
|
# 取 3x3 邻域
|
|
|
neighbors = density_map[
|
|
|
max(0, i - 1):min(i + 2, density_map.shape[0]),
|
|
|
max(0, j - 1):min(j + 2, density_map.shape[1])
|
|
|
]
|
|
|
avg_density[i, j] = np.mean(neighbors)
|
|
|
|
|
|
# 构建空洞候选区:密度低于邻域平均值的 50%
|
|
|
binary_map = (density_map < avg_density * density_threshold_ratio).astype(np.uint8)
|
|
|
|
|
|
# 连通域分析
|
|
|
structure = np.array([[1, 1, 1],
|
|
|
[1, 1, 1],
|
|
|
[1, 1, 1]])
|
|
|
labeled_map, num_features = label(binary_map, structure=structure)
|
|
|
|
|
|
holes = []
|
|
|
for label_id in range(1, num_features + 1):
|
|
|
coords = np.where(labeled_map == label_id)
|
|
|
hole_pixel_count = len(coords[0])
|
|
|
|
|
|
if hole_pixel_count >= min_area:
|
|
|
cx = np.mean(coords[1]) # x 坐标(列)
|
|
|
cy = np.mean(coords[0]) # y 坐标(行)
|
|
|
area = hole_pixel_count
|
|
|
holes.append(((cx, cy), area))
|
|
|
|
|
|
return holes
|
|
|
|
|
|
|
|
|
def detect_black_regions(binary_mask, min_area=10):
|
|
|
"""
|
|
|
检测图像中的黑色连通区域(值为 0 的区域)
|
|
|
|
|
|
:param binary_mask: 2D numpy array, 二值图(0 表示黑色区域)
|
|
|
:param min_area: int, 最小面积阈值,小于该值的区域会被忽略
|
|
|
:return: list of dict, 包含每个区域的信息:
|
|
|
[
|
|
|
{
|
|
|
'id': int,
|
|
|
'center': (cx, cy),
|
|
|
'area': int,
|
|
|
'coordinates': [(x1,y1), (x2,y2), ...]
|
|
|
},
|
|
|
...
|
|
|
]
|
|
|
"""
|
|
|
# 确保输入是二值图像(0 是黑)
|
|
|
binary = (binary_mask == 0).astype(np.uint8)
|
|
|
|
|
|
# 使用连通域分析
|
|
|
structure = np.array([[1, 1, 1],
|
|
|
[1, 1, 1],
|
|
|
[1, 1, 1]])
|
|
|
labeled_map, num_features = label(binary, structure=structure)
|
|
|
|
|
|
regions = []
|
|
|
|
|
|
for label_id in range(1, num_features + 1):
|
|
|
coords = np.where(labeled_map == label_id)
|
|
|
area = len(coords[0])
|
|
|
|
|
|
if area >= min_area:
|
|
|
cx = np.mean(coords[1]) # x 坐标(列)
|
|
|
cy = np.mean(coords[0]) # y 坐标(行)
|
|
|
|
|
|
regions.append(((cx, cy),area))
|
|
|
|
|
|
|
|
|
return regions
|
|
|
def convert_image_holes_to_real(holes, offset, resolution):
|
|
|
real_holes = []
|
|
|
x_min, y_min = offset
|
|
|
for ((cx, cy), area) in holes:
|
|
|
real_x = x_min + cx * resolution
|
|
|
real_y = y_min + cy * resolution
|
|
|
real_holes.append(((real_x, real_y), area * resolution ** 2))
|
|
|
return real_holes
|
|
|
|
|
|
|
|
|
def visualize_holes_on_image(image, holes, output_path=None):
|
|
|
"""
|
|
|
在图像上画出检测到的空洞中心和轮廓
|
|
|
"""
|
|
|
# 彩色化灰度图用于可视化
|
|
|
color_image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
|
|
|
|
|
|
for (cx, cy), _ in holes:
|
|
|
# 绘制圆形标注空洞中心
|
|
|
cv2.circle(color_image, (int(cx), int(cy)), radius=5, color=(0, 0, 255), thickness=-1)
|
|
|
|
|
|
# 显示图像
|
|
|
# plt.figure(figsize=(10, 8))
|
|
|
plt.imshow(cv2.cvtColor(color_image, cv2.COLOR_BGR2RGB))
|
|
|
# plt.title("Detected Holes")
|
|
|
plt.axis("off")
|
|
|
output_path = config.save_path("image", "_holes.png")
|
|
|
if output_path:
|
|
|
plt.savefig(output_path, bbox_inches='tight', dpi=200)
|
|
|
print(f"Saved visualization to {output_path}")
|
|
|
# plt.show()
|
|
|
|
|
|
def read_pcd_points(pcd_path):
|
|
|
"""
|
|
|
从 .pcd 文件中提取点云坐标 (x, y, z)
|
|
|
|
|
|
:param pcd_path: str, PCD 文件路径
|
|
|
:return: list of [x, y, z], 点云坐标列表
|
|
|
"""
|
|
|
points = []
|
|
|
data_started = False
|
|
|
|
|
|
with open(pcd_path, 'r') as f:
|
|
|
for line in f:
|
|
|
if line.startswith("DATA"):
|
|
|
data_started = True
|
|
|
continue
|
|
|
if data_started:
|
|
|
parts = line.strip().split()
|
|
|
if len(parts) >= 3:
|
|
|
try:
|
|
|
x = float(parts[0])
|
|
|
y = float(parts[1])
|
|
|
z = float(parts[2])
|
|
|
points.append([x, y, z])
|
|
|
except ValueError:
|
|
|
continue
|
|
|
return points
|
|
|
|
|
|
def detect_large_holes(points,sn, x_max):
|
|
|
|
|
|
# 2. 生成 2D 图像
|
|
|
x_range = (config.CUT_CONFIG_MAP[sn]["min_pt"][0],x_max) # 手动指定 X 范围
|
|
|
y_range = (config.CUT_CONFIG_MAP[sn]["min_pt"][1], config.CUT_CONFIG_MAP[sn]["max_pt"][1]) # 注意:这里要保证 y_min < y_max,否则反转一下
|
|
|
resolution = config.CAMERA_CONFIG_MAP[sn].get("resolution")
|
|
|
image, offset = point_cloud_to_2d_image(points, resolution=resolution, x_range=x_range, y_range=y_range)
|
|
|
|
|
|
# 3. 图像归一化用于可视化
|
|
|
normalized_image = cv2.normalize(image, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
|
|
|
|
|
|
# 4. 检测空洞
|
|
|
holes = detect_black_regions(normalized_image, min_area=20)
|
|
|
|
|
|
# 5. 可视化空洞
|
|
|
visualize_holes_on_image(normalized_image, holes)
|
|
|
|
|
|
# 6. 输出真实世界坐标
|
|
|
real_holes = convert_image_holes_to_real(holes, offset, resolution)
|
|
|
return real_holes
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
points = read_pcd_points("D:/PycharmProjects/Hik3D/image/2025-06-25/pcd/182109899_00DA6823936_merged.pcd")
|
|
|
sn = "00DA6823936"
|
|
|
x_max = 326
|
|
|
detect_large_holes(points,sn, x_max)
|
|
|
|