使用opencv-python读取图片文件,并使用numpy和math等库对图片进行对称反转、旋转、平移、剪切等操作。
当使用cv2.imread
读入一个图片后,完全可以将读入的图片转换为nuparray矩阵,所有对图片灰度(色度)的变化,都可以表示为是对矩阵中像素位置对应的数据的变换操作。
也即,图像变换的本质是将像素点的坐标通过某一种函数关系,所有对图像的操作都是对像素矩阵的操作。在本次内容中,还并未涉及灰度值的变换,主要是图像像素坐标的迁移如平移或旋转。
在平移旋转中,变换后的图像和原图像的像素是一一对应的;而对于剪切等变换,会有原图像上多个像素对应变换后图像像素的情况。由此,即可引出,图像映射的前向和后向区分问题。
假设变换前图像为I(x,y),变换后图像为I’(x’,y’),则变换前后的图像之间存在下列关系
由原图像像素表示出变换后的像素,以原图每个像素为基准计算被它影响的新图像素。
即为前向映射由变换后的图像像素还原出原图像像素,以新图每个像素为基准计算影响它的原图像素。
即为后向映射前向映射可能会导致多对一的情况,也会出现目标点没有像素,但是原图像有的问题。工程上多使用反向映射,避免上述问题,同时减少计算量。但反向映射需要提前知道反变换,在变换复杂的场合,其反变换会很难求得。
参考:图像变换——向前映射和向后映射
参考:基础图像操作(十三):像素重映射
终于写到正题了,开始结合python实现对图像的简单变换。
前文说过,图像变换就是矩阵变换:图像旋转也即矩阵旋转。(x’,y’)表示一个点经过旋转后的新位置,(x,y)表示未旋转前的原始位置,θ为旋转角度,编程中以弧度为单位。
注意:计算多个点的旋转,需要将每个点位置分别代入公式计算。
由以上方法,可写出旋转代码。但运行后会发现有诸多问题,这里先按下不表。
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的值可求得为:
在代码中由于u = srcX - x
和v = srcY - y
,且 x = math.floor(srcX) y = math.floor(srcY)
x和y是srcx、srcy的向下取整,像素位置间隔为1,则u和v都是小于1的数,相当于已经进行过归一化,故分母省略。
问题显现
运行上述代码,图像确实可以旋转,但并不完美,出现了几处问题。
显而易见的,旋转图片并非以图片中心做旋转,而是围绕左上角,其根源在于默认矩阵左上角为矩阵的原点,也即图像的左上角像素为图片的原点,若想以图片中心做旋转,应先对图片做搬移操作,先把旋转点平移到原点;而且图片在以原图片大小显示时并不能将旋转后图片完整呈现,以朴素的理解图像在旋转之后的显示矩形框也应该比原来的大,故应按照旋转后的需求扩大图片矩形框。
优化后的步骤:
1).先把旋转点平移到原点
2).然后进行以上旋转操作
3).按1的逆操作平移回去
就可以得到绕任意点旋转点变换矩阵:
画布大小不变的情况下,会有一部分图像超出,显示不全,所以我们需要将画布扩大为:
新的高由图片中两段蓝色线组合
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))))
新的画布扩大是基于原图左上角点扩大,显示的还是蓝色区域,同样丢失了信息。
5).我们还需要将红色区域进行平移操作显示到蓝色区域
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
,就从这个警告入手。
通过查询这个警告,发现是像素加减运算溢出异常,出现的原因是因为图片的像素一般是八位即最大值是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.)