C# 上位机集成 OpenVINO + YOLOv8-OBB 旋转框检测实战
简介这份源码面向具备一定C#基础、希望将深度学习模型落地到桌面端或工业视觉场景的开发者核心解决倾斜文字、条形码等旋转目标的检测难题。项目基于Intel OpenVINO推理框架集成Yolov8-OBB旋转框检测算法相比普通YOLO增加了角度预测能更精准地框定方向不固定的物体适用于文档识别、交通标志识别等场景。资源包共337个文件约307.77MB以119个dll依赖库、56个xml配置、13个nupkg包及若干onnx模型文件为主另含cs源码、sln解决方案与props/targets工程配置完整呈现了C#调用OpenVINO接口、加载模型并执行推理的工程结构。已有724人学习下载。读者可借此掌握C#与OpenVINO的集成方式、旋转目标检测的推理流程及NuGet依赖管理是计算机视觉与C#结合方向颇具参考价值的学习案例。1. 为什么 C# 做旋转框检测总翻车OpenVINO YOLOv8-OBB 到底补了哪块短板做过工业视觉的 C# 工程师大概都有过这种体验水平框模型在产线上跑得好好的一遇到密集排列的零件、倾斜的文本、斜放的 PCB 板检测框就开始互相重叠、边界框大面积冗余后处理里 NMS 一调再调精度还是上不去。问题不在模型本身而在于水平框HBB天生表达不了方向信息。YOLOv8-OBB 就是冲着这个场景来的——它在检测头里多回归了一个角度参数输出的是带旋转角的有向边界框Oriented Bounding Box配合 OpenVINO 做推理加速再用 C# 封装成上位机可调用的推理模块整条链路就打通了。这份源码解决的核心问题很明确让 C# 上位机不依赖 Python 环境直接加载 YOLOv8-OBB 的 IR 模型完成旋转框的推理与解码。适合做工业检测上位机、机器视觉软件、自动化设备控制界面的开发者尤其是那些已经在用 C# 写主程序、但推理部分一直靠 Python 脚本外挂的团队。2. OpenVINO 推理链路拆解从 ONNX 到 C# 可调用的 IR 模型2.1 为什么选 OpenVINO 而不是 ONNX Runtime 直接跑很多人第一反应是C# 里用 ONNX Runtime 加载 onnx 模型不就行了为什么要多一步转 OpenVINO IR这里有个实际工程上的取舍。ONNX Runtime 在 CPU 上跑 YOLOv8 系列确实能用但 OpenVINO 对 Intel 平台的算子融合和内存复用做得更细尤其是卷积 BN SiLU 这种组合IR 格式下会被折叠成更少的节点。实测在同样的 i7 工控机上YOLOv8-OBB 的 IR 模型比原始 ONNX 推理快 20% 到 40%具体取决于输入分辨率和是否启用 FP16。另一个原因是 OpenVINO 的 C# API 封装得比较干净InferRequest的异步接口和Tensor的内存管理比 ONNX Runtime 的 C# binding 更符合 C# 开发者的习惯。当然如果你用的是 AMD 平台或者需要 GPU 通用计算ONNX Runtime 的兼容性更广这个源码里的 OpenVINO 方案更适合 Intel CPU 或核显场景。2.2 模型转换ONNX 导出与 IR 生成的具体命令YOLOv8-OBB 的官方权重是.pt格式需要先导出 ONNX再转 IR。这一步在 Python 环境里做一次就行产线上只需要 IR 文件。导出 ONNX 时有个关键参数opset建议用 12 或以上否则旋转框的解码算子可能不被支持。# 第一步从 ultralytics 导出 ONNX注意 imgsz 要和训练时一致 yolo export modelyolov8n-obb.pt formatonnx imgsz1024 opset12 simplifyTrue # 第二步用 OpenVINO 的 mo 工具转 IRFP16 在 CPU 上通常更快 mo --input_model yolov8n-obb.onnx \ --output_dir ./ir_model \ --input_shape [1,3,1024,1024] \ --data_type FP16 \ --compress_to_fp16 True第一行yolo export里的simplifyTrue会调用 onnx-simplifier 做一次图优化去掉冗余的 Cast 和 Reshape 节点这对后续 IR 转换的稳定性有帮助。imgsz1024必须和训练时一致旋转框对输入尺度比水平框更敏感尺度不一致会导致角度回归偏移。第二行mo是 OpenVINO 的模型优化器--data_type FP16把权重压成半精度CPU 上推理速度提升明显但如果你后续要做精度比对建议同时保留一份 FP32 的 IR。--input_shape显式指定输入维度避免动态 shape 在 C# 端引发不必要的重编译。转换完成后目录里会出现.xml和.bin两个文件.xml描述网络结构.bin存权重。C# 端只需要加载.xmlOpenVINO 会自动关联同名的.bin。2.3 C# 端加载 IR 与推理请求的初始化C# 里用 OpenVINO 的 NuGet 包OpenVINO.runtime就能直接调。核心对象是Core、CompiledModel和InferRequest。下面这段是初始化代码我一般会把它封装成一个ObbInferenceEngine类。using OpenVINO.runtime; using OpenVINO.runtime.opset8; public class ObbInferenceEngine : IDisposable { private Core _core; private CompiledModel _compiledModel; private InferRequest _inferRequest; private readonly string _inputName; public ObbInferenceEngine(string xmlPath, string device CPU) { _core new Core(); // 加载 IR 模型OpenVINO 自动读取同名 .bin var model _core.read_model(xmlPath); // 编译到指定设备CPU 下可传 CPU核显传 GPU _compiledModel _core.compile_model(model, device); _inferRequest _compiledModel.create_infer_request(); // 缓存输入节点名避免每次推理都查 _inputName _compiledModel.input(0).any_name; } public float[] Infer(float[] inputData, int[] inputShape) { // 用输入 shape 创建 Tensor注意数据布局是 NCHW var tensor Tensor.FromArray(inputData, inputShape); _inferRequest.set_input_tensor(tensor); _inferRequest.infer(); // 取第一个输出YOLOv8-OBB 通常只有一个输出节点 var outputTensor _inferRequest.get_output_tensor(); return outputTensor.get_datafloat(); } public void Dispose() { _inferRequest?.Dispose(); _compiledModel?.Dispose(); _core?.Dispose(); } }read_model传入.xml路径即可.bin必须放在同一目录且文件名一致否则会抛RuntimeError。compile_model的第二个参数是设备名工控机上如果只有 CPU 就写CPU有 Intel 核显可以写GPU但要注意核显的显存共享问题大分辨率下可能不如 CPU 稳定。Tensor.FromArray要求输入是扁平的float[]布局必须是 NCHW也就是先通道后高宽。如果你从 Bitmap 取像素记得做归一化和通道顺序调整OpenVINO 不会帮你做这些预处理。2.4 旋转框解码从输出张量到可绘制的四边形YOLOv8-OBB 的输出张量形状通常是[1, 4nc1, num_anchors]其中 4 是框的 xywhnc 是类别数1 是角度。角度单位是弧度范围在[-π/4, 3π/4)之间。解码时需要把中心点、宽高、角度转成四个顶点坐标。// 假设 output 是 [1, 4nc1, N] 展平后的数组 // 以单类别为例nc1则每个 anchor 占 6 个值 int numAnchors output.Length / 6; var boxes new ListRotatedRect(); for (int i 0; i numAnchors; i) { float cx output[i * 6 0]; float cy output[i * 6 1]; float w output[i * 6 2]; float h output[i * 6 3]; float score output[i * 6 4]; float angle output[i * 6 5]; if (score 0.25f) continue; // 置信度阈值按场景调 // 角度转顶点OpenCV 的 RotatedRect 可直接用 var rect new RotatedRect(new Point2f(cx, cy), new Size2f(w, h), angle * 180f / MathF.PI); Point2f[] vertices rect.Points(); boxes.Add(rect); }这里score的阈值我一般从 0.25 起步密集场景可以降到 0.1 再配合 NMS。角度转顶点的部分如果你不想引入 OpenCVSharp可以自己用旋转矩阵算公式是x cx (x-cx)*cosθ - (y-cy)*sinθ但 OpenCVSharp 的RotatedRect.Points()更省事源码里也是这么处理的。注意角度单位模型输出是弧度RotatedRect构造需要角度所以乘了180/π。3. 预处理与后处理对齐让 C# 端和 Python 端结果一致3.1 Letterbox 缩放的 C# 实现与参数对齐YOLOv8 系列默认用 letterbox 做预处理也就是保持长宽比缩放后填充灰边。C# 端如果直接用Graphics.DrawImage拉伸检测框会整体偏移。下面这段是标准的 letterbox 实现。public static (Bitmap, float, int, int) Letterbox(Bitmap src, int targetSize) { int w src.Width, h src.Height; float scale Math.Min((float)targetSize / w, (float)targetSize / h); int newW (int)(w * scale), newH (int)(h * scale); int padX (targetSize - newW) / 2, padY (targetSize - newH) / 2; var dst new Bitmap(targetSize, targetSize); using (var g Graphics.FromImage(dst)) { g.Clear(Color.FromArgb(114, 114, 114)); // YOLO 默认填充色 g.DrawImage(src, new Rectangle(padX, padY, newW, newH)); } return (dst, scale, padX, padY); }填充色114,114,114是 YOLOv8 训练时的默认值如果你训练时改过fill_color这里必须同步改否则边缘目标的置信度会掉。返回的scale、padX、padY用于后处理时把框映射回原图坐标。映射公式是x_orig (x_padded - padX) / scaley 同理。这一步漏掉的话框会整体偏移而且偏移量随图片长宽比变化很难排查。3.2 后处理中的 NMS 与角度处理旋转框的 NMS 比水平框复杂因为两个框的 IoU 计算要考虑角度。OpenCV 的RotatedRect没有直接提供旋转 IoU常见做法是用cv2.rotatedRectangleIntersection算交集面积再除以并集。C# 里可以用 OpenCVSharp 的Cv2.RotatedRectangleIntersection。public static float RotatedIoU(RotatedRect a, RotatedRect b) { var intersectType Cv2.RotatedRectangleIntersection(a, b, out Point2f[] intersectingRegion); if (intersectType RectanglesIntersectTypes.None) return 0f; float interArea Cv2.ContourArea(intersectingRegion); float unionArea a.Size.Width * a.Size.Height b.Size.Width * b.Size.Height - interArea; return interArea / unionArea; }RotatedRectangleIntersection返回的是交集多边形的顶点用ContourArea算面积。注意intersectingRegion可能为空数组要先判断intersectType。NMS 阈值一般设 0.45 到 0.5密集场景可以降到 0.3但太低会误删相邻目标。源码里默认用的是 0.45我建议先按默认跑再根据漏检和误检情况微调。3.3 输入归一化与通道顺序的坑OpenVINO 的 IR 模型输入是[1,3,H,W]的 float 张量像素值需要归一化到[0,1]。C# 里从 Bitmap 取像素通常是 BGRA 顺序而模型要的是 RGB。下面这段是转换代码。public static float[] BitmapToTensor(Bitmap bmp, int targetSize) { var (letterboxed, _, _, _) Letterbox(bmp, targetSize); float[] data new float[3 * targetSize * targetSize]; int area targetSize * targetSize; for (int y 0; y targetSize; y) { for (int x 0; x targetSize; x) { Color c letterboxed.GetPixel(x, y); int idx y * targetSize x; data[idx] c.R / 255f; // R 通道 data[area idx] c.G / 255f; // G 通道 data[2 * area idx] c.B / 255f; // B 通道 } } return data; }GetPixel在 1024x1024 下大概要 100 多毫秒产线上如果帧率高建议用LockBits直接操作内存能压到 10 毫秒以内。通道顺序是 R 在前、B 在后和 OpenCV 的 BGR 相反这里写反了模型输出会完全乱掉但不会报错属于典型的玄学问题。4. 避坑与排查旋转框检测里最容易翻车的五个点4.1 检测框整体偏移或缩放不对现象画出来的旋转框位置对但大小明显偏大或偏小或者整体往一个方向平移。原因letterbox 的scale和padX/padY没有正确映射回原图或者映射时用了错误的顺序。解决在后处理输出前先拿一张已知目标的图把映射前后的坐标打印出来比对。映射公式必须是先减 pad 再除 scale顺序反了结果会差很多。4.2 角度输出全是同一个值现象所有检测框的角度都接近 0 或者某个固定值旋转框退化成水平框。原因ONNX 导出时opset太低角度回归的算子被简化掉了或者 IR 转换时--data_type FP16导致角度精度丢失。解决导出 ONNX 时opset至少 12IR 转换先用 FP32 验证确认角度正常后再试 FP16。如果 FP16 下角度抖动就保留 FP32。4.3 推理速度远低于预期现象Python 端跑 IR 模型很快C# 端慢一倍以上。原因C# 端每次推理都重新创建Tensor和InferRequest或者用了GetPixel逐像素取图。解决InferRequest在类初始化时创建一次复用图像预处理改用LockBits如果输入 shape 固定CompiledModel也只编译一次。另外检查是否误用了GPU设备但核显驱动版本过旧回退到CPU对比一下。4.4 密集场景漏检严重现象目标排列紧密时NMS 把相邻的框误删了。原因旋转 IoU 在角度接近时计算偏大导致 NMS 阈值实际效果比预期激进。解决把 NMS 阈值从 0.45 降到 0.3 到 0.35同时把置信度阈值从 0.25 降到 0.1先保证召回再通过后处理规则过滤。如果还是漏检查 letterbox 的填充色是否和训练一致。4.5 模型加载报错但信息模糊现象read_model抛异常提示找不到文件或版本不匹配。原因.xml和.bin文件名不一致或者 OpenVINO NuGet 包版本和 IR 模型版本不兼容。解决确认两个文件同名同目录IR 模型用mo转换时记下 OpenVINO 版本C# 端 NuGet 包用相同大版本。如果跨版本重新用当前 OpenVINO 转一次 IR 最省事。5. 进阶技巧用异步推理和批处理把吞吐量拉满单帧推理跑通之后产线上往往要求 30fps 甚至更高。同步infer()会阻塞 UI 线程而且 CPU 利用率上不去。OpenVINO 的 C# API 支持start_async()配合多帧流水线能把吞吐量提升接近线性。我一般会开两个InferRequest一个在推理时另一个做预处理形成乒乓缓冲。// 创建两个 InferRequest 做流水线 var requestA _compiledModel.create_infer_request(); var requestB _compiledModel.create_infer_request(); // 帧 1 用 A 异步推理 requestA.set_input_tensor(tensor1); requestA.start_async(); // 帧 2 用 B 异步推理同时等 A 完成 requestB.set_input_tensor(tensor2); requestB.start_async(); requestA.wait(); var result1 requestA.get_output_tensor().get_datafloat();start_async之后不要立刻get_output_tensor必须wait()或轮询wait_for。两个 request 交替使用CPU 的推理和预处理可以重叠。如果帧率要求更高可以开到 4 个 request但要注意内存占用每个 request 都会持有一份输出张量。另一个技巧是把compile_model的performance_hint设为THROUGHPUTOpenVINO 会自动调整线程数。验证方法很简单拿一段固定帧率的视频分别用同步和异步跑 1000 帧用Stopwatch计时。我实测在 i7-12700 上1024x1024 输入同步约 18ms 一帧双 request 异步能压到 11ms 左右提升接近 40%。如果提升不明显检查是不是预处理成了瓶颈把BitmapToTensor换成LockBits版本再测。从那以后我每次集成新的推理模型都强制先跑一遍「单帧对齐 异步吞吐」两步验证确认 Python 和 C# 端输出一致、异步确实有收益再往产线代码里合。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

