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.

275 lines
8.9 KiB
Python

9 months ago
import numpy as np
import cv2
7 months ago
import threading
from thread_pool import submit_task
9 months ago
from scipy.ndimage import label
import config
7 months ago
from logConfig import get_logger
# 使用示例
logger = get_logger()
#
# 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)
# 优化 point_cloud_to_2d_image 函数
9 months ago
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
7 months ago
# 使用向量化操作替代循环
x_coords = points[:, 0]
y_coords = points[:, 1]
z_coords = points[:, 2]
# 计算像素坐标
xi = ((x_coords - x_min) / resolution).astype(int)
yi = ((y_coords - y_min) / resolution).astype(int)
# 筛选有效坐标
valid_mask = (xi >= 0) & (xi < width) & (yi >= 0) & (yi < height)
xi = xi[valid_mask]
yi = yi[valid_mask]
z_coords = z_coords[valid_mask]
# 使用直方图统计替代循环累加
9 months ago
image = np.zeros((height, width), dtype=np.float32)
count_map = np.zeros((height, width), dtype=np.int32)
7 months ago
# 使用 bincount 进行快速统计
indices = yi * width + xi
z_sums = np.bincount(indices, weights=z_coords, minlength=height * width)
counts = np.bincount(indices, minlength=height * width)
9 months ago
7 months ago
image.flat[:] = z_sums
count_map.flat[:] = counts
9 months ago
# 防止除零错误
count_map[count_map == 0] = 1
image /= count_map
return image, (x_min, y_min)
7 months ago
def stitch(imagePath1, imagePath2):
# 读取两张图像
image1 = cv2.imread(imagePath1)
image2 = cv2.imread(imagePath2)
9 months ago
7 months ago
# 创建 Stitcher 对象
stitcher = cv2.Stitcher_create()
9 months ago
7 months ago
# 拼接图像
(status, stitched) = stitcher.stitch((image1, image2))
9 months ago
7 months ago
# 检查拼接结果
if status == cv2.Stitcher_OK:
cv2.imwrite('stitched_output.jpg', stitched)
print("图像拼接成功!")
else:
print(f"图像拼接失败,错误代码: {status}")
if __name__ == '__main__':
stitch('D:/git/test/hik3d-python/image/2025-07-01/color/105601193_-_Color.bmp'
, 'D:/git/test/hik3d-python/image/2025-07-01/color/164646720_-_Color.bmp')
9 months ago
7 months ago
def detect_black_regions(binary_mask, min_area=10,box_area=10):
9 months ago
"""
检测图像中的黑色连通区域值为 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 = []
7 months ago
count = 0
9 months ago
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 坐标(行)
7 months ago
logger.info(f"区域: {label_id} 中心: {cx, cy} 面积: {area}")
9 months ago
regions.append(((cx, cy),area))
7 months ago
count += ( area/box_area)
return regions,count
9 months ago
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):
"""
在图像上画出检测到的空洞中心和轮廓
"""
7 months ago
try:
# 确保图像数据有效
if image is None or len(image) == 0:
print("Warning: Empty image data for visualization")
return
# 彩色化灰度图用于可视化
if len(image.shape) == 2: # 灰度图
color_image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
else:
color_image = image.copy()
# 绘制空洞标记
if holes:
for (cx, cy), _ in holes:
# 确保坐标有效
if 0 <= int(cx) < color_image.shape[1] and 0 <= int(cy) < color_image.shape[0]:
cv2.circle(color_image, (int(cx), int(cy)), radius=5, color=(0, 0, 255), thickness=-1)
# 生成输出路径
if output_path is None:
output_path = config.save_path("image", "_holes.png")
# 保存图像
success = cv2.imwrite(output_path, color_image)
if success:
print(f"Saved visualization to {output_path}")
else:
print(f"Failed to save visualization to {output_path}")
except Exception as e:
print(f"Error in visualize_holes_on_image: {e}")
9 months ago
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
7 months ago
def detect_large_holes(points, sn, type):
# 获取裁剪配置
cat_map = config.CUT_CONFIG_MAP.get(sn + "_" + type, None) or config.CUT_CONFIG_MAP.get(sn)
template_map = config.TEMPLATE_CONFIG_MAP[type]
camera_map = config.CAMERA_CONFIG_MAP[sn]
9 months ago
# 2. 生成 2D 图像
7 months ago
x_range = (cat_map["min_pt"][0], cat_map["max_pt"][0])
y_range = (cat_map["min_pt"][1], cat_map["max_pt"][1])
9 months ago
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)
7 months ago
min_area = min(template_map["min_area"], (template_map["width"] * template_map["height"]) /(2*camera_map["resolution"]*camera_map["resolution"]))
9 months ago
# 4. 检测空洞
7 months ago
holes, count = detect_black_regions(normalized_image, min_area, (template_map["width"] * template_map["height"]) / 2)
if config.CAMERA_CONFIG_MAP[sn].get("save_image"):
# 创建数据副本确保异步执行时数据完整性
image_copy = normalized_image.copy()
holes_copy = list(holes) # 创建holes的副本
9 months ago
7 months ago
# 预先生成输出路径
output_path = config.save_path("image", f"_{sn}_holes.png")
# 使用线程池异步执行可视化操作
submit_task(visualize_holes_on_image, image_copy, holes_copy, output_path)
9 months ago
# 6. 输出真实世界坐标
real_holes = convert_image_holes_to_real(holes, offset, resolution)
7 months ago
return real_holes, count
9 months ago
7 months ago
# 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)
9 months ago