OpenCV轮廓提取实战:从预处理到形状匹配的完整链路
简介这份资源围绕OpenCV轮廓提取功能展开面向正在学习计算机视觉、图像处理的高校学生与开发者帮助读者理解从二值图像中计算轮廓、绘制轮廓的完整流程。内容以findContours与drawContours两个核心函数为主线逐一讲解image、contours、hierarchy、mode、method、offset等参数含义并说明CV_RETR_EXTERNAL、CV_RETR_LIST、CV_RETR_CCOMP、CV_RETR_TREE四种检索模式与CV_CHAIN_APPROX_NONE、CV_CHAIN_APPROX_SIMPLE等近似方法的差异同时给出Canny预处理配合轨迹条调参的示例代码。资源包为1个pdf文件约95KB篇幅精炼适合作为查阅手册或课堂笔记。目前已有2257人学习下载读者可借此快速掌握轮廓点集的组织方式、层级关系与绘制参数为后续形状分析、目标检测等任务打下基础。1. 轮廓提取到底在提取什么从一张零件图说起很多做视觉检测的朋友第一次用 OpenCV 的findContours都会遇到一个反直觉的现象明明图里就一个零件函数却返回了几十个轮廓。这不是代码写错了而是轮廓提取本身有一套自己的“世界观”——它只认二值图里连通的边界像素噪声、反光、纹理全都会被当成轮廓。轮廓提取功能在 OpenCV 里不是一个函数而是一条链路灰度化、滤波、二值化、形态学、findContours、轮廓筛选与拟合。这条链路上任何一环参数没调好后面全盘皆输。它适合做尺寸测量、缺陷定位、目标计数、形状匹配的从业者也适合刚接触 OpenCV 想找一个完整练手项目的新手。下面我按自己实际做检测项目的顺序把这条链路拆开讲清楚参数怎么设、坑在哪、怎么验证都落到能直接抄的代码上。2. 从彩色图到二值边界预处理链路怎么搭轮廓提取的输入质量决定了输出上限。OpenCV 的findContours只接受单通道二值图所以第一步永远是把彩色图变成“非黑即白”的边界图。这一步做不好后面调轮廓参数就是玄学。2.1 灰度化与滤波的选型理由灰度化用cvtColor转COLOR_BGR2GRAY是最常见的做法但如果你处理的是彩色零件上的特定颜色标记直接灰度化会把颜色差异抹平。我一般会先判断目标轮廓是靠亮度区分还是靠颜色区分。靠亮度就灰度化靠颜色就先转 HSV 再取通道。滤波的选择更关键。高斯滤波适合去除传感器噪声中值滤波适合去除椒盐噪声双边滤波适合保留边缘的同时去噪但速度慢。做轮廓提取时我通常用高斯滤波核大小取 5×5 或 7×7因为轮廓提取关心的是边界位置高斯滤波对边缘的模糊是可接受的而中值滤波在核较大时会改变边界的形状。import cv2 import numpy as np # 读取图像保持原始通道 img cv2.imread(part.png) if img is None: raise FileNotFoundError(图像未找到检查路径) # 灰度化轮廓提取的标准第一步 gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 高斯滤波核大小 5x5sigmaX 设为 0 表示由核大小自动推算 blurred cv2.GaussianBlur(gray, (5, 5), 0) # 自适应阈值应对光照不均的常用手段 # blockSize 必须是奇数C 是从均值中减去的常数 binary cv2.adaptiveThreshold( blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, blockSize31, C5 )这段代码里THRESH_BINARY_INV把暗背景上的亮零件变成白底黑边方便后续轮廓查找。blockSize31是经验值太小会把纹理当边界太大则丢失细小轮廓。C5控制阈值偏移值越大二值化越“保守”白色区域越小。如果光照均匀用cv2.threshold配 Otsu 自动阈值更省事但光照不均时自适应阈值更稳。2.2 形态学处理把断掉的边界接起来二值化之后经常出现边界断裂、毛刺、孤立噪点。形态学操作就是修边界的“后悔药”。开运算先腐蚀后膨胀去噪点闭运算先膨胀后腐蚀接断裂。做轮廓提取时我一般先开运算去噪再闭运算补边界。# 开运算去除孤立噪点核大小 3x3 kernel_open cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) opened cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel_open) # 闭运算连接断裂的边界核大小 5x5 kernel_close cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) closed cv2.morphologyEx(opened, cv2.MORPH_CLOSE, kernel_close)核的形状选椭圆比矩形更贴近自然边界矩形核会在斜边方向引入方块状伪影。开运算核不宜超过 5×5否则细小轮廓会被直接腐蚀掉。闭运算核可以稍大但超过 9×9 会让相邻轮廓粘连。这一步做完二值图应该是一张边界连续、噪点干净的图如果还有大片噪点说明前面的滤波或阈值没调好不要指望形态学兜底。2.3 用 findContours 拿到轮廓列表findContours在 OpenCV 4.x 里返回两个值轮廓列表和层级信息。第三个参数RETR_EXTERNAL只取最外层轮廓适合零件计数RETR_TREE取完整层级适合有孔洞的零件。CHAIN_APPROX_SIMPLE只保留拐点节省内存绝大多数场景够用。# 只取最外层轮廓适合零件计数和尺寸测量 contours, hierarchy cv2.findContours( closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) print(f原始轮廓数量: {len(contours)}) # 按面积过滤去掉面积小于 100 像素的噪点轮廓 min_area 100 valid_contours [c for c in contours if cv2.contourArea(c) min_area] print(f过滤后轮廓数量: {len(valid_contours)})contourArea返回的是轮廓围成的面积不是周长。面积过滤是最简单有效的去噪手段min_area根据你的图像分辨率和目标大小来定。如果图像是 2000×2000目标零件占 500×500那min_area设 1000 都不过分。这一步之后如果轮廓数量还是不对就要回头检查二值图而不是继续调过滤参数。3. 轮廓筛选与形状拟合把边界变成可用的几何数据拿到轮廓列表只是开始真正有用的是从轮廓里提取面积、周长、外接矩形、最小外接圆、多边形拟合这些几何量。这一步决定了你的检测系统能不能输出稳定结果。3.1 面积、周长与外接矩形的计算与取舍contourArea和arcLength是最基础的两个量。arcLength的第二个参数closedTrue表示轮廓闭合算出来是周长False算出来是曲线长度。外接矩形有两种boundingRect返回正矩形minAreaRect返回带旋转角度的最小外接矩形。做尺寸测量时如果零件会旋转必须用minAreaRect否则正矩形会把旋转后的零件框得很大尺寸全错。for i, cnt in enumerate(valid_contours): area cv2.contourArea(cnt) perimeter cv2.arcLength(cnt, True) # 正外接矩形不处理旋转 x, y, w, h cv2.boundingRect(cnt) # 最小外接矩形返回 (中心点, (宽, 高), 角度) rect cv2.minAreaRect(cnt) box cv2.boxPoints(rect) box np.int0(box) # 最小外接圆 (cx, cy), radius cv2.minEnclosingCircle(cnt) print(f轮廓{i}: 面积{area:.1f}, 周长{perimeter:.1f}, f正矩形({x},{y},{w},{h}), 最小矩形宽高{rect[1]}, 角度{rect[2]:.1f})minAreaRect返回的角度范围是 [-90, 0)不同 OpenCV 版本对角度定义略有差异跨版本迁移时要验证。boxPoints把矩形转成四个顶点方便画图和后续计算。minEnclosingCircle对圆形零件特别有用半径和圆心直接就是测量结果。3.2 多边形逼近用更少的点描述边界approxPolyDP是轮廓提取里最容易被低估的函数。它用 Douglas-Peucker 算法把轮廓近似成多边形epsilon控制逼近精度。epsilon越小多边形越贴近原始轮廓越大点越少。做矩形检测时逼近后如果顶点数是 4基本可以判定为矩形。# epsilon 取周长的 2%是常用的经验比例 epsilon 0.02 * cv2.arcLength(cnt, True) approx cv2.approxPolyDP(cnt, epsilon, True) # 根据逼近后的顶点数判断形状 vertex_count len(approx) if vertex_count 3: shape 三角形 elif vertex_count 4: # 进一步判断是矩形还是正方形 x, y, w, h cv2.boundingRect(approx) aspect_ratio w / float(h) shape 正方形 if 0.95 aspect_ratio 1.05 else 矩形 elif vertex_count 5: shape 五边形 else: shape 圆形或其他 print(f形状判断: {shape}, 顶点数: {vertex_count})epsilon取周长的 1% 到 5% 之间比较常见。太小会导致顶点数过多矩形被误判成多边形太大会把矩形压成三角形。判断正方形时用宽高比阈值 0.95 到 1.05 是宽容度实际项目里要根据相机畸变和零件公差调整。圆形判断不能只看顶点数还要结合circularity 4 * pi * area / (perimeter^2)接近 1 才是圆。3.3 轮廓层级与孔洞检测带孔洞的零件必须用RETR_TREE或RETR_CCOMP才能拿到内轮廓。层级信息hierarchy是一个四元组数组每个元素是[next, previous, first_child, parent]。外轮廓的parent是 -1内轮廓的parent指向外轮廓索引。# 用 RETR_TREE 获取完整层级 contours_tree, hierarchy_tree cv2.findContours( closed, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE ) if hierarchy_tree is not None: hierarchy_tree hierarchy_tree[0] for i, cnt in enumerate(contours_tree): parent_idx hierarchy_tree[i][3] if parent_idx -1: # 外轮廓 area_outer cv2.contourArea(cnt) print(f外轮廓{i}, 面积{area_outer:.1f}) else: # 内轮廓孔洞 area_hole cv2.contourArea(cnt) print(f 孔洞{i}, 面积{area_hole:.1f}, 所属外轮廓{parent_idx})层级遍历时要注意hierarchy的形状是 (1, N, 4)直接索引会多一层所以先取[0]。孔洞面积可以用来判断零件是否合格比如孔太小说明钻头磨损孔太大说明定位偏移。这一步的输出直接对接检测逻辑比单纯画轮廓有用得多。4. 避坑与排查轮廓提取最常见的 5 个翻车现场轮廓提取的坑大多不在findContours本身而在它前面的预处理和后面的筛选逻辑。下面这 5 个是我在项目里反复遇到的按“现象 → 原因 → 解决”写清楚。4.1 轮廓数量暴多全是噪点现象一张干净的零件图findContours返回几百个轮廓画出来满屏碎点。原因二值化阈值太敏感把表面纹理、反光、灰尘全变成了白色区域。或者滤波核太小噪声没压住。解决先加大高斯滤波核到 7×7 或 9×9再检查二值化。自适应阈值的blockSize调大到 51 或 71C调大到 10。如果还不行在findContours前加一次开运算核 5×5。最后用面积过滤兜底min_area设为目标最小面积的 1/5。4.2 轮廓断裂一个零件被拆成几段现象零件边界不连续findContours返回多个小轮廓面积都不大。原因二值化时边界像素灰度处于阈值附近部分边界被归为背景。或者闭运算核太小没把断裂接上。解决闭运算核从 5×5 加到 7×7 或 9×9。如果还断改用THRESH_BINARY配合手动阈值把阈值往背景方向调 5 到 10 个灰度级。另一种做法是先膨胀再腐蚀但膨胀核不要超过 3×3否则轮廓会变形。4.3 旋转零件尺寸测不准现象零件旋转后用boundingRect算出来的宽高变化很大测量结果不稳定。原因boundingRect返回的是与图像坐标轴对齐的正矩形旋转零件的正矩形会包含大量空白区域。解决改用minAreaRect它返回带角度的最小外接矩形。注意角度范围是 [-90, 0)宽高顺序可能互换需要根据角度归一化。如果零件是圆形直接用minEnclosingCircle更稳。4.4 孔洞轮廓丢失现象零件外轮廓正常但中间的孔洞没有被检测到。原因用了RETR_EXTERNAL它只返回最外层轮廓内轮廓全部丢弃。解决改用RETR_TREE或RETR_CCOMP。拿到层级后通过parent字段区分外轮廓和内轮廓。注意RETR_CCOMP只分两层适合简单孔洞RETR_TREE支持多层嵌套适合复杂结构。4.5 轮廓面积忽大忽小现象同一个零件连续拍几张图contourArea结果波动超过 10%。原因光照变化导致二值化边界漂移或者相机自动曝光在变。也可能是轮廓本身包含毛刺面积被拉大。解决锁定相机曝光和增益用固定光源。二值化改用固定阈值而不是自适应阈值。对轮廓做一次approxPolyDP再算面积去掉毛刺影响。如果还波动检查零件是否在视野边缘镜头畸变会导致边缘尺寸失真。5. 进阶技巧用轮廓匹配做稳定分类轮廓提取做到最后往往不只是测量还要分类。matchShapes是 OpenCV 自带的形状匹配函数用 Hu 矩比较两个轮廓的相似度返回值越小越相似。它对手写数字、简单零件分类够用但对纹理复杂的目标不够稳。# 模板轮廓提前从标准样本中提取并保存 template_cnt valid_contours[0] # 假设第一个是模板 for cnt in valid_contours[1:]: # matchShapes 返回值越小越相似0 表示完全一致 score cv2.matchShapes(template_cnt, cnt, cv2.CONTOURS_MATCH_I1, 0) if score 0.05: print(f匹配成功相似度得分: {score:.4f}) else: print(f不匹配得分: {score:.4f})matchShapes的第三个参数有三种方法CONTOURS_MATCH_I1、I2、I3区别在于对 Hu 矩取对数、开方的方式不同。实际用 I1 最多阈值 0.05 到 0.1 之间根据样本调。它最大的优势是旋转、缩放不变但前提是轮廓本身干净。如果轮廓有毛刺Hu 矩会被污染匹配得分失真。我一般会在匹配前做两件事一是用approxPolyDP把轮廓简化二是把轮廓归一化到统一尺度。归一化可以用cv2.resize把轮廓外接矩形缩放到固定大小但更稳的做法是直接用 Hu 矩的尺度不变性前提是轮廓完整。另一个进阶方向是用轮廓的几何特征做分类而不是直接匹配形状。比如提取面积、周长、圆形度、矩形度、长宽比、凸包缺陷数组成特征向量喂给简单的分类器。这样做比matchShapes更可控特征含义明确出问题容易排查。def extract_features(cnt): area cv2.contourArea(cnt) perimeter cv2.arcLength(cnt, True) if perimeter 0: return None circularity 4 * np.pi * area / (perimeter * perimeter) x, y, w, h cv2.boundingRect(cnt) aspect_ratio w / float(h) if h ! 0 else 0 rect_area w * h extent area / rect_area if rect_area ! 0 else 0 hull cv2.convexHull(cnt) hull_area cv2.contourArea(hull) solidity area / hull_area if hull_area ! 0 else 0 return { circularity: circularity, aspect_ratio: aspect_ratio, extent: extent, solidity: solidity } for cnt in valid_contours: feats extract_features(cnt) if feats: print(feats)circularity接近 1 是圆接近 0 是细长形状。extent是轮廓面积与外接矩形面积之比矩形接近 1圆形约 0.785。solidity是轮廓面积与凸包面积之比有凹陷的形状会明显小于 1。这四个特征组合起来对常见工业零件分类足够用。最后说一个我自己的习惯每次调完参数不要只看画出来的轮廓图一定要把中间二值图也保存下来。轮廓出问题90% 的原因在二值图不在findContours。把binary、opened、closed三张图并排看问题一目了然。这个习惯帮我省了无数个加班的晚上。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