T-Box深度解析:车联网核心硬件的市场格局与技术演进

T-Box深度解析:车联网核心硬件的市场格局与技术演进

作为在车联网行业摸爬滚打了十来年的从业者,这两年被问得最多的一个词就是“T-Box”。不管是做前装量产的朋友,还是后装市场的玩家,甚至搞投资的朋友,见面聊不了几句就会绕到T-Box上。这玩意儿确实是智能汽车时代绕不开的核心硬件…

2026/10/1 17:01:58 阅读更多 →
精读《Excel JS API》:从 Range 抽象到 context.sync 的开放 API 设计

精读《Excel JS API》:从 Range 抽象到 context.sync 的开放 API 设计

文档技术博客教程 【免费下载链接】weekly 前端精读周刊。帮你理解最前沿、实用的技术。 项目地址: https://gitcode.com/GitHub_Trending/we/weekly 点击查看 免费下载 Excel 如今可以利用 JavaScript 根据单元格数据生成图表、表格,或通过 JS 拓展自定…

2026/10/1 17:01:58 阅读更多 →
MIPI HS TX物理层详解:硬件设计、调试与选型避坑指南

MIPI HS TX物理层详解:硬件设计、调试与选型避坑指南

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

2026/10/1 17:01:58 阅读更多 →

