基于python的数字图像处理--学习笔记(二)

基于python的数字图像处理--学习笔记(二)

  • 基于python的简单图像矩阵变换:
    • 向前映射和向后映射:
    • 图像旋转:
    • 彩色图像邻域平均操作:
    • 去除/弱化 图片的加性高斯白噪声
    • 灰度级变换

基于python的简单图像矩阵变换:

使用opencv-python读取图片文件,并使用numpy和math等库对图片进行对称反转、旋转、平移、剪切等操作。

当使用cv2.imread读入一个图片后,完全可以将读入的图片转换为nuparray矩阵,所有对图片灰度(色度)的变化,都可以表示为是对矩阵中像素位置对应的数据的变换操作。
基于python的数字图像处理--学习笔记(二)_第1张图片
基于python的数字图像处理--学习笔记(二)_第2张图片 也即,图像变换的本质是将像素点的坐标通过某一种函数关系,所有对图像的操作都是对像素矩阵的操作。在本次内容中,还并未涉及灰度值的变换,主要是图像像素坐标的迁移如平移或旋转。

在平移旋转中,变换后的图像和原图像的像素是一一对应的;而对于剪切等变换,会有原图像上多个像素对应变换后图像像素的情况。由此,即可引出,图像映射的前向和后向区分问题。


向前映射和向后映射:

假设变换前图像为I(x,y),变换后图像为I’(x’,y’),则变换前后的图像之间存在下列关系

  1. 由原图像像素表示出变换后的像素,以原图每个像素为基准计算被它影响的新图像素。即为前向映射
    基于python的数字图像处理--学习笔记(二)_第3张图片基于python的数字图像处理--学习笔记(二)_第4张图片
  2. 由变换后的图像像素还原出原图像像素,以新图每个像素为基准计算影响它的原图像素。即为后向映射
    基于python的数字图像处理--学习笔记(二)_第5张图片基于python的数字图像处理--学习笔记(二)_第6张图片在这种情况下,我们知道输出图像上整数点位置(x’,y’)在变换前位于输入图像上的位置(x,y),一般来说这是个非整数点位置,利用其周围整数点位置的输入图像像素值进行插值,就得到了该点的像素值。我们遍历输出图像,经过坐标变换、插值两步操作,我们就能将其像素值一个个地计算出来,因此向后映射又叫图像填充映射。

前向映射可能会导致多对一的情况,也会出现目标点没有像素,但是原图像有的问题。工程上多使用反向映射,避免上述问题,同时减少计算量。但反向映射需要提前知道反变换,在变换复杂的场合,其反变换会很难求得。

参考:图像变换——向前映射和向后映射

参考:基础图像操作(十三):像素重映射


图像旋转:

终于写到正题了,开始结合python实现对图像的简单变换。

前文说过,图像变换就是矩阵变换:图像旋转也即矩阵旋转。(x’,y’)表示一个点经过旋转后的新位置,(x,y)表示未旋转前的原始位置,θ为旋转角度,编程中以弧度为单位基于python的数字图像处理--学习笔记(二)_第7张图片在这里插入图片描述

