图像修补

你们大多数人家里都会有一些旧的旧化照片,上面有黑点,一些笔触等。你是否曾经想过将其还原?我们不能简单地在绘画工具中擦除它们,因为它将简单地用白色结构代替黑色结构,这是没有用的。在这些情况下,将使用一种称为图像修复的技术。基本思想很简单:用附近的像素替换那些不良区域,使其看起来和邻近的协调。考虑下面显示的图像(摘自Wikipedia):

基于此目的设计了几种算法,OpenCV提供了其中两种。 两者都可以通过相同的函数进行访问,cv.inpaint()

第一种算法基于Alexandru Telea在2004年发表的论文“基于快速行进方法的图像修补技术”。它基于快速行进方法。考虑图像中要修复的区域。算法从该区域的边界开始,并进入该区域内部,首先逐渐填充边界中的所有内容。在要修复的邻域上的像素周围需要一个小的邻域。该像素被附近所有已知像素的归一化加权总和所代替。权重的选择很重要。那些位于该点附近,边界法线附近的像素和那些位于边界轮廓线上的像素将获得更大的权重。修复像素后,将使用快速行进方法将其移动到下一个最近的像素。FMM确保首先修复已知像素附近的那些像素,以便像手动启发式操作一样工作。通过使用标志**cv.INPAINT_TELEA**启用此算法。

第二种算法基于Bertalmio,Marcelo,Andrea L. Bertozzi和Guillermo Sapiro在2001年发表的论文“ Navier-Stokes,流体动力学以及图像和视频修补”。该算法基于流体动力学并利用了 偏微分方程。基本原理是启发式的。它首先沿着边缘从已知区域移动到未知区域(因为边缘是连续的)。它延续了等距线(线连接具有相同强度的点,就像轮廓线连接具有相同高程的点一样),同时在修复区域的边界匹配梯度矢量。为此,使用了一些流体动力学方法。获得它们后,将填充颜色以减少该区域的最小差异。通过使用标志**cv.INPAINT_NS**启用此算法。

代码

我们需要创建一个与输入图像大小相同的掩码,其中非零像素对应于要修复的区域。其他一切都很简单。我的图像因一些黑色笔画而旧化(我手动添加了)。我使用“绘画”工具创建了相应的笔触。

import numpy as np
import cv2 as cv
img = cv.imread('messi_2.jpg')
mask = cv.imread('mask2.png',0)
dst = cv.inpaint(img,mask,3,cv.INPAINT_TELEA)
cv.imshow('dst',dst)
cv.waitKey(0)
cv.destroyAllWindows()

请参阅下面的结果。第一张图片显示了降级的输入。第二个图像是掩码。第三个图像是第一个算法的结果,最后一个图像是第二个算法的结果。

### 附加资源 1. Bertalmio, Marcelo, Andrea L. Bertozzi, and Guillermo Sapiro. “Navier-stokes, fluid dynamics, and image and video inpainting.” In Computer Vision and Pattern Recognition, 2001. CVPR 2001. Proceedings of the 2001 IEEE Computer Society Conference on, vol. 1, pp. I-355. IEEE, 2001. 2. Telea, Alexandru. “An image inpainting technique based on the fast marching method.” Journal of graphics tools 9.1 (2004): 23-34.

练习

  1. OpenCV一个有关修复的交互式示例,samples/python/inpaint.py,请尝试一下。
#!/usr/bin/env python

'''
Inpainting sample.
Inpainting repairs damage to images by floodfilling
the damage with surrounding image areas.
Usage:
  inpaint.py [<image>]
Keys:
  SPACE - inpaint
  r     - reset the inpainting mask
  ESC   - exit
'''

# Python 2/3 compatibility
from __future__ import print_function

import numpy as np
import cv2 as cv

from common import Sketcher

def main():
    import sys
    try:
        fn = sys.argv[1]
    except:
        fn = 'fruits.jpg'

    img = cv.imread(cv.samples.findFile(fn))
    if img is None:
        print('Failed to load image file:', fn)
        sys.exit(1)

    img_mark = img.copy()
    mark = np.zeros(img.shape[:2], np.uint8)
    sketch = Sketcher('img', [img_mark, mark], lambda : ((255, 255, 255), 255))

    while True:
        ch = cv.waitKey()
        if ch == 27:
            break
        if ch == ord(' '):
            res = cv.inpaint(img_mark, mark, 3, cv.INPAINT_TELEA)
            cv.imshow('inpaint', res)
        if ch == ord('r'):
            img_mark[:] = img
            mark[:] = 0
            sketch.show()

    print('Done')


if __name__ == '__main__':
    print(__doc__)
    main()
    cv.destroyAllWindows()

图像去噪

通常认为噪声是零均值的随机变量。考虑一个有噪声的像素,p=p0+n,其中p0是像素的真实值,n是该像素中的噪声。你可以从不同的图像中获取大量相同的像素(例如N)并计算其平均值。理想情况下,由于噪声的平均值为零,因此应该得到p=p0。

你可以通过简单的设置自己进行验证。将静态相机固定在某个位置几秒钟。这将为你提供很多帧或同一场景的很多图像。然后编写一段代码,找到视频中所有帧的平均值(这对你现在应该太简单了)。 比较最终结果和第一帧。你会看到噪声减少。不幸的是,这种简单的方法对摄像机和场景的运动并不稳健。通常,只有一张嘈杂的图像可用。

因此想法很简单,我们需要一组相似的图像来平均噪声。考虑图像中的一个小窗口(例如5×5窗口)。 很有可能同一修补程序可能位于图像中的其他位置。有时在它周围的一个小社区中。一起使用这些相似的补丁并找到它们的平均值怎么办?对于那个特定的窗口,这很好。请参阅下面的示例图片:

图像中的蓝色补丁看起来很相似。绿色补丁看起来很相似。因此,我们获取一个像素,在其周围获取一个小窗口,在图像中搜索相似的窗口,对所有窗口求平均,然后用得到的结果替换该像素。此方法是“非本地均值消噪”。与我们之前看到的模糊技术相比,它花费了更多时间,但是效果非常好。更多信息和在线演示可在其他资源的第一个链接中找到。

