饶手测量
使用3D模型测量手掌面积
摘要
本文介绍了一种通过3D扫描模型投影与数学公式结合的方法,计算手掌的投影面积。通过加载3D模型,生成深度图,提取轮廓并计算面积,可以精确地测量手掌的平面投影面积。本文详细介绍了实现的关键步骤,并辅以数学公式和代码说明。
方法概述
核心步骤:
-
加载3D模型: 加载手掌的3D扫描模型(
.obj文件),获取顶点和三角面片数据。这里说一下使用iPhone 13 pro 的lidar雷达做的3d识别 -
生成深度图: 将3D模型投影到指定平面(如
z平面),生成2D深度图。 -
提取轮廓: 基于深度图提取手掌的边界轮廓。
-
计算面积: 使用数学方法根据轮廓点计算手掌的投影面积。
数学公式
1. 模型投影公式
将3D网格投影到二维平面时,顶点的坐标投影公式如下:
其中, 是3D顶点的坐标, 是投影后的2D坐标。
2. 轮廓面积计算公式
对于提取的轮廓点 ,使用多边形面积公式计算面积:
其中, 和 ,即轮廓点首尾相连。
3. 深度图插值与平滑
为提高深度图的质量,使用高斯滤波平滑深度值:(由傅里叶变换推出,了解即可)
where x is the distance from the origin in the horizontal axis, y is the distance from the origin in the vertical axis, and σ is the standard deviation of the Gaussian distribution(正态分布). 这可以减少噪声和不规则性,使轮廓提取更加精确。
方法1
插入手机录屏


