news 2026/10/22 6:01:08

玉米好坏检测数据集实战:COCO格式标注、切分与训练避坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
玉米好坏检测数据集实战:COCO格式标注、切分与训练避坑

简介:面向玉米品质检测与农业视觉识别场景的玉米好坏检测数据集,适合目标检测、图像分类等深度学习项目开发者使用。资源对2357张玉米图片进行了COCO格式的人工标注,平均标注准确率在89.5%以上,可直接用于深度学习模型训练、验证与对比实验;标注内容包括类别名称与目标边界框坐标,TXT文件进一步给出类别与数据组织说明。压缩包内共有2000个文件,以1995张JPG原图为主体,配以3个JSON标注文件和2个TXT说明文件,压缩包整体约122.84MB,目录结构简洁清晰,便于快速接入主流目标检测框架。文件名保留了拍摄时间信息,便于按批次划分训练集与验证集。目前已有393人学习下载,对需要真实农业图像样本的入门及进阶学习者来说,都是具有参考价值的数据基础。

1. 玉米好坏检测的第一步,往往卡在数据集而不是模型

做农产品外观质检的第一步,往往不是选模型,而是先回答一个问题:什么样的玉米算坏,坏到什么程度要剔除。这个玉米好坏检测数据集,把回答装进了一个 zip:2357 张真实玉米图片,全部经过人工标注,输出成 COCO 格式,训练出来的检测模型平均准确率在 89.5% 以上。它真正解决的痛点是,你不用再带着几十张没标准的手机图,去帮模型定义“坏玉米”。

我接触农产品质检项目这几年,最深的感受是:模型选型翻来覆去就那几种,真正拉开项目进度的,是数据标注的边界和格式。这份数据集的适用场景很明确:玉米分级、入库质检、分选线剔除次品。它适合两类人——一类是要快速跑通玉米分选算法的工程师,另一类是正准备搭质检数据流程、需要参考标注规范的团队。下面按拿到 zip 之后的实际操作顺序,把数据核验、标注复检、训练配置、避坑经验串成一条完整链路。

2. 拿到手先看三件事:类别边界、COCO 结构、2357 张图到底顶不顶用

2.1 好坏玉米的类别标注不能拍脑袋:good/bad 的边界得写进标注手册

“好坏检测”这四个字,落地到标注环节时,背后是两种完全不同的数据集设计。第一种设计是只给坏玉米画框,类别名叫 bad,正常玉米不出现任何标注,模型默认“没有被框住的就是好”。这种方案标注工作量小,适合在线剔除场景:算法一旦检出坏框,就把它从分选线上踢出去。第二种设计是 good 和 bad 两类都画框,每一根玉米都必须出现在标注里,不管是好是坏。这种方案对计数和统计场景更友好,但标注量几乎翻倍。

这个数据集的名称听起来像是“好坏都标”的二类方向。实际操作里,我一般更倾向于把“坏”再细分一层:霉变、破损、虫蛀。因为分选线末端需要按不同缺陷做不同处理,混在一个大类里会损失信息;但如果任务本身只需要一个好坏阈值,细分类会导致每个子类样本量不够,反而不如二分类稳。

真正决定数据集价值的,是标注规范里关于“灰色地带”的定义。比如玉米表皮局部变色算不算坏、霉斑面积小于整粒 5% 算不算坏、轻微压伤要不要框。这些边界不写清楚,同一个标注项目里,两个人画出来的框可能差异非常大。我习惯在标准里加三条硬约定:第一,霉斑区域必须完整包进框,宁可外扩,不可切到霉斑中心;第二,同一张图里有多个玉米,每一个都要画框,不允许挑着标;第三,框沿贴着玉米轮廓,比玉米实际轮廓多出的背景不超过 3 个像素。这套约定能显著降低后续复检的成本。

2.2 COCO 格式的取舍:为什么选它、以及最容易错的一个字段

COCO 格式本质上是一个 JSON 文件,核心由 images、annotations、categories 三块组成。images 数组存每张图的 id、宽高、文件名,annotations 数组存每个目标框的类别和坐标,categories 数组定义类别列表。这个格式之所以在目标检测里成为主流,直接原因是生态:开源检测框架大多内置了 CocoDataset 加载器,拿到 JSON 后指定路径就能开始训练。