注意:计算多个点的旋转,需要将每个点位置分别代入公式计算。

  1. 由以上方法,可写出旋转代码。但运行后会发现有诸多问题,这里先按下不表。

    import cv2 as cv
    import numpy as np
    import math
    
    
    def rotate(img, angle):
        height, width, _ = img.shape
        res = np.zeros((height, width, 3), np.uint8) #生成三通道原图片大小的变换后图片模板
        anglePi = angle * np.pi / 180.0
    
        for i in range(height):#y
            for j in range(width):#x
                # y = round(j * np.cos(anglePi) - i * np.sin(anglePi))   前向映射
                # x = round(j * np.sin(anglePi) + i * np.cos(anglePi))
                # 后向映射,下面为逆矩阵
                srcY = (j * np.cos(anglePi) + i * np.sin(anglePi))
                srcX = (-j * np.sin(anglePi) + i * np.cos(anglePi))
                # 后向映射+双线性插值
                x = math.floor(srcX)
                y = math.floor(srcY)
                u = srcX - x
                v = srcY - y
                if 0 <= x <= 255 and 0 <= y <= 255:
                    res[i, j] = (1 - u) * (1 - v) * img[x, y] + u * (1 - v) * img[x + 1, y] + (1 - u) * v * img[x, y + 1] + u * v * img[x + 1, y + 1]
                    # res[i, j] = img[x, y]
                    # res[x, y] = img[i, j]
        print(res.shape)
        return res
    
    if __name__ == '__main__':
        img = cv.imread("pic1.jpg")
        cv.imshow("rotated img", rotate(img, 20))
        print("success")
        cv.waitKey(0)
        cv.destroyAllWindows()
    
    

    res[i, j] = (1 - u) * (1 - v) * img[x, y] + u * (1 - v) * img[x + 1, y] + (1 - u) * v * img[x, y + 1] + u * v * img[x + 1, y + 1]这句代码用来实现双线性内插,变换后图像像素通过反变换发现位于原图像像素空隙处,故使用双线性内插法拟合此处像素值并赋给变换图像。如下图所示:已知Q12,Q22,Q11,Q21,要插值的点为P点,首先在x轴方向上,对R1和R2两个点进行插值,即蓝色R1的值根据Q11和Q21的值可求得为:

    基于python的数字图像处理--学习笔记(二)_第8张图片
    基于python的数字图像处理--学习笔记(二)_第9张图片
    在代码中由于u = srcX - xv = srcY - y,且 x = math.floor(srcX) y = math.floor(srcY)
    x和y是srcx、srcy的向下取整,像素位置间隔为1,则u和v都是小于1的数,相当于已经进行过归一化,故分母省略。

  2. 问题显现

    运行上述代码,图像确实可以旋转,但并不完美,出现了几处问题。
    基于python的数字图像处理--学习笔记(二)_第10张图片

    显而易见的,旋转图片并非以图片中心做旋转,而是围绕左上角,其根源在于默认矩阵左上角为矩阵的原点,也即图像的左上角像素为图片的原点,若想以图片中心做旋转,应先对图片做搬移操作,先把旋转点平移到原点;而且图片在以原图片大小显示时并不能将旋转后图片完整呈现,以朴素的理解图像在旋转之后的显示矩形框也应该比原来的大,故应按照旋转后的需求扩大图片矩形框。

    优化后的步骤:

    1).先把旋转点平移到原点

    2).然后进行以上旋转操作

    3).按1的逆操作平移回去

    就可以得到绕任意点旋转点变换矩阵:

    在这里插入图片描述4).扩大画布

    画布大小不变的情况下,会有一部分图像超出,显示不全,所以我们需要将画布扩大为:

    新的高由图片中两段蓝色线组合

    new_H=int(w∗fabs(sin(radians(angle)))+h∗fabs(cos(radians(angle))))

    新的宽由图片中两段红色线组合

    new_W=int(h∗fabs(sin(radians(angle)))+w∗fabs(cos(radians(angle))))

    基于python的数字图像处理--学习笔记(二)_第11张图片
    新的画布扩大是基于原图左上角点扩大,显示的还是蓝色区域,同样丢失了信息。

    5).我们还需要将红色区域进行平移操作显示到蓝色区域

    基于python的数字图像处理--学习笔记(二)_第12张图片
    所以,在变换矩阵M上,我们可以调整平移参数:

    M[0,2]+=(new_W−w)/2

    M[1,2]+=(new_H−h)/2

由以上,改进后的旋转代码:

# -*-coding:utf-8-*-

import cv2
from math import *
import numpy as np
from scipy.spatial.distance import pdist

# x=np.random.random(100)
# y=np.random.random(100)
#
# #方法一:根据公式求解,2维
# d1=np.dot(x,y)/(np.linalg.norm(x)*np.linalg.norm(y))
#
# # print d1
#
# #方法二:根据scipy库求解,n维
# X=np.vstack([x,y])
# d2=1-pdist(X,\'cosine\')

# print d2

img = cv2.imread("pic1.jpg")
height, width = img.shape[:2]

# (1)如何计算这个旋转角度
# degree = np.dot(x,y)/(np.linalg.norm(x)*np.linalg.norm(y))
degree = -30

# (2)旋转后的尺寸
# @radians(),角度转换为弧度
heightNew = int(width * fabs(sin(radians(degree))) + height * fabs(cos(radians(degree))))
widthNew = int(height * fabs(sin(radians(degree))) + width * fabs(cos(radians(degree))))

# (3)求旋转矩阵,以图片中心点为旋转中心 隐含M矩阵
matRotation = cv2.getRotationMatrix2D((width / 2, height / 2), degree, 1)