Mangos服务端改端利器:物品、任务、BOSS编辑器实战详解

Mangos服务端改端利器:物品、任务、BOSS编辑器实战详解

简介:这是一款面向Mangos魔兽世界模拟器服务端开发与维护者的数据编辑工具包,用于解决物品、任务、BOSS、NPC等核心数据手工修改数据库时繁琐易错的问题。压缩包共66个文件,大小仅1.63MB,内含主程序及依赖库,并配有55个…

2026/10/13 20:39:15 阅读更多 →
VC中集成SQLite:从编译到增删改查的避坑指南

VC中集成SQLite:从编译到增删改查的避坑指南

简介:本资源面向在Visual C环境下进行桌面开发的程序员,聚焦SQLite嵌入式数据库的集成与实战应用,帮助解决VC项目中数据库连接、大批量数据快速插入以及查询结果在ListCtrl控件中展示等常见问题。压缩包共61个文件,约32.07MB&…

2026/10/12 18:20:26 阅读更多 →
模拟传输课件精讲:从AM到FM的调制主线与教学技巧

模拟传输课件精讲:从AM到FM的调制主线与教学技巧

简介:这是一份电子科技大学《通信原理》(李晓峰版)第3章“模拟传输”的讲课课件,适合通信工程、电子信息类学生及考研复习者梳理模拟通信系统核心框架,也可作为教师备课参考。内容完整覆盖模拟通信系统组成、信号模型、…

