杨枝小甘露
杨枝小甘露
发布于 2025-11-11 / 2 阅读
0
0

数字图像处理

一、概述

日常生活中,我们能用到的扫描全能王、美图秀秀、PhotoShop等软件,均是数字图像处理的应用工具,如果对图像处理有较高要求,就需要深入学习数字图像处理,用高级语言及各种复杂算法来处理图像了。

数字图像处理的本质是对图像的数字化表示(像素矩阵)进行有目的的变换,对像素矩阵进行数值操作 提取有用的信息或优化视觉效果。

一副图像可以定义为一个二维离散函数f(x, y),其中(x, y)是图像中的一个像素点的坐标,而f是该点的强度值/灰度值(对于灰度图,是亮度;对于彩色图,通常是红、绿、蓝三个通道的强度值),所有的处理都是对这个函数进行数学运算。其中,当x,y,f为有限的离散量时,称该图像为数字图像。

数字图像处理的目的如下:

  • 改善视觉效果:使图像看起来更清晰、更漂亮,如锐化、去噪、对比度增强、彩色化、修复老照片等;

  • 便于机器分析:为后续的自动识别做准备:例如边缘检测、图像分割;

  • 信息提取与理解:从图像中获取我们关心的数据,如医学影像检测肿瘤大小、人脸识别识别身份、对图像进行计数等;

  • 高效存储与传输:对图像进行压缩,减少存储空间和网络带宽的占用,如:JPEG、PNG格式;

二、学习路径

1、参考教材:冈萨雷斯的《数字图像处理》

2、b站up主:

本篇文章主要基于b站:“十四阿哥很nice”的“手写图像处理库”系列视频进行学习和总结(该系视频是结合冈萨雷斯的《数字图像处理》第三章内容的实践部分),可惜up主只更新了5个视频之后就断更了~~~

俗话说,理论需要结合实践,虽然视频不是很全,还是先开动吧。

三、学习案例

3.1反色变换、对数变换

1、应用场景

反色变换和对数变换是为了:调整图像的亮度和灰度分布,解决原始图像“细节隐藏”或“动态范围不匹配”的问题,应用场景:文档、扫描件处理、医学影像增强等。

2、原理解释

反色变换:将图像中每个像素的亮度值反转,本质是对像素值做“最大值减去原像素值”的运算。若对于彩色图像(如 RGB/BGR),其反色是对每个通道的像素分别作反色运算。

对数变换:可以让图像显示更多细节,主要是为了压缩值域动态范围,从而让动态范围过宽的图像可以显示更多肉眼可见的细节。

tips:对于单通道的图像,其灰度值范围在0~255,其中0为纯黑,255为纯白。

如果给定矩阵的值域为0~25500或是-9~9,则我们需要将值域等比例映射至0~255的区间内,才能清晰看到图像。

3、实践

1)反转变换

import numpy as np
from PIL import Image
import matplotlib  # 导入主模块
matplotlib.use('TkAgg')  # 切换 Matplotlib 后端,解决 PyCharm 兼容性问题方法,切换到 Tkinter 后端(需保证 Python 安装了 tkinter,通常默认有),正确调用 use()
import matplotlib.pyplot as plt

# 解决中文显示问题
# plt.rcParams["font.family"] =  ["Microsoft YaHei", "SimSun", "SimHei"]  # 这三种字体 Windows 默认都有
# plt.rcParams["axes.unicode_minus"] = False

print("安装成功!")  # 无报错则说明安装成功
def set_Chinese():   # 中文显示工具函数
    # rcParams 本质是 matplotlib 主模块的全局配置字典
    matplotlib.rcParams["font.sans-serif"] =["SimHei"]
    matplotlib.rcParams["axes.unicode_minus"]=False

def image_inverse(input):
    value_max = np.max(input) #求输入图像最大灰度值
    output = value_max - input
    return output;

if __name__ == "__main__":
    # 用举矩阵测试反转
    # input = np.array([[0,20,160],[45,50,100],[100,45,30]])
    # output = image_inverse(input)
    # print(output)

    # 用图片测试反转
    set_Chinese()
    gray_image = np.asarray(Image.open("B_chao.jpg").convert('L'))
    inv_img = image_inverse(gray_image)

    fig = plt.figure() #创建 matplotlib 绘图窗口
    ax1 = fig.add_subplot(121) #在 fig 窗口中添加一个 “子图区域”,参数 121 是 “行 × 列 × 子图序号” 的缩写
    ax1.set_title('原图')
    ax1.imshow(gray_image, cmap='gray', vmin=0, vmax=255)

    ax2 = fig.add_subplot(122)
    ax2.set_title('反转变换结果')
    ax2.imshow(inv_img, cmap='gray', vmin=0, vmax=255)

    plt.show()

运行结果

2)对数变换

import numpy as np
import matplotlib  # 导入主模块
matplotlib.use('TkAgg')  # 切换 Matplotlib 后端,解决 PyCharm 兼容性问题方法,切换到 Tkinter 后端(需保证 Python 安装了 tkinter,通常默认有),正确调用 use()
import matplotlib.pyplot as plt

