news 2026/10/24 1:11:22

OpenCV形态学基础:腐蚀膨胀、开闭运算、梯度顶帽黑帽全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
OpenCV形态学基础:腐蚀膨胀、开闭运算、梯度顶帽黑帽全解析

图像形态学这套操作,我在实际项目里几乎每周都要碰。它不像深度学习那么玄乎,但用好了能解决一大批图像预处理问题:把扫描件里的噪点抹掉、把断裂的文字笔画接起来、把粘连的细胞分离开,甚至是从复杂背景里把划痕和凹坑单独揪出来。这篇就围绕OpenCV形态学的全套基础操作——腐蚀、膨胀、开运算、闭运算、形态学梯度、顶帽和黑帽,以及自定义核的构造方法,把它们一次性讲透。适合刚入门OpenCV的读者,也适合写了不少图像处理代码但对参数细节还拿不准的同行。

很多人一开始接触这些名词会觉得头大,七个操作,互相之间还有衍生关系。但本质上,腐蚀和膨胀是两个最底层的操作,其余开运算、闭运算、梯度、顶帽、黑帽,全部是由这两个操作叠加组合出来的。所以这篇文章会先把底层的腐蚀膨胀讲明白,再逐个拆解衍生的五种运算,最后专门讲核(结构元素)的用法和一个完整的实战案例。

1. 形态学到底在做什么:从"取小块比较"说起

1.1 形态学的基本思想

形态学(Morphology)这个词听起来挺学术,说白了就是"用一个小窗口遍历整张图,每次看窗口里的像素分布情况,然后决定把中心点的值改成什么"。这个小窗口就是核(Kernel),也叫结构元素(Structuring Element)。

你可以把核想象成一个移动的放大镜。放大镜移动到某个像素上时,能看到这个像素周围的邻居们。腐蚀和膨胀的区别,就是"看邻居时怎么下结论":

  • 腐蚀:取窗口里所有像素的最小值,作为中心点的新值。
  • 膨胀:取窗口里所有像素的最大值,作为中心点的新值。

如果图像是二值图,只有0(黑)和255(白)两种值,那逻辑就变得异常清晰:

  • 腐蚀时,只要窗口里有一个黑点,中心点就变黑。
  • 膨胀时,只要窗口里有一个白点,中心点就变白。

记住这句话,后面所有形态学运算的推导逻辑都有了。

1.2 为什么腐蚀会让白色区域变小

我见过不少人第一次看腐蚀效果图时很困惑:名字叫"腐蚀",但白色区域周围的黑边变宽了,白色本身在缩水。其实这正是"最小值"这个规则导致的。

想象一幅白底黑字的二值图。核移动到白色区域的边缘时,窗口里一半是白一半是黑,取最小值,中心点被判成黑,于是白色区域就向内部收缩了一圈。反过来,黑字区域由于取最小值依然还是黑,所以字体的形状没有本质变化,只是白色背景在"侵蚀"黑色像素的边缘。

在灰度图上理解也不难:取最小值意味着把中心点的值拉向窗口里最暗的那个像素。如果窗口经过一条亮的线条,只要线条没有把整个窗口填满,中心点亮值就会被周围暗值拉下去,结果亮线条变细甚至消失。

1.3 为什么膨胀让白色区域扩张

膨胀和腐蚀正好反过来,取最大值的结果是:只要窗口里有一个亮点,中心点就变成亮点。所以白色区域会向外扩张一圈,黑线会变细,断裂的亮线条会被接上。

小时候用蜡笔涂色,想给一个空心字母填满颜色,就在轮廓边缘反复描边,越描越宽。膨胀就是这样——把亮区域往外"描粗"。

这两个操作一"瘦"一"胖",用的时候必须记清楚:腐蚀瘦白,膨胀胖白。在二值图上这条规律百分之百成立,在灰度图上同样成立,只是视觉效果没二值图那么明显。

1.4 参数里那些细节:核大小、锚点和迭代次数

OpenCV里腐蚀膨胀的完整签名比很多人想象中要长:

