简介面向火灾预警与安全监控场景的红外火灾检测数据集采用YOLO格式标注专门用于火Fire与火源Foc两类目标检测适合森林防火、工业安全监控、智能安防及应急管理研究。数据按训练集1467张、验证集93张、测试集48张划分覆盖森林、山地、建筑、设备等多种场景红外热成像突出温度差异能有效辅助模型学习热源特征。压缩包共2000个文件以1608个txt标注文件、390个jpg红外图像为主另含1个yaml配置与1个docx说明文档整体约49MB目录结构清晰。已有263人学习下载适合需要构建实时火焰检测模型、开发火灾预警系统或开展AI消防交叉研究的开发者与研究人员可直接接入YOLO等主流框架减少数据准备与标注成本助力早期火灾识别与快速响应。1. 红外火灾检测数据集从热成像图到可用的YOLO目标检测标注红外火灾检测数据集听起来像是一个普通的目标检测压缩包但它跟你从网上下载的COCO、VOC完全不是一回事。这个数据集里装的是热成像相机捕捉的火灾场景标注了火焰区域能直接拿来训YOLO系模型。在工业消防场景里可见光摄像头在烟雾、夜间、逆光下几乎失效红外热成像却能靠温度差把火源框出来。所以这份数据集解决的痛点是消防预警系统需要能在火焰出现早期就报警而不是等烟尘遮挡后才触发。适合谁做森林防火、变电站巡检、工厂消防预警的算法工程师或者正在给消防机器人做视觉感知的学生。如果你只想练手通用目标检测这个数据集的场景分布可能不够“好看”但如果你是冲着热成像行业落地去它就是非常对口的训练原料。先说结论不要下载完就解压、就开训。这个数据集的坑基本都在你意想不到的地方比如通道数、标注归一化、小目标漏检。下面按我实际拆包的步骤来。2. 拿到压缩包先拆结构目录清单与标注格式解析2.1 解压后的目录结构与每个文件是干什么的下载得到的“红外火灾检测数据集.zip”通常是一个压缩包不要双击在里面直接把图片拖出来用最好先整体解压避免路径或中文名问题。我一般会在项目目录下统一解包unzip 红外火灾检测数据集.zip -d fire_dataset cd fire_dataset find . -maxdepth 2 -type d执行后你会看到类似下面这种目录组织这也是YOLO类工业数据集最常见的形式images/存放红外热成像原图格式以jpg或png为主labels/存放与图片同名的txt标注文件每行一个目标框classes.txt类别列表一行一个类别名顺序对应标注文件里的类别编号不同版本打包方式会有差异有的还会带一个voc_xmls/目录那是给习惯用VOC格式的人准备的中间产物。先用文件清单确认一下每个目录的文件数量看看图片和标注是否一一对应ls images | wc -l ls labels | wc -l图片数量不等于标注文件数量是最常见的“创伤型”问题。如果发现差异先别急着训练优先把缺失的标注补齐或者把没标注的图移到ignore/目录。否则训练过程会给你报一连串“image without labels”的警告最终导致验证集里全是纯背景图像。下面用表格整理一下常见文件类型和用途方便你在解压后快速对照路径内容用途images/*.jpg红外热成像原始帧模型输入labels/*.txtYOLO格式归一化标注训练监督classes.txt类别名列表后续配置data.yaml如果压缩包里还附带了一个README.txt别跳过。它往往会写清楚采集设备的波段范围、相机机位高度、标注人员标准。这些信息决定了数据集场景泛化边界。比如机位高度如果都在2米那训练出的模型在无人机20米视角下大概率会漏检因为目标尺度分布完全不同。2.2 热成像图像的通道与标注坐标先读懂再训练热成像原始数据在多数工业相机里输出的是单通道灰度温度越高像素值越大。但很多厂家为了方便预览会直接输出一张伪彩色BGR图。这就形成一个隐性差异你对着屏幕看是红蓝色图OpenCV读进来却是三通道而模型拿到手的三通道和普通可见光的三通道语义并不一致。标注格式方面YOLO的txt标注每一行对应一个目标框第1个字段类别编号从0开始对应classes.txt的行号后面4个字段归一化的中心点x、中心点y、框宽w、框高h都除以了图像宽高拿到数据集后我建议先抽样画几个框亲眼确认标注框与火焰区域的对齐程度。下面这段代码读一张图和一个标注文件再把框画出来import cv2 img_path images/fire_001.jpg label_path labels/fire_001.txt # 直接用IMREAD_COLOR读后续统一按三通道处理 img cv2.imread(img_path) h, w img.shape[:2] with open(label_path) as f: for line in f: cls, xc, yc, bw, bh map(float, line.split()) x1 int((xc - bw / 2) * w) y1 int((yc - bh / 2) * h) x2 int((xc bw / 2) * w) y2 int((yc bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.putText(img, str(int(cls)), (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite(check_fire_001.jpg, img)代码逻辑是先按三通道读图拿到宽高后把归一化的中心点坐标换算回像素坐标再画矩形框。参数上xc、yc、bw、bh都是0到1之间的小数如果哪一行出现大于1.2的值那这个标注一定有问题。我一般会用同样的循环批量生成几十张check图拖进看图软件里快速翻一遍确认所有目标的类别编号和实际内容一致。检查过程中你会注意到热成像的一个特点火焰边缘往往有一圈渐变的温度过渡标注的人可能把整个高亮区域框进去也可能只框最内部的亮核。两种风格都能训练但混合多人的不同标注风格会导致模型收敛变慢。如果发现这类情况建议统一按“高温亮核外围可见火焰”的规则重新清洗一轮。关于通道数还有一个容易被忽略的点如果你用cv2.imread()读灰度图返回的shape是(h, w)而不是(h, w, 3)。此时直接喂给预训练模型会报“expected 3 channels”或者干脆静默出错。我习惯先跑一句img cv2.imread(images/fire_001.jpg, cv2.IMREAD_COLOR) print(img.shape, img.dtype)确保输出是(h, w, 3)和uint8。如果是灰度图可以用cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)把单通道复制成三通道。这一步看起来智商税但真能拦住一批人而且是后续转YOLO格式的前提。3. 转成YOLO格式如果手里是VOC XML用脚本来一次转换3.1 为什么要把VOC转成YOLO归一化坐标与直接训练的关系很多火灾数据集早期用LabelImg标注时默认保存成VOC格式即一个XML文件里用绝对像素坐标存bndbox。YOLO系训练器要求的是归一化中心点坐标所以直接拿XML训练会报格式不支持。你需要一个转换脚本把每个XML里的绝对坐标换算成相对坐标。VOC的XML结构里每个object节点包含name和bndbox子节点bndbox里有xmin、ymin、xmax、ymax。YOLO需要的是x_center (xmin xmax) / 2 / widthy_center (ymin ymax) / 2 / heightbox_width (xmax - xmin) / widthbox_height (ymax - ymin) / height这里的width和height是图像的实际宽高必须从图片本身读而不是从XML里猜。有些标注工具会把width写进XML的size节点但偶尔会有写错的情况所以我认为最稳妥的方式是用cv2读图拿shape。3.2 转换脚本与参数说明我通常会写一个convert_voc_to_yolo.py一次性把整个xmls目录批量转换import os import cv2 import xml.etree.ElementTree as ET # 指定类别列表顺序必须与classes.txt一致 classes [fire, smoke, background] def convert(xml_path, out_dir): tree ET.parse(xml_path) root tree.getroot() img_name root.find(filename).text base_name os.path.splitext(img_name)[0] img_path os.path.join(images, img_name) img cv2.imread(img_path) if img is None: print(图片读取失败:, img_path) return h, w img.shape[:2] out_lines [] for obj in root.iter(object): cls_name obj.find(name).text if cls_name not in classes: continue cls_id classes.index(cls_name) box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) x_center ((xmin xmax) / 2) / w y_center ((ymin ymax) / 2) / h box_w (xmax - xmin) / w box_h (ymax - ymin) / h out_lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) out_path os.path.join(out_dir, base_name .txt) with open(out_path, w) as f: f.write(\n.join(out_lines)) os.makedirs(labels, exist_okTrue) xml_dir voc_xmls for name in os.listdir(xml_dir): if name.endswith(.xml): convert(os.path.join(xml_dir, name), labels)这段代码里两个参数最值得注意。classes列表的顺序决定了txt里的类别编号一旦写错后面训练出的模型会把火焰和烟雾搞反。img_path的拼接路径默认了images和voc_xmls在同一个根目录下如果你的数据集图片在另一个文件夹把这里改成绝对路径或参与传入的参数不要学我在脚本里写死。转换完成后再用上一章的抽样画框脚本重新检查一遍重点看转换前后的坐标是否一致。如果有框从画面里跑出去了很有可能是某些图片尺寸不是固定分辨率而我从XML读的width不是真实宽高。改用cv2读图之后这个错误基本能消失。3.3 划分训练集与验证集数据集准备好后训练前要划分train和val。我自己不会人工拖拽文件因为容易漏而且不好复现。我习惯用Python的shutil加固定随机种子来做import os import random import shutil random.seed(42) images [f for f in os.listdir(images) if f.endswith(.jpg)] random.shuffle(images) split int(len(images) * 0.85) for name in images[:split]: os.makedirs(train/images, exist_okTrue) os.makedirs(train/labels, exist_okTrue) shutil.copy(fimages/{name}, train/images/) shutil.copy(flabels/{name[:-4]}.txt, train/labels/) for name in images[split:]: os.makedirs(val/images, exist_okTrue) os.makedirs(val/labels, exist_okTrue) shutil.copy(fimages/{name}, val/images/) shutil.copy(flabels/{name[:-4]}.txt, val/labels/)划分比例我默认用85%比15%这个比例在中小型数据集上比较常见。种子固定为42保证每次执行结果一致方便复现别人的实验。如果数据集是按不同场景或不同采集批次组织的别用随机划分改按场景目录划分否则训练集和验证集会混入同一场景的连续帧评估指标虚高。这是后面避坑章里很重要的一条先在这里埋个坑。4. 训练YOLOv8/YOLOv12从data.yaml到命令行参数4.1 写data.yaml路径、类别数、类别名训练YOLO系模型第一步不是敲训练命令而是先写好data.yaml。这个文件告诉训练器去哪里读数据、有几类、类名是什么。我给这份红外火灾数据集建议的配置长这样train: fire_dataset/train/images val: fire_dataset/val/images nc: 2 names: [fire, smoke]这里train和val指向的是图片目录YOLO会自动在同级目录下找labels文件夹。如果你把图片和标注分开放在不同地方就用绝对路径。nc的值必须与names列表长度一致多一个少一个都会在初始化时给出报错。有一点要特别注意data.yaml里的路径是相对于你执行训练命令的终端目录解析的。如果你在项目根目录跑训练目录里的写fire_dataset/train/images没问题如果你cd到ultralytics目录里跑这个相对路径就有可能失效。我一般直接用绝对路径写死省得后面为了找路径浪费时间。4.2 训练命令与关键参数表格现代YOLO的训练入口很简单一行命令就能跑起来。以当前最常见的YOLOv8为例yolo detect train \ datafire.yaml \ modelyolov8s.pt \ epochs200 \ imgsz640 \ batch16 \ patience30 \ workers8 \ seed42 \ device0几个关键参数为什么要这样设参数取值原因modelyolov8s.pt火灾检测目标不复杂s足够速度快epochs200行业数据量小早停会提前结束imgsz640红外原图通常不大640保持细节patience30连续30轮fitness不提升就停省时间batch16显存不够时往下调够用可以上32seed42固定随机种子实验可复现训练过程中最需要盯住的不是loss曲线而是val精度曲线是否抖动。如果精度在某个epoch突然跳水然后恢复多半是学习率调度或数据增强里出现了随机性过强的操作。火焰在红外图里边缘很锐旋转增强有时会让火焰形态失真因此如果验证集精度震荡明显我建议在augment参数里关闭部分空间增强。4.3 用YOLOv12要注意的兼容点YOLOv12是目前社区里新出的检测架构很多人看到关键词“yolov12”就急着把model换成yolov12s.yaml复现训练。如果你的ultralytics版本太旧直接指定yolov12模型文件会报找不到。先把训练器的版本升级到支持YOLOv12的版本再重新生成模型配置。另一个兼容坑是YOLOv12把注意力模块引入了检测头训练显存占用会比v8同尺寸高一些。在同样的batch下v8能跑16的v12可能需要降到12或8否则会直接OOM。至于精度提升在红外火焰这种边缘清晰的目标上v12的提升没有在COCO那样明显所以如果只想快速落地用v8反而更省事。5. 常见问题与排查红外火灾数据集训练的五个坑红外火灾检测数据集体量通常不大几百张到两三千张不等。数据量越小标注和划分的坑越致命。下面五条是我在这类数据上实际踩过、也帮别人排查过的记录每一条都按“现象→原因→解决”顺序写方便你直接对照。5.1 现象模型把一切高温物体都识别成火红外火灾数据集最大的坑是负样本缺失。训练时正样本全是火焰模型学到的是“高亮区域火”于是把热成像下的烟囱、机动车排气管、夏季阳光照射的金属表面全框出来。原因数据集只提供了火灾正样本没有加入高温干扰物的背景负样本。损失函数不知道这些高亮物体不是火。解决在数据准备阶段单独挑一批不含火焰但温度偏高的红外图像标注一个background类或者在训练时把它们作为纯负样本图像加入train目录让模型看到“高亮但不该报警”的反例。5.2 现象灰度图训练时shape报错或loss不降有些红外相机导出的是单通道灰度PNG你用cv2.imread默认会读成(h, w)而不是(h, w, 3)。YOLO预处理阶段遇到这种图要么报“Unable to determine number of channels”要么干脆在dataloader里静默失败loss曲线始终不降。原因单通道输入与预训练模型的3通道卷积假设不匹配。解决统一用cv2.imread(path, cv2.IMREAD_COLOR)强制读成三通道如果原图本质是灰度图读进来后三个通道数值相同这没关系模型能学。重点是不能出现形状为(h, w)的ndarray混进数据集。5.3 现象zip压缩包解压提示伪加密或文件损坏有相当一部分下载的zip文件在Windows上双击能打开但用unzip解压时提示“missing zip entry”或“伪加密”。我遇到过不止一次尤其是从网盘下载的大体积数据集。原因部分网盘服务器对于zip文件做了分段下载文件末尾的Central Directory信息不完整也有人用压缩软件加密了文件头但实际内容没有加密属于伪加密。解决先检查文件大小是否和下载页标注一致然后用7-Zip打开它通常能忽略错误的加密位直接解压。如果确认是伪加密可以用python的zipfile模块并手动清除加密标志位不过更省事的方法是换一个下载工具重新拉一遍。5.4 现象火焰小目标漏检严重火灾场景里远处的小火苗只有几个像素训练时mosaic增强把这些小目标随裁剪丢掉了最后模型对大块火焰很敏感对小火点完全无感。这种现象在热成像的远程监控场景里非常典型。原因数据增强策略对大图小目标不友好小目标经过缩放后可能只剩1-2个像素无法提供有效梯度。解决在ultralytics配置里关闭mosaic设置mosaic0.0把imgsz适当降低到512或480等效增加小目标在输入图中的相对占比推理阶段用SAHI切图把大图切成多个640小图分别检测再合并。5.5 现象mAP很高但实际部署误报多训练时验证集mAP能到0.95但一接到现场摄像头就满屏误报帧率再高也没用。原因数据划分用随机划分训练集和验证集来自同一段连续视频的不同帧模型实际是“背题”了记住了背景纹理。工业现场的新场景光照、角度不同模型立刻露馅。解决按采集时间段或场景目录划分数据确保同一场景的画面只出现在train或val不交叉。验证指标要看per-class的recall尤其关注小目标类别的AP不要只看总体mAP。6. 进阶技巧伪彩色映射与模型验证6.1 用伪彩色映射榨干预训练模型的迁移能力很多红外火灾模型训练效果差不是因为模型不行而是因为输入分布离ImageNet预训练分布太远。一个低成本技巧是把单通道灰度图映射到伪彩色三通道再训练。这里说的不是简单复制通道而是用OpenCV的applyColorMap模拟工业相机的铁红配色import cv2 import numpy as np def to_pseudocolor(gray): # 归一化到0-255 gray cv2.normalize(gray, None, 0, 255, cv2.NORM_MINMAX) gray gray.astype(np.uint8) return cv2.applyColorMap(gray, cv2.COLORMAP_INFERNO)applyColorMap会把灰度值映射成蓝紫到黄白的渐变色在视觉上更接近真实红外相机预览在特征上也给模型提供了类似多通道高频信息。注意这种映射不能凭空增加物理信息但能加速收敛。我自己的实验里用伪彩色比直接用灰度图训练mAP普遍高出3-5个百分点代价是推理时多一步预处理对边缘设备来说可以接受。6.2 验证模型是否真的学到了“火”用只含火焰、不含人物或其他高温物体的热成像图做测试不够严谨。我习惯在验证集里故意混入一类hard negative电焊火花、炉口高温、反光金属。然后单独统计误报率看模型是否过度依赖“高亮形状”这种浅层特征。如果误报很高回到第5章补背景类负样本。从最早拿到红外火灾检测数据集到现在我每次拆包都会强制走一遍流程先看目录和classes.txt再抽样画框转换格式后重新验证一遍划分数据前确认场景不交叉最后才开训练。这套流程帮我躲过不少坑也希望帮到你。本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
C++ OpenCV获取图片宽高:原理、EXIF陷阱与批量优化 用c 获取图像的长宽,配上opencv听起来就是两行代码的事——imread把图读进来,img.cols是宽、img.rows是高。可真把这行代码塞进批量处理脚本里,最先遇到的往往不是结果,而是一串空Mat、对调的宽高、以及被缩小了八倍的尺寸数字。我… · 2026/11/3 17:29:52
面向Agent的全模态数据平台:从数据湖到智能体操作系统 1. 项目概述:这不是一场发布会,而是一次数据基础设施的“范式迁移”“云栖2026|湖生万物,助力 AI — 面向 Agent 的全模态数据平台”——这个标题里没有一个虚词。我盯着它看了三分钟,第一反应不是兴奋,而是… · 2026/11/3 17:29:52
从函数调用到MCP:AI Agent工具生态的标准化演进之路 做 AI Agent 落地的人,大概率都经历过这个阶段:先用 Function Calling 让模型能查天气、查订单,跑得挺顺;等工具多起来,开始怀疑人生。每家模型厂商的函数调用格式不一样,每个工具都要单独维护 JSON Schema… · 2026/11/3 17:28:43
条码标签打印软件流水号跳号设置与漏号排查指南 条码标签打印软件里最容易被做成“事故”的一个功能,就是流水号。我这些年帮不少仓库、产线、门店处理过标签打号的问题,其中问得最集中的一类就是——为什么我的流水号打出来是跳号的。有意思的是,“跳号”这个词在条码标签打印软件里其实是… · 2026/11/3 22:41:24
Elasticsearch中文分词优化与IK分词器实战 1. 为什么需要自定义 Elasticsearch 分词器在中文搜索场景中,默认的分词器往往无法满足业务需求。以电商平台为例,当用户搜索"苹果手机"时,系统需要准确识别这是一个完整商品名称,而不是将"苹果"和"手机… · 2026/11/3 22:41:24
Vue.js动态样式绑定实战与优化技巧 1. 动态样式绑定的核心价值在Vue.js开发中,样式管理一直是前端工程师需要面对的重要课题。传统的静态class绑定方式虽然简单直接,但在处理复杂交互逻辑时往往显得力不从心。v-bind指令(通常简写为:)为class绑定带来了革命性的灵活… · 2026/11/3 22:41:24
零基础Linux入门:从计算机基础到man、nano与SELinux 简介:《鸟哥的Linux私房菜-基础篇》第四版以PDF电子书形式呈现,面向具备基本计算机操作能力、希望系统入门Linux的初学者及需要巩固基础的IT技术人员,重点解决计算机组成、操作系统原理与Linux常用工具之间的知识断层。资源包仅含1个PDF文件&… · 2026/11/3 22:41:24
从零手搓AI工程:数据契约与特征一致性实战避坑指南 1. 从零手搓AI工程:为什么我不建议你直接调包第一次看到ai-engineering-from-scratch这个项目名的时候,我正坐在工位上啃一个调了三天都没收敛的推荐模型。当时第一反应是:又来了一个教人从零实现Transformer的教程仓库。但点进去翻了翻结构&… · 2026/11/3 22:40:22
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46
STM32F411RE与MAX14431实现SPI隔离:工业现场抗共模干扰实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
ESP32+大模型AI硬件开发:从Demo到产品的8个工程问题 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
基于MAX14431BAEE+与PIC18F85J10的SPI隔离方案设计与实践 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46