双线性插值原理与C语言实现:从坐标映射到定点优化

简介:这份资源围绕双线性插值方法及C语言实现展开,面向图像处理、计算机图形学与数值计算方向的学习者和开发者,帮助理解二维平面数据插值的数学原理并落地为可运行代码。压缩包共31个文件,约1.86MB,以h头文件、cpp源文件、obj目标文件为主,辅以csv测试数据、txt说明文档及工程配置与资源文件,构成一套完整的C语言工程结构。已有2083人学习下载,说明其在插值算法入门与实践中具有一定参考价值。资源重点呈现双线性插值的权重计算与公式实现,涵盖网格点数据结构定义、二维数组数据组织、λx与λy权重求解、边界条件处理及结果写入等关键环节,并配有测试数据便于验证插值结果。读者可据此掌握从数学表达式到C代码的转化思路,理解加权平均与空间插值的对应关系,并借助工程文件快速编译调试,为图像缩放、网格平滑等实际场景提供可复用的实现参考。

1. 双线性插值到底在算什么:从一张被拉伸的图片说起

把一张 200×200 的图放大到 800×800,如果直接按最近邻取整,边缘会像狗牙一样一格一格跳。双线性插值就是来解决这个问题的:它不直接抄最近的像素,而是拿目标点周围四个原始像素做两次线性插值,算出一个过渡色。听起来简单,但真到 C 语言里手写一遍,坐标映射、边界处理、定点优化这几个坑一个都躲不掉。这份资源给的就是一套能直接编译运行的双线性插值实现,适合正在做图像缩放、嵌入式显示、或者想搞懂插值底层原理的开发者。下面我按“它怎么算 → 怎么跑 → 怎么改”的顺序拆一遍。

2. 核心原理拆解:四个像素怎么算出目标点

2.1 从一维线性插值推到二维

线性插值在一维上就是已知两端点,求中间某位置的值。公式很直白:给定 x0、x1 和对应的 y0、y1,位置 x 处的值 y = y0 + (x - x0) * (y1 - y0) / (x1 - x0)。双线性插值把这个过程做了两次:先在水平方向对上下两行各插一次,得到两个中间值,再在垂直方向对这两个中间值插一次。

假设目标点落在原始图像的 (x, y),它的四个邻居是 (x0,y0)、(x1,y0)、(x0,y1)、(x1,y1),其中 x1 = x0+1,y1 = y0+1。先算上面一行:top = Q00 + (x - x0) * (Q10 - Q00);再算下面一行:bottom = Q01 + (x - x0) * (Q11 - Q01);最后垂直插值:result = top + (y - y0) * (bottom - top)。展开后就是那个常见的加权和公式,四个权重分别是 (1-dx)(1-dy)、dx(1-dy)、(1-dx)dy、dx·dy,加起来正好等于 1。

这里有个容易忽略的点:双线性插值不是“双线性”在数学意义上的双线性函数,它其实是两个线性函数的复合,对 x 和 y 分别线性。理解这一点,后面看代码里的权重计算就不会绕。

2.2 坐标映射:目标像素对应回原图的哪个位置

缩放的核心是建立目标图和原图之间的坐标对应关系。设原图宽 srcW、高 srcH,目标图宽 dstW、高 dstH,目标图上某像素 (dx, dy) 对应原图的位置是:

// 目标坐标 -> 原图坐标的映射 // 用 (dst + 0.5) 而不是 dst,是为了让像素中心对齐,避免整体偏移半个像素 float srcX = (dx + 0.5f) * srcW / dstW - 0.5f; float srcY = (dy + 0.5f) * srcH / dstH - 0.5f;

这个 +0.5 再 -0.5 的写法是图像缩放里的标准做法。如果直接用 dx * srcW / dstW,放大时图像会整体往左上偏半个像素,肉眼看不明显,但做图像配准或者多级缩放时会累积误差。我第一次写的时候就漏了这个,结果放大后的图和原图叠加总是差一点,查了半天才定位到坐标映射。

算出 srcX、srcY 后,取整得到左上角整数坐标 x0 = (int)srcX,y0 = (int)srcY,小数部分 dx_frac = srcX - x0,dy_frac = srcY - y0,这两个小数就是插值权重。