matRotation[0, 2] += (widthNew - width) / 2  # ?????重点在这步,目前不懂为什么加这步
matRotation[1, 2] += (heightNew - height) / 2  # ?????重点在这步

# (4)最后得到的图像,边界是黑色
imgRotation = cv2.warpAffine(img, matRotation, (widthNew, heightNew), borderValue=(0, 0, 0))

# 我把像素值 > 0 的区域提取出来
# 作二值化,将阈值设置为50,阈值类型为cv2.THRESH_BINARY,则灰度在大于50的像素其值将设置为255,其它像素设置为0
# retval, dst = cv2.threshold(imgRotation, 50, 255, cv2.THRESH_BINARY)
# cv2.imwrite('1.jpeg',img, [int( cv2.IMWRITE_JPEG_QUALITY), 95])
# cv2.imwrite('2.png',img, [int(cv2.IMWRITE_PNG_COMPRESSION), 9])
# cv2.imshow("img", img)
cv2.imshow("imgRotation", imgRotation)
# cv2.imwrite("imgRotation_1.jpg",imgRotation)
cv2.waitKey(0)

运行代码,图片以中心做旋转,且显示完整正常。


彩色图像邻域平均操作:

写不动了,其实就是矩阵平均,用3×3邻域做平均就是将原图片像素除边缘像素点之外的所有像素,以他和他八邻域像素值的平均值做代替,结果是会使图像平滑,边界不清晰,也就是模糊。

本来代码很简单,就是简单的遍历矩阵,但是在操作过程中一直不能正常显示,而是很抽象的彩色图像

import cv2 as cv
import numpy as np


def avg(img):
    height, width,_ = img.shape
    res = np.zeros((height, width, 3), np.uint8)
    for i in range(height):
        for j in range(width):
            if i - 1 >= 0 and j - 1 >= 0 and i + 1 <= height-2 and j + 1 <= width-2:
                res[i, j] = (img[i - 1, j - 1] + img[i - 1, j] + img[i - 1, j + 1] + img[i, j - 1] + img[i, j] + img[i, j + 1] + img[i + 1, j - 1] + img[i + 1, j] + img[i + 1, j + 1]) / 9

    return res


if __name__ == '__main__':
    img = cv.imread("nimg.ws.126.jpg")
    show_img = np.concatenate((img.astype(np.uint8),avg(img)),axis=0)#.reshape(-1,width,3)
    print(show_img.shape)
    cv.imshow("avg_ed img", show_img)
    print("success")
    cv.waitKey(0)
    cv.destroyAllWindows()


平滑后的图像甚至有不可名状那味了。。。

开始检查代码,代码逻辑肯定是没有问题的,使用print打印出均衡化后的像素矩阵,会发现像素的值都很小,从下半张图像上也能看出来,是什么导致像素值变小了呢?有时还会蹦出一个警告RuntimeWarning: overflow encountered in ubyte_scalars,就从这个警告入手。

基于python的数字图像处理--学习笔记(二)_第13张图片

通过查询这个警告,发现是像素加减运算溢出异常,出现的原因是因为图片的像素一般是八位即最大值是256,最小值是0,如果超出了这个范围就会出现警告,不会报错使得程序停止下来,但是会使得计算出来的结果有误。

cv.read读入图片后返回的是nparray数组,里面的元素类型应该是np.uint8,在累加过程中溢出,python不会对其自动修改内存范围,而是重新从0开始计数:

假设一个图片像素点的灰度值为136,另一个像素点的灰度值为180,两个灰度值相加出现的结果按道理来说是:316

但是得出来的结果是:60,出现这种情况的原因就是因为316溢出了0-255的范围,导致其重新从0开始计数

解决办法:

1、在计算过程中先对每个累加的像素值除以9,而不是累加后 再÷9,以确保过程中不发生溢出。

2、 在计算过程中对每个需要加的像素值进行数据类型转换,使用.astype(np.uint32) 使累加允许的最大值变大,防止溢出。

3、在cv.read读入图片文件时就对整个矩阵修改其元素类型,并在显示图像前修改回np.uint8。

import cv2 as cv
import numpy as np