# 中文显示工具函数
def set_Chinese():
    # rcParams 本质是 matplotlib 主模块的全局配置字典
    matplotlib.rcParams["font.sans-serif"] =["SimHei"]
    matplotlib.rcParams["axes.unicode_minus"]=False

def image_log(input):
    return np.log(input+1)

if __name__ == "__main__":
    set_Chinese()
    # input = np.array([[-9,-8],[1,2]])
    input = np.array([[10, 150], [250, 25500]])

    output = image_log(input)
    print(output)

    fig = plt.figure()
    ax1 = fig.add_subplot(121)
    ax1.set_title("对数变换前")
    # ax1.imshow(input,cmap='gray',vmin=-9,vmax=9)
    ax1.imshow(input, cmap='gray', vmin=0, vmax=25500)

    ax2= fig.add_subplot(122)
    ax2.set_title("对数变换后")
    ax2.imshow(output, cmap='gray') #不加 vmin/vmax:自动适配数组的实际范围,可能增强对比度(适合非标准范围的图像)。

    plt.show()

运行结果

3.2伽马变换

1、原理解释

伽马变换:基于幂函数的非线性灰度变换,核心作用是调整图像的亮度和对比度,尤其是修正设备特性导致的亮度偏差,或者增强图像中的暗部、高光细节。

gamma变换之所以能调节图像的对比度,本质原因:它能对输入图像不同灰度区间的动态范围,做相对独立的调整。

gamma曲线的特性如下:

当gamma<1,提亮图像,扩展暗部动态范围,压缩亮部动态范围(小灰度区间被扩大,大灰度区间被缩小)

当gamma>1,变暗图像,压缩暗部动态范围,扩展亮部动态范围(小灰度区间被缩小,大灰度区间被扩大)

2、实践

1)ganmma变换

import numpy as np
from PIL import Image
import matplotlib  # 导入主模块
matplotlib.use('TkAgg')  
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider, Button, RadioButtons

def set_Chinese():   # 中文显示工具函数
    # rcParams 本质是 matplotlib 主模块的全局配置字典
    matplotlib.rcParams["font.sans-serif"] =["SimHei"]
    matplotlib.rcParams["axes.unicode_minus"]=False

def gamma_trans(input,gamma=2,eps=0):
    return 255.*(((input+eps)/255.)**gamma)

def update_gamma(val):
    gamma=slider1.val
    output=gamma_trans(input_arr, gamma=gamma, eps=0)
    print("-------------\n",output)
    # 因为输出矩阵随时更新,频繁刷新,所以输出图像显示代码写在回调函数中
    ax1.set_title("伽马变换后,gamma="+str(gamma))
    ax1.imshow(output, cmap='gray', vmin=0, vmax=255)

if __name__ == "__main__":
    set_Chinese()

    input_arr = np.array([[0,50,100,150],
                         [0,50,100,150],
                         [0,50,100,150],
                         [0,50,100,150]])

    fig = plt.figure()
    ax0 = fig.add_subplot(121)
    ax0.set_title("输入矩阵")
    ax0.imshow(input_arr, cmap='gray', vmin=0, vmax=255)

    ax1 = fig.add_subplot(122)

    # 往显示主体添加滑动条
    plt.subplots_adjust(bottom=0.3) #为滑动条预留出足够的显示空间。
    s1 = plt.axes([0.25, 0.1, 0.55, 0.03],facecolor='lightgoldenrodyellow') 
    slider1 = Slider(s1, '参数gamma', 0.0, 2.0, valfmt='%.f', valinit=1.0, valstep=0.1)
    slider1.on_changed(update_gamma) #绑定滑动条的回调函数
    slider1.reset #滑动条的重置操作
    slider1.set_val(1) #滑动条的重置操作

    plt.show()

运行结果

2)ganmma变换-crt

import numpy as np
from PIL import Image
import matplotlib  # 导入主模块
matplotlib.use('TkAgg') 
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider, Button, RadioButtons

def set_Chinese():   # 中文显示工具函数
    # rcParams 本质是 matplotlib 主模块的全局配置字典
    matplotlib.rcParams["font.sans-serif"] =["SimHei"]
    matplotlib.rcParams["axes.unicode_minus"]=False

def gamma_trans(input,gamma=2,eps=0):
    return 255.*(((input+eps)/255.)**gamma)

def crt_distortion(input,gamma=2,eps=0): #模拟crt失真
    return 255.*((input/255.)**gamma)

def update_gamma(val):
    gamma=slider1.val

    #需要先对图像进行预处理(根据y),预处理过程在crt输出前,做一个失真1/y的运算----图像送入CRT前,先做y变换处理
    gamma=1/gamma
    correct_img = gamma_trans(gray_img, 1/gamma,0)
    ax1.set_title("y矫正,矫正y=1/"+str(round(gamma,2)))
    ax1.imshow(correct_img,cmap='gray', vmin=0, vmax=255)

    # 简易模拟crt输出
    output=crt_distortion(correct_img, gamma)
    print(output)
    ax2.set_title("模拟CRT输出,失真y="+str(round(gamma,2))) #round方法可以使gamma只显示小数点后两位
    ax2.imshow(output, cmap='gray', vmin=0, vmax=255)