第二个好处是扩展性。现在只画了边界框,如果后面想把任务升级成玉米实例分割,只需要在原有标注基础上补 polygon 字段,不需要推翻整个文件结构。第三个好处是合并和拆分的灵活性:数据不够时,两个 COCO 数据集可以在 JSON 层面直接拼接,类别 id 重新映射后合并训练就行。

但 COCO 格式有一个字段特别容易翻车:bbox。它的定义是[x, y, width, height],指的是框左上角的坐标和框的宽高。很多从分类任务转过来的同事,会下意识把 bbox 写成两个对角点[x1, y1, x2, y2]。这个错误在训练时通常不会报错,只会导致模型面对完全错误的坐标回归目标,最后验证 mAP 低到怀疑人生。所以拿到数据集之后,第一件事不是解压看了多少张图,而是先把所有 bbox 的合法性跑一遍检查——这部分在下一章会给可直接复制的脚本。

2.3 数据规模判断:2357 张图在新环境里的有效性与底线

要说清楚 2357 张图够不够用,先得把“够用”拆成两个维度:目标实例总量和场景多样性。玉米检测里,单张图平均会有 2 到 4 个目标,2357 张图大约对应 5000 到 9000 个标注实例。对一个二分类检测任务来说,这个规模足够让 Faster R-CNN 或 YOLO 收敛出一个可用的 baseline,前提是类别比例没有严重失衡。

真正会让数据集缩水的是“拍摄环境单一”。如果 2357 张图全部来自同一条固定产线、同一个机位、同一批玉米,相邻图片的背景几乎一致,那么模型学到的可能是光线分布,而不是玉米本身的状态。放到真实现场,换一个光源或者换一台相机,准确率就会掉下去。所以我处理任何检测数据的第一步,都是按拍摄批次给图片打上 group 标记,然后可视化抽样检查:随机抽 100 张,如果背景几乎都是同一条传送带,就要在后续切分时把批次信息用上。这个细节直接关系到第 4 章的训练集划分方式,也直接决定了 89.5% 这个指标能不能在你自己环境里复现。

3. 解压 zip 后的三个动作:核验标注、检查图片、复检人工质量

3.1 用一段检查脚本把“图片-标注”对应关系一次性核完

从 zip 解压出来的目录,常见结构是 images 文件夹放图片,annotations 文件夹放 JSON 标注。第一次处理时不要急着写训练代码,先跑一个完整性检查。这段脚本不依赖任何训练框架,只要有 Python 和 Pillow 就能跑:

import json from pathlib import Path from PIL import Image ann_path = Path("annotations/instances_train.json") img_dir = Path("images") with open(ann_path, "r", encoding="utf-8") as f: data = json.load(f) cat_ids = {c["id"]: c["name"] for c in data["categories"]} img_map = {im["id"]: im for im in data["images"]} error_list = [] for ann in data["annotations"]: img = img_map.get(ann["image_id"]) if img is None: error_list.append(f"annotation {ann['id']} 找不到对应图片") continue x, y, w, h = ann["bbox"] if w <= 0 or h <= 0: error_list.append(f"annotation {ann['id']} 的 bbox 宽高必须为正") if x < 0 or y < 0 or x + w > img["width"] or y + h > img["height"]: error_list.append(f"annotation {ann['id']} 的 bbox 超出图像边界") if ann["category_id"] not in cat_ids: error_list.append(f"annotation {ann['id']} 包含未定义的 category_id") # 检查图片文件是否损坏 for im in data["images"]: img_path = img_dir / im["file_name"] try: with Image.open(img_path) as f: f.verify() except Exception: error_list.append(f"图片损坏或无法打开: {im['file_name']}") print(f"共检查 {len(data['images'])} 张图像、{len(data['annotations'])} 个标注") if error_list: for e in error_list[:20]: print(e) else: print("基础结构检查通过")

这段脚本做的事有两部分:第一,把 images 数组建成一个以 id 为键的索引,然后逐条检查 annotation 的 image_id、bbox、category_id;第二,用 Pillow 的 verify 方法逐张确认图片文件没有损坏。三个检查点分别是逻辑对应关系、坐标合法性和类别定义完整性。