def avg(img):
    height, width ,_= img.shape
    res = np.zeros((height, width, 3), np.uint8)
    #print(res)
    print(res[2][80])
    for i in range(height):
        for j in range(width):
            if i - 1 >= 0 and j - 1 >= 0 and i + 2 <= height and j + 2 <= width:
                a=0
                # 3×3的邻域操作
                for near_x in range(3):
                    for near_y in range(3):
                        #res[i, j] = res[i, j]+img[i - near_x-1, j - near_y-1] / 9
                        #a=a+img[i - near_x-1, j - near_y-1].astype(np.uint32)
                        a=a+img[i - near_x-1, j - near_y-1]
                res[i, j]=(a/9).astype(np.int8)
    return res


if __name__ == '__main__':
    img = cv.imread("nimg.ws.126.jpg").astype(np.int32)
    height, width, _ = img.shape
    # cv.imshow("avg_ed img", avg(img))
    show_img = np.concatenate((img.astype(np.uint8),avg(img)),axis=0)#.reshape(-1,width,3)
    cv.imshow("avg_ed img", show_img)
    print("success")
    cv.waitKey(0)
    cv.destroyAllWindows()


处理后图片正常显示,也比原图像模糊了一些。

去除/弱化 图片的加性高斯白噪声

使用多张图片叠加取平均值方法弱化高斯噪声,达到去除噪声效果。

import random
import cv2 as cv
import numpy as np


def noise(img):
    img = img.astype(np.uint8)
    height, width, mode = img.shape
    for i in range(height):
        for j in range(width):
            for k in range(mode):
                img[i, j, k] += random.gauss(0, 1)

    return img


def avg_remove_noise(img, count):
    tar = np.zeros_like(img).astype(np.float32)
    for _ in range(count):
        tar += np.float32(noise(img))
    tar /= count
    # tar = np.clip(tar, 0, 255).astype(np.uint8)
    tar = tar.astype(np.uint8)
    return tar


if __name__ == '__main__':
    img = cv.imread("nimg.ws.126.jpg")
    # cv.imshow("avg_ed 2", avg_remove_noise(img, 2))
    show_img = np.concatenate((img, noise(img)), axis=0)
    show_img = np.concatenate((show_img, avg_remove_noise(img, 50)), axis=0)
    cv.imshow("avg_ed 50",show_img)
    # cv.imshow("avg_ed 100", avg_remove_noise(img, 100))
    # cv.imshow("noise_img", noise(img))
    # diff(noise(img), avg_remove_noise(img, 10))
    print("success")
    cv.waitKey(0)
    cv.destroyAllWindows()

最上面为原图像,中间的是加入了(0,1)的高斯白噪声(均值为0),最下面为通过多张图片取均值的方法消除噪声的结果,可以明显看到噪声被弱化。

缺点就是计算量大耗时长,猜测应该是50遍高斯白噪声随机过程时间比较长,单纯的叠加取均值应该不会这么长时间吧。

灰度级变换

将255灰度级的灰度图片更改为任意灰度级

import cv2 as cv
import numpy as np


def change_grey_level(img, level):
    img -= np.min(img)
    ma = np.max(img)
    height, width = img.shape
    res = np.zeros((height, width), np.uint8)
    for i in range(height):
        for j in range(width):
            for lev in range(level):
                flg=255/level
                if img[i,j]>=flg*lev and img[i,j]<flg*(lev+1):
                    res[i, j] = lev * 255 / (level - 1)  
                    break
            # for lev in range(level):
            #     flg=(lev+1)*255/level
            #     if img[i,j]<=flg:
            #         continue
            #     res[i, j]=(lev+1)*255/(level-1)
            # res[i, j] = level * (img[i, j] / ma)

    return np.clip(res, 0, 255)


if __name__ == '__main__':
    img = cv.imread("nimg.ws.126.jpg", 0)
    print(np.min(img))
    print(np.max(img))
    cv.imshow("img", img)
    print(change_grey_level(img, 3))
    cv.imshow("changed img", change_grey_level(img, 3))
    print("success")
    cv.waitKey(0)
    cv.destroyAllWindows()

中间的res[i, j] = lev * 255 / (level - 1) 最开始每太想明白,最下层一定灰度是0,所以0-255重新分配n个level,最小的 level一定是0,那么其他的 level平分255,所以阶跃是255/(level-1.)

基于python的数字图像处理--学习笔记(二)_第14张图片

你可能感兴趣的:(数字图像处理,python,opencv)