对于彩色图像,图像将转换为CIELAB色彩空间,然后分别对L和AB分量进行降噪。

OpenCV中的图像去噪

OpenCV提供了此方法的四个变体。

  1. cv.fastNlMeansDenoising()-处理单个灰度图像
  2. cv.fastNlMeansDenoisingColored()-处理彩色图像。
  3. cv.fastNlMeansDenoisingMulti()-处理在短时间内捕获的图像序列(灰度图像)
  4. cv.fastNlMeansDenoisingColoredMulti()-与上面相同,但用于彩色图像。

常用参数为: – h:决定滤波器强度的参数。较高的h值可以更好地消除噪点,但同时也可以消除图像细节。(可以设为10) – hForColorComponents:与h相同,但仅用于彩色图像。(通常与h相同) – templateWindowSize:应为奇数。(建议设为7) – searchWindowSize:应为奇数。(建议设为21)

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('die.png')
dst = cv.fastNlMeansDenoisingColored(img,None,10,10,7,21)
plt.subplot(121),plt.imshow(img)
plt.subplot(122),plt.imshow(dst)
plt.show()
  1. cv.fastNlMeansDenoisingMulti() 现在,我们将对视频应用相同的方法。第一个参数是噪声帧列表。第二个参数imgToDenoiseIndex指定我们需要去噪的帧,为此,我们在输入列表中传递帧的索引。第三是temporalWindowSize,它指定要用于降噪的附近帧的数量。应该很奇怪。在那种情况下,总共使用temporalWindowSize帧,其中中心帧是要被去噪的帧。例如,你传递了一个5帧的列表作为输入。令imgToDenoiseIndex = 2,temporalWindowSize =3。然后使用frame-1frame-2frame-3去噪frame-2。让我们来看一个例子。
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
cap = cv.VideoCapture('vtest.avi')
# 创建5个帧的列表
img = [cap.read()[1] for i in xrange(5)]
# 将所有转化为灰度
gray = [cv.cvtColor(i, cv.COLOR_BGR2GRAY) for i in img]
# 将所有转化为float64
gray = [np.float64(i) for i in gray]
# 创建方差为25的噪声
noise = np.random.randn(*gray[1].shape)*10
# 在图像上添加噪声
noisy = [i+noise for i in gray]
# 转化为unit8
noisy = [np.uint8(np.clip(i,0,255)) for i in noisy]
# 对第三帧进行降噪
dst = cv.fastNlMeansDenoisingMulti(noisy, 2, 5, None, 4, 7, 35)
plt.subplot(131),plt.imshow(gray[2],'gray')
plt.subplot(132),plt.imshow(noisy[2],'gray')
plt.subplot(133),plt.imshow(dst,'gray')
plt.show()

计算需要花费大量时间。结果,第一个图像是原始帧,第二个是噪声帧,第三个是去噪图像。

附加资源

  1. http://www.ipol.im/pub/art/2011/bcm_nlm/ (它包含详细信息,在线演示等。强烈建议访问。我们的测试图像是从此链接生成的)
  2. Online course at coursera (这里拍摄的第一张图片)

opencv 视频对象追踪

1、背景分离方法

  • 背景分离(BS)是一种通过使用静态相机来生成前景掩码(即包含属于场景中的移动对象像素的二进制图像)的常用技术。
  • 顾名思义,BS计算前景掩码,在当前帧与背景模型之间执行减法运算,其中包含场景的静态部分,或者更一般而言,考虑到所观察场景的特征,可以将其视为背景的所有内容。

背景建模包括两个主要步骤: 1. 背景初始化; 2. 背景更新。

第一步,计算背景的初始模型,而在第二步中,更新模型以适应场景中可能的变化。

在本教程中,我们将学习如何使用OpenCV中的BS。

目标

在本教程中,您将学习如何: 1. 使用**cv::VideoCapture**从视频或图像序列中读取数据; 2. 通过使用**cv::BackgroundSubtractor**类创建和更新背景类; 3. 通过使用**cv::imshow**获取并显示前景蒙版;

代码

在下面,您可以找到源代码。我们将让用户选择处理视频文件或图像序列。在此示例中,我们将使用**cv::BackgroundSubtractorMOG2**生成前景掩码。

结果和输入数据将显示在屏幕上。

from __future__ import print_function
import cv2 as cv
import argparse
parser = argparse.ArgumentParser(description='This program shows how to use background subtraction methods provided by \
                                              OpenCV. You can process both videos and images.')
parser.add_argument('--input', type=str, help='Path to a video or a sequence of image.', default='vtest.avi')
parser.add_argument('--algo', type=str, help='Background subtraction method (KNN, MOG2).', default='MOG2')
args = parser.parse_args()
if args.algo == 'MOG2':
    backSub = cv.createBackgroundSubtractorMOG2()
else:
    backSub = cv.createBackgroundSubtractorKNN()
capture = cv.VideoCapture(cv.samples.findFileOrKeep(args.input))
if not capture.isOpened:
    print('Unable to open: ' + args.input)
    exit(0)
while True:
    ret, frame = capture.read()
    if frame is None:
        break

    fgMask = backSub.apply(frame)


    cv.rectangle(frame, (10, 2), (100,20), (255,255,255), -1)
    cv.putText(frame, str(capture.get(cv.CAP_PROP_POS_FRAMES)), (15, 15),
               cv.FONT_HERSHEY_SIMPLEX, 0.5 , (0,0,0))


    cv.imshow('Frame', frame)
    cv.imshow('FG Mask', fgMask)

    keyboard = cv.waitKey(30)
    if keyboard == 'q' or keyboard == 27:
        break

2、Meanshift和Camshift

Meanshift

Meanshift背后的直觉很简单,假设你有点的集合。(它可以是像素分布,例如直方图反投影)。你会得到一个小窗口(可能是一个圆形),并且必须将该窗口移到最大像素密度(或最大点数)的区域。如下图所示:

初始窗口以蓝色圆圈显示,名称为“C1”。其原始中心以蓝色矩形标记,名称为“C1_o”。但是,如果找到该窗口内点的质心,则会得到点“C1_r”(标记为蓝色小圆圈),它是窗口的真实质心。当然,它们不匹配。因此,移动窗口,使新窗口的圆与上一个质心匹配。再次找到新的质心。很可能不会匹配。因此,再次移动它,并继续迭代,以使窗口的中心及其质心落在同一位置(或在很小的期望误差内)。因此,最终您获得的是一个具有最大像素分布的窗口。它带有一个绿色圆圈,名为“C2”。正如您在图像中看到的,它具有最大的点数。整个过程在下面的静态图像上演示:

因此,我们通常会传递直方图反投影图像和初始目标位置。当对象移动时,显然该移动会反映在直方图反投影图像中。结果,meanshift算法将窗口移动到最大密度的新位置。

OpenCV中的Meanshift

要在OpenCV中使用meanshift,首先我们需要设置目标,找到其直方图,以便我们可以将目标反投影到每帧上以计算均值偏移。我们还需要提供窗口的初始位置。对于直方图,此处仅考虑色相。另外,为避免由于光线不足而产生错误的值,可以使用**cv.inRange**()函数丢弃光线不足的值。

import numpy as np
import cv2 as cv
import argparse
parser = argparse.ArgumentParser(description='This sample demonstrates the meanshift algorithm. \
                                              The example file can be downloaded from: \
                                              https://www.bogotobogo.com/python/OpenCV_Python/images/mean_shift_tracking/slow_traffic_small.mp4')
parser.add_argument('image', type=str, help='path to image file')
args = parser.parse_args()
cap = cv.VideoCapture(args.image)
# 视频的第一帧
ret,frame = cap.read()
# 设置窗口的初始位置
x, y, w, h = 300, 200, 100, 50 # simply hardcoded the values
track_window = (x, y, w, h)
# 设置初始ROI来追踪
roi = frame[y:y+h, x:x+w]
hsv_roi =  cv.cvtColor(roi, cv.COLOR_BGR2HSV)
mask = cv.inRange(hsv_roi, np.array((0., 60.,32.)), np.array((180.,255.,255.)))
roi_hist = cv.calcHist([hsv_roi],[0],mask,[180],[0,180])
cv.normalize(roi_hist,roi_hist,0,255,cv.NORM_MINMAX)
# 设置终止条件,可以是10次迭代,也可以至少移动1 pt
term_crit = ( cv.TERM_CRITERIA_EPS | cv.TERM_CRITERIA_COUNT, 10, 1 )
while(1):
    ret, frame = cap.read()
    if ret == True:
        hsv = cv.cvtColor(frame, cv.COLOR_BGR2HSV)
        dst = cv.calcBackProject([hsv],[0],roi_hist,[0,180],1)
        # 应用meanshift来获取新位置
        ret, track_window = cv.meanShift(dst, track_window, term_crit)
        # 在图像上绘制
        x,y,w,h = track_window
        img2 = cv.rectangle(frame, (x,y), (x+w,y+h), 255,2)
        cv.imshow('img2',img2)
        k = cv.waitKey(30) & 0xff
        if k == 27:
            break
    else:
        break

我使用的视频中的三帧如下:

Camshift

您是否密切关注了最后结果?这儿存在一个问题。无论汽车离相机很近或非常近,我们的窗口始终具有相同的大小。这是不好的。我们需要根据目标的大小和旋转来调整窗口大小。该解决方案再次来自“ OpenCV Labs”,它被称为Gary布拉德斯基(Gary Bradsky)在其1998年的论文“用于感知用户界面中的计算机视觉面部跟踪”中发表的CAMshift(连续自适应均值偏移)[26]。 它首先应用Meanshift。一旦Meanshift收敛,它将更新窗口的大小为s = 2 \times \sqrt{\frac{M_{00}}{256}}。它还可以计算出最合适的椭圆的方向。再次将均值偏移应用于新的缩放搜索窗口和先前的窗口位置。该过程一直持续到达到要求的精度为止。

camshift_face

OpenCV中的Camshift

它与meanshift相似,但是返回一个旋转的矩形(即我们的结果)和box参数(用于在下一次迭代中作为搜索窗口传递)。请参见下面的代码:

import numpy as np
import cv2 as cv
import argparse
parser = argparse.ArgumentParser(description='This sample demonstrates the camshift algorithm. \
                                              The example file can be downloaded from: \
                                              https://www.bogotobogo.com/python/OpenCV_Python/images/mean_shift_tracking/slow_traffic_small.mp4')
parser.add_argument('image', type=str, help='path to image file')
args = parser.parse_args()
cap = cv.VideoCapture(args.image)
# 获取视频第一帧
ret,frame = cap.read()
# 设置初始窗口
x, y, w, h = 300, 200, 100, 50 # simply hardcoded the values
track_window = (x, y, w, h)
# 设置追踪的ROI窗口
roi = frame[y:y+h, x:x+w]
hsv_roi =  cv.cvtColor(roi, cv.COLOR_BGR2HSV)
mask = cv.inRange(hsv_roi, np.array((0., 60.,32.)), np.array((180.,255.,255.)))
roi_hist = cv.calcHist([hsv_roi],[0],mask,[180],[0,180])
cv.normalize(roi_hist,roi_hist,0,255,cv.NORM_MINMAX)
# 设置终止条件,可以是10次迭代,有可以至少移动1个像素
term_crit = ( cv.TERM_CRITERIA_EPS | cv.TERM_CRITERIA_COUNT, 10, 1 )
while(1):
    ret, frame = cap.read()
    if ret == True:
        hsv = cv.cvtColor(frame, cv.COLOR_BGR2HSV)
        dst = cv.calcBackProject([hsv],[0],roi_hist,[0,180],1)
        # 应用camshift 到新位置
        ret, track_window = cv.CamShift(dst, track_window, term_crit)
        # 在图像上画出来
        pts = cv.boxPoints(ret)
        pts = np.int0(pts)
        img2 = cv.polylines(frame,[pts],True, 255,2)
        cv.imshow('img2',img2)
        k = cv.waitKey(30) & 0xff
        if k == 27:
            break
    else:
        break

三帧的结果如下 

图像傅里叶变换 and图像分割的经典算法:分水岭算法

摘自网络,侵权立删。