cv2.erode(src, kernel, dst=None, anchor=None, iterations=1, borderType=None, borderValue=None) cv2.dilate(src, kernel, dst=None, anchor=None, iterations=1, borderType=None, borderValue=None)
  • kernel:决定窗口形状和大小。这个最关键,后面专门讲。
  • anchor:锚点,默认是核的中心点。修改锚点可以让腐蚀/膨胀的方向发生偏移,比如把锚点设到核的左上角,处理结果就会整体向某个方向偏移。99%的场景用默认值就够了,但知道有这个东西,遇到奇怪的偏移现象时心里有数。
  • iterations:反复执行的次数。很多人以为iterations=2等于用一个两倍大的核,这个说法在矩形核、正方形元素、二值图的简单场景下大致成立,但对椭圆核、十字核等形状,两者结果并不等价。反复腐蚀两次,每次都在上一次基础上取最小值,和一次性用一个更大的窗口取最小值,数学上只有矩形核才完全等价,其他形状都有差异。所以调参时,能调核大小就不堆iterations。
  • borderType:边界填充方式。默认是BORDER_CONSTANT,即把图像边界外当常数处理,腐蚀时边界外默认是最大值对应的黑色(0),膨胀时边界外默认是最小值对应的黑色(0)。这个细节很多人忽略,但在图像边缘处,腐蚀和膨胀的结果会受边界填充策略影响。想精确控制边缘行为时,就要关注borderType参数。

2. 腐蚀与膨胀的实操:代码、效果与选择逻辑

2.1 三行代码跑通腐蚀和膨胀

拿Python版的OpenCV举例,最基础的调用方式长这样:

import cv2 import numpy as np img = cv2.imread("input.jpg", cv2.IMREAD_GRAYSCALE) # 用一个5x5的矩形核 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) # 腐蚀 eroded = cv2.erode(img, kernel, iterations=1) # 膨胀 dilated = cv2.dilate(img, kernel, iterations=1) cv2.imwrite("eroded.jpg", eroded) cv2.imwrite("dilated.jpg", dilated)

C++的写法逻辑完全一致,只是把输入输出参数放在前面:

cv::Mat eroded, dilated; cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(5, 5)); cv::erode(img, eroded, kernel, cv::Point(-1, -1), 1); cv::dilate(img, dilated, kernel, cv::Point(-1, -1), 1);

C++里的Point(-1, -1)表示锚点取中心。Python接口里省略anchor参数时,OpenCV也默认用中心点。

2.2 场景选择的逻辑:什么时候腐蚀,什么时候膨胀

我在实际项目里总结的指导规则是:看白色目标要"去除"还是"连通"。

  • 如果白色目标周围有细小的白色噪点,想去除它们,就用腐蚀。噪点通常面积小,用合适的核腐蚀一遍,噪点会消失,而大目标主体还在。代价是目标本身也会瘦一圈。
  • 如果白色目标内部有黑色孔洞,或者目标之间有小缝隙想连接起来,就用膨胀。膨胀会把黑孔填小,把缝补上。代价是目标整体会胖一圈。

所以在做图像预处理时,我们几乎不会只用一次腐蚀或膨胀,而是组合使用:先去噪(腐蚀),再恢复尺寸(膨胀)——这就是下一节要讲的开运算。反过来先补洞再还原尺寸,就是闭运算。

2.3 实操中的三个典型教训

第一个教训:把灰度图直接丢进腐蚀函数,效果可能不理想。很多形态学教程的例子都是用完美的二值图,但真实项目里往往是灰度图。灰度图上腐蚀也能工作,但"取最小值"会把原图中较暗区域的信息彻底压掉,导致结果很"脏"。我的经验是,形态学之前,先根据图像特性决定到底用二值图还是灰度图。如果目标是轮廓分析,优先做阈值分割;如果目标是保留灰度梯度信息,那不要用腐蚀膨胀二值化场景的思路去套。