2.3 边界处理:越界的那一行一列怎么办

当 srcX 或 srcY 落在图像最右边或最下边时,x0+1 或 y0+1 会超出范围。常见处理有三种:一是把坐标钳制到 [0, srcW-1],越界时直接取边界像素;二是对边界像素做复制延拓;三是缩小映射范围,让目标图边缘不映射到原图边缘之外。

这份实现用的是钳制法,代码里体现为:

// 钳制坐标,防止访问越界 int x0 = (int)srcX; int y0 = (int)srcY; int x1 = x0 + 1; int y1 = y0 + 1; if (x0 < 0) x0 = 0; if (y0 < 0) y0 = 0; if (x1 >= srcW) x1 = srcW - 1; if (y1 >= srcH) y1 = srcH - 1;

钳制的好处是实现简单、不会崩,代价是边缘几个像素的插值精度略降。如果做高质量缩放,更讲究的做法是先把 srcX 限制在 [0, srcW-1] 范围内,再取整,这样 x1 最多等于 srcW-1,不会越界。两种写法结果接近,但后者少几次分支判断,在嵌入式场景下更省。

3. 代码落地:从灰度图到 RGB 的完整实现

3.1 灰度图版本:先把主流程跑通

灰度图每个像素一个字节,逻辑最干净,适合先验证算法。下面是一个完整的缩放函数:

#include <stdio.h> #include <stdlib.h> #include <math.h> // 灰度图双线性插值缩放 // src: 原图数据, srcW/srcH: 原图宽高 // dst: 目标图缓冲区, dstW/dstH: 目标宽高 void bilinear_gray(const unsigned char *src, int srcW, int srcH, unsigned char *dst, int dstW, int dstH) { for (int dy = 0; dy < dstH; dy++) { for (int dx = 0; dx < dstW; dx++) { // 1. 坐标映射,+0.5/-0.5 保证像素中心对齐 float srcX = (dx + 0.5f) * srcW / dstW - 0.5f; float srcY = (dy + 0.5f) * srcH / dstH - 0.5f; // 2. 取四个邻居的整数坐标 int x0 = (int)floorf(srcX); int y0 = (int)floorf(srcY); int x1 = x0 + 1; int y1 = y0 + 1; // 3. 钳制边界 if (x0 < 0) x0 = 0; if (y0 < 0) y0 = 0; if (x1 >= srcW) x1 = srcW - 1; if (y1 >= srcH) y1 = srcH - 1; // 4. 计算小数权重 float fx = srcX - floorf(srcX); float fy = srcY - floorf(srcY); if (fx < 0) fx = 0; if (fy < 0) fy = 0; // 5. 取四个像素值 float v00 = src[y0 * srcW + x0]; float v10 = src[y0 * srcW + x1]; float v01 = src[y1 * srcW + x0]; float v11 = src[y1 * srcW + x1]; // 6. 两次水平插值 + 一次垂直插值 float top = v00 + (v10 - v00) * fx; float bottom = v01 + (v11 - v01) * fx; float value = top + (bottom - top) * fy; // 7. 四舍五入写回 dst[dy * dstW + dx] = (unsigned char)(value + 0.5f); } } }

逐段看:坐标映射那两行决定了缩放比例,srcW/dstW 是缩放因子;floorf 取整得到左上角,fx、fy 是插值权重;四个像素值取出来后,先水平后垂直,顺序不影响结果。最后 +0.5 再截断是四舍五入的常用写法,比 roundf 快,在批量处理时差别明显。

参数上,src 和 dst 都是行优先存储的一维数组,调用者负责分配内存。srcW、srcH、dstW、dstH 必须为正,否则除零。这个函数没有做参数校验,实际项目里建议在入口加一层断言。

3.2 RGB 版本:三个通道分别插值

彩色图每个像素三个字节,插值时要对 R、G、B 分别做同样的计算。最直接的做法是把灰度版本套三层循环,但那样代码重复。更好的方式是把通道数作为参数:

// 通用多通道双线性插值 // channels: 通道数,灰度=1,RGB=3,RGBA=4 void bilinear_multi(const unsigned char *src, int srcW, int srcH, int channels, unsigned char *dst, int dstW, int dstH) { for (int dy = 0; dy < dstH; dy++) { for (int dx = 0; dx < dstW; dx++) { float srcX = (dx + 0.5f) * srcW / dstW - 0.5f; float srcY = (dy + 0.5f) * srcH / dstH - 0.5f; int x0 = (int)floorf(srcX); int y0 = (int)floorf(srcY); int x1 = x0 + 1; int y1 = y0 + 1; if (x0 < 0) x0 = 0; if (y0 < 0) y0 = 0; if (x1 >= srcW) x1 = srcW - 1; if (y1 >= srcH) y1 = srcH - 1; float fx = srcX - floorf(srcX); float fy = srcY - floorf(srcY); if (fx < 0) fx = 0; if (fy < 0) fy = 0; // 对每个通道独立插值 for (int c = 0; c < channels; c++) { float v00 = src[(y0 * srcW + x0) * channels + c]; float v10 = src[(y0 * srcW + x1) * channels + c]; float v01 = src[(y1 * srcW + x0) * channels + c]; float v11 = src[(y1 * srcW + x1) * channels + c]; float top = v00 + (v10 - v00) * fx; float bottom = v01 + (v11 - v01) * fx; float value = top + (bottom - top) * fy; dst[(dy * dstW + dx) * channels + c] = (unsigned char)(value + 0.5f); } } } }

索引计算从y * srcW + x变成(y * srcW + x) * channels + c,这是交错存储(interleaved)格式的标准寻址。如果图像是按平面存储(planar),比如 YUV420,寻址方式完全不同,需要每个通道单独传指针。这份实现针对的是交错格式,也是 BMP、PNG 解码后最常见的布局。

channels 参数让同一个函数能处理灰度、RGB、RGBA,避免了复制粘贴。代价是内层多了一次循环和乘法,编译器一般能优化得不错,但在极端性能场景下,把 channels 固定为 3 手动展开会更快。

3.3 编译与运行:一个最小可跑的例子

把上面的函数放进一个 main 里,生成一张测试图并缩放:

int main(void) { int srcW = 4, srcH = 4; int dstW = 8, dstH = 8; // 构造一张 4x4 灰度测试图,值从 0 到 255 递增 unsigned char *src = malloc(srcW * srcH); for (int i = 0; i < srcW * srcH; i++) src[i] = (unsigned char)(i * 255 / (srcW * srcH - 1)); unsigned char *dst = malloc(dstW * dstH); bilinear_gray(src, srcW, srcH, dst, dstW, dstH); // 打印结果,检查插值是否平滑 for (int y = 0; y < dstH; y++) { for (int x = 0; x < dstW; x++) printf("%3d ", dst[y * dstW + x]); printf("\n"); } free(src); free(dst); return 0; }

编译命令:

gcc -O2 -o bilinear bilinear.c -lm ./bilinear

-lm是因为用了 floorf,需要链接数学库。-O2打开优化,浮点运算会快不少。运行后能看到 8×8 的输出值从 0 平滑过渡到 255,没有跳变,说明插值生效了。

如果想验证正确性,可以拿一张实际图片,用 Python 的 PIL 做同样的缩放,逐像素对比。允许有 ±1 的误差,因为四舍五入方式可能不同。差异超过 2 就说明坐标映射或边界处理有问题。

4. 避坑与排查:双线性插值最容易翻车的几个点

4.1 图像整体偏移半个像素

现象:放大后的图和原图叠加,边缘总是对不齐,像是整体平移了一点。原因:坐标映射时用了dx * srcW / dstW而没有做像素中心对齐。解决:改成(dx + 0.5) * srcW / dstW - 0.5。这个坑在图像拼接、多分辨率金字塔里特别明显,单看一张图不容易发现。

4.2 边缘出现黑边或异常值

现象:缩放后图像最右列或最下行出现纯黑或者颜色突变。原因:x1 或 y1 越界,读到了缓冲区之外的内存,或者读到了相邻行的数据。解决:在取四个像素之前钳制 x0、x1、y0、y1 到合法范围。更稳妥的做法是在函数入口断言srcW > 0 && srcH > 0 && dstW > 0 && dstH > 0,并在调试版本里加边界检查。

4.3 缩小图像时出现摩尔纹或混叠

现象:把大图缩小后,细密纹理变成奇怪的波纹。原因:双线性插值在缩小时只采样了四个邻居,没有做低通滤波,高频信息被混叠。解决:缩小倍数较大时,先用盒式滤波或高斯滤波做预模糊,再插值。或者改用面积平均(area averaging),它对缩小更合适。双线性插值更适合放大,缩小场景要额外处理。

4.4 浮点性能不达预期

现象:在嵌入式设备上跑 1080P 缩放,帧率只有个位数。原因:每个像素都做浮点乘除和 floorf,运算量大。解决:把坐标映射改成定点整数运算,用移位代替除法。比如把缩放因子放大 16 位,用整数乘加算权重。另一个办法是预计算每一行的 x0、x1、fx,因为同一行内这些值只跟 dx 有关,可以提前算好存数组,避免重复计算。

4.5 通道顺序搞反导致颜色错乱

现象:RGB 图缩放后红蓝互换。原因:源数据是 BGR 格式(比如某些 BMP 或 OpenCV 默认),但代码按 RGB 解析。解决:确认输入数据的通道顺序,或者在插值前做一次通道交换。这个坑在跨平台移植时最常见,Windows 的 BMP 是 BGR,而 PNG 解码库通常输出 RGB。

5. 二次开发:定点优化与 SIMD 加速的改造思路

把浮点版本改成定点版本,是嵌入式场景下最实用的改造。核心思路是把所有小数权重放大成整数,用乘法和移位代替浮点运算。假设用 8 位小数精度,缩放因子放大 256 倍:

// 定点版坐标映射,SCALE = 256 #define SCALE 256 int srcX_fixed = ((dx * 2 + 1) * srcW * SCALE / dstW - SCALE) / 2; int x0 = srcX_fixed >> 8; // 整数部分 int fx = srcX_fixed & 0xFF; // 小数部分,0~255 // 插值用整数乘加 int top = (v00 * (SCALE - fx) + v10 * fx) >> 8; int bottom = (v01 * (SCALE - fx) + v11 * fx) >> 8; int value = (top * (SCALE - fy) + bottom * fy) >> 8;

这样每个像素只有整数乘法和移位,在 ARM Cortex-M 这类没有 FPU 的芯片上能快 5 到 10 倍。精度损失在 8 位精度下肉眼不可见,如果要求更高可以提到 12 位或 16 位,代价是中间结果要用 32 位甚至 64 位整数防止溢出。

再进一步是 SIMD 加速。x86 平台可以用 SSE 的_mm_mullo_epi16和_mm_mulhi_epi16一次处理 8 个像素;ARM 平台用 NEON 的vmull_s16。改造时把内层循环按 8 或 16 个像素分组,权重广播到向量寄存器,四个邻居的像素值用 gather 或者预先转置。这一步复杂度较高,建议先用定点版跑通,确认性能瓶颈确实在插值上,再上 SIMD。

验证改造是否正确,我一般会保留一份浮点参考实现,用随机生成的图像做逐像素对比,误差超过 1 就停下来查。这个习惯帮我省了很多次“看起来对了但实际有细微偏差”的返工。从那以后我每次做数值优化,都强制走一遍参考对比,希望这个思路也能帮到你。

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

相关新闻

嵌入式面试被深挖底层细节?技术负责人关注的四个维度与应对

嵌入式面试被深挖底层细节?技术负责人关注的四个维度与应对

1. 为什么简历很漂亮&#xff0c;面试官却盯着底层细节不放这几年我经常遇到一种情况&#xff1a;候选人的简历写得相当漂亮&#xff0c;项目经验一条比一条硬&#xff0c;用到的技术栈也紧跟行业趋势&#xff0c;可一到技术负责人的面试环节&#xff0c;就变成了大型翻车现场。…

2026/10/20 13:00:55 阅读更多 →
顺序队列、链式队列与约瑟夫环的C++实现与避坑指南

顺序队列、链式队列与约瑟夫环的C++实现与避坑指南

队列这个结构&#xff0c;刚学数据结构那会儿我觉得它比栈简单——不就是先进先出嘛&#xff0c;能有多难。结果第一次用数组手写循环队列&#xff0c;被取模运算绕得头晕&#xff1b;第一次用链表实现队列&#xff0c;指针操作写反了导致内存泄漏&#xff1b;第一次做约瑟夫环…

2026/10/20 12:59:53 阅读更多 →
REA模型实战:用事件驱动解决业务与财务数据不一致

REA模型实战:用事件驱动解决业务与财务数据不一致

去年年中&#xff0c;我在梳理"模拟项目X"的订单履约链路时&#xff0c;被一个简单的问题卡住了&#xff1a;财务说账上还有120万应收账款&#xff0c;业务说早该催款的只有80万&#xff0c;两边数字对不上。系统里明明都用了同一套订单数据&#xff0c;为什么算出来…

2026/10/20 12:59:53 阅读更多 →

最新新闻

智能驾驶感知算法岗面试高频考点与实战指南

智能驾驶感知算法岗面试高频考点与实战指南

智能驾驶感知算法岗的面试&#xff0c;跟我平时帮人做模拟面试时看到的准备方式&#xff0c;几乎不在一个频道上。很多人拿到一份八股题集就开始背&#xff1a;Transformer结构、Focal Loss公式、NMS流程、BEV感知演进……背得滚瓜烂熟&#xff0c;结果一进面试间&#xff0c;被…

2026/10/20 13:38:46 阅读更多 →
Hyper-Laplacian先验快速去卷积:从原理到工程实现

Hyper-Laplacian先验快速去卷积:从原理到工程实现

简介&#xff1a;这份资源是图像去模糊方向经典论文《Fast Image Deconvolution using Hyper-Laplacian Priors》的配套Matlab实现&#xff0c;面向从事图像恢复、计算摄影与视觉研究的学生和开发者&#xff0c;用于解决模糊图像复原中卷积核未知、边缘细节难以保持的问题。压缩…

2026/10/20 13:38:46 阅读更多 →
TensorFlow从零手写ResNet18:80行代码详解残差连接与BasicBlock实现

TensorFlow从零手写ResNet18:80行代码详解残差连接与BasicBlock实现

1. 为什么80行代码值得认真拆解ResNet18这个网络结构&#xff0c;搞深度学习的人几乎没有不知道的。2015年那篇残差学习的论文出来之后&#xff0c;基本上把深层网络训练难的问题给解决了。但很多人第一次接触ResNet&#xff0c;要么是直接调tf.keras.applications.ResNet50&am…

2026/10/20 13:38:46 阅读更多 →
Hyper-Laplacian先验快速去卷积:查找表加速非凸优化

Hyper-Laplacian先验快速去卷积:查找表加速非凸优化

简介&#xff1a;这份资源是图像去模糊领域经典论文《Fast Image Deconvolution using Hyper-Laplacian Priors》的配套Matlab实现&#xff0c;面向从事图像恢复、计算摄影与视觉研究的学生和开发者&#xff0c;用于解决模糊图像复原及盲去卷积中卷积核与清晰图像联合估计的问题…

2026/10/20 13:38:46 阅读更多 →
10月arXiv机器学习论文盘点:强化学习、长尾分布与数据效率

10月arXiv机器学习论文盘点:强化学习、长尾分布与数据效率

1. 另类入口&#xff1a;当arXiv变成一座每周更新的森林如果说学术搜索像一张精准的地图&#xff0c;那arXiv的cs.LG板块就更像一片每周定时更新的森林——你永远知道自己能在这里找到最新的机器学习论文&#xff0c;但也正因为"最新"这两个字&#xff0c;每次走进来…

2026/10/20 13:38:46 阅读更多 →
AI编程工具进化:从代码补全到能独立交付项目的“许愿机”

AI编程工具进化:从代码补全到能独立交付项目的“许愿机”

说实话&#xff0c;我已经不太想用“Coding Agent”来称呼手上这批AI开发工具了。不是矫情&#xff0c;是最近连续几周用它们做真正的项目——不是教它写个冒泡排序或者生成一段样板代码&#xff0c;而是从需求梳理、数据结构设计到部署脚本、线上监控告警的完整链路走下来——…

2026/10/20 13:37:45 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

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

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

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

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

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

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 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/20 10:02:08 阅读更多 →
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/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练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/20 9:50:45 阅读更多 →