if __name__ == "__main__":
    set_Chinese()

    gray_img = np.asanyarray(Image.open("./lena.jpg").convert('L'))

    fig = plt.figure()
    ax0 = fig.add_subplot(131)
    ax1 = fig.add_subplot(132)
    ax2 = fig.add_subplot(133)

    ax0.set_title("原始图片")
    ax0.imshow(gray_img, cmap='gray', vmin=0, vmax=255)

    # 往显示主体添加滑动条
    plt.subplots_adjust(bottom=0.3) #为滑动条预留出足够的显示空间。
    s1 = plt.axes([0.25, 0.1, 0.55, 0.03],facecolor='lightgoldenrodyellow')
    slider1 = Slider(s1, 'CRT失真y', 0.0, 4.0, valfmt='%.f', valinit=1.0, valstep=0.1) 
    slider1.on_changed(update_gamma) #绑定滑动条的回调函数
    slider1.reset #滑动条的重置操作
    slider1.set_val(2) #滑动条的重置操作

    plt.show()

运行结果

3)ganmma变换-增加对比度

import numpy as np
from PIL import Image
import matplotlib  # 导入主模块
matplotlib.use('TkAgg')  
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider, Button, RadioButtons

def set_Chinese():   # 中文显示工具函数
    # rcParams 本质是 matplotlib 主模块的全局配置字典
    matplotlib.rcParams["font.sans-serif"] =["SimHei"]
    matplotlib.rcParams["axes.unicode_minus"]=False

def gamma_trans(input,gamma=2,eps=0):
    return 255.*(((input+eps)/255.)**gamma)

def update_gamma(val):
    #获取滑块数值,做y
    gamma=slider1.val
    output=gamma_trans(gray_img, gamma, 0.2)

    #显示y变换结果图像
    ax3.clear()
    ax3.set_title("伽马变换结果,gamma="+str(round(gamma, 2)))
    ax3.imshow(output,cmap='gray', vmin=0, vmax=255)
    #显示y变换结果图像的灰度分布直方图
    ax4.clear()
    ax4.set_xlim(0, 255)  # 设置x轴分布范围
    ax4.set_ylim(0, 0.15)  # 设置y轴分布范围
    ax4.grid(True, linestyle=":", linewidth=1)
    ax4.set_title("伽马变换后,灰度值分布直方图", fontsize=12)
    ax4.hist(output.flatten(), bins=50, density=True, color='r', edgecolor='k')  # 调用hist直接获

if __name__ == "__main__":
    set_Chinese()

    gray_img = np.asanyarray(Image.open("./DRfeet.png").convert('L'))

    fig = plt.figure()
    ax1 = fig.add_subplot(221)
    ax2 = fig.add_subplot(222)
    ax3 = fig.add_subplot(223)
    ax4 = fig.add_subplot(224)

    # ax1显示原图
    ax1.set_title("原始图片")
    ax1.imshow(gray_img, cmap='gray', vmin=0, vmax=255)

    # ax2显示原图的灰度分布直方图
    ax2.grid(True, linestyle=":", linewidth=1) 
    ax2.set_title("原图灰度分布直方图", fontsize=12)
    ax2.set_xlim(0, 255) #设置x轴分布范围,x 轴:代表灰度值(0~255,0 = 黑,255 = 白)
    ax2.set_ylim(0, 0.15)  # 设置y轴分布范围,y 轴:代表 “像素在该灰度区间的频率密度”(因 density=True,表示该区间像素数占总像素数的比例)
    ax2.hist(gray_img.flatten(), bins=50, density=True, color='r', edgecolor='k') 

    # 往显示主体下方添加滑动条
    plt.subplots_adjust(bottom=0.3)  # 为滑动条预留出足够的显示空间。
    s1 = plt.axes([0.25, 0.1, 0.55, 0.03],
                  facecolor='lightgoldenrodyellow')  
    slider1 = Slider(s1, '参数gamma', 0.0, 2.0, valfmt='%.f', valinit=1.0,
                     valstep=0.1)  
    slider1.on_changed(update_gamma)  # 绑定滑动条的回调函数
    slider1.reset  # 滑动条的重置操作
    slider1.set_val(1.0)  # 滑动条的重置操作

    plt.show()

运行结果

3.3分段变换

1、原理解释

分段变换是一种“按区间定义不同映射规则”的数值方法,核心是对数据的不同范围做针对性调整。

假若衣服图像的灰度值恰好落在如图所示的这段曲线内,动态范围只会产生微弱的变化,无法产生肉眼可感知的对比度变换

  • 伽马变换:斜率大于1的区间,扩展灰度动态范围;斜率小于1的区间,压缩灰度动态范围;

  • 幂函数的问题:曲线中有一个点的斜率为1,就会造成在1附近的这段曲线,无法有效改变图像动态范围

由此引出:分段变换---精准控制不同区间的数值映射,设配具体场景的需求。

2、实践

import numpy as np
from PIL import Image
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider
import matplotlib  # 导入主模块
matplotlib.use('TkAgg')  

