一、 前言

图像的灰度直方图

灰度直方图是关于灰度级分布的函数,是对图像中灰度级分布的统计。灰度直方图是将数字图像中的所有像素,按照灰度值的大小,统计其出现的频率。灰度直方图是灰度级的函数,它表示图像中具有某种灰度级的像素的个数,反映了图像中某种灰度出现的频率。【from百度百科】

计算直方图

1.使用opencv的函数
cv2.calcHist(images, channels, mask, histSize, ranges)
  • 参数1:要计算的原图,以方括号的传入,如:[img]。
  • 参数2:类似前面提到的dims,灰度图写[0]就行,彩色图B/G/R分别传入[0]/[1]/[2]。
  • 参数3:要计算的区域ROI,计算整幅图的话,写None。
  • 参数4:也叫bins,子区段数目,如果我们统计0-255每个像素值,bins=256;如果划分区间,比如0-15,16-31…240-255这样16个区间,bins=16。
  • 参数5:range,要计算的像素值范围,一般为[0,256)。

eg:

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('test.jpg')
# hist是256x1数组,每个值对应于该图像中具有相应像素值的像素数
hist = cv.calcHist([img],[0],None,[256],[0,256])
# 绘制直方图
plt.plot(hist)
plt.show()
2. 使用Numpy函数
bincount(x, weights=None, minlength=None)

相对应的函数详解可参考:numpy.bincount详解
eg:

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('test.jpg')
# ravel()函数将二维矩阵展平变成一维数组
hist = np.bincount(img.ravel(), minlength=256) 
# 绘制直方图
plt.plot(hist)
plt.show()

二、 OTSU算法简介

OTSU算法是一种图像二值化算法。其算法假设图像中存在一个阈值 T ,判断图像中每个像素与T的大小关系,可以将所有像素分为背景 C0 和 前景 C1 两类,且当选取到最佳T阈值时,背景部分与前景部分的差别最大,而OTSU 算法利用最大类间方差来衡量这一差别。由于OTSU算法采用了最大类间方法的思想,因此该算法也被OTSU最大类间方差法

缺点:对图像噪声敏感;只能针对单一目标分割;当目标和背景大小比例悬殊、类间方差函数可能呈现双峰或者多峰,这个时候效果不好

三、 数学原理