傅里叶变换的作用

  对于数字图像这种离散的信号,频率大小表示信号变换的剧烈程度或者说信号变化的快慢。频率越大,变换越剧烈,频率越小,信号越平缓,对应到的图像中,高频信号往往是图像中的边缘信号和噪声信号,而低频信号包含图像变化频繁的图像轮廓及背景灯信号。

  需要说明的是:傅里叶变换得到的频谱图上的点与原图像上的点之间不存在一一对应的关系。

1,图像经过二维傅里叶的变换后,其变换稀疏矩阵具有如下性质:若交换矩阵原点设在中心,其频谱能量集中分布在变换稀疏矩阵的中心附近。若所用的二维傅里叶变换矩阵的云巅设在左上角,那么图像信号能量将集中在系数矩阵的四个角上。这是由二维傅里叶变换本身性质决定的。同时也表明一股图像能量集中低频区域。

2,图像灰度变化缓慢的区域,对应它变换后的低频分量部分;图像灰度呈阶跃变化的区域,对应变换后的高频分量部分。除颗粒噪音外,图像细节的边缘,轮廓处都是灰度变换突变区域,他们都具有变换后的高频分量特征。

右边为频率分布图谱,其中越靠近中心位置频率越低,越亮(灰度值越高)的位置代表该频率的信息振幅越大。

右边图中,每一个点

1)它到中点的距离描述的是频率

2)中点到它的方向,是平面波的方向

3)那一点的灰度值描述的是它的幅值

OpenCV中的傅里叶变换

OpenCV为此提供了**cv.dft**()和**cv.idft**()函数。它返回与前一个相同的结果,但是有两个通道。第一个通道是结果的实部,第二个通道是结果的虚部。输入图像首先应转换为np.float32。我们来看看怎么做。

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('messi5.jpg',0)
dft = cv.dft(np.float32(img),flags = cv.DFT_COMPLEX_OUTPUT)
dft_shift = np.fft.fftshift(dft)
magnitude_spectrum = 20*np.log(cv.magnitude(dft_shift[:,:,0],dft_shift[:,:,1]))
plt.subplot(121),plt.imshow(img, cmap = 'gray')
plt.title('Input Image'), plt.xticks([]), plt.yticks([])
plt.subplot(122),plt.imshow(magnitude_spectrum, cmap = 'gray')
plt.title('Magnitude Spectrum'), plt.xticks([]), plt.yticks([])
plt.show()

现在我们要做DFT的逆变换。在上一节中,我们创建了一个HPF,这次我们将看到如何删除图像中的高频内容,即我们将LPF应用到图像中。它实际上模糊了图像。为此,我们首先创建一个高值(1)在低频部分,即我们过滤低频内容,0在高频区。

rows, cols = img.shape
crow,ccol = rows/2 , cols/2
# 首先创建一个掩码,中心正方形为1,其余全为零
mask = np.zeros((rows,cols,2),np.uint8)
mask[crow-30:crow+30, ccol-30:ccol+30] = 1
# 应用掩码和逆DFT
fshift = dft_shift*mask
f_ishift = np.fft.ifftshift(fshift)
img_back = cv.idft(f_ishift)
img_back = cv.magnitude(img_back[:,:,0],img_back[:,:,1])
plt.subplot(121),plt.imshow(img, cmap = 'gray')
plt.title('Input Image'), plt.xticks([]), plt.yticks([])
plt.subplot(122),plt.imshow(img_back, cmap = 'gray')
plt.title('Magnitude Spectrum'), plt.xticks([]), plt.yticks([])
plt.show()

分水岭算法

图像的灰度空间很像地球表面的整个地理结构,每个像素的灰度值代表高度。其中的灰度值较大的像素连成的线可以看做山脊,也就是分水岭。其中的水就是用于二值化的gray threshold level,二值化阈值可以理解为水平面,比水平面低的区域会被淹没,刚开始用水填充每个孤立的山谷(局部最小值)。

当水平面上升到一定高度时,水就会溢出当前山谷,可以通过在分水岭上修大坝,从而避免两个山谷的水汇集,这样图像就被分成2个像素集,一个是被水淹没的山谷像素集,一个是分水岭线像素集。最终这些大坝形成的线就对整个图像进行了分区,实现对图像的分割。

图2

在该算法中,空间上相邻并且灰度值相近的像素被划分为一个区域。

分水岭算法的整个过程:

  1. 把梯度图像中的所有像素按照灰度值进行分类,并设定一个测地距离阈值。
  2. 找到灰度值最小的像素点(默认标记为灰度值最低点),让threshold从最小值开始增长,这些点为起始点。
  3. 水平面在增长的过程中,会碰到周围的邻域像素,测量这些像素到起始点(灰度值最低点)的测地距离,如果小于设定阈值,则将这些像素淹没,否则在这些像素上设置大坝,这样就对这些邻域像素进行了分类。
图3

4. 随着水平面越来越高,会设置更多更高的大坝,直到灰度值的最大值,所有区域都在分水岭线上相遇,这些大坝就对整个图像像素的进行了分区。

用上面的算法对图像进行分水岭运算,由于噪声点或其它因素的干扰,可能会得到密密麻麻的小区域,即图像被分得太细(over-segmented,过度分割),这因为图像中有非常多的局部极小值点,每个点都会自成一个小区域。

其中的解决方法:

  1. 对图像进行高斯平滑操作,抹除很多小的最小值,这些小分区就会合并。
  2. 不从最小值开始增长,可以将相对较高的灰度值像素作为起始点(需要用户手动标记),从标记处开始进行淹没,则很多小区域都会被合并为一个区域,这被称为基于图像标记(mark)的分水岭算法

OpenCV中分水岭算法

在OpenCV中,我们需要给不同区域贴上不同的标签。用大于1的整数表示我们确定为前景或对象的区域,用1表示我们确定为背景或非对象的区域,最后用0表示我们无法确定的区域。然后应用分水岭算法,我们的标记图像将被更新,更新后的标记图像的边界像素值为-1。

下面对相互接触的硬币应用距离变换和分水岭分割。

图6

先使用 Otsu’s 二值化对图像进行二值化。

import cv2
import numpy as np