def set_Chinese():   # 中文显示工具函数
    # rcParams 本质是 matplotlib 主模块的全局配置字典
    matplotlib.rcParams["font.sans-serif"] =["SimHei"]
    matplotlib.rcParams["axes.unicode_minus"]=False


#三段对比度拉伸变换,其中x1,y1和x2,y2为分段点
def three_linear_transformation(x,x1,y1,x2,y2): #x是输入矩阵
    #1、检查参数,避免分母为0
    if x1==x2 or x2 == 255:
        print("[INFO] x1=%d,x2=%d -> 调用词函数必须满足x1!=x2且x2!=255]" %(x1,x2))
        return None

    #2、执行分段线性变换
    m1=(x<x1) #比较运算,当x<x1时,置1,否则置0
    m2=(x1 <= x)&(x <= x2) #注意这里要用&,且注意运算顺序
    m3=(x>x2)
    out = (y1/x1*x)*m1 \
            +((y2-y1)/(x2-x1)*(x-x1)+y1)*m2 \
             +((255-y2)/(255-x2)*(x-x2)+y2)*m3

    #3、获取分段线性函数的点集,用于绘制函数图像
    # 这段代码的核心是通过布尔掩码过滤和分段函数计算
    x_point = np.arange(0,256,1)
    cond2=[True if(i>x1 and i<x2) else False for i in x_point] 
    y_point = (y1/x1*x_point)*(x_point<x1) \
                +((y2-y1)/(x2-x1)*(x_point-x1)+y1)*cond2 \
                +((255-y2)/(255-x2)*(x_point-x2)+y2)*(x_point>x2)
    return out, x_point, y_point

def update_trans(val): 
    #读入4个滑动条的值
    x1,y1 = slider_x1.val,slider_y1.val 
    x2,y2 = slider_x2.val,slider_y2.val

    #执行分段线性变换
    out,x_point,y_point =three_linear_transformation(gray_img,x1,y1,x2,y2)

    #绘制变换结果图像
    ax2.clear()
    ax2.set_title("分段线性变换结果", fontsize=8)
    ax2.imshow(out, cmap='gray', vmin=0, vmax=255)

    #绘制函数图像
    ax3.clear()
    ax3.annotate('(%d,%d)'%(x1,y1),xy=(x1,y1),xytext=(x1-15,y1+15))
    ax3.annotate('(%d,%d)'%(x2,y2),xy=(x2,y2),xytext=(x2+15,y2-15))
    ax3.set_title("分段线性函数图像", fontsize=8)
    ax3.grid(True, linestyle=':', linewidth=1)
    ax3.plot([x1,x2], [y1,y2], 'ro')
    ax3.plot(x_point, y_point, 'g')

    # 显示变换结果的灰度分布直方图
    ax5.clear()
    ax5.grid(True, linestyle=':', linewidth=1)
    ax5.set_title("变换结果分布直方图", fontsize=8)
    ax5.set_xlim(0, 255)  # 设置x轴分布范围
    ax5.set_ylim(0, 0.15)  # 设置y轴分布范围
    ax5.hist(out.flatten(), bins=50, density=True, color='r', edgecolor='k')  # 调用hist直接获

#主函数
if __name__ == "__main__":
    set_Chinese()
    #以灰度值方式读取图像,
    gray_img = np.asanyarray(Image.open("../day2/lena.jpg").convert('L'))
    print("[INFO]原图尺寸为:",gray_img.shape)

    #创建一个显示主体,并分成五个显示区域
    fig = plt.figure()
    ax1 = fig.add_subplot(231)
    ax2 = fig.add_subplot(232)
    ax3 = fig.add_subplot(233)
    ax4 = fig.add_subplot(234)
    ax5 = fig.add_subplot(235)

    #显示原图及灰度分布直方图
    ax1.set_title("原始输入图片", fontsize=8)
    ax1.imshow(gray_img,cmap='gray',vmin=0,vmax=255)

    ax4.grid(True, linestyle=':',linewidth=1)
    ax4.set_title('原图灰度分布直方图',fontsize=8)
    ax4.set_xlim(0, 255)  # 设置x轴分布范围
    ax4.set_ylim(0, 0.15)  # 设置y轴分布范围
    ax4.hist(gray_img.flatten(), bins=50, density=True, color='r', edgecolor='k')  # 调用hist直接获

    #创建四个滑动条,用于调节x1,x2,y1,y2四个值
    plt.subplots_adjust(bottom=0.3)
    x1 = plt.axes([0.25, 0.2, 0.45, 0.03],facecolor='lightgoldenrodyellow')
    slider_x1 = Slider(x1, '参数x1', 0.0, 255., valfmt='%.f', valinit=91,valstep=1)
    slider_x1.on_changed(update_trans)

    y1 = plt.axes([0.25, 0.16, 0.45, 0.03], facecolor='lightgoldenrodyellow')
    slider_y1 = Slider(y1, '参数y1', 0.0, 255., valfmt='%.f', valinit=0, valstep=1)
    slider_y1.on_changed(update_trans)

    x2 = plt.axes([0.25, 0.06, 0.45, 0.03], facecolor='white')
    slider_x2 = Slider(x2, '参数x2', 0.0, 254., valfmt='%.f', valinit=138, valstep=1)
    slider_x2.on_changed(update_trans)

    y2 = plt.axes([0.25, 0.02, 0.45, 0.03], facecolor='lightgoldenrodyellow')
    slider_y2 = Slider(y2, '参数y2', 0.0, 254., valfmt='%.f', valinit=100, valstep=1)
    slider_y2.on_changed(update_trans)

    update_trans(None)
    plt.show()