很多格式错误是静默的——比如 bbox 超出图像边界几像素,训练时并不会报错,但会让模型在边框附近学出奇怪的特征。所以每接一份新数据,我都先跑一遍这几十行代码。损坏图片的检查尤其重要,某次项目里训练到一半突然崩溃,最后定位到就是两张 JPEG 文件头损坏,模型加载图像时直接抛异常。

3.2 从零标注到 COCO 的操作流程:类别表、边界框规范与工具选型

如果你拿到的不只是这份现成数据集,而是想复刻一套自己场景的标注管线,生产流程大致是:收集原始图片,准备一张类别表,用标注工具画框,导出 COCO JSON,最后抽检。

类别表是第一步,也是源头。我一般把类别表做成一个简单的文本文件,一行一个类别名,顺序固定。常见设定是good和bad两行,或者good、mildew、broken、insect四行。类别顺序一旦确定,整个标注过程中不要再改动。如果中间删掉一个类别,后面所有已标注数据的 category_id 都会错位,处理起来非常麻烦。

标注工具有两类选择。一类是传统的手动标注工具,打开图片后手动拖框,保存后自动生成 COCO 或 VOC 格式的标注文件;另一类是带预标注功能的工具,先用已有的检测模型跑一遍,把预测结果加载进标注界面,人工只需要修正错误的框和补充漏检的目标。后者能把效率提高一倍,但对标注员的审核能力要求更高——预标注如果有系统性偏差,人工很容易“顺着错的改对的”,把错误的框修得更顺手,而不是质疑它本身不合理。

画框规范在 2.1 里已经提过,这里补充一个执行细节:同一张图如果有两棵玉米重叠,重叠部分的归属要提前约定。常见做法是两个框都完整画出整个玉米,允许框之间产生重叠;另一种做法是切掉遮挡部分,只圈可见区域。两种方案都能训练出可用的模型,但必须统一。混着用会让模型对遮挡目标的学习目标不一致。

3.3 人工标注复检:抽检 10% 到 20% 的统一对比原则

“人工标注”是这份数据集的卖点,也是质量风险点。人工标注不是“画过一遍”就结束了,没有经过复检的标注,边界框的一致性是未知的。所以数据交付前,我会强制安排一轮抽检:从总数中随机抽 10% 到 20%,找另一个人按同一份标注规范重新标一遍,然后对比两轮结果。

对比指标有两个。第一个是边界框 IoU,计算两个框的交并比。如果复检框和原框的 IoU 低于 0.7,说明有一个人的画框习惯明显偏离规范;第二个是类别一致率,两轮标注里类别标签一致的比例,低于 95% 就得回去看是不是规范里存在歧义。比如“霉斑面积小于 5% 算不算坏”这种定义,很可能在两条标注里得到截然不同的判断。

复检的意义不只是找出错框,更重要的是定位“歧义区域”。如果发现很多争议框都集中在某一种特定形态的玉米上——比如胚芽部位天然发黑被误判成霉变——那就说明规范描述得不够细。把歧义样本单独挑出来,更新标注规范,再全量重新检查同类图片。处理完这一轮之后,数据集才算真正达到了可训练的状态。

4. 训练前的最后两分钟:数据切分、模型配置和参数选择

4.1 按拍摄批次切分:防止同场景图片同时出现在训练集和测试集

很多初学者切分数据时,直接对图片列表做一次随机划分。这在玉米检测这类场景里有隐患:同一批拍摄的玉米图片,背景、光线甚至玉米的位置都高度相似,随机切分后,训练集中某一批照片的“孪生兄弟”可能出现在测试集里。模型相当于提前见过了答案,指标虚高,真正换到新现场就露馅。

正确的做法是以拍摄批次为组,整组切分。假设数据集 JSON 里每张图带一个 group 字段,对应拍摄批次,可以这样操作:

import json import random from collections import defaultdict random.seed(42) with open("annotations/instances.json", "r", encoding="utf-8") as f: data = json.load(f) # 将图像按拍摄批次分组 groups = defaultdict(list) for img in data["images"]: group = img.get("group", "batch_00") groups[group].append(img["id"]) train_ids, val_ids, test_ids = [], [], [] for group_id, img_ids in groups.items(): random.shuffle(img_ids) n = len(img_ids) # 一般按 70/15/15 切分,尽量每个批次都保留一份测试样本 train_ids += img_ids[: int(n * 0.7)] val_ids += img_ids[int(n * 0.7): int(n * 0.85)] test_ids += img_ids[int(n * 0.85):] print(f"train={len(train_ids)}, val={len(val_ids)}, test={len(test_ids)}")

这里的关键参数是随机种子和切分比例。随机种子固定下来,保证每次跑出来的划分结果一致,方便对比不同模型配置的差异;切分比例上,我倾向于 70/15/15,而不是常用的 80/10/10,因为检测任务里测试集的评估方差本身就大,多留 5% 的测试图能让指标更稳定。

如果原始数据没有 group 字段,也可以退而求其次:按文件名前缀聚类,或者按拍摄时间戳聚类。前提都是同一个原则——同一场景的图片不能被同时分到训练集和测试集。

4.2 一份可直接上手的 Faster R-CNN 配置:类别数、数据路径、评估频率

数据切好之后,用开源检测框架训练是效率最高的方式。以 mmdetection 为例,配置文件改动量很小,核心是把检测头的类别数改成 2,并指定数据集的路径:

_base_ = [ "../_base_/models/faster_rcnn_r50_fpn.py", "../_base_/datasets/coco_detection.py", "../_base_/schedules/schedule_1x.py", "../_base_/default_runtime.py", ] model = dict( roi_head=dict( bbox_head=dict(num_classes=2) ) ) dataset_type = "CocoDataset" data_root = "data/corn/" data = dict( train=dict( type=dataset_type, ann_file=data_root + "annotations/instances_train.json", img_prefix=data_root + "images/", ), val=dict( type=dataset_type, ann_file=data_root + "annotations/instances_val.json", img_prefix=data_root + "images/", ), test=dict( type=dataset_type, ann_file=data_root + "annotations/instances_test.json", img_prefix=data_root + "images/", ), )

配置里改动最大的就是num_classes=2,对应 good 和 bad 两个类别。原来是 80 类的 COCO 预训练权重,换到 2 类时,倒数第二层的全连接结构不同,框架会在加载预训练权重时跳过不匹配的层,这部分不用手工处理。

我一般不会从头训练。加载 COCO 预训练权重再微调,比随机初始化收敛快很多,通常几十个 epoch 就能达到还不错的精度。数据路径data_root建议改成绝对路径或者统一规范的相对路径,否则换一台机器训练,光找数据就能浪费不少时间。

4.3 决定训练效果的四组参数:epoch、batch size、学习率、缩放增强

训练参数看着多,玉米检测里真正影响结果的只有四组。第一组是 epoch。二类检测任务、几千张图规模,我一般先跑 50 个 epoch 看趋势,验证集 mAP 如果还在明显上升就继续加 30 到 50 个。盲目跑上百个 epoch 不一定是好事,小数据集上后期很容易过拟合。

第二组是 batch size。显存够的前提下,batch size 8 到 16 是合理区间。如果 batch size 太小,BN 层的统计量不稳定,训练 loss 会一直抖;如果显存不够,可以把输入图像的短边缩到 640 左右,而不是硬上大图。

第三组是学习率,要跟着 batch size 走。一个常用的参考基准是:batch size 64、学习率 0.02 起步;如果 batch size 减半,学习率也相应减半。我用 batch size 16 时,初始学习率一般设 0.005,配合前 500 步的 warmup,把学习率从很小的值慢慢升到目标值。学习率设得太大,训练初期 loss 会直接爆掉;设得太小,几十轮 epoch 下来权重几乎没怎么更新。

第四组是多尺度训练增强。玉米表面缺陷的尺度变化很大,一张图里的霉斑可能只占十几个像素,也可能占满半个果穗。开启多尺度训练,比如短边在 480 到 960 像素之间随机缩放,会让模型对不同尺寸的目标都有学习机会。代价是训练时间增加,但对检测指标的提升非常直接。标题里的“平均准确率 89.5% 以上”,大概率是在这种常规调参基础上得到的;想复现这个数字,参数范围不用太激进,重点还是数据和标注的质量。