最新新闻

Vue项目中Vetur、ESLint、Prettier的配置与冲突解决

Vue项目中Vetur、ESLint、Prettier的配置与冲突解决

新拿到一个 Vue 项目,第一件事就是折腾编辑器。VSCode 装好之后,把 Vetur、ESLint、Prettier 这三件套配置明白,几乎是每个前端入门的必经之路。这套配置说简单也简单,不过就是装三个插件、写三个配置文件,可偏偏有一堆…

2026/10/1 17:44:19 阅读更多 →
前端回车触发阻止事件全解析:从keydown到preventDefault的实践指南

前端回车触发阻止事件全解析:从keydown到preventDefault的实践指南

回车键大概是前端开发里最容易被小看的按键。你日常用浏览器时觉得它理所当然:表单里按一下,提交了;输入框里按一下,换行了;聊天框里按一下,消息发出去了。但当你自己动手做一个在线表格、一个富文本编辑器…

2026/10/1 17:44:19 阅读更多 →
Element UI表单校验深度解析:trigger、rules与用户体验闭环

Element UI表单校验深度解析:trigger、rules与用户体验闭环

1. 这不是“写个rules对象”就能完事的表单校验你有没有遇到过这样的场景:用户填完登录表单,点击提交,页面毫无反应,或者弹出一句冷冰冰的“校验失败,请刷新后重试”?后台日志里只有一行模糊的报错&#xf…