运行结果

3.4直方图均衡化

1、原理解释

直方图均衡化:核心是通过调整图像的灰度分布,让原本集中在某一区间的灰度值均匀分布到整个灰度范围(通常为0~255),从而提升图像细节。

图像的灰度直方图:统计每个灰度值(0~255)在图像中出现的次数(频次),很坐标是灰度值,纵坐标是频次

均衡化的目标:把“集中型”直方图变为均匀型直方图——让每个回去区间像素量尽可能均衡

实现原理:通过累积分布函数(CDF)讲原始灰度值映射到新的灰度值,拉伸密集区间、压缩稀疏区间,最终扩大灰度动态范围。

2、实践

import numpy as np
from PIL import Image
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider
import matplotlib  # 导入主模块
matplotlib.use('TkAgg')  

# 中文显示工具函数
def set_Chinese():
    # rcParams 本质是 matplotlib 主模块的全局配置字典
    matplotlib.rcParams["font.sans-serif"] =["SimHei"]
    matplotlib.rcParams["axes.unicode_minus"]=False

#获取图像的概率密度
def get_pdf(in_img):
    total = in_img.shape[0]*in_img.shape[1] #计算图片总像素数
    return [np.sum(in_img == i)/total for i in range(256)] #求概率密度,#掩码举证就是由True和False组成的和im_img同形状的矩阵

#直方图均衡化(核心代码)
def hist_equal(in_img):
    #1、求输入图像的概率密度
    Pr = get_pdf(in_img)

    #2、构造输出图像(初始化成输入)
    out_img = np.copy(in_img)

    #3、执行“直方图均衡化”(执行累积分布函数变换)
    y_points = []#存储点集,用于画图
    SUMK =0. #累加值存储变量
    for i in range(256):
        SUMK = SUMK +Pr[i] #SUMK 是从灰度 0 到灰度 i 的所有概率之和,范围在 0~1 之间。
        out_img[(in_img ==i)] = SUMK*255. #灰度值归一化 ,灰度值映射(SUMK * 255):将累积概率(0~1)缩放至 0~255 的灰度范围,得到新的灰度值。“原始图像中所有灰度为 i 的像素,在输出图像中统一更新为新映射值”
        y_points.append(SUMK*255.) #构造绘制函数图像的点集,y_points 存储每个原始灰度 i 对应的新灰度值
    return out_img,y_points

if __name__ == "__main__":
    set_Chinese()
    # 以灰度值方式读取图像,
    #gray_img = np.asanyarray(Image.open("../day2/lena.jpg").convert('L'))
    gray_img = np.asanyarray(Image.open("./planet.png").convert('L'))

    #对原图执行直方图均衡化
    out_img,y_points =hist_equal(gray_img)

    #创建一个显示主体,并分成6个显示区域
    fig = plt.figure()
    ax1,ax2=fig.add_subplot(231),fig.add_subplot(232)
    ax3, ax4 = fig.add_subplot(234), fig.add_subplot(235)
    ax5 = fig.add_subplot(236)

    #窗口显示:原图,原图灰度分布,结果图像,结果图像灰度分布
    ax1.set_title("原图",fontsize = 8)
    ax1.imshow(gray_img,cmap='gray',vmin=0,vmax=255)

    ax2.grid(True,linestyle=':', linewidth = 1)
    ax2.set_title("原图分布", fontsize=8)
    ax2.hist(gray_img.flatten(),bins=50,density=True,color='r',edgecolor='k') #将多维数组(如二维图像数组)“展平” 为一维数组,即将所有元素按顺序排列成一个连续的序列。

    ax3.set_title("直方图均衡化结果",fontsize = 8)
    ax3.imshow(out_img,cmap='gray',vmin=0,vmax=255)

    ax4.grid(True,linestyle=':', linewidth = 1)
    ax4.set_title("结果分布",fontsize = 8)
    ax4.set_xlim(0,255) #设置x轴分布范围
    ax4.set_ylim(0,0.2) #设置y轴分布范围
    ax4.hist(out_img.flatten(),bins=50,density=True,color='r',edgecolor='k')


    #窗口显示:绘制对应的灰度变换函数
    ax5.set_title("对应灰度变换",fontsize=12)
    ax5.grid(True,linestyle=':', linewidth = 1)
    ax5.plot(np.arange(0, 256, 1),y_points,color='r',lw=2) #np.arange(0,256,1)
    ax5.text(25,70,r'$s_k=255\times\sum_{i=0}^{k}p_r(i)$',fontsize=9)

    plt.show()

