写在前面
在我许多期博客中,如根据内容的图画缩放、图画手绘/素描风格转换、图画的部分特征信息及全景图画拼接,都有涉及到图画梯度的概念,因为梯度确实是一种具有高信息熵且易于提取的信息,在图画处理、核c $ V 1算机视觉、机器视觉领域研讨中应用广泛,我在许多彻h ` @底不同的图画研讨方向的文献中见到过图画梯度的身影。能够说,绝大多数图画相关的研讨都能够运用图画的梯度去完结特定效果的优化,仅仅一些情况下该优化的效果很小。可是,这并不影响图画梯度的价值。而图画T – 8 3 ( X边际、图画几许特U s _ ] p – #征更是当前核算机视觉、模式辨认研讨的要点关注目标,图画边际信息对行人检测、人脸辨认等人工智能主流研讨方向有着不行替代的效果。
在本4 R i – F 6 e f &期博客中,咱们将探讨如下问题:
- 图画梯度与图画边际
- 图画几许检测原理及其完结
Let’s begin!!
1.图画梯度与图画边际
咱们来看一下维基百科关于IB z Z omage Gradient(图画梯度)的解说。
“An image gradient is a directional change in the intensi n ) K 2 U ) N .ty or color in an image. The gradient of the image is one of the fundamental building blocks in image processing. For example, the Canny edge dek o N V } $ q Ztector uses image gradie t N I G p . 9nt for edge detection. In graphi$ M q C .cs software for digital image editing, the term gradient or color gradient is also used for a graf d 8 9 t )dual blen8 f !d of colo ~ % jr which can be considered as an even gradation from low to high values, as used from white to black in the images to the right. Mathematically, thR I z O r A m Ie gradient of a two-variable function (here the image intensity function) at eA ! Pach image point is a 2D vector with the components given by the derivatives in the horizontal and vertical directions. At each image point, the gradient vw O 9 | ` j Xector points in the dio s : p 9 x j g lrection of largest possible intensity increase, and the length of the gradient vector corresponds to the rate of chang0 i 5 e l i re in that direction.”
“图画梯度是指图画强度或色彩的方向改动。图画梯度是图画处理的根底结构之一。例如,Canny边际检测器运用图画梯度进行边际检测。在用于数字图画编辑的图形软件中,梯度或色彩梯度一词也用于表明色彩的逐步混合,能够以为是从低到高值的均匀渐变,如下图中从白色到黑色所运用的渐变。数学上,一个双变量函数(这里是图画强度函数)在每个图画点处的梯度是一个二维向量,其分量由水平缓笔直方向上的导数给出。在每个图画点上,梯度向量指向或许最大强度增加的方向,梯度向量的长度对应于该方向的改动率。”
上述内容十分严谨地界说了图画和视觉中的图画梯度,可是依然$ ? # 2需求指出,蓝字部分的} | g g e + 3 ?内容存在一些难以了解的部分,如色彩的逐步混合,其实这里指的是Photoshop(或其他的图形编辑软件)中渐变的效果,当然,这些渐变也属于梯度。
维基百科用了较浅显的言语去描述图画梯度,相比之下+ * $ ; { /,百度百科对图l ) Z & S g w p c画梯度的解说就显/ D ; c %得更专业,更简略了解,如下:
i h , 4 P“图画梯度能够把图画看作二维离散函数,图画梯度其实便是这个二维离散函数的f y ? C % O ] . V求导:
图画梯度:
其间,为灰度图画在第行第个像素上的像素值。
上面说的是D H k x d D *简略的梯度界说= u } , q @ Y,其实还有更多更杂乱的梯度公式。“
我更喜爱百度百科更接近实质的解说,简略来说,图画梯度便是图画在不同方向上的改动趋势,能够将图画视为只存在水平缓笔直两方向存在梯度改动,进行临近像素的梯度提取。R 8 y / * 8 U K当然,这么做会存在必定的问题,那便是这样求解得到的梯度信息对噪声敏感,即对噪声鲁棒性差。
接下来,k M = L q g u将以实践的图画为例,更进一步的解说1.1 图画梯度、图画边际及梯度提取算子,并介绍现在最广泛应用的1.2 canny边际检测算法原理及完结。
1.1图画梯度、图画边际及梯度提取) F c i C | j _ ;算子
说起图画梯度,其实咱们得好好Q z Q x f谈一谈图画边际,从许多曾经的视觉研讨试验中,咱们能 m t够发现,边际是很特别的一种信息,因为人眼辨认物体,靠的正9 % S 6 f :是边际,而非色彩、纹路。在绘画中咱们能够经过线条去绘制任/ n t B何相同物体,但若该手绘物体缺失了部分边际,咱们就难以辨认。但相反的,咱们对缺失了纹路、色彩的物体,却依然具有强壮的辨别才能。v y ;
Walther等人在2011年进行了这样一个视觉研讨,让视觉受试者观看海滩、城市大街、森林、高速公路、山脉和办公室* z I w j ~ 7的照片和线条图时收集功用磁共振成像数据。尽管在场景统计数据上有明显的差异,A h N * t .但研讨者们还是能够经过P# + d ] . } h ) 5PA(the parahippocampal place area,海马旁区和RSC(tY a e N ? D .he ret-rosplenial cortex,脾叶皮层)解码初级视觉皮层中线条图的fMRI(FunctionalF Q _ u z F & magnetic resonance imaging,功用磁共振成像)数据Y : f – A V u L |和彩色照片的活动数据的场景类别。更值得留意的是,在PPA和RSC中,从线条图辨认的错F s W t 0 {误模式与x C 5 A从彩色照片辨认的错误模式十分相似。这些数据表明,在这些区域,用于差异场景类别% ] 3 O : ~ V的信息关于线c i ~ 条图和照片来说是相似的。
而咱们在议论图画梯度的时分,许多情况下关注的是图画边际。因而,图画的边际检测的意图,即为1 V ( ` H确认图画突变(不接连)的当地
- 明显,图画内高信息熵信息隐藏在边际信息傍边
- 边际传递的信息多余像素点本身
边际的由来5 K N – [ ~首要源于:
- 外表a y m % ?的不接连
- 深度(间隔)的不接连
- 外表色彩f J / Q _ o Z c的不接连
- 光照的不接连
现已介绍完梯度与边际,* : 7 8 U # v咱们现在就先来看看梯度的核算办法。在先前度 % G G + }娘百科(百度百科)关于图画梯度现已有了一个数理上的界说,当然,关于图画梯E l K度的表明和核算办法有许多,关于一幅给定的数字图画c A % 1 Q ! J,咱们将其视作二维离散函数,那么常见的梯度表征和核算办法有:
- 梯度向量
- 梯度幅值
- 梯度方向
而在实践编程运算时,咱们一般选用算子经过卷积取得一幅图画的梯度信息,假如不明白何为图画中的卷积运算,能够看看下图。
实践上,上图中的原图画巨细为(左边),而卷积核为(中间),因而得到的卷积成果是的(右侧)。原图画巨细为(m为奇数W a Z = %)的图h I = j画,经过(k为奇数)的卷积核卷积后,得到的卷积成果图画巨细为。
以最简略的Prewitt算子为例,运用算子卷积的图画梯度成果如下:
不难发现,在梯度图画中,值越高的部分,越接近人眼视觉所观察到的边际。因而,咱们能够用梯度更直接的解说边际:边际是图画能量急剧改动的当地,是图画梯度高的当地。
但这也存在一个问题,图画的梯度是对邻域像素值改动敏感的,而图画内的噪声则会导致部分非边际区域相同具有较高的梯度,这会导致边际检测x C t | v的噪声鲁棒性差。
为了进7 z K 4 t * + D ,步噪声鲁棒性,Canny在, T S 3 4 & %1986年提出了一种边际检测算法,该算法是边际检测中最经典,最( 7 $ J v :具影响力的办法,至今还有十分7 K Q d 广泛的应用,是核算机视觉领域E I p的根底结构组成之一,论文被引超20000余次。
1.2 Canny边际检测算法原理
Cann. 3 K 2 h E O Jy边际检测算法能够分为v U u T J ` * y四步:
- 噪声按捺
- 核算图画梯度及方向
- 非极大值按捺
- 双阈值Y z ( M Q检测
- 边际连接
关于噪声按捺和梯度及梯度方向的核算,Canny边际检测算法并没有进行创新,仅仅做了单纯的高斯核卷积滑润,以此进行噪声按捺,并经过差分运算得到梯度及梯度方向。C) H o Sanny边际检测突破性的创新是集中在后三步上的。
非极大值按捺
非极大值0 B S Y ^按捺的思Y H b + ( K 0 v q维在于:当图画的– [ D q } ^ D梯度到达部分(特定巨细的邻域)最; 2 p # j n ,大值的时分,咱们以为它是边际;关于非极大值的3 i [ r b X点,即便它超过了咱们所界说的“边际梯度阈值”,t Q U S 2 n仍不以为它是边际。而且,预界说了八个或许的方向(左上、上、右上、右、右F Z r } + ; ~ 2 D下、下、左下、左),关于该方向上的梯度值,将非极大值归零。
具体操作:1.挑选幅值大于阈值的像素点,设为待验证点;2.遍历待验证点,若为: k o t % F部分最大值,保存,反之去归零;3.保存下来的点便对错极大值按捺后的边际点。
双阈值检测
说到双阈值检测,咱4 0 ?们得先说选用单一阈值的缺陷,如下图所示。
清楚明了,选用单一阈值会有两种问题随之发生:1.阈值过高,会导致部分梯度较低的边际被当成噪声除掉,导致边际不接连;2.阈值过低,会有过多不应该作为边际的梯度信息被O j _ w V _ |误判为边际而包括进边际图。
而双阈f $ I $ j 8 i值解决了单Y $ p z F阈值的上述问题= ) % N s u V 。双阈值界说了两种阈值,低阈值与高阈值:若梯度小于低阈值,则不是边际;若梯度大于高阈值,则为强边际l – _ R;若梯度在凹凸阈值间,则为弱边际。
至此,便是双阈值检测的部分,乍一看仿佛与单一阈值没有差异,只需s 4 Z A ^ * x o G高于低阈值就能作为弱边际,那这么一来G l U u v好像跟单阈值就没什么差异了。诚然,假如缺少了“边际连接”这下一过程,双阈值和单阈值的实质确实是相同的。
边际连接
在开端解说“边际连接”这一o z 0 e ^ |过程前,咱们不妨来仔细观察一下“单阈~ ! –值”与“双阈值”的图画,这关于“边际连接”原理的了解十[ 2 H Z _分有m V T l V a协助。
在单阈值图画中,存在部分“离群”的线条,其间有一部分其实并非实在的边际,而是被误判的高梯度方位;其间也有一部分不连通的实在边际,这又导致了边际的不接连。
而在双阈值图画中,较亮的线条为强5 E i 7 F N X边际,稍暗一些的线条为弱边际。不难发现,一条不与强边际相连的弱边际,基本都是被误判为边际的区域。相反,与强边际相连的弱边际,则囊括了绝大多数单阈值所没有断定包括的“实在边际”。
那么现在,咱们能够明白边际连接的原理了:D 1 t Q N c N运用高阈值来确c H / + * Q认首要边际轮廓(强边际),并运用与强边际连接的低阈值所得弱边际来添补缺失边际,并避免引入噪声(不与强边际相连的弱边际就归零)。
由此,完好地解说了Canny边际检测算法的原理,~ ; d 9 ( v P I w咱们进入代码$ x d F w ? E n h部分。
1.3 代码实战 E { x s `部分
### edge.py
import numpy as np
import math
def conv(image, kernel):
"""卷积的一J . d t { D ` ?个完结.
关于恣意一个像素点,该本版选用了点积运算以及 np.sum()来进行快速的加权求和
Ac J J x ~ q |rgs:
图画: 尺度为(Hi, Wi)的numpy数组= H g g.
卷积核(kernel): 尺度为(Hk, Wk)的numpy数组.
Re4 V U Q 4turns:
out: 尺度为(Hi, Wi)的numpy数组.
"""
Hi, Wi = image.shape
Hk, Wk = kernel.shape
out = ny # ? } Op.zeros((Hi, Wi))
# 关于本次作业, 咱们将运用鸿沟值来对图画进行填充.
# 这是因为,假如运用0进行填充,会使得在图画鸿沟上的导数十分大,
# 但一般而言,咱们期望忽略图画鸿沟上的边际(图画鸿沟永远不会被以为是边际).
padL E X 9 _ ] H n_width0 = Hk // 2
pad_width1 = Wk // 2
pad_width = ((pad_width0,pad_wiv o Z @ m N 1 ,dth0),(pC 2 Q x { u D 7ad_width1,pad_width1))
padded = np.pad(image, pad_width, mode='edge')
### YOUR CODE HERE
for i in range(Hi):
for j in range(Wi):
out[i][j] = (padded[i:i + Hk,j:j + Wk] * kernel).sum()
### END YOUR CODE
return out
def gaussian_5 t & I N d c pkernel(size, sigma):
""" 生成高斯卷积核.
该函数按照高斯核的公式,生成了一个卷积核矩阵。
提示:
- 运用 np.pi 以及 np.exp 当核算 pi 以及 exp()函数.
参数:
size: 一个整数,表明输r d j ! C出的m U V xkernel的尺度.
sigma: 一个float,对应于高斯公式中的sigma,用来控制权重的分配.
返回值:
kernel: 尺o ! i F f p度为(size, size)的numpy数组.
""1 Q X G"
kernel = np.zeros((size, size))
### YOUR CO+ N 7 / gDE HERE
k=(size-1)/2
for i% O k ; K 8 # . P in range(sie 1 v @ ~ Z [ze):
for j in range(size):
kernel[i][j]=1/(2*np.pi*sigma**2)*np.exp(((i-k)**2+(j-k)**2)/(-2*sigma**2))
#kenel[i][j]=1/(2*np.pi*power(sigma,2))*np.exp(((power(i-k,2)+(power(S @ L cj-k,2))/(-2*power(sigma,2)))
### END YOUR CODE
return kernel
dq _ Aef partiau l C h e 8 pl_x(img):
""W r B e ," 核算输入图画水平方向的偏导.
提示:
- 你能够运用你在前面完结的conv函数.
参! H t数:
img: 尺度为(Hi, Wi)的numpy数组.
输出:
out: 水平方向的梯度图画
"""
Hi, Wi = imq + Kg.shape
out = No0 K y .ne
padded = np.padM Y , S o 8 A(img, 1, mode='edge')
padded1 = np.zeros(padde8 p % U T !d.shape)
### YOUR CODE HERE
for i in range(1,Hi+1):
for j in range(1,Wi+1):
padded1[i][j]=(padded[i][j+1]-padded[i][jy d Q + e-1])/2
### END YOUR CODE
out=padded1[1:Hi+J 1 b r1,1:Wi+1]
return out
def partia+ ] ? c Il_y(img):
""" 核算输入图画竖直0 ; [方向的偏导.
提示:
- 你能够运用你在前面完结的conv函数/或许用前面刚开发的partial_x函数.
参数:
img: 尺度为(Hi, Wi)的numpy数组.
输出:
out: 竖直方向的梯度图画
"""
Hi, Wi = im + T t : Y umg.shape
out = None
paddY @ 4 , A # U a ged = np.pad(img, 1, mode='edge')
padded1 = np.zeros(padded.shape)
### YOUR CODE HERE
for i in ri 5 #ange(1,Hi+1):
for j in range(1,Wi+1):
padded1[i][j]=(padded[i+1][j]-padded[i-1][j])/2
### END YOUR CODE
out=padded1[1:Hi+1,f Y ) g , . L F1:Wi+1]
return out
def gradient(img):
""" 核算输入图画的梯度巨细和方向.
参数:
img: 灰度图. 尺度为 (H, W) 的Numpy数组.
返回值:
G: 输入图画的梯度值图画。它的每个像素点值都是该像素点的梯度值.
尺度为 (H, W) 的Numpy数组.
theta: 输入图画的梯度方向图画(角度, 0 <= theta < 3K } 4 ,60),每个像素点都代表该像素点的梯度方向.
尺度为 (H, W) 的Numpy数组.
提示:
- 能够运用 np.sqrt 以及 np.arctan2 来核算平方根以及横竖切值
"") w / 1 q Y @ i"
Hi, Wi = imK c j B cg.shape
G = np.zeros(img.shape)
theta = np.zeros(img.shape)
outx=partial_x(img)
outy=partial_y(* I k Eimg)
### YOUR CODE HE! B n m 5RE
for i in range(Hi):
for j in range(Wi):
G[i][j]=np.sqh 8 A lrt(outx[i][j]**2+outy[i][j]*2 _ } E z q*2)
for i in range(Hi):
for j in range(Wb Q { 7 N - Xi):
theta[i][j]=np.arS n 9 g [ n ~ &ctan2(outy[i][j],outx[i][j])* 180 / np.pi
if theta[i: Y 2][j0 c ( i]<0:
theta[i][j]=theta[i][j]+360
if theta[i][j]>360:
theta[i][j]=theta[i][j]-360
### END YOUR CODE
return G, theta
def non_maximum_suppre| s ( 0 r . H dssiO ( e w ) [ . Gon(G, theta):
""" 完结非极大值按捺
关于恣意一个像素点,该函数都完结了在梯度方向上(theta)的非极大值按捺
参数:
G: 梯度幅值图画,T / X ( 5 ` +尺度为 (H, W)$ t U p的Numpy数组.
theta: 梯度方向图画,尺度为 (H, W)的Numpy数组.
Returns:
out: 非极大值按捺今后的图画。假如像素点不是部分极大值点,则置0,不然保存原梯度幅值.
"""
padded = np.pad(G, 1, mode='edge')
H, W = G.shape
out = np.zeros((H, W))
# 将梯度方向round到最近的45上来
theta =L u f ) np.floor((theta + 22.5) / 45) * 45
### BEGIN YOUR CODE
for i in range(1,H+1):
for j in range(1,W+1):
thetaij=theta[i-1][j-1]
if thetaij==45:
if padded[i][j]&0 T u @gt;padded[i+1][j+1] and padded[i][j]>padded[i-1][j-1]:
out[i-1][j-1]=paddS H ped[i][h / v t Q ej]
else:
out[i` C V-1][j-1]=0
if thetaij==225:
if padded[i][j]>padded[i+1][j+1] and padded[i][j]&gI [ y Lt;padded[i-1][j-1]:
out[i-1][j-1]=paq q } * H / n b 9dded[i][j]
else:
out[i-1][j-1]=0
if thetaij==90:
if padded[i][j]>pal ~ i | sdded[i+1][j] and padded[i][jO ) 7 A h U]>paddeM u C Dd[i-1][j]:
out[i-1][j-1]=padded[i][j]
else:
out[i-1][j-1]=0
if thetaij==? h e c G V #270:
if padded[i][jQ e u = ; U I]>padded[i+1][j] and padde( X , j 1 ?d[i][j]>padded[i-1][j]:
out[i-1][j-1]=padd[ 5 ; p n { ced[i][j]
else:
out[i-1][j-1]=0
if theta- L 3 j $ %ij==135:
if padded[i][j]>padde. d Q 9d[i-1][j+1] and padded[i][j]&Z 3 D _ * I Bgt;padded[i+1][j-1]:
out[i-1][j-1]=padded[i][j]
else:
out[i-1][j-1]=0
if thetaij==315:
if padd, U ^ l ; - o #ed[i][j]>padded[i-1][j+1] and padded[i][j]>: I ) ! z I J R i;padded[i+1][j-1@ = v w L k c H]:
out[i-1][j-1]=padded[i][j]
else:
out[i-1][j-1]=0
if thetaij==0:
if padded[i][j]&r R g 5gt;padded[i][j-1] and padded[i][j` n 1 E (]>paddeT 4 5 0 ) fd[i][j+1]:
out[i-1][j-1]=pJ e y o 3 oadded[i][j]
else:
out[i-1][j-1]=0
if thetaij==180:
if padded[i][j]>padded[ic a X b # I P][j-1] and padded[i][j]>padded[i]L e K[j+1]:
out[i-1][j-1]=padded[i]P g = @ A u s X[j]
else:
oue 2 c C T 0 t[i-1][j-1]=0
if thetaij==360:
if padded[i][j]>padded[i][j-1] and padded[i][j]>padded[i][j+1]:
out[i-1][j-1]m [ L=padded[i][j]
else:
out[i-1][j-1]=0
### END YOUR CODE
return out
def double_thresholding(img, high, low):
"""
参数:
img: 非极大值按捺今后的梯度幅值图画$ A - S % + $ G b.
high: 关于强边际(strong edge)而言的高阈值.
low: 关于弱边际(strong edge)而言的高阈值.
返回值:
strong_edges: 布尔类型的数组,N ( ^ u q H .代表强边际。
强边际指的是梯度的幅值大于高阈值的像素点
弱边际: 布尔类型的数组,代表弱Z _ g u边际。
强边际指的是梯度的幅值小于或许等于高阈值,而且( 2 k b c大于低阈值的像素点。
"""
st] P Z *rong_edges =p 6 c s ] ~ K np.zeros(img.shape,* o E 9 d # 5 dtype=np.bool)
weak_edges = no ) ) l Z $ ~p.zero[ y t x 0 ` / 3s(img.shape, dtyn V ( Z / S s @ }pe=np.bool)
H, W = img.shape
### YOUR CODE HERE
fH F ~ z v a k dor i in range(H):
for j in range(W):
iF k i Z q yf6 0 y [ v t y J img[i][j]>high:
strong_edges[i][j]=img[i][j]
if img[i][j| 5 R]<=h] u ) X Kigh and img[i][j]>low:D T y 5
weak_edges[i][j]=img[i]* 2 w ^ 8 S[j]
###( Q I a m I K END YOUR CODE
retu@ { 6rn strong_edges, weak_edges
def get_neigh: R u g d pbors(y, x, H, W)O r K z:
""" 返回坐标为 (y, x) 的像素点点的街坊(3 h ` J Anev y ? v F D & 1 7ighbor).x @ o l T t
关于一幅尺度为(H, W)的图画,返回像素点 (y, x) 的街坊的一切有用索引.
一个有用的索引 (i, j) 应该满意:
1.; f 2 i >= 0 and i &u t 6 6 h $lt; H
2. j >= 0 and j < W
3. (i, j) != (y,$ Z o K O o Q T + x)
参数:
y, x: 像素点的方位
H, W: 图] ) 画的尺度
返回值:
neighbors: 该像素点的街坊的索引值[) 4 . 8 T V M(i, j)]所组成的list .
"""
ne 0 $ E k c V Gighbors = []
for i in (y-1, y, y+1):
for j in (x-1, x, x+1):
if i >= 0 and i < H and j >3 - $ m ( P= 0 and j < W:
if (i == y and j == x):
continue
neiR G @ ? p jgh! 8 ( e (bors.append((i, j))
retu7 $ srn neighbors
d$ X | V w M T + Ief link_edges(strongk ! W q_edges, weak_edges):
+ ` d H L ) N""" 找出与实在的边际相连接的弱边际] Y N,并将它们连接到一同.
关于每个强边际点,它们都是实在的边际。
咱们需求遍历每个实在的边际点,然后在弱边际中找出与之相邻的像素点,并把他们连接起来。
在这里,咱们以为假如像素点(a,v ? V @ = @ b)与像素点(c, d)相连,只需(a, b)坐落(c, d)的八邻域内。
Args:
strong_edgeU ? K ns: 尺度为 (H, W)的二值图4 K y a k i画.
weak_edges: 尺度为 (H, W)的二值图画.
Returns:
edges: 尺度为 (H, W)的二值图B A X 7 ( |画.
提示:
弱边际一旦与强边际相连,那它便是实在X | s Q k s的边际,与强边际的方位相同。
这@ Q R J . |句话的意思是,一旦一个弱边际像素点被检测r J 0成为了一个实在的边际点,一切与它相连的其它弱边际也应该被归为实在的边际。
所以在编程的时分,你只遍历一遍强边际是不行的,因为此刻或许有新的弱边际点被标记为实在的边际点。
"""
H, W = strong_edge) 5 J V h ls.shape
indices = np.stack(np.nonzero(strong_edges)).T
edges = np.zeros((H, W), dtype=np.bool)
# 生成新的拷贝
weak_edges = np.copy(weak_edges)
edges = npB : ) 2 K n $ ^.copy(strong_edges)
g=0;
padded_weak = np.pad(weak_edges, 1, mode='edge')
edges1=np.pad(strong_edges, 1, mode='edge')
### YOUR CODE HERd Y & & z 1 ^ WE
while(g!=-1):
g=0;
k=0;
for i in range(H):
for j in range(W):
#if strong_edx ^ 1ges[i][@ L D F x 4 = ~ 9j]!=0:
if edges1[i+1][j+1]!=0:
if padded_weak[i][j]!=0 and edges1[i][j]!=padded_8 m 1 n a M , B Mweak[i][j]:
edges1m J + o : ; ^[i][j]=padded_weak[i][j]
k=k+1
if p/ 5 2 ! ? `added_weak[i][j+1K O ] / ) D ^ K]!=0 a= F I D & q wnd edges1[i][j+1]q 9 c J Y . :!=padded_weak[i][j+1]:
edges1[i][j+1]=padded_weak[i][j+1]
k=k+1
if padded_weak[i][j+2]!=0 and edgesc # _ H r N h s 01[i][j+2]!=padded_weak[i][j+2]:
edges1[i][j+2]=padded_weak[i][j+2]
k=k+1
if paH / ] Udded_weak[i+1][j]!=0 and edges1[i+1][j` 2 ? N I a 0]!=padded_weak[i+1][j]M _ e C K c ^:
edges1[i+1][j]=padded_weak[i+1][j]
k=k` * P o k ,+1
if padded_weak[i+1][j+2]!=0 and edges1[i+1][j+2]!=padded_weak[i+1][j+2]:
edges1[i+1][j+2]=padded_weak[i+1][j+2]
k=k+1
if padded_weak[i+2][j]!=0 and edges1[i+2][j]!=padded_weak[i+2][j]:
edges1[i+2][j]=padded_weak[i+2][j]
k=k+1
if padded_weak[i+2][j+1]!=0 and edges1[i+2][j+1]!=padded_weak[i+2][j+1]:
edges1[i+2][j+1]=padded_weak[i+2]O q o ? 5 ; *[j+1]
k=k+1
if padded_weak[i+2][j+2]!=0 and edgeD N R t ~ 7 Ws1[i+2][j+2]!=padded_weak[i+2][j+2]:
edges1[i+2][j+2]=padded_we; s b L , 9 `ak[i+2][j+2]
k=k+1
if k==0:
g=-1;
else:
strong_egdes=I _ ? Fnp.copy(edges1[1:H+1,1:W+1])
edges=edges1[1:H+1,1:W+1]
#z 2 t l v O e 6## END YOUR CODE
return edges
def canny(img, kernel_size=5, sigma=1.4, high=20, low=15):
""" 将上面所完结的函数组合到一同,完结canny边际检测器8 ` R - c ) e R.
Args:
img: 输入图画
kernel_size: int, 表明keV p Y *rnel的巨细
sigma: float, 用来核算ker/ 0 F | cnel.
high: 为强边际而设的高阈值.
low: 为弱边际而设的低阈值.
Returns:
edge: 输出的边际图画
"""
### YOUR CODE HERE
kernel = gaussian_kernel(kernel_size, sigma)% t / % Y b
smoothed = conv(img, kernel)
G,theta=gradient(smoothed)
nms = non_maximum_suppression(G, theF m 5 *tas B A J 1 ; @ m !)
strong_edges, weak_edget ! 7 * d Ls = double_thresholding(nms, high, lowK + H M)
edge = lin2 Z 2 r 4 g ^k_edges(strong_edges, weak_edges)
### END YOUR CODE
reH 4 r 5 & fturn edge
1.4 施行例
初始化
# 初始化装备
import numpy as np
i] 7 s = ( ?mport m= [ F F ! 4 m Aatplotlib.pyplot as plt
fra 4 Dom time import time
from skime a h 6age import io
%matplotlib inline
plt.rcParams['figure.figsize'] = (1B | D X V 3 B5.0, 12.0) # 设置默许尺度
plt.rcParams['imaB u n H b = Mge.interpolation'] = 'nearest'
plI O n / W at.rcParams['image.cmap'] = 'grb ( ! [ k -ayJ z R'
# 自动重装外部模块
%load_ext autoreload
%autoreload 2
首| n 6 : @ . 2先运用高斯滤波器来对图片进行滑润处理。高/ + t斯核巨细为 的高斯滤波器,该滤波器能够由以下方程给出
from edge import conv, gaussian_kernel
# 界说一个 3x3 的高斯 kernel,_ 1 d I g 7并将其sigma值设为 1
kernel = gaussian_kernel(3, 1)
kernel_test = np.array(
[[ 0.05854983, 0.096] w y m 53235, 0.05854983],
[ 0.09653235Q N ~ ;, 0.15915494, 0.09653235],
[ 0.05854983= X ) u, 0.09653235, 0.05854983]]
)
print(kernel)
# 检测生成的高斯kernel是否正确
if not np.allf C O E W z h H close(kernel, kernel_test):
pr. # = e B @ J C Eint('Incorrect values! Please check your implementation.')
输出成果:
[[0.^ % , o :05854983 0.09653K o e : M S235 0.V P [ q05854983]
[0.09653235 0.15915494 0.09653235]? a F W
[0.05854983o E y 5 V w # 4 0.09653235 0.05854983]]
找一幅图画进行测验。
# 用& P A k不同的尺度以及siL ^ ; w vgma值来进行测验
kernel_size = 5
sigma = 1.4
# 载入/ ) H w % W Y + n图片
img = io.imread('iguana.png', as_grey=True). o e 6 A ] z
# 生成高斯kernel
kernel = gaussian_p 1 7 ] V ] b r Rkernel(kernel_size, sigma)
# 运| K ? C ^ #用kernel来对图片进行滑润
smoothed = conv2 . D(img, kernel)
plt.subplot(1,2,1)
plt.imshow(img)
plt.title('Original image')
plt.axis('off')
plt.subplot(1,2,2)
plt.imshow(smoothed)
plt.title('Smoothed i~ k amage')
plt.} x z 7 : 9 |axis('off')
pltD L | ^ 2 t w.show()
这里有个小问答:改动kernel_size 以及 sigma别离具有什么效果?
改动核的巨细:
会导致中心像素点受临域像素值的影响更大,从到导致高斯滤波的含糊效果更佳明显。
改动sigma的巨细:
若减小,会使高斯滤波核C S n中心权重占比变大,中心方位临域的权重占比都减小,若sigma趋于0,则高斯滤波基本没有效果;若增大,会使高斯滤波核的中心方位权重占比减小P V $ % W G Y P,中心方0 i a ] @ s h U位领域的权重占比增大,若sigma趋于无穷,则高斯滤波等于均值滤波。
咱们回到施行例部分b ` m Q I,找出c @ i G求取图画差分所对应的kernel和
from edge import partial_x, partial_y
# 测验案例
I = np.array(
[[0, 0,! * y 0],
[0, 1, 0],
[0, 0, 0]]
)
# 期望的输出成果
I_x_test = np.array(
[[ 0, 0, 0],
[ 0.5, 0, -0.5],
[ 0, 0, 0]]
)
I_y_test = np.array(
[[ 0, 0.5, 0],
[ 0, 0, 0],
[ 0, -0.5, 0]]
)
# 核算梯度
I_x = partial_x(I)
I_y{ o ] p + w D ) E = partial_y(I)
print(I_x)
prv / % w N Sint(I_y)
# 确认partial_x and partialr y | e 3_y^ 7 [ h是否编写正确
if nO % Q X x ] 5ot np.all(I_x == I_x_| G [ [ Z m Q atest):
print('partial_x incorrect')
if not np.all(I_y == I_y_test):
print('partial_y incorrect')
输出成果:
[[ 0. 0. 0. ]
[ 0.5x } 6 t V b p , p 0. -0.5]
[ 0. 0. 0. ]]
[[ 0. 0.5 0. ]
[ 0. 0. 0. ]
[ 0. -0.5 0. ]]
核算x、y方向上的梯度
# 核算滑润后的图画的差分
Gx = par4 W M i n h tial_x(smoothed)
Gy = partial_y(smoothed)
plt.subplot(1,2,1)
plt.imshow(Gx)
plt.title('Deriva} * ~ H : - y Stive in x direction')
plt.axis('off')
plt.subpl2 s G @ 1 fot(1,2,2)
plt.imshowg l ) R 0 A .(Gy)
plt.title('Derivative in y direction')
plt.axis('off')
plt.show()
成果如下
现在,让咱们用两个方向的差分来核算图片的梯度巨细以及方向,完结edge.py所界说的梯度和梯度方向的求解。
froE w 2 . R : x 3m edge import gradient
G, theta = gradient: H ! r ^(smoothed)
if not np.all(G >= 0):
print('Magnitude of gradients should be non-negative.')H & I R N g A S
if not np.allP $ ~ I b $ B V((theta >= 0) * (theta < 360)):
print('Direction of gradients should be in range 0 <= theta < 360')
plt.imshow(G)
plt.title('Gradient magnitude')
plt.axis('off')
plt.show()
能够发现,找出来的梯度图画比较粗大而且含3 J h糊,并不适合直接用来表明的边际。 此刻咱们能够运用非极大值按捺的办法v # j x r来找出“g F ) 4 1 { + ? X尖利”的( ^ ]边际。简略来说,这能够经过保存(梯度)图画中的部分极大值点而且扔掉其他点来进行。
from edge import non_maximum_suppression
# 测验例
g =: 8 o I v M np.arr9 B *ay(
[[0.4, 0.5, 0.6],
[0.3, 0.5, 0.7],
[0.4, 0.5, 0.6]]
)
# 输出非极大值按捺的成果
# 改动梯度方向:经! h @ D _过四个方历来测验,即0,45,90,135。你能够将输出成果与自己笔算的成果做比照。
for angle in range(0, 180, 45):
print('Thetas:', angle)
t = np.ones((3, 3)) * anglev i : + A ^ 3 N P # InitW U J g dialize theta
#print] I i b }(t)
print(non_maximum_suppression(g, t))
输出成果:
Thetas: 0
[[0. 0. 0.]
[0. 0. 0.]
[0. 0. 0.]]
Thetas: 45
[[0. 0. 0. ]
[0. 0. 0.7]
[0. 0. 0. ]]
Thett G y r s 1 2as: 90
[[0. 0. 0. ]
[0.? # C o [ 0. 0.7]
[0. 0. 0. ]]
Thetas: 135| 9 @ T T a J m
[[0. 0. 0. ]
[0. 0. 0.7]
[0. 0. 0. ]]
nms = non_maximum_suppression(G, theta)
plt.imshow(nms)
plt.d * # r 4 ( } Etitle('! w M k {Non-maximum suG ( W g # Mppressed')
plt.axis('off')
plt.show()
在经u K j } L ` ;过非极大值按捺后,` r N依然b 4 _ 8 s存在许多像素点。这些像素点有些是边际,Y s u f E A & g但有些是由噪声或许色彩改动(例g S I如粗糙的外表)所导致的。要消除这部分影响,最简略的办法是增加一个阈值断定,只允许相应强度大于阈值的点被标记成边际。Canny边际s B e P /检测算法选用了双阈值算法。大于高阈值的像素点被标记为强边际,小于Z – = U | L e | !低阈值的像素点能够以为对错边际而且被移除,而在凹凸阈值之间的点被标记为弱边际。| 9 w
from edge import double_G ! J 4 H 5thresh@ ~ [ & w )olding
low_threshold = 0.02
high_threshold = 0.03
strong_edges, weak_edges = double_thresholding b V(nms, high[ # % ] 2 B T_threshold, low_threshold)
asa ( isert(np.sum(strong_edges & weak_edges) == 0)
edges=strong_eo O _ # dges * 1.0 + weak_edges * 0.5
plt.subplot(1,2,1)
plt.imshow(strong_edg% U x Q P v &esc _ M 3 j 2 Q G)
plt.title(G = - R - 'Strong Edges')
plt.axis('off0 a m ~ } f A a D')
plt.subplot(1,2,2)
plt.imsK 3 W V s G f a Xhow(edges)
plt.title('Strong+Weak Edges')
plt.axis('off')
plt.show()
一般而言,强边际被以为是“肯定的边际”} r F a y T y t #, 因而能够直接以为该像素点便是边际. 而弱边际则需求进一步判断。假如弱边际点与边际相连接,则以为该像素点是边际。这背后的逻辑是这样o m { S ! @ y .的:噪声或许色彩改动点不太或许发` y i O生强边际(假如设定好恰当对的阈值规模),因而强边际只能由源图画中7 ( ^的边际发生。而弱边际可由边际发生,也1 c E 8 7能够由噪声和色彩改动发生。而由噪声和色彩改动发生的弱边际点,一般来说都均有散@ ) * ~ h W ! [布; ) G : 9 7 i在整幅图片上,只有一小部分是与强边际相连接的,大部分与强边际相连接的点都是实在的边际像素点。
先用一个简略测验例来展示强弱边际连接及判别的过程。N r N
from edge import get_neighbors, li` 8 2 M / ! 5 Onk_edges
test_strong = npl R H.array[ 3 m * ; w ] % L(
[[1, 0, 0, 0],
[0, 0, 0, 0],J 1 8 ~ 9 ?
[0, 0, 0, 0],
[0, 0, 0, 1]]
)
test_weak = np.array(
[[0, 0, 0, 1],
[0, 1, 0, 0],
[1, 0, 0, 0],
[0, 0,; Y O 1, 0]]
)
test_linked = link_edgeO e B _ y _ ^s(test_strong, test_weak)
plt.subz $ a zplot(1, 3, 1)
plt.imshow(test_strong)
plt.title('Strong edges')
plt.subplot(1, 3, 2)
plt.imshow(test_weak)
plt.title('Weak edges')
plt.subplot(1, 3, 3)
plt.imshow(test_linked)
plt.title('Link2 a ued edges')
plt.show()
接下来,咱们在图画中进行边际连接
edges = link_V B m ~ ` { 6 ~ Jedges(strong_edges, weak_edges)
plt^ k ~.imshow(edges)
plt.axis('off')
plt.show()
测验一组不同超参数选取下的Canny边际检测成果
from edge import canny
# 载入图画
iK ~ q b E ,mg = io.imread('iguana.png', as_greY L P gy=True)
# 运转cans F 6 I z I wny边际检测器
ep k p K 3 Cdges = canny(img, kernel_size=l % t o [ q o5, sigma=1.4, high=0.03, low=0.02)
print (edges.shape)
plt.subplot(1, 3, 1)
plt.imshow([ p = ( Y q E r Sedges)
plt.axis('off')
plt.title('Your result')
2 图画几许特征检测
图画的特征首要有图画的色彩特征、纹路特征、几许特征和空间联系特征。其间T d g,几许特征也称形状特征。在物体辨认、检测领域有十分重要的效果。Hough改换是t r 5 O o p l Z其间十分经典的办法。
2.1 Hot P o 1 W 2 . cugh/ C z ) @ & R改换简介
Hough改换r z 9 ; x l V 9是在1962年由Hough所创造,随后在1972年Duda第u C 8 = x [ 1 l一次用K U M J ! G 9 E T它T H | N J z检测图画中的直线。该改换的意图是为了寻觅图画中的直线: B 3 * i J V结构,但需求留意的是,Hough改换相同能够检测圆等其他几许结构,只需其参数方程是已知的。
长处
- 概念简略
- 操作简略
- 被遮挡的图形仍具有几许检测的交过
- 关于有参数的几许特征均有用
缺陷
- 关于参数较多的j a a C图形,核算杂乱度高
- 每次只能检测一种几许特征
- 以直线检测为例,不能确认线段的t e h V A )长度与方位,不能差异两段共线的线段
假定
假定咱们现在现已经过边际检测算法得到了图画完好的E [ 9 h o = @ 边际结构,能够发现,某些像素点构成了图画的几许结构。
接下来,咱们将以直线检测为例,说明Hough改换的原理。
2.2 Hough改换m Q S原理——以直线检测为例
关于恣意一个像素点,或许存在许多直线都经过该点,且一切经过该点的直线均具有如下方程
一般而言,都具有两个方位参数,则该方程能够改为
由此,咱们O L =将视作参数,+ R Y视作变量,那么在xy空间而且经过点的直线,在ab空间+ W g u n都经过点
那么这是一条在空间,由d = * a n ^ V b K和作为参数的直线,因而,一个在空间的点给定了一条在空间的直线;而另一个点将会给出空间的另一条直线。若和是共线的,那么它们在空间的直线必定相交,交点坐标便是直线的参数
假如咱们将空间中直线上的点全部映射到空间,会发现几个十分风趣的现象。
- 在d P B 9 { =空间的两点和B i t 6 k T D确认了一条直线,该两点对应与空间的两条直线。
- 在空间,这些直线会交于,该交点便是空间上,和所连成的直线的参数。
- 一切在空间中与连线上的点,都对应于一条在空( f O B P间内的直线,而且这些直线都相交于同一g X ? – L =点
根据上述特性,咱们能够完结一种根据Hough改换的直n – { – R v线检测,过程如下:
- 将参数空间离散取值,分为一个个单元,该单元被称为累积单元(accumulator cells)
- 核算直线与} @ v L q g W单元相交次数
- 关于恣意一组被检测为边际的点对和检测它们在空间内的交点
- 将交点所在的单元的计数+1
- 若单元的计数超过某一设定的计数(votes,此处的votes也能够了解为阈值,可是解说成计数更适宜),那么该单元对应于空间的一条直线
下图是一个Hough改换的成果例,右图显示了计数值前20的直线
由此,完好地解说了Hough改换在直线检测中的原理,在其间若对参数方= h &程进行修改,就能够完结对其他几许形状的检测。
2.3 代码实战
### hough.py
import numpy as np
import math
def hough_transform(img):
""4 _ P 9 d X"
运用下面的参q 0 a W数化方程:
rho = x * cos(theta) + y * sin(t0 U H k ) ? s $heta)
将点 (x,y) 转到Hough空间中类似于正弦函数( sn 5 Kine-like function)的一条曲线.
参数:
img: 尺度为 (H, W)的二值图画.
返回值:
accu# E * I w g 4mulator: 形状为 (m, n)的Numpy数组.
rhos: 形状为 (m, )的Numpy数组.
thetas: 形状为 (n, )的Numpy数组.
"""
# 设置 rho 和 theta 的规模
W, H = img.shape
diag_len = int(np.ceil(np.sqrt(W * W + H * H)))
rhos = np.linspace(-d( 0 7 4 ,iag_len, diag_len, diag_len * 2.0 + 1)
thetas = np.deg2rad(np.arange(-90.0, 90.0)* w ^ Y k)
# 核算一些会重复运用的值
cos_t = np.cos(% U U V xthetas)
sin_t = np.sin(thetas% x c * s ; a)
num_thetas = len(thetas)
# 在Hough 空间中初始化 accumulator
accumulator = nR N up.zeros((2 * diag_len + 1h o F + * B I, num_thetas), dtype=np.uint64)
ys, xs = np.nN D b X . [ . , ]onzero(img)
# 将图画中的每一个像素点(x,y)转到对应的Hough空间中
# 找出每个thetas所对应的rhos
# 并在accumulator的相应方位上加1
### YOUR CODE HERE
for i in range(lenJ / e p(xs)):
x=xs[i]
y=ys[i]
for j in range(num_thetas):
rho=np.floor(x*cos_t[j]+y*sin_t[j])+diag_lp P a C l b len
accumulator[int(rho),j]+=1
### END YOUR CODE
return accumulator, rhos, thetas
2.4 道路检测施行例
一般而言,车道都是细长的直线,而且具有较高的亮度。这种条件保证了它能被咱们的算法较好地V ] $ / O R T检测出来。运转下面代码,载x ^ ( n P Q入图片并检测图片中的边际。
from edge import c+ C B Nanny
# 载入图画s A i
img = io.imread('road.jpg', as_grey=True)
# 运用Canny检测器进行边际检测
edges = canny(img, kernel_size=5G I O :, sigma=1.4, high=0.03, low=0.02_ ( d @ = W m ( a)
plB R & X q ? Ut.subplot(211)
plt.imshow(img)
plt.ag _ g gxis('off')
plt.title('Input Image')
plt.subplot(212)
plt.imshow(edges)
plt.axis('off')
plt.title('Edges')
plt.show()
能够发现,Canny算法能够找出图片中的道路。可是,咱们也能够看到这里边呈现了许多咱们不需求的物体的边际。考虑到所抓取的图片的空间方位,咱们清楚道路总是在图片的下半部m W 5 P N d z分,因而能够运用这一点消除一部分不期望得到的边际8 – Q C P p ; )。下面的代码界说了一个二值化的模板(mask),用来抓取ROI(Region of interest, 感兴趣区域)的边际。
H, W = img.shape
# Generate mask for ROI (Region of Interest)
mask = np.zeros((H, W))
for i in range(H):
for j in range(W):
if i > (H / W) * j and i &J x 5 3 { 6 ~gt; -(H / W) * j + H:
mask[i, j] = 1
# Extract edges in ROI
roi = edges * mask
plt.subplot(1,2,1)
plt.imshow(mask)
plt.title('Mask')
plt.axis('off')
plt.sv F ; b @ e Q Vubplot(1,2,2)
plt.imd / K 7 0 x ?show(roi)
plt.title('Edges in ROI')
plt.axis('off')
plt.show()
尽管咱们运用了mask把边际限制在了ROI区域,可是所找出的边际仍旧是一: ; [ #团像素点,而不是连在一同的直线。一般的,咱们习惯于用参数方程来表明直线。它的斜率是而截距是。在这里,咱们运用Hough改换来寻觅这条直线。
一般,直线 能够表明成参数空间的一个点。但它不能表明竖直方F 4 x 7 # {向的直线,所以咱们选用了类似于极坐标的参数表达形式和表明直线a $ @ { s C t 9 c:
运用极坐标的表明办法,咱们就将空间转到了空l B r ) ) S 9间(或许说Hough空间)。带入Z = , S `不同边际像素点的坐标,咱们就能够得到-空间的一系列曲线,曲线的交点便是咱们所求的参数。这样,咱们就求得了参数坐标下直线的方程,也能够进一步地将其B N ` M W t a 1 :转成空间的直线方程。
在求取曲线的交点的过程中,咱们能够避免直接对交点进行求取,而是某个点被多少条曲线经过。例如,曲{ D f线1经过了点,曲线2经过了点,那么点的计数值便是2e E Q – d 0 ,,而点的计数值便是1,这样9 m E 2 7 & E经过比较计数值,咱们能够确认点便是曲线的交点。
from hough import hou5 r K 9 N : P s pgh_transform
# 对ROI区域的边际像素点作Hough改换
acc, rhos, thetas = hough_transform(roi)
# 右侧的车道
xs_right = []
ys_right = []
# 左边的车道
xs_left = []
ys_left = []
#咱们以为两条道路在前20条检测到的直线里边
for i in rangel S 5 ? ^ e(20):
idx = np.argmax(acc)
r_idx = idx // acc.shape[1]
t_idx = idx % acc.shape[1]
acc[r_% i ~ q DidB l }x, t_idx] = 0 # 将最大值置0,避免一向检测到同一条直线
rho = rhos[r_idx]
theta = thetas[t_idx]
# 将Hough空间(-空间)的一个点改换到x-y空间的一条线.
a = - (np.cos(theta)/np.sin(theta)) # 斜率
b = (ro ] s K :hoB / p O 1 H - Y/np.sin(theta)) # 截距
# 查看是否检测到了左右两条车道
if xs_right and xs_left:
brr + [eak
i- h ff a < 0: # 左边车道
if xs_left:
continue
xs = xs_left
ys = ys_left
else: # 右侧车道
if xs_right:
continue
xs = xs_right
ys = ys_right
for x in range(img.shape[1]):
y = a * x + b
if y > img.shK b gape[0] * 0.6 and y < img.shape[0]:
xs.append(x)
ys.append(int(round(y)))
plt.im: c D m B *show(img)
plt.plot(xs_left, ys_l- A c =eft, linewidth=5.0)
plt.plot(xs_right, ys_right, linewidth=5.0)
pl N J , _ n St.axis('off')
plt.show()
由此,咱们完结了本期关于图画梯度、图画边` + C (际、几许特征、检测与提取的原理与完结。
你或许还会感兴趣
科研基本功——高效文献检索与文献阅读保姆级教程
核算视觉——图画、质量y w ] d y =、点评
色彩视觉l * B q I——杂谈篇
核算视觉——根据暗通道先验的图画去雾算法@ K [ l n ~
写在最终
创作不易,卑微求赞,您的点赞能让更多人看到这篇文章~
谢谢!