import numpy as np
import open3d as o3d
import matplotlib.pyplot as plt
from scipy.spatial import ConvexHull
from skimage.measure import find_contours
from scipy.ndimage import gaussian_filter # 添加这行
from scipy.interpolate import griddata # 添加这行
def load_obj_file(file_path):
"""
加载 .obj 文件并返回三角网格和顶点数据
""" mesh = o3d.io.read_triangle_mesh(file_path)
if not mesh.has_triangles():
raise ValueError("The provided .obj file does not contain triangle mesh data.")
return mesh
def generate_depth_map(mesh, plane_axis='z', resolution=1024):
"""
将 3D 模型投影到一个特定平面以生成深度图
""" # 将 open3d Mesh 转换为 numpy 格式
vertices = np.asarray(mesh.vertices)
face_indices = np.asarray(mesh.triangles)
# 打印模型顶点范围
print(f"模型顶点范围: min={vertices.min(axis=0)}, max={vertices.max(axis=0)}")
# 投影到给定平面
if plane_axis == 'z':
projection_axes = [0, 1] # x, y
depth_axis = 2 # z
elif plane_axis == 'y':
projection_axes = [0, 2] # x, z
depth_axis = 1 # y
elif plane_axis == 'x':
projection_axes = [1, 2] # y, z
depth_axis = 0 # x
else:
raise ValueError("Invalid plane_axis. Must be one of ['x', 'y', 'z'].")
# 提取投影坐标
xy = vertices[:, projection_axes]
depth = vertices[:, depth_axis]
# 打印深度值统计信息
print(f"深度值统计: min={depth.min()}, max={depth.max()}, mean={depth.mean()}")
# 构建深度图网格
min_xy = xy.min(axis=0)
max_xy = xy.max(axis=0)
extent = max_xy - min_xy
pixel_size = extent / resolution
grid_shape = (resolution, resolution)
# 初始化网格
grid = np.full(grid_shape, np.nan, dtype=np.float32)
# 映射点到网格
for face in face_indices:
face_vertices = vertices[face]
face_xy = face_vertices[:, projection_axes]
face_depth = face_vertices[:, depth_axis]
# 计算面片的边界框
min_face_xy = face_xy.min(axis=0)
max_face_xy = face_xy.max(axis=0)
# 映射到网格坐标
grid_min = np.floor((min_face_xy - min_xy) / pixel_size).astype(int)
grid_max = np.ceil((max_face_xy - min_xy) / pixel_size).astype(int)
# 确保在有效范围内
grid_min = np.maximum(grid_min, 0)
grid_max = np.minimum(grid_max, resolution - 1)
# 填充该区域
for y in range(grid_min[1], grid_max[1] + 1):
for x in range(grid_min[0], grid_max[0] + 1):
if 0 <= x < resolution and 0 <= y < resolution:
grid[y, x] = np.nanmax([grid[y, x], face_depth.mean()])
# 添加平滑处理
grid = gaussian_filter(grid, sigma=2)
# 填充空洞
xx, yy = np.meshgrid(np.arange(grid.shape[1]), np.arange(grid.shape[0]))
valid_mask = ~np.isnan(grid)
grid = griddata(
(xx[valid_mask], yy[valid_mask]),
grid[valid_mask],
(xx, yy),
method='nearest'
)
# 打印深度图统计信息
print(
f"深度图统计: min={np.nanmin(grid)}, max={np.nanmax(grid)}, mean={np.nanmean(grid)}, 非零值数量={np.count_nonzero(~np.isnan(grid))}")
return grid, extent
def extract_contours_and_area(depth_map, threshold=None, pixel_size=(1.0, 1.0)):
"""
提取轮廓并计算面积
""" if threshold is None:
# 使用百分位数而不是平均值
valid_depths = depth_map[~np.isnan(depth_map)]
threshold = np.percentile(valid_depths, 25) # 使用第25百分位数作为阈值
print(f"使用的动态阈值: {threshold}")
# 创建二值图像
binary_image = np.zeros_like(depth_map)
binary_image[~np.isnan(depth_map) & (depth_map > threshold)] = 1
# 可视化深度图直方图
valid_depth_values = depth_map[~np.isnan(depth_map)]
plt.hist(valid_depth_values.flatten(), bins=50, color='blue', alpha=0.7)
plt.title("Depth Value Histogram")
plt.xlabel("Depth Value")
plt.ylabel("Frequency")
plt.show()
# 提取轮廓
contours = find_contours(binary_image, level=0.5)
if not contours:
raise ValueError("No contours found in the depth map.")
print(f"找到的轮廓数量: {len(contours)}")
for i, contour in enumerate(contours):
print(f"轮廓 {i} 点数量: {len(contour)}")
# 获取最大轮廓
contour = max(contours, key=len)
print(f"最大的轮廓点数: {len(contour)}")
# 如果轮廓点不足,直接抛出错误
if len(contour) < 3:
raise ValueError("轮廓点不足,无法计算凸包面积。")
# 计算面积
contour = np.array(contour)
x = contour[:, 1] * pixel_size[0]
y = contour[:, 0] * pixel_size[1]
area = 0.5 * np.abs(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1)))
return area, contour
def analyze_model_dimensions(vertices):
"""
分析模型在xyz三轴上的尺寸
""" # 获取xyz轴的最大最小值
x_min, y_min, z_min = vertices.min(axis=0)
x_max, y_max, z_max = vertices.max(axis=0)
# 计算各轴的范围
x_range = x_max - x_min
y_range = y_max - y_min
z_range = z_max - z_min
# 计算各轴的均值和中位数
x_mean, y_mean, z_mean = vertices.mean(axis=0)
x_median = np.median(vertices[:, 0])
y_median = np.median(vertices[:, 1])
z_median = np.median(vertices[:, 2])
# 打印详细信息
print("\n=== 模型尺寸分析 ===")
print(f"\nX轴 (宽度):")
print(f" 最小值: {x_min:.2f}mm")
print(f" 最大值: {x_max:.2f}mm")
print(f" 范围: {x_range:.2f}mm")
print(f" 均值: {x_mean:.2f}mm")
print(f" 中位数: {x_median:.2f}mm")
print(f"\nY轴 (高度):")
print(f" 最小值: {y_min:.2f}mm")
print(f" 最大值: {y_max:.2f}mm")
print(f" 范围: {y_range:.2f}mm")
print(f" 均值: {y_mean:.2f}mm")
print(f" 中位数: {y_median:.2f}mm")
print(f"\nZ轴 (深度):")
print(f" 最小值: {z_min:.2f}mm")
print(f" 最大值: {z_max:.2f}mm")
print(f" 范围: {z_range:.2f}mm")
print(f" 均值: {z_mean:.2f}mm")
print(f" 中位数: {z_median:.2f}mm")
print("\n模型总体尺寸:")
print(f" 宽 × 高 × 深: {x_range:.2f}mm × {y_range:.2f}mm × {z_range:.2f}mm")
print(f" 表面积估计: {x_range * z_range:.2f}平方毫米")
return {
'x': {'min': x_min, 'max': x_max, 'range': x_range, 'mean': x_mean, 'median': x_median},
'y': {'min': y_min, 'max': y_max, 'range': y_range, 'mean': y_mean, 'median': y_median},
'z': {'min': z_min, 'max': z_max, 'range': z_range, 'mean': z_mean, 'median': z_median}
}
def main(obj_file_path):
# 加载 .obj 文件
mesh = load_obj_file(obj_file_path)
# 分析模型尺寸
vertices = np.asarray(mesh.vertices)
dimensions = analyze_model_dimensions(vertices)
# 生成深度图(投影到 Z 平面)
depth_map, extent = generate_depth_map(mesh, plane_axis='z', resolution=1024)
# 调试:可视化深度图
plt.imshow(depth_map, cmap='viridis')
plt.colorbar(label="Depth")
plt.title("Generated Depth Map")
plt.show()
# 输出像素大小
pixel_size = extent / depth_map.shape
print(f"像素大小: x={pixel_size[0]:.6f}, y={pixel_size[1]:.6f}")
# 提取轮廓并计算面积
try:
area, contour = extract_contours_and_area(depth_map, threshold=None, pixel_size=pixel_size)
print(f"投影平面的面积: {area:.6f}")
# 可视化深度图和轮廓
plt.figure(figsize=(10, 5))
plt.subplot(1, 2, 1)
plt.title("Depth Map")
plt.imshow(depth_map, cmap='viridis')
plt.colorbar(label="Depth")
plt.subplot(1, 2, 2)
plt.title("Contours")
plt.imshow(depth_map, cmap='gray')
plt.plot(contour[:, 1], contour[:, 0], color='red') # 绘制轮廓
plt.show()
except ValueError as e:
print(f"错误: {e}")
if __name__ == "__main__":
obj_file = "C://Users//xfc05//Downloads//Untitled_Scan_19_39_18//textured_output.obj" # 替换为您的 .obj 文件路径
main(obj_file)
如何计算动态阈值(25百分位数)
动态阈值的计算基于 百分位数 (Percentile) 概念。以下将详细解释 25百分位数 的含义、计算方法以及代码实现。
百分位数的定义
百分位数 是一个统计学术语,用于描述数据分布的特定位置。第 p 百分位数 表示一组数据中有 p% 的数据小于或等于该值。
- 25百分位数 是第一四分位数,表示数据中 最小的25%的值的上限。
- 百分位数公式的数学表达: 其中,( Q_p ) 是第 ( p ) 百分位数。
计算25百分位数的步骤
1. 数据准备
确定需要计算百分位数的一组数据(如深度图中的非 NaN 有效深度值)。
2. 数据排序
将数据按从小到大的顺序排列。
3. 确定位置索引
对于第 ( p ) 百分位数,位置索引 ( i ) 的计算公式为:
其中:
- ( N ):数据点的个数。
- ( p ):百分位数(例如,25百分位数对应 ( p = 25 ))。
4. 线性插值
如果 ( i ) 是整数,则直接取第 ( i ) 个数据点的值作为百分位数。如果 ( i ) 是小数,则通过线性插值计算百分位数:
其中:
- :位置索引的整数部分对应的数据点。
- :位置索引的小数部分。
用代码实现动态阈值
以下代码展示如何计算25百分位数:
代码实现
import numpy as np
# 示例数据:深度图中的有效深度值(非 NaN)
depth_values = np.array([0.2, 0.5, 0.4, 0.1, 0.3, 0.6, 0.7, 0.8, 0.4, 0.2, 0.5])
# 使用 NumPy 计算 25 百分位数
percentile_25 = np.percentile(depth_values, 25)
print(f"25百分位数: {percentile_25}")
解释
-
np.percentile函数:np.percentile(data, p)计算数据集中第 ( p ) 百分位数的值。- 在代码中,
p=25表示计算第25百分位数。
-
计算过程:
- 将
depth_values排序为[0.1, 0.2, 0.2, 0.3, 0.4, 0.4, 0.5, 0.5, 0.6, 0.7, 0.8]。 - 根据公式:
- ( i = 2.5 ) 表示第25百分位数位于第2和第3个数据点之间(索引从0开始计数)。
- 使用线性插值计算:
- 将
-
输出结果:
25百分位数: 0.25
动态阈值的优点
-
自适应性:
- 数据分布可能在不同深度图中有所不同,动态阈值根据当前数据的分布自动调整,避免使用固定阈值带来的误差。
-
鲁棒性:
- 相比平均值或固定值,百分位数更能减少极值(如噪声或异常点)的影响。例如,25百分位数可以屏蔽掉较小的异常值。
-
适应性强:
- 通过调整百分位数(如25%、50%、75%),可以灵活选择感兴趣的区域。例如:
- 第25百分位数:选择较深的区域。
- 第75百分位数:选择较浅的区域。
- 通过调整百分位数(如25%、50%、75%),可以灵活选择感兴趣的区域。例如:
总结
- 25百分位数 是一种动态阈值计算方法,能够有效过滤掉无关或背景区域。
- 通过
np.percentile函数可以快速计算百分位数,适用于各种数据分布。 - 在深度图处理中,使用25百分位数可以提取较深的感兴趣区域(如手掌部分),为后续的轮廓提取和面积计算提供基础。
详细解释:轮廓提取与轮廓面积计算
什么是轮廓(Contour)?
1. 基本概念
- 轮廓 是一个闭合或开放的曲线,表示图像中强度(像素值)相同的像素点的边界。
- 在二值图像中(像素值仅为0或1),轮廓是值为1的像素点构成的边缘。
2. 轮廓的意义
- 轮廓是物体的边界,可以表示感兴趣区域(如手掌)的形状。
- 提取轮廓后,可以进一步计算面积、周长、形状特征等。
5. 确保轮廓点数足够
if len(contour) < 3:
raise ValueError("轮廓点不足,无法计算凸包面积。")
- 为什么至少需要3个点?
- 面积计算需要闭合的多边形,而多边形至少需要3个点。
- 如果轮廓点不足,无法形成一个有效的多边形,直接抛出错误。
总结
1. 轮廓的作用
- 轮廓是描述二值图像中感兴趣区域形状的基础。
- 提取轮廓后,可以进行面积计算、周长计算、形状分析等操作。
2. 面积公式的优点
- 面积公式适用于任意闭合多边形。
- 通过
pixel_size转换,可以将像素面积精确转换为实际单位面积。
3. 可视化轮廓
- 可以使用
matplotlib可视化轮廓,验证提取结果:plt.imshow(binary_image, cmap='gray') plt.plot(contour[:, 1], contour[:, 0], color='red') plt.title("Contour Visualization") plt.show()
通过这段代码,我们可以从二值图像中提取手掌区域的轮廓,并精确计算其面积,为后续分析提供基础。