news 2026/10/6 1:23:33

庐山派K230开发板实战:CanMV下YOLO目标检测从模型转换到部署

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
庐山派K230开发板实战:CanMV下YOLO目标检测从模型转换到部署

拿到庐山派K230开发板,我最先干的事就是在CanMV环境下把YOLO跑起来。这块板子最大的卖点就是板载KPU神经网络加速单元,而官方在CanMV固件里把YOLO的加载、前处理、推理、后处理全部封装成了一个YOLO模块API,用MicroPython就能直接调用。这篇文章就把这部分API的使用和模型部署过程完整过一遍,从YOLOv5/v8/v11的选型到ONNX转kmodel,再到板端摄像头实时检测,全程按照我能跑通的流程写,踩过的坑也一并列出来。

庐山派K230本体的定位是边缘AI原型验证板,适合做智能摄像头、工业质检、机器人视觉这类场景。它和树莓派不一样的地方在于,K230有一颗专门的KPU算力单元,跑YOLO这种CNN模型不需要靠CPU硬扛,功耗和帧率表现都比纯CPU方案好很多。对刚接触嵌入式AI的同学来说,最友好的点是CanMV自带MicroPython环境,你不用碰复杂的交叉编译,也能在板子上完成模型推理。这篇文章适合三类人看:想在K230上快速跑通YOLO的,想搞懂kmodel转换细节的,还有做毕业设计或产品原型需要把检测结果接入业务系统的。

1. YOLO模块API总览:这块开发板凭什么敢碰YOLO

1.1 RISC-V + KPU,YOLO模块API的设计逻辑

K230是嘉楠科技推出的一款RISC-V架构双核处理器,大核负责跑Linux或RTOS,小核专门处理实时任务,旁边挂了一颗KPU加速器。KPU不是GPU那种通用并行计算单元,它更接近于一个专用的CNN加速器,对卷积、池化、全连接这些算子做了硬件级优化。这意味着不是随便一个神经网络模型都能直接跑,只有KPU支持的算子才能被映射到硬件上执行。

CanMV固件里的YOLO模块API,本质上就是把“模型加载、输入图像预处理、KPU推理、输出解码、NMS去重、画框显示”这几步做成了黑盒。你不需要关心KPU寄存器怎么配,也不用管张量怎么排布,只需要传入kmodel文件路径和一帧图像,就能拿到检测结果。这种设计思路对工业落地特别重要,因为大部分做产品的团队不需要重新造轮子,他们关心的是“模型能不能跑、跑多快、结果准不准”。

模块API把前后处理分得很清楚。前处理包括图像resize、RGB通道转换、归一化,这部分在CPU上完成,K230的双核RISC-V跑这些纯计算逻辑绰绰有余。真正的卷积推理全部交给KPU,输出层产生的原始张量再由API做解码,把每个候选框的坐标、置信度、类别提取出来。由于NMS这类动态逻辑KPU不支持,所以NMS是在CPU端完成的,这也是理解后面模型转换时“为什么不能把NMS塞进模型”的关键。

1.2 YOLOv5/v8/v11,在K230上到底怎么选

标题里同时出现YOLOv5、v8、v11不是随便排列的,这三代模型的部署逻辑和K230的适配程度有本质区别,实际项目里真得认真挑一下。

YOLOv5是部署生态最成熟的版本,网上现成的训练代码、权重文件、教程最多。它的C3结构在KPU上映射得很顺,算子类型简单,nncase转换时几乎不会报错。如果你的数据集不大、检测目标相对固定,YOLOv5s或YOLOv5n的kmodel是性价比最高的选择,实测320x320输入下K230能跑到30帧以上。

YOLOv8把C3换成了C2f,检测头改成了解耦头,精度确实比v5高,但算子种类也变多了。nncase转换时需要注意SiLU激活函数的支持情况,K230的KPU对SiLU有专门优化,所以大部分情况下没问题。唯一要留意的是导出ONNX时,如果用了torch>=2.0的某些新算子,可能导致转换报错,这时候一般通过升级nncase版本解决。