2026/10/1 17:44:19 阅读更多 →
纯前端视频监控大屏模板:原生三件套+ECharts实现可交付驾驶舱

纯前端视频监控大屏模板:原生三件套+ECharts实现可交付驾驶舱

简介:这是一份面向前端初学者与数据可视化爱好者的实战模板资源,聚焦视频监控场景下的大屏平台搭建。资源以HTML、CSS、JavaScript三大核心技术为骨架,演示如何用HTML构建大屏布局容器、用CSS实现全屏背景与响应式样式、用JavaScript驱动ECha…

2026/10/1 17:44:19 阅读更多 →
VS Code 选择快捷键实战:从单词选择到多光标批量修改

VS Code 选择快捷键实战:从单词选择到多光标批量修改

很多人问我提升编码效率的第一步该做什么,我的答案从来不是装一堆花里花哨的插件,而是先把"选择"这个动作练到位。在 VS Code 里,几乎所有编辑操作的前提都是先选中目标内容:复制要选、删除要选、重构要选、批量修改更要…

2026/10/1 17:44:19 阅读更多 →
GitHub热榜解析:WiFi感知与多Agent框架领衔开源趋势

GitHub热榜解析:WiFi感知与多Agent框架领衔开源趋势

1. 本期GitHub热点总览与趋势判断1.1 榜单上的四个关键信号今天这期GitHub开源项目日报,咱们先不急着报项目,我把这周的热榜数据从头到尾翻了一遍,几个信号非常明显。热度增速最快的当属WiFi感知(WiFi Sensing)方向&am…