运行结果

3.5直方图规定化

1、原理解释

直方图规定化:让一副图像的灰度分布,主动”模仿“另一副目标图像的灰度分布的技术

简单来说:借目标图的直方图形状,改变原图的灰度分布,最终让原图的视觉风格、对比度和目标图趋于一致。

  • 直方图均衡化:目标是 “均匀分布”(固定目标),不管原始图像是什么样,都往 “灰度值铺满 0-255” 的方向调整。

  • 直方图规定化:目标是 “自定义分布”(目标由你选),比如想让图 A 的直方图长得和图 B 一样,就以图 B 为 “模板”,调整图 A 的灰度。

使用直方图均衡化的问题如下:把0处的峰值矫正到155附近,修正图像无法保持原有色调,原因:由于超灰度区间的概率密度过大,导致变换函数直接呈现“阶跃”。

因此设想,想要实现如下图的灰度分布直方图变换 ------使用直方图规定化

把0-255共256个数送进变换函数G,得到灰色的表格

2、实践

1)直方图规定化:均匀分布

import numpy as np
from PIL import Image
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider
import matplotlib  # 导入主模块
matplotlib.use('TkAgg')  # 切换 Matplotlib 后端,解决 PyCharm 兼容性问题方法,切换到 Tkinter 后端(需保证 Python 安装了 tkinter,通常默认有),正确调用 use()

# 中文显示工具函数
def set_Chinese():
    # rcParams 本质是 matplotlib 主模块的全局配置字典
    matplotlib.rcParams["font.sans-serif"] =["SimHei"]
    matplotlib.rcParams["axes.unicode_minus"]=False

#获取图像的概率密度
def get_pdf(in_img):
    total = in_img.shape[0] * in_img.shape[1] #计算图像的总像素数
    return [ np.sum(in_img == i)/total for i in range(256) ] #求概率密度

#直方图均衡化(核心代码)
def hist_equal(in_img):
    #1、求原始图像的概率密度
    Pr = get_pdf(in_img)

    #2、构造输出图像(初始化成输入)
    out_img = np.copy(in_img)

    #3、执行“直方图均衡化”(执行累积分布函数变换)
    y_points = [] #存储点集,用于画图
    SUMk = 0 #累加值存储变量
    for i in range(256):
        SUMk = SUMk + Pr[i]  # SUMK 是从灰度 0 到灰度 i 的所有概率之和,范围在 0~1 之间。
        out_img[(in_img == i)] = SUMk * 255 # 灰度值归一化 ,灰度值映射(SUMK * 255):将累积概率(0~1)缩放至 0~255 的灰度范围,得到新的灰度值。“原始图像中所有灰度为 i 的像素,在输出图像中统一更新为新映射值”
        y_points.append(SUMk * 255)  # 构造绘制函数图像的点集,y_points 存储每个原始灰度 i 对应的新灰度值
    return out_img.astype("int32"), y_points

#生成均匀分布
def gen_eq_pdf():
    return [ 0.0039 for i in range(256)] #均匀概率密度1/256 = 0.0039

#构造目标图像的映射表(核心代码)
def gen_target_table(Pv):
    table = []
    SUMq = 0.
    for i in range(256):
        SUMq = SUMq + Pv[i]
        table.append(round(SUMq*255, 0)) #四舍五入
    return table

#直方图规定化(核心代码)
def hist_specify(in_img=None):
    #1、拿到目标图像规定的概率密度,并构造映射表
    Pv = gen_eq_pdf()#均匀分布, 假设生成均匀分布的PDF(目标分布)
    table = gen_target_table(Pv) # 目标分布的累积映射表
    print(table)

    #2、对原始图像做直方图均衡
    ori_eq_img, T_points = hist_equal(in_img) # 得到中间均衡化图像,ori_eq_img是原始图像经过直方图均衡化后的结果(灰度分布已被拉伸为近似均匀分布)

    #3、构造输出图像(初始化成输入)
    out_img = np.copy(ori_eq_img)

    #4、执行“直方图规定化”(做逆映射B'->B)
    mal_val = 0 #逆映射值初始化为0
    for v in range(256):
        if v in ori_eq_img: #存在于B’,如果当前灰度v存在于均衡化图像中
            if v in table: #存在于映射表,如果当前灰度v在目标映射表中存在对应值
#拿到指定值最后出现的索引,python数组中的index下标只能拿到最小下标,多以要将table数组颠倒,再来拿下标,相当于拿到最大下标
                mal_val = len(table) - table[::-1].index(v)-1 
            out_img[(ori_eq_img == v)] = mal_val #找不到映射关系时,取前一个映射值

    return out_img,T_points,table,Pv