img = cv2.imread('coins.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)
图7

先使用开运算去除图像中的细小白色噪点,然后通过腐蚀运算移除边界像素,得到的图像中的白色区域肯定是真实前景,即靠近硬币中心的区域(下面左边的图);膨胀运算使得一部分背景成为了物体到的边界,得到的图像中的黑色区域肯定是真实背景,即远离硬币的区域(下面中间的图)。

剩下的区域(硬币的边界附近)还不能确定是前景还是背景。可通过膨胀图减去腐蚀图得到,下图中的白色部分为不确定区域(下面右边的图)。

# noise removal
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3,3))
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2)

sure_bg = cv2.dilate(opening, kernel, iterations=2)  # sure background area
sure_fg = cv2.erode(opening, kernel, iterations=2)  # sure foreground area
unknown = cv2.subtract(sure_bg, sure_fg)  # unknown area
图8

剩下的区域不确定是硬币还是背景,这些区域通常在前景和背景接触的区域(或者两个不同硬币接触的区域),我们称之为边界。通过分水岭算法应该能找到确定的边界。

由于硬币之间彼此接触,我们使用另一个确定前景的方法,就是带阈值的距离变换

下面左边的图为得到的距离转换图像,其中每个像素的值为其到最近的背景像素(灰度值为0)的距离,可以看到硬币的中心像素值最大(中心离背景像素最远)。对其进行二值处理就得到了分离的前景图(下面中间的图),白色区域肯定是硬币区域,而且还相互分离,下面右边的图为之前的膨胀图减去中间这个表示前景的图。

# Perform the distance transform algorithm
dist_transform = cv2.distanceTransform(opening, cv2.DIST_L2, 5)
# Normalize the distance image for range = {0.0, 1.0}
cv2.normalize(dist_transform, dist_transform, 0, 1.0, cv2.NORM_MINMAX)

# Finding sure foreground area
ret, sure_fg = cv2.threshold(dist_transform, 0.5*dist_transform.max(), 255, 0)

# Finding unknown region
sure_fg = np.uint8(sure_fg)
unknown = cv2.subtract(sure_bg,sure_fg)
图9

现在我们可以确定哪些是硬币区域,哪些是背景区域。然后创建标记(marker,它是一个与原始图像大小相同的矩阵,int32数据类型),表示其中的每个区域。分水岭算法将标记的0的区域视为不确定区域,将标记为1的区域视为背景区域,将标记大于1的正整数表示我们想得到的前景。

我们可以使用 cv2.connectedComponents() 来实现这个功能,它是用0标记图像的背景,用大于0的整数标记其他对象。所以我们需要对其进行加一,用1来标记图像的背景。

cv2.connectedComponents() 将传入图像中的白色区域视为组件(前景)。

# Marker labelling
ret, markers = cv2.connectedComponents(sure_fg)
# Add one to all labels so that sure background is not 0, but 1
markers = markers+1
# Now, mark the region of unknown with zero
markers[unknown==255] = 0

注意:得到的markers矩阵的元素类型为 int32,要使用 imshow() 进行显示,需要将其转换为 uint8 类型( markers=np.uint8(markers) )。

我们对得到的markers进行显示:

markers_copy = markers.copy()
markers_copy[markers==0] = 150  # 灰色表示背景
markers_copy[markers==1] = 0    # 黑色表示背景
markers_copy[markers>1] = 255   # 白色表示前景

markers_copy = np.uint8(markers_copy)
图10

标记图像已经完成了,最后应用分水岭算法。然后标记图像将被修改,边界区域将被标记为-1。

# 使用分水岭算法执行基于标记的图像分割,将图像中的对象与背景分离
markers = cv2.watershed(img, markers)
img[markers==-1] = [0,0,255]  # 将边界标记为红色

经过分水岭算法得到的新的标记图像和分割后的图像如下图所示:

图11

任何两个相邻连接的组件不一定被分水岭边界(-1的像素)分开;例如在传递给 watershed 函数的初始标记图像中的物体相互接触。

总结

我们通过一个例子介绍了分水岭算法的整个过程,主要分为以下几步:

  1. 对图进行灰度化和二值化得到二值图像
  2. 通过膨胀得到确定的背景区域,通过距离转换得到确定的前景区域,剩余部分为不确定区域
  3. 对确定的前景图像进行连接组件处理,得到标记图像
  4. 根据标记图像对原图像应用分水岭算法,更新标记图像

参考:

OpenCV Watershed Algorithm

IMAGE SEGMENTATION AND MATHEMATICAL MORPHOLOGY

Classic Watershed

https://zhuanlan.zhihu.com/p/67741538

图像直方图反向投影

理论

这是由**Michael J. Swain**和**Dana H. Ballard**在他们的论文《通过颜色直方图索引》中提出的。

用简单的话说是什么意思?它用于图像分割或在图像中查找感兴趣的对象。简而言之,它创建的图像大小与输入图像相同(但只有一个通道),其中每个像素对应于该像素属于我们物体的概率。用更简单的话来说,与其余部分相比,输出图像将在可能有对象的区域具有更多的白色值。好吧,这是一个直观的解释。(我无法使其更简单)。直方图反投影与camshift算法等配合使用。

我们该怎么做呢?我们创建一个图像的直方图,其中包含我们感兴趣的对象(在我们的示例中是背景,离开播放器等)。对象应尽可能填充图像以获得更好的效果。而且颜色直方图比灰度直方图更可取,因为对象的颜色对比灰度强度是定义对象的好方法。然后,我们将该直方图“反投影”到需要找到对象的测试图像上,换句话说,我们计算出属于背景的每个像素的概率并将其显示出来。在适当的阈值下产生的输出使我们仅获得背景。

假设我们现在有一个四行四列得灰度图,它得灰度值如下图:

说这幅图有什么特征呢?直观上看类似于一个边角,但这是直观上,怎么表示出来呢?深度学习是靠神经网络黑箱计算出来得,我们可以用直方图。

那我们就计算这幅灰度图得直方图,如果以组距为1计算直方图并反向投影到原图,得到得为下图:

可以大概表述一下边角得特征:左下角有6个像素值相同得三角形区域,中间斜向下有四个像素值相同得边界线,以此类推。这就是用直方图得到得边角得特征。

那如果以组距为2计算直方图呢?反向投影后为:

可以看到特征描述得更为广泛了,就像深度学习里,提取更高层次得特征,虽然更为普适,但也会忽略掉一些细节特征。

我们就是拿这个反向投影所表达得特征信息,去和整幅图做对比,来得到特征相似得部分,达到分割得效果。

为什么要归一化呢,直方图反向投影到原图后,原图各位置表示的是整幅图中等于该点像素值的数量,归一化后就变成概率了。

通过图像的反向投影矩阵,我们实际上把原图像简单化了,简单化的过程实际上就是提取出图像的某个特征。所以以后我们可以用这个特征来对比两幅图,如果两幅图的反向投影矩阵相似或相同,那么我们就可以判定这两幅图这个特征是相同的。

Numpy中的算法

  1. 首先,我们需要计算我们要查找的对象(使其为“ M”)和要搜索的图像(使其为“ I”)的颜色直方图。
import numpy as np
import cv2 as cvfrom matplotlib import pyplot as plt
#roi是我们需要找到的对象或对象区域
roi = cv.imread('rose_red.png')
hsv = cv.cvtColor(roi,cv.COLOR_BGR2HSV)
#目标是我们搜索的图像
target = cv.imread('rose.png')
hsvt = cv.cvtColor(target,cv.COLOR_BGR2HSV)
# 使用calcHist查找直方图。也可以使用np.histogram2d完成
M = cv.calcHist([hsv],[0, 1], None, [180, 256], [0, 180, 0, 256] )
I = cv.calcHist([hsvt],[0, 1], None, [180, 256], [0, 180, 0, 256] )
  1. 求出比值R=MI。然后反向投影R,即使用R作为调色板,并以每个像素作为其对应的目标概率创建一个新图像。即B(x,y) = R[h(x,y),s(x,y)] 其中h是色调,s是像素在(x,y)的饱和度。之后,应用条件B(x,y)=min[B(x,y),1]。
h,s,v = cv.split(hsvt)
B = R[h.ravel(),s.ravel()]
B = np.minimum(B,1)
B = B.reshape(hsvt.shape[:2])
  1. 现在对圆盘应用卷积,B=D∗B,其中D是圆盘内核。
disc = cv.getStructuringElement(cv.MORPH_ELLIPSE,(5,5))
cv.filter2D(B,-1,disc,B)
B = np.uint8(B)
cv.normalize(B,B,0,255,cv.NORM_MINMAX)
  1. 现在最大强度的位置给了我们物体的位置。如果我们期望图像中有一个区域,则对合适的值进行阈值处理将获得不错的结果。
ret,thresh = cv.threshold(B,50,255,0) 

OpenCV的反投影

OpenCV提供了一个内建的函数**cv.calcBackProject**()。它的参数几乎与**cv.calchist**()函数相同。它的一个参数是直方图,也就是物体的直方图,我们必须找到它。另外,在传递给backproject函数之前,应该对对象直方图进行归一化。它返回概率图像。然后我们用圆盘内核对图像进行卷积并应用阈值。下面是我的代码和结果:

import numpy as np
import cv2 as cv
roi = cv.imread('rose_red.png')
hsv = cv.cvtColor(roi,cv.COLOR_BGR2HSV)
target = cv.imread('rose.png')
hsvt = cv.cvtColor(target,cv.COLOR_BGR2HSV)
# 计算对象的直方图
roihist = cv.calcHist([hsv],[0, 1], None, [180, 256], [0, 180, 0, 256] )
# 直方图归一化并利用反传算法
cv.normalize(roihist,roihist,0,255,cv.NORM_MINMAX)
dst = cv.calcBackProject([hsvt],[0,1],roihist,[0,180,0,256],1)
# 用圆盘进行卷积
disc = cv.getStructuringElement(cv.MORPH_ELLIPSE,(5,5))
cv.filter2D(dst,-1,disc,dst)
# 应用阈值作与操作
ret,thresh = cv.threshold(dst,50,255,0)
thresh = cv.merge((thresh,thresh,thresh))
res = cv.bitwise_and(target,thresh)
res = np.vstack((target,thresh,res))
cv.imwrite('res.jpg',res)

leetcodeday19 –删除链表的倒数第 N 个结点

总结方法:对于链表,一般在链表开头先添加一个头元素,该元素指向链表的第一个元素。

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

示例 1:

输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]

示例 2:

输入:head = [1], n = 1
输出:[]

示例 3:

输入:head = [1,2], n = 1
输出:[1]

提示:

  • 链表中结点的数目为 sz
  • 1 <= sz <= 30
  • 0 <= Node.val <= 100
  • 1 <= n <= sz

一般方法:两次for循环

第一次计算长度,第二次重构链表

class Solution:
    def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:
        def getLength(head: ListNode) -> int:
            length = 0
            while head:
                length += 1
                head = head.next
            return length
        
        dummy = ListNode(0, head)
        length = getLength(head)
        cur = dummy
        for i in range(1, length - n + 1):
            cur = cur.next
        cur.next = cur.next.next
        return dummy.next

改进版:

双指针

思路与算法

我们也可以在不预处理出链表的长度,以及使用常数空间的前提下解决本题。由于我们需要找到倒数第n 个节点,因此我们可以使用两个指针first 和 second 同时对链表进行遍历,并且 first 比second 超前 n个节点。当first 遍历到链表的末尾时,second 就恰好处于倒数第 n 个节点。

具体地,初始时 first 和 second 均指向头节点。我们首先使用 first 对链表进行遍历,遍历的次数为 n。此时,first 和 second 之间间隔了 n−1 个节点,即 first 比second 超前了 n个节点。

在这之后,我们同时使用 first 和second 对链表进行遍历。当 first 遍历到链表的末尾(即 first 为空指针)时,second 恰好指向倒数第 n 个节点。

根据方法一和方法二,如果我们能够得到的是倒数第 n 个节点的前驱节点而不是倒数第 n 个节点的话,删除操作会更加方便。因此我们可以考虑在初始时将 second 指向哑节点,其余的操作步骤不变。这样一来,当first 遍历到链表的末尾时,second 的下一个节点就是我们需要删除的节点。

# @lc code=start
# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:
        dummy = ListNode(0, head)
        first = head
        second = dummy
        for i in range(n):
            first = first.next

        while first:
            first = first.next
            second = second.next
        
        second.next = second.next.next
        return dummy.next