YOLOv11是更新的版本,它最大的变化是引入了C3k2结构和更高效的检测头设计,同等计算量下精度更好。K230跑v11完全可行,因为基础卷积和激活函数没有超出KPU支持范围,但要注意v11的导出代码需要和训练代码版本严格配套,否则导出ONNX后中间节点的名称对不上,后处理解析会乱套。

我的建议是:原型阶段直接用YOLOv8n,生态好、精度够;如果对帧率有极致要求,退回YOLOv5n;想做精度展示或小目标检测,再上YOLOv11,毕竟它的小目标召回率确实比前两代好一点。

1.3 适合谁看、能解决什么问题

这篇内容不是单纯教你怎么调用一个库,而是带你走完“模型训练—格式转换—板端部署—结果输出”的完整闭环。如果你手里已经有一块庐山派K230,想跑通自带demo之外的自己训练的数据集,这篇文章能直接给你答案。如果你还没有板子,只是在调研K230能不能做目标检测,阅读完也能判断出它的性能边界。

整个API的设计目标就是降低嵌入式AI的上手门槛。传统嵌入式Linux上部署YOLO,你需要交叉编译OpenCV、写C++推理代码、手动管理内存,一套流程下来至少一周。K230的CanMV方案把时间压缩到半天,代价是灵活度不如纯C++方案,但对于大部分应用场景,损失这点灵活度完全可以接受。

2. YOLO模块API核心接口拆解

2.1 模型加载与初始化:YOLO类参数逐个说

使用YOLO模块的第一步是实例化YOLO对象。以CanMV固件里常见的MaixPy API为例,初始化代码大致是这个样子:

from maix.nn import YOLO detector = YOLO( model="/sd/models/yolov8n.kmodel", class_names=["person", "cat", "dog"], input_size=(320, 320), conf_th=0.5, nms_th=0.45, max_boxes=50 )

model参数指向kmodel文件的路径,这个文件是部署的核心,后面会专门讲怎么从ONNX转过来。class_names是类别名称列表,它的顺序必须和训练时的类别顺序一致,一旦错位,检测结果就会张冠李戴。input_size是模型输入尺寸,nncase转换时模型input节点的shape是多少,这里就得填多少,尺寸不匹配最常见的报错就是shape mismatch。

conf_th是置信度阈值,低于这个值的框会被丢弃。我建议初始调试阶段设成0.3,方便观察模型的所有可能输出,等确认效果没问题再调回0.5,减少误检。nms_th是NMS的IoU阈值,默认0.45适用于大多数场景,如果检测目标重叠严重,可以试着降到0.35,不过要小心导致相邻目标被合并。max_boxes限制单帧最多输出多少个框,防止某些极端画面下后处理耗时过高。

注意:class_names在YOLOv5的kmodel里不会写入模型文件,它完全由API层负责映射。所以模型转换时不需要管类别名,真正决定类别数量的是检测头最后一层的输出通道数,这个在训练时就已经固定了。

2.2 detect推理接口:输入图像与输出结果的约定

初始化完成后,最核心的调用就是detect方法。代码示例如下:

img = cam.read() # 从摄像头采集一帧 objs = detector.detect(img) # 执行推理,返回检测结果

这里有个容易踩坑的地方:detect接收的图像格式。K230的摄像头sensor可以直接输出RGB888或RGB565,YOLO模块API内部会统一转成模型需要的RGB888格式。如果自己从文件读取图像,务必先转换成RGB888,不能直接把JPEG字节流转进去,否则前处理解析会出错,轻则报错,重则输出全为0的检测结果。

detect方法的返回值是一个列表,列表里每个元素是一个检测对象,包含以下属性:

  • x, y:目标框左上角坐标,单位为像素,坐标系原点在图像左上角
  • w, h:目标框的宽度和高度,单位为像素
  • class_id:类别索引,对应class_names数组的下标
  • score:置信度分数,范围在0到1之间