第二个教训:核的大小选择比数值本身更依赖图像分辨率。同样的5x5核,在1000x1000的图上是局部小操作,在100x100的缩略图上就可能把整个目标抹掉。通常我会先观察图像里需要保留的最小结构尺寸——文字笔画宽度、细胞直径、线条粗细——然后让核的尺寸略小于这个最小结构尺寸。

第三个教训:腐蚀膨胀处理的是"亮区域"还是"暗区域",会直接导致效果完全相反。如果你要处理的目标是黑色文字,腐蚀对于白色背景来说是"瘦白",也就等于"胖黑",结果是黑字变粗;膨胀反之,黑字变细。很多人看处理结果觉得"反了",其实是对亮暗身份的预设反了。所以在动手之前,先想清楚:你的前景到底是亮的还是暗的。

3. 开运算和闭运算:去噪声与填孔洞的两板斧

3.1 开运算:先腐蚀后膨胀,去掉前景噪声

开运算就是先腐蚀再膨胀,用同一颗核连续做两次操作。

opening = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)

C++同样:

cv::morphologyEx(img, opening, cv::MORPH_OPEN, kernel);

为什么这么做有效?先腐蚀,会把白色目标周围的小噪点、细小毛刺先去掉,目标整体变瘦;再膨胀,目标恢复到接近原来的尺寸。但是,那些已经被腐蚀步骤完全消灭掉的小噪点,在膨胀阶段不会再回来了,因为膨胀只放大现存区域,不能凭空产生一个已经被消除的区域。

我把开运算理解为"先瘦身再健身"。瘦身时把多余的赘肉(噪声毛刺)减掉,健身时恢复正常体型。赘肉已经没了,就不会再长出来。

读到这里你应该明白:开运算不能简单说成"去除亮噪声",它其实是在去除了小白色区域的前提下,尽量保留大白色目标的形状。

3.2 闭运算:先膨胀后腐蚀,填补暗部洞洞

闭运算和开运算完全对称:

closing = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)

先膨胀:白色区域变大,把内部的黑色孔洞填上,把邻近的白色碎片连接起来;再腐蚀:整体缩小,白区域恢复原尺寸。那些被膨胀阶段填掉的孔洞,在腐蚀阶段不会再重新打开。

闭运算最适合处理"目标内部有洞"的情况。比如二值化后的文字笔画中间有裂缝,或者一个圆形目标中心有个黑点,闭运算可以把这些瑕疵补干净。

3.3 开闭运算的效果对比表

操作顺序主要效果典型应用
开运算先腐蚀后膨胀去除白色前景上的小噪点、毛刺文档扫描去噪、指纹图去毛刺
闭运算先膨胀后腐蚀填补前景内部的孔洞、连接断裂区域文字笔画补全、字符分割前处理

有一个高频场景是OCR方向。做字符识别前,如果发现字符笔画有断裂,先闭运算补缝;如果字符边缘有噪声毛刺,纯字符区域外有小黑点,但你的前景是深色(黑字),就要对背景做开运算或者直接对二值图取反后做开运算。这个"前景到底是黑还是白"的判断,我见过太多人栽在这里。

3.4 核的选择对开闭运算的影响

开闭运算的核,大小和形状直接决定了"什么算噪声、什么算特征"。

还是拿OCR场景说:印刷体文字的笔画宽度如果是4像素,开运算用5x5矩形核时,会把笔画宽度只有2像素的细线给吃掉,因为腐蚀阶段无法保留宽度小于核尺寸的亮线。但用3x3核时,细线能存留下来。所以开运算核的尺寸必须小于最小笔画宽度,否则目标本身就成了"噪声",会被一并删除。

闭运算同理,核的尺寸必须大于孔洞的最大宽度,否则填不上。先测目标尺寸,再选核,永远是正确步骤。

4. 形态学梯度、顶帽与黑帽:三个容易被低估的工具

4.1 形态学梯度:边缘轮廓的提取利器

形态学梯度定义为膨胀结果减去腐蚀结果:

gradient = cv2.morphologyEx(img, cv2.MORPH_GRADIENT, kernel)