该算法的原理主要涉及 均值、方差等概念和部分公式的推导

  1. 将图片的像素值分为 [1, 2, …, L] 个水平,用ni 表示各个水平像素值的像素个数,那么很容易得到总像素个数N为:N = n1 + n2 + … + nL

  2. 利用像素值对应个数与总数的商作为某个像素值出现的概率,定义pi
    p i = n i / N , p i ≥ 0 , ∑ i = 1 L p i = 1 (1) p_i = n_i/N, p_i ≥ 0, \sum_{i=1}^L p_i = 1 \tag1 pi=ni/N,pi0,i=1Lpi=1(1)

  3. 定义两个量w0 , w1为C0,C1的局部概率之和,并且得到二者的关系
    w 0 = P r ( C 0 ) = ∑ i = 1 k p i = w ( k ) (2) w_0 = Pr(C_0) = \sum_{i=1}^k p_i = w(k) \tag2 w0=Pr(C0)=i=1kpi=w(k)(2)
    w 1 = P r ( C 1 ) = ∑ i = k + 1 L p i = 1 − w ( k ) (3) w_1 = Pr(C_1) = \sum_{i=k+1}^L p_i = 1 - w(k) \tag3 w1=Pr(C1)=i=k+1Lpi=1w(k)(3)

  4. 由此我们得到总的数学期望【期望是平均数随样本趋于无穷的极限,所以可以将期望 ≈ 均值】和C0 , C1各自的数学期望并指出三者的关系,式中 i 代表像素值,除于各自概率的和用于进行归一。
    u 0 = ∑ i = 1 k P r ( i ∣ C 0 ) = ∑ i = 1 k i ∗ p i / w 0 = u ( k ) w ( k ) (4) u_0 = \sum_{i=1}^k Pr(i|C_0) = \sum_{i=1}^k i * p_i / w_0 = \frac{u(k)}{w(k)} \tag4 u0=i=1kPr(iC0)=i=1kipi/w0=w(k)u(k)(4)
    u 1 = ∑ i = k + 1 L P r ( i ∣ C 1 ) = ∑ i = k + 1 L i ∗ p i / w 1 = u ( T ) − u ( k ) 1 − w ( k ) (5) u_1 = \sum_{i=k+1}^L Pr(i|C_1) = \sum_{i=k+1}^L i * p_i / w_1 = \frac{u(T) - u(k)}{1 - w(k)} \tag5 u1=i=k+1LPr(iC1)=i=k+1Lipi/w1=1w(k)u(T)u(k)(5)
    w ( k ) = ∑ i = 1 k p i (6) w(k) = \sum_{i=1}^k p_i \tag6 w(k)=i=1kpi(6)
    u ( k ) = ∑ i = 1 k i ∗ p i (7) u(k) = \sum_{i=1}^k i * p_i \tag7 u(k)=i=1kipi(7)
    u T = u ( L ) = ∑ i = 1 L i ∗ p i (8) u_T = u(L) = \sum_{i=1}^L i * p_i \tag8 uT=u(L)=i=1Lipi(8)
    上述公式间的关系为:
    w 0 u 0 + w 1 u 1 = u T , w 0 + w 1 = 1 (9) w_0 u_0 + w_1 u_1 = u_T , w_0 + w_1 = 1 \tag9 w0u0+w1u1=uT,w0+w1=1(9)

  5. 各自局部方差和总方差为
    σ 0 2 = ∑ i = 1 k ( i − u 0 ) 2 P r ( i ∣ C 0 ) = ∑ i = 1 k ( i − u 0 ) 2 p i / w 0 (10) σ^2_0 = \sum_{i=1}^k (i - u_0)^2 Pr(i|C_0) = \sum_{i=1}^k (i - u_0)^2 p_i / w_0 \tag{10} σ02=i=1k(iu0)2Pr(iC0)=i=1k(iu0)2pi/w0(10)
    σ 1 2 = ∑ i = k + 1 L ( i − u 1 ) 2 P r ( i ∣ C 1 ) = ∑ i = k + 1 L ( i − u 1 ) 2 p i / w 1 (11) σ^2_1 = \sum_{i=k+1}^L (i - u_1)^2 Pr(i|C_1) = \sum_{i=k+1}^L (i - u_1)^2 p_i / w_1 \tag{11} σ12=i=k+1L(iu1)2Pr(iC1)=i=k+1L(iu1)2pi/w1(11)
    σ T 2 = ∑ i = 1 L ( i − u T ) 2 p i (12) σ^2_T = \sum_{i=1}^L (i - u_T)^2 p_i \tag{12} σT2=i=1L(iuT)2pi(12)

  6. within_class variance 类内差异
    σ w 2 = w 0 σ 0 2 + w 1 σ 1 2 (13) σ^2_w = w_0 σ^2_0 + w_1 σ^2_1 \tag{13} σw2=w0σ02+w1σ12(13)
    between_class variance 类间差异
    σ b 2 = w 0 ( u 0 − u T ) 2 + w 1 ( u 1 − u T ) 2 (14) σ^2_b = w_0(u_0 - u_T)^2 + w_1(u_1 - u_T)^2 \tag{14} σb2=w0(u0uT)2+w1(u1uT)2(14)

  7. 当最大化类间差距时,即最小化类内差距,因为 二者的和为定值。
    σ w 2 + σ b 2 = σ T 2 (15) σ^2_w + σ^2_b = σ^2_T \tag{15} σw2+σb2=σT2(15)


四、 python实现