5. 避坑记录:从标注格式到指标验证,四个最常把人绕晕的坑

5.1 现象:训练集和测试集 mAP 差距巨大,换现场数据直接翻车

这个坑我踩过一次。当时用一份玉米图片数据集训练,验证集 mAP 到了 0.85,团队都很兴奋,结果拿去新车间实测,检测率直接掉了一半。后来复盘发现,问题根本不在模型,而在数据切分:所有图片都是同一条传送带上拍的,随机切分时,训练集和测试集里混进了大量同一时段的连续帧,等于测试集被剧透了。

原因:按文件列表做纯随机切分,没有考虑拍摄批次的边界。

解决:改成按 group 批次切分,测试集只保留“模型没见过的场景”里的图片。如果数据本身只有一个批次,最优做法是单独留出一小块完全不同的采样环境,比如换一盏灯、换一个背景后重新拍几十张,专门做测试。

5.2 现象:把玉米须或背景中的包装物误检成坏玉米

模型训练完,推理时最容易看到的错误是把玉米须检成坏玉米。玉米须是细长的丝状物,颜色偏褐偏黄,和霉变的初期形态在图像上非常相似。如果标注阶段把玉米须包进了框里,模型就学会了把这种纹理当成缺陷特征。

原因:标注规范里没有规定“框内不得包含非玉米组织”,或者标注员图省事,把玉米须一起框进去了。

解决:检查训练集里所有 bad 类别的框,凡是框内包含明显玉米须的样本,重新修框或者直接删除。训练完再对验证集做一次预测可视化,专门看 False Positive 的图片,确认误检对象集中在哪一类背景。除了玉米须,常见的还有包装袋上的文字、人手指和传送带边缘。这些负样本要么通过修标注排除,要么额外采集一些空背景图,加到训练集里。

5.3 现象:坏玉米类别反复漏检,好玉米几乎满分

二分类检测里,如果 good 类样本占绝大多数,bad 类样本很少,模型很容易陷入“全部预测成好玉米”的偷懒解。验证集上整体准确率看着很高,但坏玉米的 recall 很低,分选线漏过去的次品全在这类场景。

原因:类别不平衡。先不看图片总数,而是按类别统计目标实例数。如果 bad 类实例数量连 good 类的 20% 都不到,就需要处理。

解决:两种常用手段。第一个是在训练时对 bad 类加高 loss 权重,让每个坏玉米样本在训练中对梯度的影响更大;第二个是数据增强,把 bad 类目标通过 copy-paste 方式贴到更多背景图上,变相增加坏样本数量。还有一种体感上更直接的方法:加大模型对靠近图像边缘目标的敏感度,很多漏检的坏玉米其实是贴着边的。

5.4 现象:模型啥都不检,所有目标的置信度都很低

另一类常见现象是:训练过程 loss 正常下降,但推理时所有预测框的置信度都不超过 0.3,几乎没有输出。如果数据集标的是 COCO 格式,这个坑大概率出现在类别 id 的错位上。

原因:COCO JSON 里的 categories 数组有可能从 0 开始编号,而框架内部的类别 id 约定从 1 开始。category_id 全部错位后,模型训练时并没有明显报错,但类别对应关系是乱的,推理结果自然对不上。

解决:检查 JSON 里 categories 数组的实际定义。如果一个叫 good 的类别 category_id 是 0,需要把它改成 1,并同步更新所有 annotation 里的 category_id。改完后重新跑 3.1 的校验脚本,确认类别表、标注文件和模型配置三者完全一致,再开始训练。

6. 进阶操作:验证集阈值校准,一个把准确率再往上拉一步的步骤

很多人把 89.5% 当成一个固定上限,其实模型输出的置信度阈值是可调的,调整阈值对最终准确率的影响常常比换骨干网络还大。检测模型训练完成后,默认阈值通常设在 0.5,但这个值不一定适合玉米质检场景。更合理的做法是跑完测试后,单独分析验证集上的置信度分布,挑一个匹配现场要求的阈值。