因为膨胀让白色区域变大一圈,腐蚀让白色区域变小一圈,两者相减,留下的就是边缘轮廓带。这个操作在对比强烈、边缘清晰的图像上特别省事,不用再额外算子找边缘。

它的价值和Canny边缘检测不一样。Canny是从梯度幅值和双阈值角度出发,做出来往往是单像素细边缘;形态学梯度做出来是框选整个目标轮廓的亮带,宽度受核大小影响。实际项目里,如果只想知道"物体大概在哪里",形态学梯度更快更稳。

4.2 顶帽运算:把亮背景上的小亮斑抠出来

顶帽运算是原图减去开运算结果:

tophat = cv2.morphologyEx(img, cv2.MORPH_TOPHAT, kernel)

回忆一下开运算:它会删除比核小的亮区域。所以"原图 —— 开运算结果"留下来的,恰好就是那些被开运算删掉的亮细节:细小亮斑点、亮线、亮划痕。

光照不均的场景里,顶帽是神器。比如工业检测中,一张金属表面图有均匀的漫反射,但表面刻了一道细微的亮划痕。直接用阈值分割很难做,因为整体亮度分布不均匀;先做顶帽,把划痕提取成独立的高亮前景,再配合二值化,检测就变得非常干净。

4.3 黑帽运算:把暗背景上的小暗斑抠出来

黑帽运算是闭运算结果减去原图:

blackhat = cv2.morphologyEx(img, cv2.MORPH_BLACKHAT, kernel)

闭运算会填掉前景里的暗色小孔洞,那"闭运算结果 - 原图"留下来的,就是那些被填上的暗色细节。典型应用是提取图像里的暗缺陷,比如金属表面的凹坑、焊接点的气孔、电路板上的暗色污点。

我看过一篇做药片缺陷检测的项目报告,检测药片表面的暗色凹坑,用的就是黑帽提取暗缺陷,省掉了大量手工特征工程。这类方法在工业视觉中非常多见。

4.4 顶帽和黑帽的互补关系与灰度图用法

顶帽和黑帽是互补的:前者提取亮细节,后者提取暗细节。同一张图,如果你不知道缺陷是亮还是暗,最简单的做法是把顶帽和黑帽的结果叠在一起看,或者分别做阈值后合并。

灰度图上这两个操作依然有效,因为灰度值的大小天然对应了亮度。复杂光照背景下,顶帽和黑帽不止在二值图上能用,直接对灰度图操作,往往能起到一种"局部背景减除"的效果。这是形态学里最被人低估的好处——不需要提前知道背景亮度分布,只要前景细节比核小,就能被分离出来。

5. 自定义核:从默认矩形到任意结构元素

5.1 最常用的三种核形状

OpenCV的getStructuringElement提供三种内置形状:

kernel_rect = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) kernel_ellipse = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) kernel_cross = cv2.getStructuringElement(cv2.MORPH_CROSS, (5, 5))

MORPH_RECT是全1矩阵,各方向的腐蚀/膨胀力度一致;MORPH_ELLIPSE是一个椭圆,四个角是0;MORPH_CROSS是十字形的1,只有横向和纵向中心线。

实际选择逻辑很简单:矩形核适合各向同性处理;十字核适合只关心水平垂直邻接关系的场景,比如街道文字识别时,只希望按水平和垂直方向扩展,不想让斜对角像素参与;椭圆核则介于两者之间,边缘更自然,不会把目标搞成方块。

5.2 手动构造核:直接用NumPy矩阵

getStructuringElement之外,OpenCV完全允许你手动构造核。毕竟核的本质就是一个二维数组:

custom_kernel = np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]], dtype=np.uint8) dilated = cv2.dilate(img, custom_kernel, iterations=1)

这个核只有上下左右四个方向和中心是1,斜角方向是0,相当于一个十字核。想设计更复杂的结构,比如一带线状的核、对角线方向的核,直接用NumPy写出来就行。