注意坐标是相对于输入图像尺寸的像素值,不是相对于模型输入尺寸的归一化值。这一点API已经帮你换算好了,拿到的坐标可以直接用于画框,不需要额外缩放。如果你把检测结果通过串口发给上位机,上位机也只需要按原图像分辨率解析就行。

2.3 可视化与结果回调:draw和get_result的配合

YOLO模块API提供了两个配套方法:draw和get_result。draw方法直接在图像上绘制检测框和标签,适合本地验证逻辑。get_result则返回结构化的JSON字符串,适合对接业务系统。

detector.draw(img, objs) # 在原图上绘制检测框和类别文字 json_str = detector.get_result(objs) # 生成JSON字符串,用于协议传输

draw方法的内部实现会遍历所有检测框,先画矩形,再在框上方写出类别名和置信度。如果你觉得默认的线宽、颜色不好看,可以自己用OpenCV或内置的image模块重写绘制逻辑,API并不强制要求用画框函数。

get_result返回的JSON格式大致是:

[ {"x": 120, "y": 80, "w": 45, "h": 110, "class_id": 1, "score": 0.92}, {"x": 300, "y": 200, "w": 60, "h": 130, "class_id": 0, "score": 0.85} ]

这个结构很直观,上位机解析起来成本很低。实际产品里我会直接把这段JSON通过串口或Socket发出去,让上位机负责业务联动,板子只专心做检测,这种边界清晰的架构比把业务逻辑全塞进板子更可靠。

3. 从训练到kmodel:部署链路全流程

3.1 在PC上训练并导出ONNX

K230的YOLO模块API只接受kmodel格式,所以训练产物要做两轮转换:PyTorch权重先导出为ONNX,再用nncase把ONNX编译成kmodel。

训练环节不用我多说,常规流程是准备数据集、用labelimg打标、划分train/val、配置yaml文件、执行训练脚本。有一点需要强调:数据集打标时一定要统一标注格式,YOLO格式的txt文件里,类别id从0开始,坐标是归一化后的cx, cy, w, h。训练脚本会根据这些txt生成对应的标签文件,如果标注和训练脚本之间的映射关系不一致,模型训练出来大概率没法用。

训练完成后导出ONNX,以YOLOv8为例,命令大致是:

yolo export model=best.pt format=onnx opset=12 imgsz=320

这里有两个关键参数。opset建议固定为12或13,太高的opset可能引入KPU不支持的算子。imgsz要和你后续板端推理的输入尺寸保持一致,如果训练时用的640,部署时想跑320,要么重新训练,要么用模型resize能力,但最稳妥的做法是直接按部署尺寸导出。同时要注意导出后的ONNX里不能包含后处理逻辑,YOLOv8的导出脚本默认会去掉NMS部分,只需要拿到检测头输出的原始张量。

YOLOv5的导出类似,但因为是老版本,需要自己处理一下输出格式:

python export.py --weights best.pt --include onnx --img 320 --opset 12

导出完成后,可以用Netron打开ONNX文件,确认最后几个输出节点的名称和维度,这一步对后面nncase转换时的算子对齐很有帮助。

3.2 nncase工具链:ONNX转kmodel

nncase是嘉楠科技提供的一套神经网络编译器,专门把ONNX、TFLite等格式的模型编译成KPU能执行的kmodel。使用方式是在PC上写好Python脚本,调用nncase的库完成编译,然后把生成的kmodel文件拷贝到板子上。

安装nncase工具链是第一步。到嘉楠官方GitHub仓库下载对应平台(Windows/Linux)的whl包,推荐直接在Python 3.8环境下安装:

pip install nncase==2.x.x pip install nncase-kpu==2.x.x

版本号必须和你的K230固件匹配。我踩过最大的坑就是nncase版本过新,生成的kmodel板端无法加载,提示版本不支持,后来对齐官方release版本才解决。