假设一图像为灰度图,大小为M*N,背景部分较暗,OTSU算法步骤如下:

  1. 选取一个分割阈值T,统计计算属于前景的像素点数(像素值小于T)在全部像素点的占比 w 0 w_0 w0 以及其平均灰度 u 0 u_0 u0 ; 同理统计属于背景部分的 w 1 w_1 w1 u 1 u_1 u1 ;
  2. 计算图像全部像素的平均灰度 u T = w 0 ∗ u 0 + w 1 ∗ u 1 u_T = w_0 * u_0 + w_1 * u_1 uT=w0u0+w1u1 以及类间方差

g = w 0 ∗ ( u 0 − u T ) 2 + w 1 ∗ ( u 1 − u T ) 2 = w 0 ∗ w 1 ∗ ( u 0 − u 1 ) 2 (16) g = w_0 * (u_0 - u_T)^2 + w_1 * (u_1 - u_T)^2 = w_0 * w_1 * (u_0 - u_1)^2 \tag{16} g=w0(u0uT)2+w1(u1uT)2=w0w1(u0u1)2(16)

  1. 遍历全部分割阈值T,重复1-2步骤,得到使类间方差 g 最大的阈值T;
import numpy as np
import cv2 as cv

#将图片转为灰度图
# https://blog.csdn.net/weixin_43635647/article/details/99625105
img = cv.imread('test.jpg', 0)
cv.imshow("img", img)
cv.waitKey()

def OTSU(img_gray, GrayScale):
    assert img_gray.ndim == 2, "must input a gary_img"  #shape有几个数字, ndim就是多少
    img_gray = np.array(img_gray).ravel().astype(np.uint8)
    u1=0.0#背景像素的平均灰度值
    u2=0.0#前景像素的平均灰度值
    th=0.0

    #总的像素数目
    PixSum=img_gray.size
    #各个灰度值的像素数目
    PixCount=np.zeros(GrayScale)
    #各灰度值所占总像素数的比例
    PixRate=np.zeros(GrayScale)
    #统计各个灰度值的像素个数
    for i in range(PixSum):
        #默认灰度图像的像素值范围为GrayScale
        Pixvalue=img_gray[i]
        PixCount[Pixvalue]=PixCount[Pixvalue]+1
    
    #确定各个灰度值对应的像素点的个数在所有的像素点中的比例。
    for j in range(GrayScale):
        PixRate[j]=PixCount[j]*1.0/PixSum
    Max_var = 0
    #确定最大类间方差对应的阈值
    for i in range(1,GrayScale):#从1开始是为了避免w1为0.
        u1_tem=0.0
        u2_tem=0.0
        #背景像素的比列
        w1=np.sum(PixRate[:i])
        #前景像素的比例
        w2=1.0-w1
        if w1==0 or w2==0:
            pass
        else:#背景像素的平均灰度值
            for m in range(i):
                u1_tem=u1_tem+PixRate[m]*m
            u1 = u1_tem * 1.0 / w1
             #前景像素的平均灰度值
            for n in range(i,GrayScale):
                u2_tem = u2_tem + PixRate[n]*n
            u2 = u2_tem / w2
            #print(u1)
            #类间方差公式:G=w1*w2*(u1-u2)**2
            tem_var=w1*w2*np.power((u1-u2),2)
            #print(tem_var)
            #判断当前类间方差是否为最大值。
            if Max_var<tem_var:
                Max_var=tem_var#深拷贝,Max_var与tem_var占用不同的内存空间。
                th=i
    return th 
th = OTSU(img,256)
print("使用numpy的方法:" + str(th))		# 结果为 135

直接使用 opencv的函数

import numpy as np
import cv2 as cv
#该函数返回的第一个值就是输入的thresh值,第二个就是处理后的图像
img = cv.imread('test.jpg',0)
retVal, a_img = cv.threshold(img, 0, 255, cv.THRESH_OTSU)
print("使用opencv函数的方法:" + str(retVal))	# 结果为 134
cv.imshow("a_img",a_img)
cv.waitKey()

左图为原图,右图为二值化后的图像
结果


五、 参考文章

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