2026/10/12 18:20:26 阅读更多 →

最新新闻

AI工程师必修课:标量、向量、矩阵、张量的工程本质

AI工程师必修课:标量、向量、矩阵、张量的工程本质

1. 这不是数学课,是AI工程师的“肌肉记忆”训练你打开任何一本深度学习框架的官方文档,不到三分钟就会撞上这几个词:标量、向量、矩阵、张量。它们不像“卷积”“反向传播”那样自带技术光环,也不像“Transformer”“LoRA”那样刷…

2026/10/14 5:14:02 阅读更多 →
用TypeScript重写经典RPG:数据驱动架构与可视化编辑器实战

用TypeScript重写经典RPG:数据驱动架构与可视化编辑器实战

1. 为什么我要用 TypeScript 重写经典RPG,还顺手造了个编辑器把《仙剑奇侠传》用 TypeScript 从零重写成浏览器版,是我给自己定的一次项目练习。如果只做几个演示场景,我根本不会开这个坑;真正让我想动手的,是那个更麻…

2026/10/14 5:14:02 阅读更多 →
矩量法原理与工程实战:从积分方程到RWG基函数

矩量法原理与工程实战:从积分方程到RWG基函数

我一直觉得,做电磁场仿真的人,如果没亲手把矩量法的代码写过一遍,很难说自己真的懂高频电磁场数值计算。这个标题里的“矩量法原理”看起来是理论课,但实际做项目、做设计时,它是商业软件HFSS、FEKO里最核心的底层算法…