复杂度分析

  • 时间复杂度:O(L),其中 L 是链表的长度。
  • 空间复杂度: O(1)。

leetcodeday18 —-四数之和

给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):

0 <= a, b, c, d < n
a、b、c 和 d 互不相同
nums[a] + nums[b] + nums[c] + nums[d] == target
你可以按 任意顺序 返回答案 。

示例 1:

输入:nums = [1,0,-1,0,-2,2], target = 0
输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
示例 2:

输入:nums = [2,2,2,2,2], target = 8
输出:[[2,2,2,2]]

思路:三数之和+一层遍历

# @lc code=start
class Solution:
  def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
    n = len(nums)
    nums.sort()
    result=list()
    def threeSum(nums: List[int],targets: int,fst:int) -> List[List[int]]:
        n = len(nums)
        ans = list()
        
        # 枚举 a
        for first in range(n):
            # 需要和上一次枚举的数不相同
            if first > 0 and nums[first] == nums[first - 1]:
                continue
            # c 对应的指针初始指向数组的最右端
            third = n - 1
            target = targets-nums[first]
            # 枚举 b
            for second in range(first + 1, n):
                # 需要和上一次枚举的数不相同
                if second > first + 1 and nums[second] == nums[second - 1]:
                    continue
                # 需要保证 b 的指针在 c 的指针的左侧
                while second < third and nums[second] + nums[third] > target:
                    third -= 1
                # 如果指针重合,随着 b 后续的增加
                # 就不会有满足 a+b+c=0 并且 b<c 的 c 了,可以退出循环
                if second == third:
                    break
                if nums[second] + nums[third] == target:
                    ans.append([fst,nums[first], nums[second], nums[third]])
        
        return ans
    
    for first in range(n):
        if first > 0 and nums[first] == nums[first - 1]:
                continue
        targets= target-nums[first]
        result.extend(threeSum(nums[first+1:],targets,nums[first]))
    return result

leetcodeday17 电话号码的字母组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

示例 1:

输入:digits = “23”
输出:[“ad”,”ae”,”af”,”bd”,”be”,”bf”,”cd”,”ce”,”cf”]
示例 2:

输入:digits = “”
输出:[]
示例 3:

输入:digits = “2”
输出:[“a”,”b”,”c”]

提示:

  • 0 <= digits.length <= 4
  • digits[i] 是范围 ['2', '9'] 的一个数字

思路:遍历递归求解

# @lc code=start
class Solution:
    def letterCombinations(self, digits: str) -> List[str]:
        if digits=="": 
            return []
        n=len(digits)
        sets={"2":["a","b","c"],"3":["d","e","f"],"4":["g","h","i"],"5":["j","k","l"],"6":["m","n","o"],"7":["p","q","r","s"],"8":["t","u","v"],"9":["w","x","y","z"]}
        def upsort(strs): 
            if len(strs)==1: 
                return sets[strs]
    
            result = upsort(strs[:-1])
            rev=[]
            for i in range(len(result)):
                 for j in sets[strs[-1]]:
                    rev.append(result[i]+j)
            return rev
        return upsort(digits)

leetcodeday16 —最接近的三数之和

这个思路和上一个题差不多,不过简单一些,只需要找出三个数的和。因此给出官方答案。

给你一个长度为 n 的整数数组 nums和 一个目标值 target。请你从 nums中选出三个整数,使它们的和与 target 最接近。

返回这三个数的和。

假定每组输入只存在恰好一个解。

示例 1:

输入:nums = [-1,2,1,-4], target = 1
输出:2
解释:与 target 最接近的和是 2 (-1 + 2 + 1 = 2) 。

排序 + 双指针
思路与算法

题目要求找到与目标值 target 最接近的三元组,这里的「最接近」即为差值的绝对值最小。我们可以考虑直接使用三重循环枚举三元组,找出与目标值最接近的作为答案,时间复杂度为 O(N^3)。然而本题的 N 最大为 1000,会超出时间限制。

那么如何进行优化呢?我们首先考虑枚举第一个元素 a,对于剩下的两个元素 b 和 c,我们希望它们的和最接近 target−a。对于 b 和 c,如果它们在原数组中枚举的范围(既包括下标的范围,也包括元素值的范围)没有任何规律可言,那么我们还是只能使用两重循环来枚举所有的可能情况。因此,我们可以考虑对整个数组进行升序排序,这样一来:

假设数组的长度为 n,我们先枚举 a,它在数组中的位置为 i;

为了防止重复枚举,我们在位置 [i+1,n) 的范围内枚举 b 和 c。

当我们知道了 b 和 c 可以枚举的下标范围,并且知道这一范围对应的数组元素是有序(升序)的,那么我们是否可以对枚举的过程进行优化呢?

答案是可以的。借助双指针,我们就可以对枚举的过程进行优化。我们用 p_b和 p_c
分别表示指向 b 和 c的指针,初始时,p_b指向位置 i+1,即左边界;p_c指向位置 n-1,即右边界。在每一步枚举的过程中,我们用 a+b+c 来更新答案,并且:

如果 a+b+c≥target,那么就将 p_c向左移动一个位置;

如果 a+b+c<target,那么就将 p_b向右移动一个位置。这是为什么呢?我们对 a+b+c≥target 的情况进行一个详细的分析:

如果 a+b+c≥target,并且我们知道 p_b到 p_c这个范围内的所有数是按照升序排序的,那么如果 p_c不变而 p_b向右移动,那么 a+b+c 的值就会不断地增加,显然就不会成为最接近 target 的值了。因此,我们可以知道在固定了 p_c的情况下,此时的 p_b就可以得到一个最接近target 的值,那么我们以后就不用再考虑 p_c了,就可以将 p_c向左移动一个位置。

同样地,在 a+b+c<target 时:

如果 a+b+c<target,并且我们知道 p_b到 p_c这个范围内的所有数是按照升序排序的,那么如果 p_b不变而 p_c向左移动,那么 a+b+c 的值就会不断地减小,显然就不会成为最接近 target 的值了。因此,我们可以知道在固定了 p_b的情况下,此时的 p_cp就可以得到一个最接近 target 的值,那么我们以后就不用再考虑 p_b了,就可以将 p_b向右移动一个位置。实际上,p_b和 p_c就表示了我们当前可以选择的数的范围,而每一次枚举的过程中,我们尝试边界上的两个元素,根据它们与 target 的值的关系,选择「抛弃」左边界的元素还是右边界的元素,从而减少了枚举的范围。这种思路与盛最多水的容器 中的双指针解法也是类似的。

小优化

本题也有一些可以减少运行时间(但不会减少时间复杂度)的小优化。当我们枚举到恰好等于target 的 a+b+c时,可以直接返回 target 作为答案,因为不会有再比这个更接近的值了。

另一个优化与 三数之和的官方题解 中提到的类似。当我们枚举 a, b, c中任意元素并移动指针时,可以直接将其移动到下一个与这次枚举到的不相同的元素,减少枚举的次数。

class Solution:
    def threeSumClosest(self, nums: List[int], target: int) -> int:
        nums.sort()
        n = len(nums)
        best = 10**7
        
        # 根据差值的绝对值来更新答案
        def update(cur):
            nonlocal best
            if abs(cur - target) < abs(best - target):
                best = cur
        
        # 枚举 a
        for i in range(n):
            # 保证和上一次枚举的元素不相等
            if i > 0 and nums[i] == nums[i - 1]:
                continue
            # 使用双指针枚举 b 和 c
            j, k = i + 1, n - 1
            while j < k:
                s = nums[i] + nums[j] + nums[k]
                # 如果和为 target 直接返回答案
                if s == target:
                    return target
                update(s)
                if s > target:
                    # 如果和大于 target,移动 c 对应的指针
                    k0 = k - 1
                    # 移动到下一个不相等的元素
                    while j < k0 and nums[k0] == nums[k]:
                        k0 -= 1
                    k = k0
                else:
                    # 如果和小于 target,移动 b 对应的指针
                    j0 = j + 1
                    # 移动到下一个不相等的元素
                    while j0 < k and nums[j0] == nums[j]:
                        j0 += 1
                    j = j0

        return best

转置卷积、微步卷积、空洞卷积

1、转置卷积 又可以称为 反卷积(数据从低维到高维)

转置卷积是一个将低维特征转换到高维特征。为什么叫做转置卷积呢?其实就是引入了转置的思想。

  • 假设我们现在有一个p维的向量Z,然后有个d维的向量X,p<d.
  • 这样就会出现 Z = W·X,其中W的维度为(p,d),叫做转换矩阵.
  • 现在,我们要从Z通过相似的方法来得到X,这样我们不难想到:X= W.T · X 其中W.T的维度是(d,p),但是这两个W并不是同一个值,而是具有转置的形式而已。

上面的例子是一维向量的情况,在卷积操作中,也可以借用这个思想,从低维到高维的转变可以在形式上看成是转置操作。

  • 比如我们现在对一个4 * 4的输入做3 * 3的卷积操作(m=3核的大小,stride=1,padding=0),得到一个2 * 2的特征映射
  • 如果我们想对这个2 * 2特征映射进行3 * 3卷积,并反过来得到4 * 4的输出,就可以用到转置卷积:

如上图所示,对2 * 2的特征映射先做(m-1) padding得到6 * 6的输入,然后对其进行3*3的卷积操作,从而得到4 * 4的特征映射。 同样,这个两个3 * 3的卷积参数不是一致的,都是可学习的。

2、微步卷积(步长不为1的转置卷积(反卷积))

微步卷积其实是一个转置卷积的一个特殊情况,就是卷积操作的stride ≠ 1。因为在现实中,为了大幅度降低特征维数,卷积的步长会大于1。同样,为了大幅度提高特征维度,我们也可以用通过卷积来实现,这种卷积stride < 1 ,所以叫做微步卷积。

  • 如果卷积操作stride>1,其对应的转置卷积步长为1/s :就是在输入特征之间插入s – 1个0,来使得步长变’小’。
  • 例如,我对一个5 * 5的输入做3 * 3的卷积操作(m=3, padding=0,但是stride=2),从而我得到的特征输出为2 * 2.
  • 现在对其进行微步卷积:

跟转置卷积一样,先对2 * 2的输入做(m-1)padding ,然后再在特征之间插入stride -1个0,从而得到一个7 * 7的特征输入,然后对其做3 * 3 的卷积操作,得到5 * 5的特征输出。

如何计算反卷积:

当输入的矩阵高宽为n,核大小为k,padding为p,stride为s

  • 当输入的矩阵高宽为 n ,核大小为 k ,padding为 p , stride为 s 。
  • 转置卷积作用后的尺寸变化: \(n^{1}=s n+k-2 p-s\) 。如果想让高宽成倍增加,那么 \(k=2 p+s\) 。
  • 卷积作用后的尺寸变化: \(n^{1}=\left\lfloor\frac{n-k+2 p+s}{s}\right\rfloor\) 。如果想让高宽成倍减少,那么 \(k=2 p+1\)。

1、当填充为0步长为1时

将输入填充 k − 1 。(k是 卷积核大小)
将核矩阵上下,左右翻转。
之后正常做填充为0(无填充),步幅为1的卷积。

2 当填充为 p 步幅为1时

将输入填充 k − p − 1 。
将核矩阵上下,左右翻转。
之后正常做填充为0,步幅为1的卷积。

3 当填充为 p pp 步幅为s ss时

在行和列之间插入s − 1 行和列。
将输入填充 k − p − 1。
将核矩阵上下,左右翻转。
之后正常做填充为0,步幅为1的卷积。

3、空洞卷积(膨胀卷积)

通常来说,对于一个卷积层,如果希望增加输出单元的感受野,一般由三个方式:

  1. 增加卷积核大小
  2. 增加层数
  3. 进行pooling操作

其中1和2都会增加参数量,而3会丢失特征信息。这样我们就可以引入‘空洞卷积’的概念,它不增加参数量,同时它也可以增加输出的感受野。
它主要是通过给卷积核插入空洞来增加其感受野大小,如果卷积核每两个元素之间插入d-1个空洞,那么卷积核的有效大小为:M = m + (m-1)*(d-1)