You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

212 lines
6.9 KiB
Python

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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)