拓冰建站拓冰建站
首页 / 资讯中心 / 正文

双目立体视觉全流程:标定、校正、SGBM视差与点云重建实战

老读者都知道这个系列不太喜欢绕弯子今天直接开讲。做视觉的人早晚都会碰一次双目立体——只要你想用两个普通摄像头恢复场景深度就躲不开从相机标定到点云生成这条链路。这篇是这个系列的第三十期我把整条流程从单目标定、双目标定、立体校正、SGBM 视差到最终点云重建完整拆一遍最后补上几个我自己踩过多次的坑。拍着胸脯说这条链路本质不复杂核心就是一个视差公式和几个矩阵。但真上手你会发现 80% 的时间都在处理细节标定板拍多少张才够、RMS 怎么看、SGBM 参数为什么是这些值、点云为什么歪了。这篇文章面向两类人一是刚入门打算复现整套流程的初学者二是已经跑通 demo 但被质量调优折磨的项目开发。无论哪一类我希望每段结论你都能直接用到自己的数据上。1. 先看整条链路从像素差到三维坐标到底经过哪几步1.1 一个公式解释双目为什么能测距双目立体视觉最底层的事就是利用“同一物体在左右两张图上的位置差异”来推算距离。想象你闭上一只眼睛再换另一只眼前物体会在背景上“跳”一下这个位移就是视差。物体越近跳得越明显物体越远跳得越不明显。用公式表达就是Z f × B / dZ物体到左相机光心的深度单位与 B 一致通常用毫米f相机以像素为单位的焦距来自内参矩阵里的 fxB两台相机光心之间的直线距离也就是基线d同一个点像素在左图 x 坐标减去右图 x 坐标之差。举个例子一个镜头标定后 fx 800两台相机基线 120 mm如果某个点在左右图中横坐标差了 10 个像素那它的深度就是 800 × 120 / 10 9600 mm也就是 9.6 米。如果视差变成 100 像素深度就是 960 mm。这个反比关系是整套系统的地基后面每一步其实都在为精确计算 d 和 B 服务。1.2 五步流程与各环节输出物从原始的双目图像到最终点云工程上一般拆成五个环节环节输入输出OpenCV 核心函数1. 单目标定棋盘格图像集左右相机内参 K、畸变系数calibrateCamera2. 双目标定左右图像点集 内参旋转 R、平移 t、本质矩阵 E、基础矩阵 FstereoCalibrate3. 立体校正左右原图 R/t校正后图像、投影矩阵 P1/P2、Q 矩阵stereoRectify / remap4. 视差计算校正后的左右图视差图 disparityStereoSGBM5. 立体重建视差图 Q 矩阵三维点云reprojectImageTo3D也就是说你最终拿到的点云其实是每一步输出层层传递的结果。任何一个环节的误差都会在后面放大所以“链路思维”特别重要——你不可能跳过标定直接调 SGBM也不可能在校正不准的情况下得到干净的点云。1.3 顺序为什么不能乱很多人问能不能直接给两张图算视差不做标定和校正。答案是可以试但结果通常没法用。原因很简单真实镜头有畸变两台相机也不可能严格平行放置直接匹配时同一个点在右图里的搜索范围是一个二维区域计算量暴增误匹配率也高得离谱。先标定获得内参再用双目标定获得相对位姿最后通过校正把图像“拉”到理想的双目平行模型上匹配才变成纯粹的一维搜索。这就是为什么哪怕只为了测个距离也别省掉标定的原因它不是加分项是及格线。2. 张正友标定法实操棋盘格、内参、畸变和 RMS 怎么读2.1 为什么今天的相机标定都绕不开棋盘格相机标定家族里常见的有三种摄像头内参标定、手眼标定、传感器之间的外参标定。今天这期说的是第一种但它是后面所有标定的基础。提到内参标定几乎绕不开张正友标定法。它的核心贡献是告诉大家用一块平面棋盘格就能完成标定不需要昂贵且精度要求极高的三维标定块。棋盘格的角点天然是精确的亚像素特征而且整个棋盘格位于同一个平面上这个几何约束让算法有足够的方程去解出相机内参。我们只需要在不同角度、不同距离拍几十张照片找到角点再交给 OpenCV 解算。整个过程不需要高精度机械装置这也是它能成为事实标准的原因。2.2 标定前必须想清楚的几个采集细节我见过太多人在标定阶段就翻车核心原因基本都是采集动作不对。几条实测有效的规则标定板要平整最好是玻璃夹层板或贴在高刚性铝板上。软纸板一弯角点坐标离谱解出来的内参全是错的。拍摄数量建议 15~25 张。少于 10 张时外参自由度太高容易过拟合。每一张都要让棋盘格出现在画面不同位置别每次都摆在正中央否则畸变系数尤其是边缘部分的 k1、k2 根本约束不住。倾斜角度要有跨度最好覆盖 0~45 度的俯仰和左右旋转。只拍正面平视会让相机中心附近的约束过强而边缘像差永远不触发。拍摄距离最好接近实际工作距离。你要标定一台用来测 1~5 米障碍物的相机就别拿 20 厘米距离的照片凑数。光照要均匀避免棋盘格表面反光。反光会让 findChessboardCorners 在同一张照片上删了又找而且亚像素定位精度会下降。固定好相机或标定板拍摄时轻微移动即可不要出现运动模糊。这里有个很容易被忽略的点棋盘格的物理边长必须用卡尺量准。很多厂家标称 24 mm实际可能差零点几毫米。别小看这个误差内参的尺度、后续基线长度、点云尺寸都会跟着错。2.3 单目标定的最小可运行代码Python 版本最直观逻辑完整import cv2 import numpy as np pattern (9, 6) # 内角点数量横向9个纵向6个 square_mm 24.0 # 棋盘格每个格子的实际边长毫米 objp np.zeros((pattern[0] * pattern[1], 3), np.float32) objp[:, :2] np.mgrid[0:pattern[0], 0:pattern[1]].T.reshape(-1, 2) objp * square_mm obj_pts, img_pts [], [] image_paths [...] # 你的标定图片路径 for path in image_paths: img cv2.imread(path) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners cv2.findChessboardCorners(gray, pattern, None) if not ret: continue criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-3) corners cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) obj_pts.append(objp) img_pts.append(corners) rms, K, dist, rvecs, tvecs cv2.calibrateCamera( obj_pts, img_pts, gray.shape[::-1], None, None) print(RMS:, rms) print(内参矩阵:\n, K) print(畸变系数:, dist.ravel())这里唯一要解释的黑盒是 cornerSubPix。findChessboardCorners 得到的是像素级整数角点cornerSubPix 会在角点邻域内做亚像素迭代优化精度能达到 0.01 像素级别。这一步直接影响后续标定质量千万别省。2.4 RMS、内参矩阵和畸变系数怎么判断好坏calibrateCamera 返回的 rms 是全部角点的平均重投影误差单位是像素。我的经验值如下rms 0.1非常理想内参可信0.1 ~ 0.3可用但要确认它是不是被某几张差图拉高的0.3 ~ 0.5建议查一下拍摄集剔除模糊角度奇怪的图0.5基本可以认定标定失败优先检查标定板平整度、采集数量、反光。内参矩阵 K 形如[[fx, 0, cx], [0, fy, cy], [0, 0, 1]]fx、fy 表示焦距与像素尺寸的换算结果cx、cy 是主点位置。如果 fx 和 fy 相差过大说明像素不是标准正方形如果 cx、cy 偏离画面中心太多说明镜头光轴和传感器中心没对齐这些都正常关键是它们要稳定。畸变系数 dist 包含径向畸变 k1、k2、k3 和切向畸变 p1、p2。一般 k1、k2 的量级通常在十的负二次方如果出现零点几甚至更大就要警惕是不是把畸变、内参和外参绕成一团了这时候重新标定通常比强行收拾更省时间。3. 双目标定两台相机之间的 R 和 t比任何参数都重要3.1 单目标定完成之后还缺什么单目标定只解决“一台相机到底是怎么看世界的”但立体视觉需要回答更关键的问题左右相机之间的相对位姿是什么。换句话说右相机在左相机坐标系里旋转了多少、平移了多少。这个相对位姿就是双目标定要求解的东西。有了它我们才知道基线 B 到底多长也才能把两台相机校正成理想的双目平行模型。很多人问“我量了两个相机中心距离 120 毫米能不能直接当作 B 填进去”答案是不能。你量的是外壳间距不是光学中心的距离而且还包含安装角度误差必须通过算法反算。3.2 stereoCalibrate 到底解出了什么双目标定的输入是同一时刻左右相机拍摄到的同一块棋盘格的角点。因为棋盘格上的 3D 坐标已知左右图上的 2D 点也已知相当于拿到了若干组“双目观测”OpenCV 通过多视图几何关系解出两台相机之间的旋转矩阵 R 和平移向量 t。代码核心部分rms_stereo, K1, d1, K2, d2, R, T, E, F cv2.stereoCalibrate( obj_pts_all, # 所有帧的棋盘格3D点 img_pts_left_all, # 左相机对应2D角点 img_pts_right_all, # 右相机对应2D角点 K1, d1, K2, d2, # 单目标定的结果 gray.shape[::-1], # 图像尺寸 flagscv2.CALIB_FIX_INTRINSIC) print(双目RMS:, rms_stereo) print(R:\n, R) print(T (mm):, T.ravel())注意这里我用了 CALIB_FIX_INTRINSIC表示双目标定过程中只优化两台相机之间的相对位姿不再动单目标定得到的内参和畸变。这个做法在排错时特别好用如果最终结果不对可以分别验证单目标定和双目标定而不是一堆参数搅在一起。帧匹配逻辑也要说清楚左右两张图必须是同一时刻拍摄的我建议保存图像时就按帧号对应。写代码时先分别对左右图片找角点只有左右两边都成功的那一帧才加入 obj_pts_all、img_pts_left_all 和 img_pts_right_all。3.3 基线的选择与标定结果的验收规则基线 B 是双目标定的直接输出也就是 T 向量的模长。基线的选择直接影响深度精度近距离测量0.5~5 米基线 5~12 厘米比较舒服远距离测量10 米以上基线最好到 30 厘米甚至更宽基线越长近处共同视野越小遮挡区域越大基线越短视差变化越微弱远距离深度误差迅速膨胀。验收双目标定结果我一般看三个指标双目 RMS 同样要小于 0.2 像素基线的数值和用卷尺量的光心距差距在 5% 以内偏差过大要检查 square_mm 是否写错R 矩阵应该非常接近单位阵。如果旋转量过大说明两台相机的相对姿态估算紊乱多半是标定板拍得不够或左右图像时间不同步。4. 立体校正把左右相机“掰正”到共面平行4.1 极线约束为什么匹配必须做校正双目标定之后我们其实已经知道左右相机之间的位姿但真实安装中两台相机不可能做到光轴严格平行、成像平面共面。如果不校正同一个点在左图位于第 300 行在右图可能位于第 312 行匹配时需要在二维区域内搜索计算量和误匹配率都不可接受。立体校正的目的就是把左右图像通过重采样变成“理想双目”的样子两幅图像行对齐任意一个空间点投影到左右图上的 y 坐标相同。这样匹配搜索就从二维直接降成一维水平搜索这也叫极线约束。OpenCV 的 stereoRectify 基于 Bouguet 算法原理大致是先把双目标定得到的旋转矩阵分解成左右各一半让两机光轴先变成平行再构造一个把极点推向无穷远的校正旋转矩阵使极线与图像扫描线完全水平。4.2 校正矩阵和 remap 的完整代码校正这一步的代码结构在 Python 里是固定的R1, R2, P1, P2, Q, roi1, roi2 cv2.stereoRectify( K1, d1, K2, d2, gray.shape[::-1], R, T, alpha0) map1_l, map2_l cv2.initUndistortRectifyMap( K1, d1, R1, P1, gray.shape[::-1], cv2.CV_32FC1) map1_r, map2_r cv2.initUndistortRectifyMap( K2, d2, R2, P2, gray.shape[::-1], cv2.CV_32FC1) rect_l cv2.remap(img_l, map1_l, map2_l, cv2.INTER_LINEAR) rect_r cv2.remap(img_r, map1_r, map2_r, cv2.INTER_LINEAR)这里有几个经验值得单独说initUndistortRectifyMap 返回的两个矩阵按顺序原样传给 remap 的第三、四参数。很多人在这里把 map1 和 map2 传反结果图像翻转或者直接错乱。alpha 参数影响校正后图像的黑边处理。alpha0 会裁剪掉几乎所有无效区域适合后续匹配alpha1 保留全部像素但会出现大片空白。实际项目我一般用 alpha0再用 roi1/roi2 框出有效区域。remap 用双线性插值 INTER_LINEAR 就够了不要在这里用太高阶的插值耗时大且收益不明显。4.3 用一条水平线验证校正结果校正做得好不好最直观的办法就是在两张校正图上叠画几条水平线然后肉眼判断场景里的同一个物体是否落在同一高度。如果左图某个门把手在第 240 行右图它也在第 240 行附近说明校正基本成功。更严格一点的做法是手工选取 5 个以上的特征点棋盘格角点、纸箱边缘、表盘中心都行统计左右校正图上的 y 坐标差。中心区域的偏差应该控制在 2~3 像素以内。如果偏差达到十个像素以上回头检查双目标定阶段是不是有错帧、画面是不是没同步不要急着调 SGBM。5. SGBM 视差计算参数原理和工程调优的十几个细节5.1 视差匹配的本质在一维搜索空间里找最像的块校正完成后真正核心的计算才开场。StereoSGBM 做的事情可以这样理解对左图每个像素在右图同一行上滑动一个小窗口寻找颜色和梯度最接近的窗口中心位置两者的横向差距就是视差 d。SGBM 的 S 是 Semi-Global半全局。它在逐像素匹配的基础上增加了对相邻像素视差变化的平滑惩罚能在弱纹理区域减少误匹配同时又不像全局匹配那样计算量爆炸。它输出的是 CV_16S 格式的定点数真实视差需要除以 16。匹配是一个天生有歧义的过程光滑墙面、重复纹理、遮挡边缘都没有稳定的匹配信号所以 SGBM 质量的高低很大程度上取决于参数是否贴合你的场景和图像分辨率。5.2 参数表与推荐值这些数字别乱抄C 版本创建 SGBM 的代码很典型cv::Ptrcv::StereoSGBM sgbm cv::StereoSGBM::create( 0, // minDisparity最小视差 96, // numDisparities必须为16的倍数 5, // blockSize必须为奇数 8 * 1 * 5 * 5, // P1 32 * 1 * 5 * 5, // P2 1, // disp12MaxDiff 63, // preFilterCap 25, // uniquenessRatio 100, // speckleWindowSize 32, // speckleRange true); // mode: MODE_SGBM cv::Mat disparity; sgbm-compute(rect_l, rect_r, disparity); cv::Mat disp8; cv::normalize(disparity, disp8, 0, 255, cv::NORM_MINMAX, CV_8U);参数含义和参考值整理成表参数作用我的常用值minDisparity从哪个视差值开始搜索0numDisparities搜索范围必须是 16 的倍数64~128blockSize匹配窗口边长奇数5 或 7P1、P2视差平滑惩罚P18×blockSize²P24×P1disp12MaxDiff左右一致性检查容忍差1~3preFilterCap预滤波截断值63uniquenessRatio最佳匹配的独特程度越高越严格10~30speckleWindowSize散斑滤波窗口大小50~200speckleRange散斑内允许的视差波动16~32这里最容易被新手改坏的参数是 blockSize 和 P2。blockSize 太小会让视差图全是椒盐噪声太大则会把边缘磨平我通常先给 5分辨率高或纹理弱时试 7 甚至 9。P2 是平滑惩罚强度太大整个视差图会糊成一整块太小则弱纹理区域到处都是洞。需要特别提醒numDisparities 决定了最近的能测深度但它不是越大越好。搜索范围变大后弱纹理处的误匹配机会也变多。实际项目里先根据最近目标距离估算最大视差假设最近距离 0.5 米fx800B120mm最大视差就是 800 × 120 / 500 192 像素numDisparities 取 208 或者 192 的倍数向上取整。5.3 从视差图到有效掩码除以 16 和无效像素SGBM 输出的 disparity 是 CV_16S内部用 1/16 像素作为视差精度档位所以真实视差值要除以 16。如果你直接拿原始值做 reprojectImageTo3DOpenCV 会在内部替你做除法但如果自己写公式算深度一定要记得除以 16。无效匹配会被置成最小负数或者固定负值。生成掩码最稳妥的方法valid_mask disparity 0视差小于等于 0 的像素在物理上不可信要么是没匹配上要么匹配到了不可信的低置信度位置。5.4 WLS 后处理与弱纹理区域的补洞思路SGBM 输出经常带有空洞和噪点特别是在墙面、天空、反光面上。普通中值滤波可以去掉一部分椒盐噪声但对大面积空洞无能为力。想要更平滑的结果推荐 OpenCV contrib 模块里的 WLS 视差滤波。它会把彩色图像的边缘信息引进来引导视差平滑边缘保持效果比单纯滤波好很多。import cv2.ximgproc as ximgproc left_matcher cv2.StereoSGBM_create(...) right_matcher ximgproc.createRightMatcher(left_matcher) wls ximgproc.createDisparityWLSFilter(left_matcher) wls.setLambda(8000.0) wls.setSigmaColor(1.5) left_disp left_matcher.compute(rect_l, rect_r) right_disp right_matcher.compute(rect_r, rect_l) filtered_disp wls.filter(left_disp, rect_l, None, right_disp)需要单独安装 opencv-contrib-python。WLS 对弱纹理区域能补出连贯的平面但不能拯救完全相同的纹理因为它本质上还是靠“图像边缘约束视差连续”来工作。6. Q 矩阵与点云生成重投影公式必须吃透6.1 从视差到三维坐标的两种等价写法校正完成后的理想双目模型深度和视差的关系在前面已经写过。真正把每个像素变成三维点可以有两种理解方式。第一种是直接按公式算。对于校正后图像中的像素 (u, v)已知视差 dZ f × B / d X (u - cx) × Z / f Y (v - cy) × Z / f这里的 f 是校正后左相机的像素焦距 fxcx、cy 是校正后左相机的主点。这种方式直观适合调试。第二种是使用 OpenCV 在 stereoRectify 阶段输出的 Q 矩阵。它把 (u, v, d, 1) 一次映射成四维齐次坐标 (X, Y, Z, W)再除以 W 就得到三维点。Q 矩阵的形式大致是Q [[1, 0, 0, -cx], [0, 1, 0, -cy], [0, 0, 0, f], [0, 0, -1/B, (cx - cx)/B]]其中 cx、cx 分别是左右校正图主点的横坐标。如果左右主点相同Q[3][3] 为 0深度公式就退化成 Z f × B / d。推导过程不复杂但工程上我们直接用 reprojectImageTo3D 就够了关键是要理解 Q 矩阵在替我们干这件事。6.2 reprojectImageTo3D 的坑和正确用法C 调用cv::Mat xyz; cv::reprojectImageTo3D(disparity, xyz, Q, true, CV_32F);Python 调用xyz cv2.reprojectImageTo3D(disparity, Q, handleMissingValuesTrue, ddepthcv2.CV_32F)几个坑提前说disparity 如果是 CV_16S函数会自动除以 16。如果你已经手动除以了 16 变成浮点就不要再传原始格式否则深度会整体错 16 倍。handleMissingValuesTrue 时无效像素会被赋予该函数内部约定的巨大值 10000并非 0。点云处理时建议用xyz[..., 2] 0或者xyz[..., 2] 10000过滤别直接把所有点都写进文件。xyz 是 H×W×3 的三通道矩阵每个通道分别存 X、Y、Z单位与标定时棋盘格的物理单位一致。你 square_mm 填了毫米这里就是毫米。6.3 手动验证一组坐标把公式写进脑子永远不要相信一段没验证过的重建代码哪怕 API 是官方文档里抄的。我最常用的验证方法是从视差图里取一个明显物体上的点手算它的三维坐标再和代码结果对比。延续前面的例子图像分辨率 640×480fx800主点 (320, 240)基线 120 mm。取某个像素 (u400, v300)视差 d50 像素则Z 800 × 120 / 50 1920 mm X (400 - 320) × 1920 / 800 192 mm Y (300 - 240) × 1920 / 800 144 mm如果代码输出 xyz[300, 400] 和 (192, 144, 1920) 对得上链路才算真的通了。这个习惯能帮你排查一半以上的“点云全乱”问题。6.4 点云的保存、可视化和后处理得到 xyz 矩阵后直接生成点云的简易方法import numpy as np points xyz.reshape(-1, 3) valid np.isfinite(points).all(axis1) (points[:, 2] 0) (points[:, 2] 10000) points points[valid]保存为最朴素的 PLY 文件def save_ply(path, points): with open(path, w) as f: f.write(ply\n) f.write(format ascii 1.0\n) f.write(element vertex %d\n % len(points)) f.write(property float x\n) f.write(property float y\n) f.write(property float z\n) f.write(end_header\n) for p in points: f.write(%.3f %.3f %.3f\n % (p[0], p[1], p[2]))可视化我只推荐两种方式轻量查看用 CloudCompare它是处理点云的瑞士军刀能直接打开 PLY还能做配准、测量、剖面程序内处理用 Open3D可以做统计滤波、体素降采样、ICP 配准配合点云可视化窗口调试非常方便。PCL 的 C 版本我也常用但部署成本高于 Open3D具体看项目环境选型。7. 常见问题排查为什么你的点云歪了、有洞、不达标7.1 点云整体倾斜或尺度不对如果你重建出来的点云地面是斜的或者物体长宽比明显不对第一反应不要怀疑 SGBM先查标定物理尺寸。最常见的原因就是 square_mm 和真实棋盘格边长不一致。比如标称 24 mm 的棋盘格你填了 24 cm那所有深度、所有坐标都会放大 10 倍地面自然就“塌”了。第二常见的原因是双目标定没有固定内参。当内参、畸变、R、t 全部同时优化时算法会把误差摊到各个参数上看起来 RMS 还行但校正矩阵已经严重失真。回到 CALIB_FIX_INTRINSIC 的流程问题通常会消失。第三常见原因是双目图像不是严格同步拍摄。如果左右相机采集有几十毫秒的延迟物体一旦运动视差里就会混入运动误差点云边缘会出现撕裂感。7.2 深度空洞和边缘飞点空洞的主要来源有三个遮挡、弱纹理和反光。遮挡是无解的几何事实只能通过多帧融合或后续补洞弱纹理可以靠 WLS 后处理改善反光则需要调整相机曝光角度或者把场景里的高光尽量压掉。边缘飞点是另一种常见现象。物体轮廓附近SGBM 的匹配窗口横跨前景和背景两个深度平面很容易选到错误窗口中心造成前景“膨胀”或背景“下陷”。缓解办法blockSize 尽量小disp12MaxDiff 开起来点云生成后用统计滤波去掉离群点。Open3D 的 remove_statistical_outlier 默认参数在多数场景下都够用一般设置 nb_neighbors20, std_ratio1.0 效果就不错。7.3 坐标系方向与可视化差异OpenCV 输出的是右撇子相机坐标系原点在左相机光心X 向右Y 向下Z 沿光轴向前。很多点云工具默认把 Z 当“地面向上”一打开就会看到点云“躺”着。这不是重建错了只是坐标系约定不同。习惯的做法是重建完成后手动做一次旋转变换想把 Y 改为向上就让 X 不变、Y 取反、Z 和 Y 交换再拼成 4×4 变换矩阵。如果项目要和激光雷达点云对齐同理需要额外做一次手眼标定用外参把两套坐标系统一到基点下。7.4 用卷尺验证整条链路的推荐做法链路到底行不行最后一定要做一次物理验证。我的做法是在场景里放几个已知距离的目标最好是不同远近的纸箱或人像立牌。程序读目标区域点云的中位深度和卷尺实际距离对比。误差在 5% 以内这套系统就可以进入正常开发误差超过 10%优先排查标定而不是继续调 SGBM。这里有个规律你要有预期双目测距误差随距离按平方增长2 米处误差 2 厘米的系统到 10 米处可能误差到半米以上。所以别指望一台短基线相机既做近处高精度又能覆盖几十米的探测范围场景需求决定了基线长度和相机焦距的取舍。我个人的习惯是验证完再把所有参数归档一次图像分辨率、棋盘格尺寸、标定 RMS、SGBM 参数、验证距离和误差。过一个月后回来调试看到这份记录比自己重新摸索快得多。双目立体这条链路参数互相影响又多又深一套可复现的调试日志比任何高端技巧都更值钱。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门