我实际做过一个车道线检测项目,车道线在图像里是斜向的亮线。如果用矩形核去膨胀,噪声会被强化到各个方向;我直接构造了一个倾斜45度的核,让膨胀只沿着车道方向扩张,效果立竿见影。这种场景就是把"核的形状"作为一种先验知识嵌入算法的典型例子。

5.3 核的值不一定非是0和1

在OpenCV里,核矩阵不限于二进制。给核赋予不同权重,腐蚀和膨胀的计算就会带上加权性质。虽然cv2.erode的核要求是单通道整数矩阵,但你可以传入非0/1的值,比如用中心权重更高的核,让窗口中心的像素在比较中占更大比重。这类高级用法在标准形态学里不常用,但遇到特殊场景时是一条可选的解决路径。

5.4 核的大小与迭代次数的关系再展开

前面说过,矩形核下,iterations=2近似等于核大小翻倍。实际测试中,对于5x5核迭代2次,效果接近9x9核迭代1次,但又不完全一致,原因是边界处的填充次数不同。迭代2次时,第一次腐蚀的边界结果会参与第二次运算,某些边缘细节会被多处理一层。

从性能角度,我更推荐增大核而不是增迭代次数。因为每次迭代都会生成一张中间图,多一次迭代就多一次全图遍历。而加大核虽然单次计算量变大,但只遍历一次,通常总时间反而小一些。当然在现代优化库下二者差距可能不大,但代码可读性和调参直觉上,我还是倾向直接调核。

6. 综合实战:从含噪图像到干净轮廓的完整流程

6.1 任务描述与处理流程设计

假设现在有一张印刷电路板的灰度图像,上面有导线、焊盘和微小的灰尘噪声。任务目标是提取出所有导线和焊盘的干净轮廓,同时尽量去掉噪声。

我的处理流程是:

  1. 读入灰度图,先做高斯模糊,降低随机噪声干扰。
  2. Otsu二值化,得到二值图。
  3. 开运算去除白色前景上的小噪声。
  4. 闭运算填补导线上的断裂缝隙。
  5. 形态学梯度提取轮廓。
  6. 再用连通域分析和面积过滤,去掉残余的小区域。

6.2 逐步代码与效果分析

import cv2 import numpy as np img = cv2.imread("pcb.jpg", cv2.IMREAD_GRAYSCALE) # 模糊降噪 blurred = cv2.GaussianBlur(img, (3, 3), 0) # Otsu二值化 _, binary = cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) # 观察前景是亮还是暗,这里假设前景是亮色 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) # 开运算去噪 opened = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel, iterations=1) # 闭运算补缝 closed = cv2.morphologyEx(opened, cv2.MORPH_CLOSE, kernel, iterations=2) # 形态学梯度 gradient = cv2.morphologyEx(closed, cv2.MORPH_GRADIENT, kernel)

这个流程里面每一步都有讲究。开运算用3x3核,是因为噪声颗粒尺寸多数在2-3像素以内,不能用大核,否则会把细导线一起干掉。闭运算用两次迭代,是因为导线断裂缝隙比较长,一次可能补不干净,但也别用大核去强行弥补,否则会把不该连接的相邻导线黏在一起。

做完梯度之后,还可以再做一个膨胀让轮廓线加粗,然后用cv2.findContours去提取。实际项目里,这个流程跑下来,目标轮廓质量远好于直接对原图做Canny。

6.3 参数调整的一张速查表

操作目的推荐核类型推荐操作
去除白色小噪点RECT开运算
填充前景内孔洞RECT闭运算
提取亮色小缺陷、划痕ELLIPSE顶帽
提取暗色小凹坑ELLIPSE黑帽
提取整体轮廓RECT梯度
沿特定方向连接断线CUSTOM膨胀

6.4 案例复盘:为什么开闭运算顺序不能乱

很多人问,开运算和闭运算能不能互换顺序?"先开再闭"和"先闭再开"结果一样吗?

不一样。开运算先删小亮噪声,闭运算再补洞;闭运算先补洞,可能把噪声洞和导线缝隙一起补上,再到开运算时,因为噪声区域和导线已经连成了一片,开运算删不掉它。所以"先开后闭"才是去噪补缝的正确顺序——先清外乱,再修内伤。

7. 常见问题与排查技巧:把踩过的坑一次列全

7.1 高频问题速查表

现象大概率原因排查与解决
腐蚀后目标直接消失核尺寸大于目标最小尺寸缩小核尺寸
膨胀后目标糊成一片核尺寸过大或迭代次数过多调小核,或改用开运算
开运算去不干净噪声噪声尺寸大于核增大核,但注意不要吃掉目标
闭运算不填洞洞宽度大于核尺寸增大核或增加迭代次数
顶帽结果几乎全黑前景细节可能比核大,或图像没有细小亮结构改黑帽,或缩小核并检查图像动态范围
形态学处理后图像边缘异常边界填充策略影响检查borderType参数
处理结果方向偏移锚点被修改确认anchor设置,恢复默认中心点

7.2 灰度图与彩色图的注意点

OpenCV的erode、dilate和morphologyEx都支持多通道图像,彩色图会逐通道做相同处理。但多数情况下,形态学操作前应该先转灰度图,因为形态学对亮度信息敏感,对彩色的色相信息不敏感。彩色图直接做形态学会出现颜色边缘伪影,而且计算量是灰度图的3倍。

如果必须在彩色图像上操作,常用的做法是转到HSV空间,只对V通道做形态学,再合并回原图。这个方案比直接对BGR三通道做操作要可靠得多。

7.3 性能优化与经验总结

处理大图时,形态学操作的时间主要花在全图遍历上。三个性能优化方向:

第一,能缩小图像就先缩小。很多形态学操作的目标是定位,不是精确测量,缩小后再做速度能提升数倍。第二,用灰度图工作,少用彩色图。第三,两三次迭代能完成的,不要用大核一次性做,但从时间和精度综合看,还是推荐先调核大小。

还要提一个很多人忽视的点:morphologyEx内部会自动为你分配中间图像,如果你的程序在循环里对每帧图像调用形态学操作,尽量提前用cv2.Mat或NumPy数组预分配结果图,减少反复分配内存的开销。在嵌入式设备上,这个优化有时能让帧率翻倍。

7.4 最后一条体会

形态学操作的精髓不是记住七个函数名,而是理解"核"在中间起的作用。开闭运算、梯度、顶帽、黑帽,差别只在组合顺序和相减方式,但这五种变化足以覆盖图像预处理里大半的规则噪声问题。我这些年做图像处理,最深刻的感受就是:遇到一张脏图,先别急着上深度学习模型,用形态学做几轮预处理,往往信噪比就已经高了很多。很多看起来需要复杂模型才能解决的问题,用几个基础算子加上合适的核,就能得到一个相当不错的结果。希望这篇能把你的底子打好,后面用起来会顺手得多。

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

多目标跟踪数据关联算法实战:NNDA/PDA/JPDA/IMM MATLAB详解

简介:本资源是一套完整的多目标跟踪数据关联算法MATLAB实现代码,面向信号处理、雷达跟踪与智能感知领域的研究生、算法工程师及科研人员,聚焦解决复杂环境下目标识别、误关联与模型切换等核心问题。代码涵盖NNDA(最近邻&#xff0…

作者头像 李华
网站建设 2026/10/24 1:09:45

基于AB32VG1的TWS蓝牙耳机开发:RISC-V评估板与RT-Thread实践

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

作者头像 李华
网站建设 2026/10/24 1:09:40

虚拟机快照使用全攻略:从原理到避坑实践

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

作者头像 李华
网站建设 2026/10/24 1:09:10

前端图片灯箱Lightbox实现方案与性能优化指南

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

作者头像 李华
网站建设 2026/10/24 1:07:11

Spring Boot项目Linux快速启动全流程:环境准备到systemd托管

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

作者头像 李华
网站建设 2026/10/24 1:06:43

Apache Calcite 深度解读:从 SQL 解析到优化器与适配器开发实战

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

作者头像 李华