视觉替代称重,根据体积与形状估算重量,不用电子秤。

📅 发布时间:2026/7/17 18:16:57 👁️ 浏览次数:
视觉替代称重,根据体积与形状估算重量,不用电子秤。
基于机器视觉的体积重量估算系统一、实际应用场景描述在物流分拣中心和工业生产线上需要对传送带上的包裹或工件进行实时重量检测和分拣。传统方案依赖电子秤称重存在以下问题- 节拍不匹配称重工序导致流水线减速影响整体效率- 机械故障率高称重模块频繁承受冲击载荷易损坏- 空间占用大称重工位需要独立区域增加产线长度- 维护成本昂贵定期校准和维修耗时耗力本系统通过工业相机从上方拍摄物体利用计算机视觉技术估算物体体积和密度结合几何形状计算重量实现非接触、无称重设备的重量检测。二、引入痛点1. 堆叠遮挡问题多个物体重叠时难以准确分割2. 密度未知不同材质物体密度差异大影响精度3. 透视变形相机角度导致尺寸测量偏差4. 复杂形状不规则物体体积计算困难5. 光照变化车间环境光影响边缘检测精度三、核心逻辑讲解重量估算原理根据质量-体积-密度关系W ρ × V × g其中- W重量 (N)- ρ物体密度 (kg/m³)- V体积 (m³)- g重力加速度 (9.81 m/s²)实现步骤1. 图像采集获取物体俯视图像2. 预处理去噪、增强边缘特征3. 分割提取分离前景物体与背景4. 尺寸标定建立像素与实际尺寸的映射关系5. 形状识别分类物体几何形状立方体、圆柱体、球体等6. 体积计算基于形状公式计算体积7. 密度匹配根据外观特征匹配预设密度库8. 重量估算综合计算并显示结果关键技术点- 透视校正消除相机倾斜导致的测量误差- 三维重建基于阴影和纹理的深度估计- 密度数据库常见材质密度参考表四、代码模块化实现项目结构visual_weight_estimation/├── main.py # 主程序入口├── camera_calibrator.py # 相机标定模块├── image_preprocessor.py # 图像预处理模块├── object_segmenter.py # 物体分割模块├── shape_analyzer.py # 形状分析与体积计算模块├── density_matcher.py # 密度匹配模块├── weight_calculator.py # 重量计算模块├── utils.py # 工具函数├── density_database.json # 密度数据库├── calibration_params.json # 标定参数└── README.md # 项目文档1. utils.py - 工具函数模块工具函数模块包含通用辅助函数和数据处理工具import jsonimport numpy as npfrom typing import Tuple, List, Dict, Optionalfrom dataclasses import dataclassimport cv2dataclassclass PixelToMetric:像素到实际尺寸的映射参数pixels_per_mm: float 1.0pixel_size_x: float 0.001 # mm per pixel (x方向)pixel_size_y: float 0.001 # mm per pixel (y方向)focal_length: float 12.0 # 焦距 (mm)sensor_width: float 6.4 # 传感器宽度 (mm)def save_json(data: dict, filepath: str) - None:保存字典数据到JSON文件with open(filepath, w, encodingutf-8) as f:json.dump(data, f, indent4, ensure_asciiFalse)print(f数据已保存至 {filepath})def load_json(filepath: str) - dict:从JSON文件加载数据try:with open(filepath, r, encodingutf-8) as f:return json.load(f)except FileNotFoundError:print(f警告未找到文件 {filepath}返回空字典)return {}def pixel_to_metric(pixel_value: float, conversion: PixelToMetric) - float:像素值转换为实际尺寸毫米参数:pixel_value: 像素值conversion: 转换参数对象返回:实际尺寸 (mm)return pixel_value * conversion.pixel_size_xdef metric_to_pixel(metric_value: float, conversion: PixelToMetric) - float:实际尺寸转换为像素值return metric_value / conversion.pixel_size_xdef calculate_pixel_area(contour: np.ndarray, conversion: PixelToMetric) - float:计算轮廓的实际面积平方毫米参数:contour: OpenCV轮廓数组conversion: 转换参数返回:实际面积 (mm²)area_pixels cv2.contourArea(contour)area_mm2 area_pixels * (conversion.pixel_size_x * conversion.pixel_size_y)return area_mm2def calculate_pixel_perimeter(contour: np.ndarray, conversion: PixelToMetric) - float:计算轮廓的实际周长毫米perimeter_pixels cv2.arcLength(contour, True)perimeter_mm perimeter_pixels * conversion.pixel_size_xreturn perimeter_mmdef create_ellipse_mask(image_shape: Tuple[int, int], center: Tuple[float, float],axes: Tuple[float, float], angle: float) - np.ndarray:创建椭圆掩码用于投影分析参数:image_shape: 图像尺寸 (height, width)center: 椭圆中心 (cx, cy)axes: 长短轴 (major_axis, minor_axis)angle: 旋转角度 (度)返回:二值掩码图像mask np.zeros(image_shape[:2], dtypenp.uint8)cv2.ellipse(mask, (int(center[0]), int(center[1])),(int(axes[0]/2), int(axes[1]/2)), angle, 0, 360, 255, -1)return maskdef extract_object_roi(image: np.ndarray, bbox: Tuple[int, int, int, int]) - np.ndarray:根据边界框提取物体ROI区域参数:image: 原始图像bbox: 边界框 (x, y, w, h)返回:裁剪后的ROI图像x, y, w, h bboxreturn image[y:yh, x:xw].copy()def compute_image_histogram(image: np.ndarray, bins: int 256) - Tuple[np.ndarray, np.ndarray]:计算图像灰度直方图hist cv2.calcHist([image], [0], None, [bins], [0, 256])return np.arange(bins), hist.flatten()def normalize_image(image: np.ndarray) - np.ndarray:图像归一化到0-1范围return image.astype(np.float32) / 255.0def apply_morphology(img_binary: np.ndarray, operation: str,kernel_size: int 5) - np.ndarray:应用形态学操作参数:img_binary: 二值图像operation: 操作类型 (open, close, dilate, erode)kernel_size: 核大小返回:处理后的图像kernel np.ones((kernel_size, kernel_size), np.uint8)operations {open: cv2.MORPH_OPEN,close: cv2.MORPH_CLOSE,dilate: cv2.MORPH_DILATE,erode: cv2.MORPH_ERODE}if operation not in operations:raise ValueError(f无效操作: {operation})return cv2.morphologyEx(img_binary, operations[operation], kernel)2. camera_calibrator.py - 相机标定模块相机标定模块负责建立像素与实际尺寸的映射关系import cv2import numpy as npfrom typing import Tuple, Optionalfrom utils import PixelToMetric, save_json, load_jsonclass CameraCalibrator:def __init__(self):初始化相机标定器self.conversion_params PixelToMetric()self.calibration_points [] # 存储标定点 [(pixel_coord, real_coord), ...]self.is_calibrated Falsedef add_calibration_point(self, pixel_coord: Tuple[float, float],real_coord: Tuple[float, float]) - None:添加标定点参数:pixel_coord: 图像中的像素坐标 (x, y)real_coord: 实际物理坐标 (x, y) 单位mmself.calibration_points.append((pixel_coord, real_coord))print(f添加标定点: 像素{pixel_coord} - 实际{real_coord}mm)def calibrate_from_checkerboard(self, image: np.ndarray, checkerboard_size: Tuple[int, int],square_size_mm: float) - bool:使用棋盘格进行相机标定参数:image: 棋盘格图像checkerboard_size: 棋盘格内角点数量 (cols, rows)square_size_mm: 每个方格的实际尺寸 (mm)返回:标定是否成功gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) if len(image.shape) 3 else image# 查找棋盘格角点ret, corners cv2.findChessboardCorners(gray, checkerboard_size, None)if not ret:print(错误未能检测到棋盘格角点)return False# 亚像素级精确化criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)corners_refined cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)# 计算像素到实际尺寸的转换cols, rows checkerboard_sizetotal_squares_x cols - 1total_squares_y rows - 1# 计算总像素距离top_edge_pixels np.linalg.norm(corners_refined[0][0] - corners_refined[1][0])left_edge_pixels np.linalg.norm(corners_refined[0][0] - corners_refined[cols][0])# 计算每像素对应的毫米数pixels_per_mm_x (total_squares_x * square_size_mm) / top_edge_pixelspixels_per_mm_y (total_squares_y * square_size_mm) / left_edge_pixels# 更新转换参数self.conversion_params.pixels_per_mm (pixels_per_mm_x pixels_per_mm_y) / 2self.conversion_params.pixel_size_x 1.0 / pixels_per_mm_xself.conversion_params.pixel_size_y 1.0 / pixels_per_mm_yself.is_calibrated Trueprint(f棋盘格标定完成:)print(f X方向: {pixels_per_mm_x:.4f} 像素/mm)print(f Y方向: {pixels_per_mm_y:.4f} 像素/mm)return Truedef calibrate_from_reference_object(self, image: np.ndarray, reference_object: Dict) - bool:使用参考物体进行标定参数:image: 包含参考物体的图像reference_object: 参考物体信息{type: rectangle/circle,known_dimensions: {width: 100, height: 50}, # mmbbox: (x, y, w, h) # 图像中的边界框}返回:标定是否成功x, y, w, h reference_object[bbox]known_dims reference_object[known_dimensions]# 根据实际物体类型计算像素比例if reference_object[type] rectangle:# 矩形物体直接使用宽高pixels_per_mm_x w / known_dims[width]pixels_per_mm_y h / known_dims[height]elif reference_object[type] circle:# 圆形物体使用直径diameter_pixels max(w, h)pixels_per_mm diameter_pixels / known_dims[diameter]pixels_per_mm_x pixels_per_mm_y pixels_per_mmelse:print(错误不支持的参考物体类型)return Falseself.conversion_params.pixels_per_mm (pixels_per_mm_x pixels_per_mm_y) / 2self.conversion_params.pixel_size_x 1.0 / pixels_per_mm_xself.conversion_params.pixel_size_y 1.0 / pixels_per_mm_yself.is_calibrated Trueprint(f参考物体标定完成: {self.conversion_params.pixels_per_mm:.4f} 像素/mm)return Truedef manual_calibration(self, pixel_distance: float, real_distance_mm: float) - None:手动标定通过已知长度的物体进行标定参数:pixel_distance: 图像中测量的像素距离real_distance_mm: 对应的实际长度 (mm)pixels_per_mm pixel_distance / real_distance_mmself.conversion_params.pixels_per_mm pixels_per_mmself.conversion_params.pixel_size_x 1.0 / pixels_per_mmself.conversion_params.pixel_size_y 1.0 / pixels_per_mmself.is_calibrated Trueprint(f手动标定完成: {pixels_per_mm:.4f} 像素/mm)def get_conversion_params(self) - PixelToMetric:获取当前标定参数return self.conversion_paramsdef save_calibration(self, filepath: str calibration_params.json) - None:保存标定参数到文件params_dict {pixels_per_mm: self.conversion_params.pixels_per_mm,pixel_size_x: self.conversion_params.pixel_size_x,pixel_size_y: self.conversion_params.pixel_size_y,focal_length: self.conversion_params.focal_length,sensor_width: self.conversion_params.sensor_width,is_calibrated: self.is_calibrated}save_json(params_dict, filepath)def load_calibration(self, filepath: str calibration_params.json) - bool:从文件加载标定参数params_dict load_json(filepath)if not params_dict:return Falseself.conversion_params PixelToMetric(pixels_per_mmparams_dict.get(pixels_per_mm, 1.0),pixel_size_xparams_dict.get(pixel_size_x, 0.001),pixel_size_yparams_dict.get(pixel_size_y, 0.001),focal_lengthparams_dict.get(focal_length, 12.0),sensor_widthparams_dict.get(sensor_width, 6.4))self.is_calibrated params_dict.get(is_calibrated, False)print(f标定参数已从 {filepath} 加载)return True3. image_preprocessor.py - 图像预处理模块图像预处理模块负责图像增强、去噪和边缘准备import cv2import numpy as npfrom typing import Tuple, Optionalclass ImagePreprocessor:def __init__(self, config: Optional[dict] None):初始化图像预处理器参数:config: 配置字典包含预处理参数default_config {gaussian_kernel: (5, 5),gaussian_sigma: 1.0,clahe_clip_limit: 2.0,clahe_tile_grid: (8, 8),canny_low: 50,canny_high: 150,morph_kernel: 3}self.config default_configif config:self.config.update(config)def denoise(self, image: np.ndarray) - np.ndarray:图像去噪使用高斯滤波去除随机噪声if len(image.shape) 3:gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)else:gray image.copy()denoised cv2.GaussianBlur(gray, self.config[gaussian_kernel],self.config[gaussian_sigma])return denoiseddef enhance_contrast(self, image: np.ndarray) - np.ndarray:对比度增强使用CLAHE限制对比度自适应直方图均衡化clahe cv2.createCLAHE(clipLimitself.config[clahe_clip_limit],tileGridSizeself.config[clahe_tile_grid])enhanced clahe.apply(image)return enhanceddef edge_detection(self, image: np.ndarray) - np.ndarray:边缘检测使用Canny算子检测物体边缘edges cv2.Canny(image, self.config[canny_low], self.config[canny_high])return edgesdef morphological_processing(self, binary_image: np.ndarray) - np.ndarray:形态学处理填充小孔洞连接断开的边缘kernel np.ones((self.config[morph_kernel], self.config[morph_kernel]), np.uint8)# 闭运算填充小孔洞closed cv2.morphologyEx(binary_image, cv2.MORPH_CLOSE, kernel)# 开运算去除小噪点opened cv2.morphologyEx(closed, cv2.MORPH_OPEN, kernel)return openeddef full_preprocess(self, image: np.ndarray) - Tuple[np.ndarray, np.ndarray, np.ndarray]:完整预处理流程参数:image: 原始BGR图像返回:tuple: (去噪图像, 增强图像, 边缘图像)# 1. 去噪denoised self.denoise(image)# 2. 对比度增强enhanced self.enhance_contrast(denoised)# 3. 边缘检测edges self.edge_detection(enhanced)# 4. 形态学处理processed_edges self.morphological_processing(edges)return denoised, enhanced, processed_edgesdef background_subtraction(self, current_frame: np.ndarray,background: np.ndarray) - np.ndarray:背景减除分离前景物体与静态背景# 转换为灰度if len(current_frame.shape) 3:current_gray cv2.cvtColor(current_frame, cv2.COLOR_BGR2GRAY)else:current_gray current_frameif len(background.shape) 3:bg_gray cv2.cvtColor(background, cv2.COLOR_BGR2GRAY)else:bg_gray background# 计算绝对差分diff cv2.absdiff(current_gray, bg_gray)# 阈值化_, foreground cv2.threshold(diff, 30, 255, cv2.THRESH_BINARY)return foregrounddef update_background_model(self, current_frame: np.ndarray,background: np.ndarray, alpha: float 0.05) - np.ndarray:更新背景模型运行平均法参数:current_frame: 当前帧background: 当前背景模型alpha: 更新速率 (0-1)返回:更新后的背景模型if len(current_frame.shape) 3:current_gray cv2.cvtColor(current_frame, cv2.COLOR_BGR2GRAY)else:current_gray current_frameif len(background.shape) 3:bg_gray cv2.cvtColor(background, cv2.COLOR_BGR2GRAY)else:bg_gray backgroundupdated_bg cv2.addWeighted(bg_gray, 1 - alpha, current_gray, alpha, 0)return updated_bg.astype(np.uint8)4. object_segmenter.py - 物体分割模块物体分割模块负责从图像中提取独立的物体区域import cv2import numpy as npfrom typing import List, Tuple, Optionalfrom dataclasses import dataclassfrom utils import apply_morphology, extract_object_roidataclassclass ObjectRegion:物体区域数据类contour: np.ndarraybbox: Tuple[int, int, int, int] # (x, y, w, h)centroid: Tuple[float, float] # (cx, cy)area_pixels: floatarea_mm2: floatperimeter_pixels: floatperimeter_mm: floatconvex_hull: np.ndarrayaspect_ratio: floatextent: float # 轮廓面积/外接矩形面积solidity: float # 轮廓面积/凸包面积class ObjectSegmenter:def __init__(self, min_area: int 1000, max_area: int 500000):初始化物体分割器参数:min_area: 最小物体面积像素max_area: 最大物体面积像素self.min_area min_areaself.max_area max_areadef segment_objects(self, binary_image: np.ndarray,conversion: PixelToMetric) - List[ObjectRegion]:从二值图像中分割物体参数:binary_image: 预处理后的二值图像conversion: 像素到实际尺寸的转换参数返回:物体区域列表# 查找轮廓contours, _ cv2.findContours(binary_image, cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)objects []for contour in contours:# 面积过滤area cv2.contourArea(contour)if area self.min_area or area self.max_area:continue# 计算各种几何特征obj_region self._analyze_contour(contour, conversion)objects.append(obj_region)return objectsdef _analyze_contour(self, contour: np.ndarray,conversion: PixelToMetric) - ObjectRegion:分析单个轮廓提取几何特征# 边界框bbox cv2.boundingRect(contour)x, y, w, h bbox# 质心M cv2.moments(contour)if M[m00] ! 0:cx M[m10] / M[m00]cy M[m01] / M[m00]else:cx, cy x w/2, y h/2# 面积和周长area_pixels cv2.contourArea(contour)perimeter_pixels cv2.arcLength(contour, True)# 凸包convex_hull cv2.convexHull(contour)# 纵横比aspect_ratio float(w) / h if h 0 else 0# Extent: 轮廓面积/外接矩形面积rect_area w * hextent area_pixels / rect_area if rect_area 0 else 0# Solidity: 轮廓面积/凸包面积hull_area cv2.contourArea(convex_hull)solidity area_pixels / hull_area if hull_area 0 else 0# 转换为实际尺寸area_mm2 area_pixels * (conversion.pixel_size_x * conversion.pixel_size_y)perimeter_mm perimeter_pixels * conversion.pixel_size_xreturn ObjectRegion(contourcontour,bboxbbox,centroid(cx, cy),area_pixelsarea_pixels,area_mm2area_mm2,perimeter_pixelsperimeter_pixels,perimeter_mmperimeter_mm,convex_hullconvex_hull,aspect_ratioaspect_ratio,extentextent,soliditysolidity)def remove_overlapping(self, objects: List[ObjectRegion],overlap_threshold: float 0.3) - List[ObjectRegion]:移除重叠的物体保留较大的参数:objects: 物体区域列表overlap_threshold: 重叠阈值超过此值视为重复检测返回:去重后的物体列表if len(objects) 1:return objects# 按面积降序排序sorted_objects sorted(objects, keylambda x: x.area_pixels, reverseTrue)keep_list []for obj in sorted_objects:is_duplicate Falsefor kept_obj in keep_list:# 计算两个物体的IOUiou self._calculate_iou(obj.bbox, kept_obj.bbox)if iou overlap_threshold:is_duplicate Truebreakif not is_duplicate:keep_list.append(obj)return keep_listdef _calculate_iou(self, bbox1: Tuple[int, int, int, int],bbox2: Tuple[int, int, int, int]) - float:计算两个边界框的交并比IOUx1, y1, w1, h1 bbox1x2, y2, w2, h2 bbox2# 计算交集区域x_left max(x1, x2)y_top max(y1, y2)x_right min(x1 w1, x2 w2)y_bottom min(y1 h1, y2 h2)if x_right x_left or y_bottom y_to利用AI解决实际问题如果你觉得这个工具好用欢迎关注长安牧笛