先明确一个概念:目标检测里的“准确率”不是二分分类里那个单点准确率,它背后有一条 precision-recall 曲线。阈值高,检出来的目标更可信,但会漏掉一部分低置信度的坏玉米;阈值低,召回上去了,但误检也多了。玉米分选场景中,把坏玉米漏过去造成的损失往往比误杀好玉米更大,所以阈值应该往低回调。具体怎么调,可以用一段简单的统计脚本对验证集预测结果做扫描:

import numpy as np # preds 是模型在验证集上的输出,每条结构为 # [置信度, 预测类别, 真实类别 0 或 1] preds = [] # 需要从模型评估日志中导出 best_threshold = 0.5 best_f1 = 0.0 for threshold in np.arange(0.3, 0.8, 0.05): tp = fp = fn = 0 for score, pred_cls, true_cls in preds: if score >= threshold and pred_cls == 1: if true_cls == 1: tp += 1 else: fp += 1 elif true_cls == 1: fn += 1 precision = tp / (tp + fp) recall = tp / (tp + fn) f1 = 2 * precision * recall / (precision + recall) if f1 > best_f1: best_f1 = f1 best_threshold = threshold print(f"最优阈值: {best_threshold:.2f}, F1: {best_f1:.4f}")

这段代码的思想是:对坏玉米这个正类,扫描不同置信度阈值下的 precision、recall 和 F1 值,选择 F1 最高的阈值作为部署阈值。注意这里只统计了坏玉米类别,因为实际业务关心的是“把坏玉米从产线上摘除掉”,好玉米的误杀代价可以通过另一组阈值来控制。代码里的 preds 需要从模型评估结果里导出,不同框架导出方式有差异,但扫描逻辑是通用的。

阈值校准之后,还有一个小技巧能稳定指标:把验证集预测错误的图片全部导出,按“漏检的坏玉米”和“误检的好玉米”两类整理,再回到第 3 章的复检流程,把这些错误样本对应的标注框重新审核一遍。我经常发现,模型某几次吃力的样本,其实原标注本身就有问题——框歪了几像素,或者霉斑边缘被截断。把这些标注修正后重新训一次,效果往往比换一个更大的预训练模型来得更实在。

这些年做农产品检测项目,我已经养成一个习惯:任何数据集到手,先压缩对模型结构的期待,放大对标注质量的诚实检验。一份数据标注不一致,再先进的模型也只能在一个虚假的指标上挣扎。搞清楚好坏边界、跑通校验脚本、按批次切分、复检争议样本,这套流程做扎实了,89.5% 才可能真正复制到你的现场里。希望这段经验能帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 4:05:49

全球经纬度数据包拆解:省市区联动、地图打点与坐标坑

简介&#xff1a;一份以全球行政区域层级结构为核心的地理数据包&#xff0c;覆盖除中国以外的各国城市地区&#xff0c;以及中国全部省市区县&#xff08;含特别行政区&#xff09;&#xff0c;每个行政节点均附带经纬度坐标&#xff0c;适合地图应用开发、地区联动组件设计、…

作者头像 李华
网站建设 2026/10/19 6:40:05

理解Linux内核的心智模型:从设计哲学到工程实践

1. 为什么“内核心智模型”这个提法在2024年突然被高频提及最近三个月&#xff0c;我在给几家做边缘计算设备和国产信创操作系统的团队做内核调优咨询时&#xff0c;明显感觉到一个变化&#xff1a;客户不再只问“怎么把内存占用压到200MB以下”&#xff0c;也不再只盯着“启动…

作者头像 李华
网站建设 2026/10/19 19:42:27

营养早餐黄金公式:三套快手方案,稳住血糖精力满格

早上八点的地铁口&#xff0c;左手一杯拿铁&#xff0c;右手一个面包&#xff0c;是很多人想象中的“高效早餐”。但我得说句实在话&#xff1a;这顿下去&#xff0c;十点半之前你大概率会迎来一波困意和心慌。问题不在你精神不够强&#xff0c;而在你给身体加的是不是“燃料”…

作者头像 李华
网站建设 2026/10/21 20:38:41

Agent 原理与实战:从零构建简化版 Claude Code 的 Plan and Execute 架构

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

作者头像 李华