if __name__ == "__main__":
    set_Chinese()
    # 以灰度值方式读取图像,
    gray_img = np.asanyarray(Image.open("./lena_dark.png").convert('L'))

    #对原图执行“直方图规定化”
    out_img,T_pts,G_pts,spec_hist = hist_specify(gray_img)

    #创建1个显示主体,并分成6个显示区域
    fig = plt.figure()
    ax1, ax2,ax3= fig.add_subplot(231), fig.add_subplot(232),fig.add_subplot(233)
    ax4, ax5,ax6 = fig.add_subplot(234), fig.add_subplot(235),fig.add_subplot(236)

    #窗口显示:原图,原图灰度分布、规定PDF,结果图像,结果图像灰度分析,对应变换函数
    # 窗口显示:原图
    ax1.set_title("原图", fontsize=8)
    ax1.imshow(gray_img, cmap='gray', vmin=0, vmax=255)

    # 窗口显示:原图灰度分布
    ax2.grid(True, linestyle=':', linewidth=1)
    ax2.set_title("原图分布", fontsize=8)
    ax2.set_xlim(0, 255)  
    ax2.set_ylim(0, 1) 
    ax2.hist(gray_img.flatten(), bins=255, density=True, color='black', edgecolor='black') #bins=255:将 0~255 的灰度值均匀分成 255 个区间

    # 窗口显示:规定PDF
    ax4.set_title("直方图规定化结果", fontsize=8)
    ax4.imshow(out_img, cmap='gray', vmin=0, vmax=255)

    # 窗口显示:结果图像灰度分析
    ax5.grid(True, linestyle=':', linewidth=1)
    ax5.set_title("结果分布", fontsize=8)
    ax5.set_xlim(0, 255)  
    ax5.set_ylim(0, 1)  
    ax5.hist(out_img.flatten(), bins=255, density=True,color='black',edgecolor='black')

    #绘制规定概率密度函数
    ax3.set_title("规定pdf", fontsize=8)
    ax3.grid(True, linestyle=':', linewidth=1)
    ax3.plot(np.arange(0, 256, 1), spec_hist, color='r', lw=1)

    #窗口显示:绘制对应的灰度变换函数
    ax6.set_title("灰度变化:橙色T(k),绿色G(q)", fontsize=10)
    ax6.grid(True, linestyle=':', linewidth=1)
    ax6.plot(np.arange(0,256,1),T_pts,'-',color='orange',lw=2)
    ax6.plot(np.arange(0,256,1),G_pts,'k--',color='green',lw=2)

    plt.show()

运行结果

2)直方图规定化:多峰正态分布

import numpy as np
from PIL import Image
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider
import matplotlib  # 导入主模块
matplotlib.use('TkAgg')  # 切换 Matplotlib 后端,解决 PyCharm 兼容性问题方法,切换到 Tkinter 后端(需保证 Python 安装了 tkinter,通常默认有),正确调用 use()

# 中文显示工具函数
def set_Chinese():
    # rcParams 本质是 matplotlib 主模块的全局配置字典
    matplotlib.rcParams["font.sans-serif"] =["SimHei"]
    matplotlib.rcParams["axes.unicode_minus"]=False

#获取图像的概率密度
def get_pdf(in_img):
    total = in_img.shape[0] * in_img.shape[1] #计算图像的总像素数
    return [ np.sum(in_img == i)/total for i in range(256) ] #求概率密度

#直方图均衡化(核心代码)
def hist_equal(in_img):
    #1、求原始图像的概率密度
    Pr = get_pdf(in_img)

    #2、构造输出图像(初始化成输入)
    out_img = np.copy(in_img)

    #3、执行“直方图均衡化”(执行累积分布函数变换)
    y_points = [] #存储点集,用于画图
    SUMk = 0 #累加值存储变量
    for i in range(256):
        SUMk = SUMk + Pr[i]  # SUMK 是从灰度 0 到灰度 i 的所有概率之和,范围在 0~1 之间。
        out_img[(in_img == i)] = SUMk * 255 
        y_points.append(SUMk * 255)  # 构造绘制函数图像的点集,y_points 存储每个原始灰度 i 对应的新灰度值
    return out_img.astype("int32"), y_points

#生成均匀分布
def gen_eq_pdf():
    return [ 0.0039 for i in range(256)] #均匀概率密度1/256 = 0.0039

#生成多峰正态分布
#means 各峰均值
#stds 各峰标准差
#amp1 各正态分布幅值(和为1)
#bias 概率密度函数总体抬升量(若没有抬升量,容易造成图像过暗)
def gen_mul_norm_pdf(x,means,stds,ampl,bias):
    pdf = np.zeros([256,],dtype = float)
    for i in range(len(means)):
        pdf_temp = ampl[i]*np.exp(-(x-means[i])**2/(2*stds[i]**2))\
                    /(stds[i]*np.sqrt(2*np.pi))
        pdf = pdf + pdf_temp
    pdf = pdf + bias #总体抬升概率密度,避免出现零值
    pdf2 = pdf/np.sum(pdf) #由于做了抬升,所以要重新归一化
    return pdf2.tolist()



#构造目标图像的映射表(核心代码)
def gen_target_table(Pv):
    table = []
    SUMq = 0.
    for i in range(256):
        SUMq = SUMq + Pv[i]
        table.append(round(SUMq*255, 0)) #四舍五入
    return table