转换脚本的核心逻辑大致如下:

import nncase # 创建编译器实例 compiler = nncase.Compiler() # 导入ONNX模型 with open("yolov8n.onnx", "rb") as f: model_content = f.read() compiler.import_onnx(model_content, model_layout="NCHW") # 配置编译选项 compile_options = nncase.CompileOptions() compile_options.target = "k230" compile_options.input_layout = "NCHW" compile_options.dump_ir = True compiler.compile(compile_options) # 生成kmodel kmodel = compiler.gen_dbg_kmodel() with open("yolov8n.kmodel", "wb") as f: f.write(kmodel)

这段代码最重要的两个地方是target设成"k230"和input_layout设成"NCHW"。K230的KPU内部对张量排布有要求,nncase会在编译阶段自动插入Transpose算子完成NCHW到NHWC的转换,不需要手动操心。编译完成后,在相同的目录下会生成kmodel文件以及一些中间调试文件,板端只需要kmodel和对应的内存布局信息。

3.3 量化校准:精度与速度的平衡

kmodel的一个关键特征是量化。K230的KPU主要支持int8计算,所以nncase在编译时会把float32的ONNX模型量化成int8。量化过程需要一组校准图片来统计激活值的分布范围,这组图片叫校准集。

校准集的选择直接影响量化后模型的精度。我一般从训练集的验证部分随机抽100到200张图片,覆盖各种光照和场景,保证分布足够多样。图片数量太少或太单一,会导致量化时统计出的数值范围偏差大,部署后检测精度崩塌。

import nncase import cv2 import numpy as np calib_images = [] for i in range(200): img = cv2.imread(f"calib/{i}.jpg") img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = cv2.resize(img, (320, 320)) calib_images.append(img) # 传入校准数据 compiler.compile(compile_options, calib_data=calib_images)

校准集图片的预处理方式必须和训练时保持一致。YOLO训练一般会做letterbox,也就是等比缩放后填充灰色边,如果你直接resize成正方形,检测效果会打折扣。K230的YOLO模块API内部对输入图像的处理逻辑是等比缩放加填充,所以校准集也应该用同样的方式处理,才能让量化统计结果更接近真实推理场景。

量化后精度会有一定下降,这是正常现象。如果下降幅度过大,比如mAP掉了超过5个百分点,可以检查两个方向:一是校准集分布是否和实际使用场景差异太大,二是模型最后一层是否被错误地量化了。有些情况下可以尝试对敏感层做混合量化,保留float32精度,但K230上这种操作的性能和内存开销需要权衡,我一般只在关键项目中用。

3.4 把kmodel放进板子常见方式

kmodel生成后,拷贝到板子的方式有三种,按推荐程度排序。

第一种是拷贝到SD卡,然后把YOLO对象里的model参数指向SD卡路径。这种方式最灵活,换模型只需替换SD卡上的文件,适合开发和调试阶段。

detector = YOLO(model="/sd/models/yolov8n.kmodel", ...)

第二种是烧录到板载Flash文件系统里。这种方式适合量产场景,模型和固件一起烧录,不容易被误删。CanMV固件的文件系统挂载路径一般是"/flash",把kmodel放进去即可。

第三种是通过网络传输,用SCP或Web方式把kmodel推到板子的可写分区。这种方式适合现场更新模型,但前提是板子上运行了网络服务。

我建议开发阶段直接走SD卡,简单直接;做产品原型时再考虑烧录到Flash。无论哪种方式,都要注意kmodel文件的路径不能有中文和空格,否则文件系统解析会出问题。

4. 板端部署实操:从零跑通检测demo

4.1 环境准备与文件结构

庐山派K230刷好CanMV固件后,通过USB连接电脑,通常会出现一个虚拟串口。使用MobaXterm或PuTTY连接,波特率115200,就能进入MicroPython交互界面。也可以通过CanMV IDE连接,IDE里可以上传文件、查看图像。

SSH能进系统后,建议先看一下文件系统结构:

ls /sd ls /flash

把准备好的kmodel文件放到SD卡的models目录下,类别名文件如果做协议用也可以一并放上去。推荐的文件结构是:

/sd/ ├── models/ │ └── yolov8n.kmodel ├── main.py └── test.jpg

main.py是板端主程序,test.jpg用于离线测试。

4.2 摄像头初始化与主循环

完整的摄像头实时检测代码,我按照实际跑通的版本整理如下,可以直接抄作业:

from maix import camera, display, app from maix.nn import YOLO detector = YOLO( model="/sd/models/yolov8n.kmodel", class_names=["person", "cat", "dog"], input_size=(320, 320), conf_th=0.5, nms_th=0.45, max_boxes=50 ) cam = camera.Camera() disp = display.Display() while not app.need_exit(): img = cam.read() # 采集一帧 objs = detector.detect(img) # 目标检测 detector.draw(img, objs) # 绘制结果 disp.show(img) # 显示到屏幕

这段代码已经是一个功能完整的实时检测应用。camera模块负责从sensor取图,display模块负责输出到屏幕或HDMI。如果你买的是没有屏幕的裸板,可以把disp.show(img)注释掉,改为把结果编码成JPEG存储或发送。

摄像头初始化参数可以在创建Camera对象时设置:

cam = camera.Camera( width=640, height=480, fps=30, pix_format="RGB888" )

sensor输出的分辨率不需要和模型输入一致,YOLO模块API会自行缩放。但要注意,缩放操作是逐像素插值,如果原图分辨率太高而模型输入太小,边缘细节容易丢失。一般来说640x480的原图配合320x320的模型输入,效果已经足够。

4.3 保存检测结果和打印日志

调试阶段不要把检测结果只显示在屏幕上,加一段日志输出对确认问题非常有帮助:

for obj in objs: print( "class:", detector.class_names[obj.class_id], "score:", obj.score, "box:", obj.x, obj.y, obj.w, obj.h )

每一帧都打印全部检测框会刷屏刷得很厉害,实际调试时我习惯每10帧打印一次。打印本身会占用CPU时间,影响帧率,所以正式部署时可以关掉或者降频。

如果要保存检测后的图片,可以用内置的image模块:

from maix import image img.save("/sd/output/frame_001.jpg")

保存JPEG的过程需要编码,耗时大约几十毫秒,jpeg质量可根据需要调整。如果要做长时间记录,建议只在帧率有余量时才保存,否则会拖慢整体速度。

4.4 把检测结果做成HTTP或串口输出

实时显示只是第一步,实际项目里更常见的需求是把检测结果传给上位机或云端。K230支持网络通信,可以在CanMV里直接用socket发送UDP包或HTTP请求。

最简单的做法是在检测循环里,把get_result生成的JSON通过UDP发给上位机:

import socket import json sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) server_addr = ("192.168.1.100", 9000) while not app.need_exit(): img = cam.read() objs = detector.detect(img) if len(objs) > 0: data = detector.get_result(objs).encode() sock.sendto(data, server_addr)

这种架构很适合做智能安防或门禁联动,板子只负责视觉感知,业务逻辑跑在服务器上。如果现场没有网络环境,也可以走串口,K230的UART接口同样可以把JSON字符串发出去,下位机MCU或工控机解析后执行动作。

5. 常见问题与避坑实录

5.1 kmodel加载失败或输入尺寸不一致

加载kmodel时报错,最常见的两个原因是文件路径错误和模型输入尺寸不匹配。路径错误很好排查,确认文件确实在指定目录下,且文件名没有拼写错误。这里要注意MicroPython环境对路径大小写敏感,/SD和/sd是不同路径。