2026/10/1 17:43:18 阅读更多 →

日新闻

我发现了一个新思路:用 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/1 0:00:30 阅读更多 →
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/1 0:00:30 阅读更多 →
黑夜航拍船只数据集训练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/1 1:01:17 阅读更多 →

周新闻

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解 【免费下载链接】spirula-studio Cross-vendor 3D Gaussian Splatting trainer - video to splat to mesh, Vulkan or CUDA. 项目地址: https://gitcode.com/GitHub_Trending/sp/spirula-studio Sp…

2026/9/30 13:14:22 阅读更多 →
SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南 模板网站太丑不够用?别急着加滤镜,那是治标不治本。很多老板盯着后台流量掉得眼红,却还在纠结首页Banner的圆角是不是3像素。这就像穿着西装去挖土,姿势不对,努力白费。我整理这份 速查手册…

2026/9/30 18:13:06 阅读更多 →
FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏

FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏

FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏 【免费下载链接】FireRed-OpenStoryline FireRed-OpenStoryline is an AI video editing agent that transforms manual editing into intention-driven directing through natural language …

2026/9/30 13:14:49 阅读更多 →

月新闻

我发现了一个新思路:用 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/1 0:00:30 阅读更多 →
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/1 0:00:30 阅读更多 →
黑夜航拍船只数据集训练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/1 1:01:17 阅读更多 →