#直方图规定化(核心代码)
def hist_specify(in_img=None):
    #1、拿到目标图像规定的概率密度,并构造映射表
    #Pv = gen_eq_pdf()#均匀分布, 假设生成均匀分布的PDF(目标分布)
    Pv = gen_mul_norm_pdf(np.arange(0,256,1),[18,170],[23,23],[0.93,0.07],0.002)
    table = gen_target_table(Pv) # 目标分布的累积映射表
    print(table)

    #2、对原始图像做直方图均衡
    ori_eq_img, T_points = hist_equal(in_img) # 得到中间均衡化图像,ori_eq_img是原始图像经过直方图均衡化后的结果(灰度分布已被拉伸为近似均匀分布)

    #3、构造输出图像(初始化成输入)
    out_img = np.copy(ori_eq_img)

    #4、执行“直方图规定化”(做逆映射B'->B)
    mal_val = 0 #逆映射值初始化为0
    for v in range(256):
        if v in ori_eq_img: #存在于B’,如果当前灰度v存在于均衡化图像中
            if v in table: #存在于映射表,如果当前灰度v在目标映射表中存在对应值
    #拿到指定值最后出现的索引,python数组中的index下标只能拿到最小下标,多以要将table数组颠倒,再来拿下标,相当于拿到最大下标
                mal_val = len(table) - table[::-1].index(v)-1 
            out_img[(ori_eq_img == v)] = mal_val #找不到映射关系时,取前一个映射值

    return out_img,T_points,table,Pv

if __name__ == "__main__":
    set_Chinese()
    # 以灰度值方式读取图像,
    gray_img = np.asanyarray(Image.open("../day4/planet.png").convert('L'))

    #对原图执行“直方图规定化”
    out_img,T_pts,G_pts,spec_hist = hist_specify(gray_img)

    #创建1个显示主体,并分成6个显示区域
    fig = plt.figure()
    ax1, ax2,ax3= fig.add_subplot(231), fig.add_subplot(232),fig.add_subplot(233)
    ax4, ax5,ax6 = fig.add_subplot(234), fig.add_subplot(235),fig.add_subplot(236)

    #窗口显示:原图,原图灰度分布、规定PDF,结果图像,结果图像灰度分析,对应变换函数
    # 窗口显示:原图
    ax1.set_title("原图", fontsize=8)
    ax1.imshow(gray_img, cmap='gray', vmin=0, vmax=255)

    # 窗口显示:原图灰度分布
    ax2.grid(True, linestyle=':', linewidth=1)
    ax2.set_title("原图分布", fontsize=8)
    ax2.set_xlim(0, 255) 
    ax2.set_ylim(0, 1) 
    ax2.hist(gray_img.flatten(), bins=255, density=True, color='black', edgecolor='black') 

    # 窗口显示:规定PDF
    ax4.set_title("直方图规定化结果", fontsize=8)
    ax4.imshow(out_img, cmap='gray', vmin=0, vmax=255)

    # 窗口显示:结果图像灰度分析
    ax5.grid(True, linestyle=':', linewidth=1)
    ax5.set_title("结果分布", fontsize=8)
    ax5.set_xlim(0, 255)  
    ax5.set_ylim(0, 1)  
    ax5.hist(out_img.flatten(), bins=255, density=True,color='black',edgecolor='black')

    #绘制规定概率密度函数
    ax3.set_title("规定pdf", fontsize=8)
    ax3.grid(True, linestyle=':', linewidth=1)
    ax3.plot(np.arange(0, 256, 1), spec_hist, color='r', lw=1)

    #窗口显示:绘制对应的灰度变换函数
    ax6.set_title("灰度变化:橙色T(k),绿色G(q)", fontsize=10)
    ax6.grid(True, linestyle=':', linewidth=1)
    ax6.plot(np.arange(0,256,1),T_pts,'-',color='orange',lw=2)
    ax6.plot(np.arange(0,256,1),G_pts,'k--',color='green',lw=2)

    plt.show()

运行结果

四、tips

在学习过程中,敲一遍代码能够加深对于数字图像处理的理解。

比如,直方图均衡化,是把集中型直方图变为均匀型直方图,为什么做了直方图均衡化之后,没有改变图像的形状?

  • 直方图均衡化的本质:直改变“灰度值”,不改变“像素位置”,图像的“形状”由像素的空间位置关系决定(例如一个矩形的形状由其边缘像素的坐标排列决定),而直方图均衡化的操作是保持像素的位置不变,只是灰度值被替换。

  • 改变像素的位置:让图像中每个像素的坐标发生移动---原本在图像(x,y)位置的像素,被挪到(x‘,y’)的位置,最终会改变图像中物体的形状大小位置。例如:图像平移:把整张图向右移动100个像素,向下移动50个像素,物体的位置变了,但是形状没变

    • 改变像素位置:动的是”像素在哪“(坐标),会影响图像的形状大小位置;

    • 直方图均衡化:动的是“像素是什么灰度”(数值),像素的坐标完全不变、所以形状、结构都不变,只变明暗对比。

所以,图像成像的原理是:图像 = 像素的位置(坐标) + 像素的数值(灰度 / 颜色),两者共同决定了我们看到的画面。不能把像素的位置和数值混淆!


评论