输入尺寸不一致的报错信息通常是shape mismatch。这种情况要回头看模型转换时的onnx输入尺寸,以及YOLO构造时输入的input_size,两者必须严格一致。从YOLOv8导出的ONNX,输入节点名通常是images,shape为[1, 3, 320, 320],对应的input_size就应该填(320, 320)。如果kmodel是之前为640尺寸编译的,部署时还按320推理,必然报错。

5.2 检测框位置偏移、置信度全是0

模型能加载,但推理结果明显不对,检测框位置乱飘或者置信度全部为0,这类问题通常出在图像预处理环节。YOLO模块API对输入图像的要求是RGB888格式,如果摄像头初始化的pix_format设成了RGB565,API内部转换时没有正确识别,就会出现颜色通道错乱和位置偏移。

另外,原图比例和模型输入尺寸不一致时,如果API内部使用letterbox方式缩放,坐标换算逻辑和纯resize不同,直接拿检测结果的像素坐标在原图上画框,就会有偏移。解决办法是检查原图宽高比和模型输入宽高比是否一致,不一致时要信任API的letterbox处理,不要手动resize后再传给detect。

5.3 量化后精度崩塌

模型在PC上测试精度很高,转成kmodel后检测效果明显下降,这是量化问题。对比训练时的验证集和实际部署场景的图片,如果光照、目标大小、背景差异过大,量化误差就会被放大。我处理过的一个项目里,训练集全是室内白天光照,部署在户外夜间场景,量化后几乎检测不到目标,后来增加夜间图片重新做校准,效果立刻恢复。

还有一种情况是量化校准集数量不足。nncase校准过程如果只给几十张图片,统计出的激活值范围偏差较大。建议校准集至少100张,最好200张以上,并且要包含目标出现和未出现的画面,让模型学习到空白背景下的输出分布。

5.4 性能优化与线程配置

YOLO模块API初始化时有一个thread_num参数,控制KPU推理使用的线程数量。默认值是1,对大多数模型够用。如果模型较大或分辨率高,可以适当增加到2,但线程过多反而会因调度开销拖慢速度,我测试过的经验是2到4之间取平衡。

性能优化的大方向是先看瓶颈在推理还是前后处理。打印单帧各阶段耗时,如果KPU推理占比过大,考虑降低模型输入尺寸或换更小的模型结构;如果前处理和画框占比过大,优化图像转换逻辑,减少不必要的内存拷贝。

K230上YOLOv8n在320x320输入下,实测帧率能稳定在30帧以上,前提是模型已经正确量化,且display输出没有占用过多CPU。如果接了高分辨率屏幕显示,显示刷新会吃掉不少算力,帧率下降一两倍都不奇怪。

我在实际项目里的体会是,K230的YOLO模块API已经把部署门槛拉到了一个非常低的位置,真正决定项目成败的反而不是API本身,而是模型转换时对量化、校准、输入尺寸这些细节的把控。跑通一个demo很快,但要稳定运行在真实场景里,必须耐心做模型和板端的联合调优。最后再分享一个小技巧:测试新模型时,先用离线图片跑一遍结果,再切到摄像头实时推理,这样能把“模型转换问题”和“摄像头采集问题”分开排查,节省大量调试时间。

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

STM32上电复位到RTOS第一个任务:启动流程与PendSV切换全解析

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

作者头像 李华
网站建设 2026/10/6 1:20:52

电赛多电压输出电源板设计:从原理图到PCB调试完整指南

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

作者头像 李华
网站建设 2026/10/6 1:20:51

华为GVRP配置实战:三种注册模式与动态VLAN传播详解

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

作者头像 李华
网站建设 2026/10/6 1:20:51

Allegro Z-Copy全面解析:从板框到禁布区的5分钟高效复制

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

作者头像 李华
网站建设 2026/10/6 1:20:22

ESP32-S3 Mini与C3 Mini选型:PSRAM和USB是关键分水岭

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

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

Allegro 16.6不规则焊盘封装实战:从几何建模到车规量产

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

作者头像 李华