与一维信号一样,还可以使用各种低通滤波器(LPF),高通滤波器(HPF)等对图像进行滤波。LPF有助于消除噪声,使图像模糊等。HPF滤波器有助于在图像中找到边缘。
OpenCV
提供了一个函数cv.filter2D
来将内核与图像进行卷积。卷积运算可参考我的卷积神经网络。其运算结果为:
y i j = ∑ u = 1 U ∑ v = 1 V w u v x i − u + 1 , j − v + 1 y_{ij} = \sum\limits^U_{u=1}\sum\limits^V_{v=1}w_{uv}x_{i-u+1, j-v+1} yij=u=1∑Uv=1∑Vwuvxi−u+1,j−v+1
cv.filter2D(src, ddepth, kernel, dst, anchor, delta, borderType)
- src: 原图像
- dst: 目标图像,与原图像尺寸和通过数相同
- ddepth: 目标图像的所需深度
- kernel: 卷积核(或相当于相关核)。单通道浮点矩阵
- anchor: 内核的描点,指示内核中过滤点的相对位置
- delta: 将他们存储在dst中之前,将可选值添加到已过滤的像素中,类似于偏置
- borderType: 像素外推法
kernel = np.ones((9,9),np.float32)/25
dst = cv.filter2D(src,-1,kernel)
plt.subplot(121),plt.imshow(src),plt.title('Original')
plt.xticks([]), plt.yticks([])
plt.subplot(122),plt.imshow(dst),plt.title('Averaging')
plt.xticks([]), plt.yticks([])
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1FXpJSwj-1634039461913)(9.PNG)]
通过将图像与低通滤波器内核进行卷积来实现图像模糊。这对于消除噪音很有用,它实际从图像中消除了高频部分(例如噪声,边缘)。因此,在此操作中边缘有些模糊。
OpenCV
主要提供了四种类型的模糊技术。
通过将图像与归一化框滤镜进行卷积来完成的。它仅获取内核区域下所有像素的平均值,并替换中心元素。这是通过 cv.blur()
或 cv.boxFilter()
完成的。应指定内核的宽度和高度。
cv.blur(img, ksize)
- img: 原图像
- ksize: 核大小
它只取内核区域下所有像素的平均值并替换中心元素。 3 × 3 3\times 3 3×3 标准化的盒式过滤器如下:
K = 1 9 [ 1 1 1 1 1 1 1 1 1 ] K = \frac{1}{9} \begin{bmatrix} 1 & 1 & 1 \\ 1 & 1 & 1 \\ 1 & 1 & 1 \end{bmatrix} K=91⎣⎡111111111⎦⎤
特征:核中区域贡献率相同。
作用:对于椒盐噪声的滤除效果比较好
cv.boxFilter(img, -1, ksize, normalize=True)
方框滤波
当normalize=True
时,与均值滤波结果相同,normalize=False
,表示对加和后的结果不进行平均操作,大于255的用255表示。
kernel = np.ones((9,9),np.float32)/25
dst = cv.filter2D(src,-1,kernel)
plt.subplot(121),plt.imshow(src),plt.title('Original')
plt.xticks([]), plt.yticks([])
plt.subplot(122),plt.imshow(dst),plt.title('Averaging')
plt.xticks([]), plt.yticks([])
res1 = cv.blur(src, (27, 27))
res2 = cv.boxFilter(src, -1, (27, 27), True)
res3 = cv.boxFilter(src, -1, (27, 27), False)
plt.subplot(131), plt.imshow(res1), plt.title('Blur')
plt.subplot(132), plt.imshow(res2), plt.title('boxFilter True')
plt.subplot(133), plt.imshow(res3), plt.title('boxFilter False')
高斯滤波是一种线性平滑滤波,适用于消除高斯噪声,广泛应用于图像处理的减噪过程。通俗的讲,高斯滤波就是对整幅图像进行加权平均的过程,每一个像素的值,都由其本身和邻域内其他像素值经过加权平均后得到。高斯滤波的具体操作是:用一个卷积核扫描图像中的每一个像素,用卷积确定的邻域内像素的加权平均灰度值去替代模板中心像素点的值。
数值图像中,高斯滤波主要可以使用两钟方法实现。一种是离散化窗口滑窗卷积,另一种是通过傅里叶变换。最常见的就是滑窗实现,只有当离散化的窗口非常大,用滑窗计算量非常大的情况下,可能会考虑基于傅里叶变化的实现方法。
离散化窗口滑窗卷积时主要利用的是高斯核,高斯核的大小为奇数,因为高斯卷积会在其覆盖区域的中心输出结果。
高斯卷积是通过高斯函数计算出来的:
G ( x , y ) = 1 2 π σ 2 exp { − x 2 + y 2 2 σ 2 } G(x, y) = \frac{1}{2\pi\sigma ^2}\exp\{-\frac{x^2+y^2}{2\sigma^2}\} G(x,y)=2πσ21exp{−2σ2x2+y2}
输出的卷积有两种形式:
- 小数类型:直接计算得到的值
- 将得到的值进行归一化处理,即将左上角的值归化为一,其他每个系数都除以左上角的系数,然后取整。在使用整数模板时,则需要在模板的前面加一个系数,该系数为模板系数之和的倒数。
以上可以看出,高斯滤波模板中最重要的参数就是高斯分布的标准差 σ \sigma σ,它代表着数值的离散程度,如果 σ \sigma σ 较小,那么生成的模板中心系数越大,而周围的系数越小,这样对图像的平滑效果就不是很明显;相反, σ \sigma σ 较大时,则生成的模板的各个系数相差就不是很大,比较类似于均值模板,对图像的平滑效果就比较明显。
src: 输入图像,图像可以具有任意数量的通道,这些通道可以独立处理,但深度应为
CV_8U
,CV_16U,CV_16S,CV_32F或CV_64F
dst: 输出图像的大小类型与src相同
ksize: 高斯内核大小,必须为整数和奇数,或为0,然后根据 sigma 算出
sigmaX: X方向上的高斯核标准偏差
sigmaY: Y方向上的高斯核标准差,如果为0,则将其设置为等于sigmaX,如果两个sigma都为0,则分别从
ksize.width
和ksize.height
计算得出。
blur = cv.GaussianBlur(src, (27, 27), 0)
plt.subplot(121), plt.imshow(src), plt.title('Unchanged')
plt.subplot(122), plt.imshow(blur), plt.title('Gussian')
cv.medianBlur(src, ksize)
提取内核区域下所有像素的中值,并将中心元素替换为该中值。这对于消除图像中的椒盐噪声非常有效。在上述过滤器中,中心元素是新计算的值,该值可以是图像 中的像素值或者新值。但是在中值模糊中,中心元素总是被图像中的某些像素值代替。有效降低噪音。内核大小应为正奇数。
向原图像中加入50000个椒盐噪声值,利用中位滤波器去噪。
newimg=np.array(src)
noisecount=50000
for k in range(0,noisecount):
xi=int(np.random.uniform(0,newimg.shape[1]))
xj=int(np.random.uniform(0,newimg.shape[0]))
newimg[xj,xi]=255
dst = cv.medianBlur(newimg, 5)
plt.subplot(121), plt.imshow(newimg), plt.title("Noise")
plt.subplot(122), plt.imshow(dst), plt.title('Median')
cv.bilateraFilter()
在去除噪声的同时保持边缘清晰锐利非常有效。但与其他过滤器相比,该操作速度较慢。高斯滤波器采用像素周围的邻域并找到其高斯加权平均值。高斯滤波器是空间的函数,也就是说,滤波时会考虑附近的像素。它不考虑像素是否具有几乎相同的强度。它也不考虑像素是否是边缘像素。因此它也模糊了边缘。
双边滤波器在空间中也采用高斯滤波器,但是又有一个高斯滤波器,它是像素差的函数。空间的高斯函数确保仅考虑附近像素的模糊,而强度差的高斯函数确保仅考虑强度与中心像素相似的那些像素的模糊,由于边缘的像素强大变化较大,因此可以保留边缘。
cv.bilateralFilter(src, d, sigmaColor, sigmaSpace, dst, borderType)
src: 原始图像
dst: 目标图像
d: 过滤期间使用的各像素邻域的直径
sigmaColor: 色彩空间的sigma参数,该参数较大时,各像素邻域内相距较远的颜色会被混合到一起,从而造成更大范围的半相等颜色
sigmaSpace: 坐标空间的sigma参数,该参数较大时,只要颜色相近,越远的像素会相互影响
borderType: 边界类型,指定如何确定图像范围外的像素的取值
dst1 = cv.bilateralFilter(newimg, 17, 75, 75)
dst2 = cv.bilateralFilter(newimg, 9, 75, 75)
dst3 = cv.bilateralFilter(newimg, 9, 105, 105)
plt.subplot(221), plt.imshow(newimg), plt.title("Noise")
plt.xticks([]), plt.yticks([])
plt.subplot(222), plt.imshow(dst1), plt.title('d=17')
plt.xticks([]), plt.yticks([])
plt.subplot(223), plt.imshow(dst2), plt.title('d=9')
plt.xticks([]), plt.yticks([])
plt.subplot(224), plt.imshow(dst3), plt.title('d=9, sigma=105')
plt.xticks([]), plt.yticks([])
形态变换是一些基于图像形状的基本操作。通常在二进制图像上进行。它需要两个输入,一个是原始图像,一个是决定 操作性质的结构元素 或 内核。两种基本的形态学算子是侵蚀和膨胀。然后,它们的变体形式也开始起作用。
侵蚀的基本思想就像土壤侵蚀一样,它侵蚀前景物体的边界(尽量使前景保持白色),内核通过图像(在2D卷积中),原始图像中的一个像素只有当内核下的所有像素都是1时才被认为是1,否则它就被侵蚀(变为0)。
结果是,根据内核的大小,边界附近的所有像素都会被丢弃。因此,前景物体的厚度或大小减小,或只是图像中的白色区域减小,它有助于去除小的白色噪声,分离两个连接的对象等。
cv.erode(src, kernel, anchor, iterations, borderType, borderValue)
src: 输入图片
kernel: 卷积核大小
anchor: 表示
element
结构中瞄点的位置,默认为(-1, -1),在核的中心位置iterations: 迭代次数
borderType: 边界样式
img = cv.imread('E:/Computer/Desktop/Basis/j.png')
kernel = np.ones((5, 5), dtype=np.uint8)
ero = cv.erode(img, kernel, iterations=1)
plt.subplot(121), plt.imshow(img), plt.title('old')
plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(ero), plt.title('erode')
plt.xticks([]), plt.yticks([])
与侵蚀正好相反,如果内核下的至少一个像素为1,则像素元素为1。因此,它会增加图像中的白色区域或增加前景对象的大小。通常,在消除噪音的情况下,腐蚀后会膨胀。因为腐蚀会消除白噪声,但也会缩小物体,因此,我们对齐进行了扩张。由于噪音消失了,它们不会回来,但我们的目标区域增加了。在连接对象的损坏部分时也很有用。
cv.dilate(src, kernel, anchor, iterations, borderType, borderValue)
plt.subplot(121), plt.imshow(ero), plt.title('erode')
plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(cv.dilate(ero, kernel, iterations=-1)), plt.title('dilate')
plt.xticks([]), plt.yticks([])
cv.morphrlogyEx(src, op, kernel)
- src: 输入图像
- op: 运算方式
- kernel: 内核
开运算 = 先腐蚀,再膨胀
- 开运算能够除去孤立的小点,毛刺和小桥,而总的位置和形状不变。
- 开运算是一个基于几何运算的滤波器。
- 结构元素大小的不同将导致滤波效果的不同。
- 不同的结构元素的选择导致了不同的分割,即提取出不同的特征。
newimg=np.array(img)
noisecount=50
for k in range(0,noisecount):
xi=int(np.random.uniform(0,newimg.shape[1]))
xj=int(np.random.uniform(0,newimg.shape[0]))
newimg[xj,xi]=255
opening = cv.morphologyEx(newimg, cv.MORPH_OPEN, kernel)
plt.subplot(121), plt.imshow(newimg), plt.title('noise')
plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(opening), plt.title('opening')
plt.xticks([]), plt.yticks([])
先膨胀,再腐蚀
- 闭运算能够填平小孔,弥补小裂缝,而总的位置和形状不变
- 闭运算是通过填充图像的凹角来滤波图像的
- 结构元素大小的不同将导致滤波效果的不同
- 不同结构元素的选择导致了不同的分割
newimg=np.array(img)
noisecount=500
for k in range(0,noisecount):
xi=int(np.random.uniform(0,newimg.shape[1]))
xj=int(np.random.uniform(0,newimg.shape[0]))
newimg[xj,xi]=0
opening = cv.morphologyEx(newimg, cv.MORPH_CLOSE, kernel)
plt.subplot(121), plt.imshow(newimg), plt.title('noise')
plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(opening), plt.title('opening')
plt.xticks([]), plt.yticks([])
膨胀图像减去腐蚀图像得到的差值图像,看起来像是图像轮廓
gradient = cv.morphologyEx(img, cv.MORPH_GRADIENT, kernel)
plt.subplot(121), plt.imshow(img), plt.title('img')
plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(gradient), plt.title('gradient')
plt.xticks([]), plt.yticks([])
原图像减去腐蚀之后的图像得到的差值图像,称为图像的内部梯度
图像膨胀后再减去原来的图像得到的差值图像,称为图像的外部梯度
方向梯度是使用 X 方向与 Y 方向的直线作为结构元素之后得到的梯度图像, X的结构元素分别膨胀与腐蚀得到图像之后求差值得到称为X方向梯度,用Y方向直线做结构分别膨胀与腐蚀之后得到图像求插值之后称为Y方向梯度。
图像与图像开运算的差值
kernel = np.ones((9, 9), dtype=np.uint8)
tophat = cv.morphologyEx(img, cv.MORPH_TOPHAT, kernel)
图像与图像闭运算之差
blackhat = cv.morphologyEx(img, cv.MORPH_BLACKHAT, kernel)
在上面,利用Numpy
手动创建了一个矩形的结构元素,但是在某些情况下,可能需要其他形状的内核,因此,OpenCV
提供了一个函数 cv.getStructuringElement(op, ksize)
。传递形状和大小,即可获得所需内核。
# 矩形内核
>>> cv.getStructuringElement(cv.MORPH_RECT,(5,5))
array([[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1]], dtype=uint8)
# 椭圆内核
>>> cv.getStructuringElement(cv.MORPH_ELLIPSE,(5,5))
array([[0, 0, 1, 0, 0],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[0, 0, 1, 0, 0]], dtype=uint8)
# 十字内核
>>> cv.getStructuringElement(cv.MORPH_CROSS,(5,5))
array([[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0],
[1, 1, 1, 1, 1],
[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0]], dtype=uint8)
OpenCV
提供三种类型的梯度滤波器或高通滤波器,即 Sobel
,Scharr
和Laplacian
。
Sobel
算子是高斯平滑加微分运算的联合运算,因此它更抗噪声。可以指定要采用的导数方向,垂直或水平(分别通过参数yorder
和xorder
)。还可以通过ksize
指定内核大小,如果 ksize=-1
,则使用 3 × 3 3 \times 3 3×3 的Scharr
滤波器,比 3 × 3 3 \times 3 3×3 Sobel
滤波器具有更好的结果。
Sobel(src, ddepth, dx, dy, dst, ksize, scale, delta, borderType)
Scharr(src, ddepth, dx, dy, dst, ksize, scale, delta, borderType)
Laplacian(src, ddepth, dx, dy, dst, ksize, scale, delta, borderType)
- src: 输入需要处理的图像
- ddepth: 输出图像深度,为了避免溢出,一般选择
CV_32F
- dx: 表示x方向上的差分阶数
- dy: 表示y方向上的差分阶数
- dst: 输出与src相同大小和相同通道数的图像
- ksize: 算子的大小
- scale: 缩放导数的比例常数默认情况没有伸缩系数
- delta: 一个可选的增量,将会加到最终的dst中。
- borderType: 判断图像边界的模式。
计算了由关系 Δ s r c = ∂ 2 s r c ∂ x 2 + ∂ 2 s r c ∂ y 2 \Delta src = \frac{\partial ^2 src}{\partial x^2} + \frac{\partial ^2 src}{\partial y^2} Δsrc=∂x2∂2src+∂y2∂2src 给出的图像的拉普拉斯图,如果 ksize = 1
,它是每一阶导数通过 Sobel
算子计算。然后使用以下内核用于过滤:
k e r n e l = [ 0 1 1 1 − 4 1 0 1 1 ] kernel = \begin{bmatrix} 0 & 1 & 1 \\ 1 & -4 & 1 \\ 0 & 1 & 1 \end{bmatrix} kernel=⎣⎡0101−41111⎦⎤
laplacian = cv.Laplacian(src,cv.CV_64F)
sobelx = cv.Sobel(src,cv.CV_64F,1,0,ksize=5)
sobely = cv.Sobel(src,cv.CV_64F,0,1,ksize=5)
这是一个多阶段算法,由于边缘检测容易受到图像中的噪声影响,因此第一步是使用 5 × 5 5 \times 5 5×5 高斯滤波器消除图像中的噪声。
然后使用 Sobel
核在水平和垂直方向上对平滑的图像进行滤波,以及在水平方向 ( G x ) (Gx) (Gx) 和垂直方向 ( G y ) (Gy) (Gy) 上获得一阶导数。从这两张图片中,我们可以找到每个像素的边缘渐变和方向,如下所示:
E d g e _ G r a d i e n t ( G ) = G x 2 + G y 2 A n g l e ( θ ) = tan − 1 ( G y G x ) Edge\_Gradient \; (G) = \sqrt{G_x^2 + G_y^2} \\ Angle \; (\theta) = \tan^{-1} \bigg(\frac{G_y}{G_x}\bigg) Edge_Gradient(G)=Gx2+Gy2Angle(θ)=tan−1(GxGy)
渐变方向始终垂直于边缘。将其舍入为代表垂直,水平和两个对角线方向的四个角度之一。
在获得梯度大小和方向后,将对图像进行全面扫描,以去除可能不构成边缘的所有不需要的像素。为此,在每个像素处,检查像素是否是其在梯度方向上附近的局部最大值。查看下面的图片:
点A在边缘(垂直方向)上。渐变方向垂直于边缘。点B和C在梯度方向上。因此,将A点与B点和C点进行检查,看是否形成局部最大值。如果是这样,则考虑将其用于下一阶段,否则将其抑制(置为零)。 简而言之,你得到的结果是带有“细边”的二进制图像。
该阶段确定哪些边缘全部是真正的边缘,哪些不是。为此,我们需要两个阈值minVal
和maxVal
。强度梯度大于maxVal
的任何边缘必定是边缘,而小于minVal
的那些边缘必定是非边缘,因此将其丢弃。介于这两个阈值之间的对象根据其连通性被分类为边缘或非边缘。如果将它们连接到“边缘”像素,则将它们视为边缘的一部分。否则,它们也将被丢弃。
边缘A在maxVal
之上,因此被视为“确定边缘”。尽管边C低于maxVal
,但它连接到边A,因此也被视为有效边,我们得到了完整的曲线。但是边缘B尽管在minVal
之上并且与边缘C处于同一区域,但是它没有连接到任何“确保边缘”,因此被丢弃。因此,非常重要的一点是我们必须相应地选择minVal
和maxVal
以获得正确的结果。
在边缘为长线的假设下,该阶段还消除了小像素噪声。 因此,我们最终得到的是图像中的强边缘。
cv.Canny(image, threshold1, threshold2, edges, apertureSize, L2gradient)
image: 要检测的图像
threshold1: 阈值1(最小值)
threshold2: 阈值2(最大值),使用此参数进行明显的边缘检测
edges: 图像边缘信息
apertureSize:
sobel
算子(卷积核)大小L2gradient: 布尔值
True: 使用更精确的L2范数进行计算
E d g e _ G r a d i e n t ( G ) = G x 2 + G y 2 Edge\_Gradient(G) = \sqrt{G^2_x + G^2_y} Edge_Gradient(G)=Gx2+Gy2Fasle: 使用L1范数
E d g e _ G r a d i e n t ( G ) = ∣ G x ∣ + ∣ G y ∣ Edge\_Gradient(G) = |G_x| + |G_y| Edge_Gradient(G)=∣Gx∣+∣Gy∣
edges = cv.Canny(src,100,200)
通常,我们过去使用的是恒定大小的图像。但是在某些情况下,我们需要使用不同分辨率的(相同)图像。例如,当在图像中搜索某些东西(例如人脸)时,我们不确定对象将以多大的尺寸显示在图像中。在这种情况下,我们将需要创建一组具有不同分辨率的相同图像,并在所有图像中搜索对象。这些具有不同分辨率的图像集称为“图像金字塔”(因为当它们堆叠在底部时,最高分辨率的图像位于顶部,最低分辨率的图像位于顶部时,看起来像金字塔)。
有两种图像金字塔。高斯金字塔 和 拉普拉斯金字塔
高斯金字塔中的较高级别(低分辨率)是通过删除较低级别(较高分辨率)图像中的连续行和列而形成的。然后,较高级别的每个像素由基础级别的5个像素的贡献与高斯权重形成。通过这样做, M × N M\times N M×N图像变成 M / 2 × N / 2 M/2×N/2 M/2×N/2图像。因此面积减少到原始面积的四分之一。它称为Octave。当我们在金字塔中越靠上时(即分辨率下降),这种模式就会继续。同样,在扩展时,每个级别的面积变为4倍。我们可以使用cv.pyrDown()
和cv.pyrUp()
函数找到高斯金字塔。
cv.pyrDown(src, dst=None, dstsize=None, borderType=None)
- src: 输入图像
- dst: 输出图像,与src类型、大小相同
- dstsize: 表示降采样之后的目标图像的大小
cv.pyrUp(src, dst=None, dstsize=None, borderType)
a = cv.imread('E:/Computer/Desktop/opencv/apple.jpg', 0)
o = cv.imread('E:/Computer/Desktop/opencv/orange.jpg', 0)
G = a.copy()
gpA = [G]
for i in range(6):
G = cv.pyrDown(G)
gpA.append(G)
for i in range(len(gpA)):
plt.subplot(3, 3, i+1), plt.imshow(gpA[i])
G = o.copy()
gpO = [G]
for i in range(6):
G = cv.pyrDown(G)
gpO.append(G)
for i in range(len(gpA)):
plt.subplot(3, 3, i+1), plt.imshow(gpO[i])
普拉斯金字塔由高斯金字塔形成。没有专用功能。拉普拉斯金字塔图像仅像边缘图像。它的大多数元素为零。它们用于图像压缩。拉普拉斯金字塔的层由高斯金字塔的层与高斯金字塔的高层的扩展版本之间的差形成。
lpA = [gpA[5]]
for i in range(5,0,-1):
GE = cv.pyrUp(gpA[i], dst=GE, dstsize=gpA[i-1].shape)
L = cv.subtract(gpA[i - 1], GE)
lpA.append(L)
plt.subplot(2, 3, 6-i)
plt.imshow(L)
lpB = [gpB[5]]
for i in range(5,0,-1):
GE = cv.pyrUp(gpO[i], dst=GE, dstsize=gpA[i-1].shape)
L = cv.subtract(gpO[i - 1], GE)
lpB.append(L)
plt.subplot(2, 3, 6-i)
plt.imshow(L)
金字塔的一种应用是图像融合。例如,在图像拼接中,需要将两个图像堆叠在一起,但是由于图像之间的不连续性,可能看起来不太好。在这种情况下,使用金字塔混合图像可以无缝混合,而不会在图像中保留大量数据。
import math
LS = []
for la,lb in zip(lpA,lpB):
rows,cols = la.shape
ls = np.hstack((la[:,0: math.ceil(cols/2)], lb[:,math.ceil(cols/2):]))
LS.append(ls)
# 现在重建
ls_ = LS[0]
for i in range(1,6):
ls_ = cv.pyrUp(ls_, dstsize=LS[i].shape)
ls_ = cv.add(ls_, LS[i])
# 图像与直接连接的每一半
real = np.hstack((a[:,:math.ceil(cols/2)],o[:,math.ceil(cols/2):]))
plt.imshow(real)