2026/10/14 5:14:02 阅读更多 →
信息系统项目管理师备考:从失败到成功的实战经验

信息系统项目管理师备考:从失败到成功的实战经验

1. 项目背景与核心痛点去年备考信息系统项目管理师(高项)的经历,简直是我职业生涯中最煎熬的120天。作为在IT行业摸爬滚打8年的技术经理,本以为凭借多年项目管理经验能轻松过关,结果连续两次栽在案例分析题上&#xff…

2026/10/14 5:14:02 阅读更多 →
FreeSWITCH GB28181国标视频接入模块源码包:信令与媒体面解析

FreeSWITCH GB28181国标视频接入模块源码包:信令与媒体面解析

简介:这是一套面向FreeSWITCH开发者与视频监控集成工程师的GB28181国标视频接入模块源码包,用于对接符合《GB/T 28181-2016》标准的网络摄像机、NVR及视频平台,可作为国标视频统一接入网关的核心组件,无需额外中间件即可编译集成。…

2026/10/14 5:14:02 阅读更多 →
微信小程序云开发超市库存管理系统源码解析与二次开发指南

微信小程序云开发超市库存管理系统源码解析与二次开发指南

最近我拆了一套微信小程序超市库存管理系统,也就是标题里那个71788编号的源码包。整套系统基于微信小程序云开发实现,包含商品管理、入库出库、库存预警、供应商管理和基础报表,完成度比预想中高不少,适合做小程序进销存项目的参考…

2026/10/14 5:13:02 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 22:01:27 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →