Python关于图像处理的模块包特别多,可参见此链接:[Python中的十大图像处理工具]。
目前用的比较多的还是opencv-python、numpy和PIL。
本文就这三个库封装了一些常用的工具类(以opencv-python为主),
功能包括:
1.图像拼接
2.图像旋转
3.图像裁剪
4.图像批量命名
5.在图像中添加中文
6.在图像中绘制线条(绊线)
7.图像亮度和对比度调节
8.图像光照补偿
9.视频转图像
10.视频片段截取
11.视频连接
12.利用背景减法获取矩形框(用于视频中动态目标的位置检测)
13.利用帧间差分法获取矩形框(用于视频中动态目标的位置检测)
14.图像的轮廓提取(Sobel与Canny算子)
15.目录中的图像转成视频文件
代码如下所示:
# -*- coding:utf-8 -*-
# 图像本质上是包含数据点像素的标准Numpy数组
import os
import time
import cv2
import numpy as np
from PIL import Image, ImageDraw, ImageFont
def imageJoint(img1Path, img2Path, axis=1, isZoomBoolean=False, zoomScale=2):
'''
图像拼接
:param img1Path: 图像1的路径
:param img2Path: 图像2的路径
:param axis: 连接方式,可选值为0和1,0表示上下连接,1表示左右连接,默认为1
:param isZoomBoolean: 是否进行图像缩放(长宽各缩小一倍)
:param zoomScale: 图像缩放比例,默认长宽各缩小1倍
:return:
'''
img1 = cv2.imread(img1Path)
img2 = cv2.imread(img2Path)
if isZoomBoolean is True:
hh, ww, channel = img1.shape
img1 = cv2.resize(img1, (ww // zoomScale, hh // zoomScale))
img2 = cv2.resize(img2, (ww // zoomScale, hh // zoomScale))
# image = np.hstack([img1, img2]) # 水平拼接
# image = np.vstack([img1, img2]) # 垂直拼接
image_joint = np.concatenate([img1, img2], axis=axis) # axis=0时为垂直拼接;axis=1时为水平拼接
cv2.imwrite("image_joint.jpg", image_joint)
def imageRotate(imagePath, angle):
'''
图像旋转
:param img2Path: 图像路径
:param angle: 旋转角度
'''
image = cv2.imread(imagePath)
(h, w) = image.shape[:2] # 返回(高,宽,色彩通道数),此处取前两个值返回
# 抓取旋转矩阵(应用角度的负值顺时针旋转)。参数1为旋转中心点;参数2为旋转角度,正值表示逆时针旋转;参数3为各向同性的比例因子
M = cv2.getRotationMatrix2D((w / 2, h / 2), -angle, 1.0)
# 计算图像的新边界维数
newW = int((h * np.abs(M[0, 1])) + (w * np.abs(M[0, 0])))
newH = int((h * np.abs(M[0, 0])) + (w * np.abs(M[0, 1])))
# newW = int(h * fabs(sin(radians(angle))) + w * fabs(cos(radians(angle))))
# newH = int(w * fabs(sin(radians(angle))) + h * fabs(cos(radians(angle))))
# 调整旋转矩阵以考虑平移
M[0, 2] += (newW - w) / 2
M[1, 2] += (newH - h) / 2
# 执行实际的旋转并返回图像
img_rotate = cv2.warpAffine(image, M, (newW, newH)) # borderValue 缺省,默认是黑色
cv2.imwrite('./rotateImage.jpg', img_rotate)
def imageCut(imagePath):
'''
图像裁剪
:param imagePath: 图像路径
:return:
'''
img = cv2.imread(imagePath)
# 在图像中绘制ROI矩形框
fold_box = cv2.selectROI('Cut image, press the Space bar or Enter to finish!', img, False)
# 以ROI矩形框的左顶点为起始点,绘制对角直线
startPoint = (fold_box[0], fold_box[1]) # 裁剪的左上角像素点
endPoint = (fold_box[0] + fold_box[2], fold_box[1] + fold_box[3]) # 裁剪的右下角像素点
cutImg = img[startPoint[1]:endPoint[1], startPoint[0]:endPoint[0]]
cv2.imwrite("cutImage.jpg", cutImg)
def imageCut2(imagePath, startPoint=(0, 0), endPoint=(0, 0)):
'''
图像裁剪
:param imagePath: 图像路径
:param startPoint: 裁剪的左上角像素坐标
:param endPoint: 裁剪的右下角像素坐标
:return:
'''
img = cv2.imread(imagePath)
cutImg = img[startPoint[1]:endPoint[1], startPoint[0]:endPoint[0]]
cv2.imwrite("cutImage.jpg", cutImg)
def renameImageBatch(imgPath, newPath, startNumber=0):
'''
传入图片所在目录,将目录中的图片以纯数字形式批量重命名,并以jpg格式保存
:param imgPath: 图片所在路径
:param newPath: 命名后的图片保存的新路径
:param startNumber: 起始数字
:return:
'''
for imgName in os.listdir(imgPath):
print(imgName)
imageFullPath = imgPath + imgName
startNumber += 1
img = cv2.imread(imageFullPath)
cv2.imwrite(newPath + str(startNumber) + ".jpg", img)
cv2.destroyAllWindows()
def addChineseToImage(img, text, textPoint=(0, 0), textColor=(255, 0, 0)):
'''
传入opencv读取后的图像,在图像中添加中文
:param img: 需要添加中文的图像
:param text: 需要添加的中文文本
:param textPoint: 文本位置的像素坐标,此处默认图像左上角(0,0)处
:param textColor: 文本颜色,此处默认为红色
:return: 返回cv2形式的目标图像
'''
# cv2转PIL(cv2和PIL中颜色的hex码的储存顺序不同)
img_cv2 = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img_pil = Image.fromarray(img_cv2)
# PIL图片上打印汉字
draw = ImageDraw.Draw(img_pil)
# 参数1:字体文件路径,simhei为黑体(Windows系统自带);参数2:字体大小,默认10
font = ImageFont.truetype("simhei.ttf", 20, encoding="utf-8")
# font = ImageFont.truetype('NotoSerifCJK-Regular.ttc', 20)
# 参数1:文本像素坐标;参数2:打印文本;参数3:字体颜色;参数4:字体
draw.text(textPoint, text, textColor, font=font)
# PIL图片转cv2 图片
img_target = cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)
return img_target
def foldLineToImage(imagePath, isVertical=False, isHorizontal=False, lineColor=(255, 0, 0),
lineSize=3):
'''
在图像中绘制线条
:param imagePath: 图像原始路径
:param isVertical: 是否绘制垂直线条,默认False
:param isHorizontal: 是否绘制水平线条,默认False
:param lineColor: 线条颜色
:param lineSize: 线条粗细
:return:
'''
image = cv2.imread(imagePath)
# 在图像中绘制ROI矩形框
fold_box = cv2.selectROI('Draw line, press the Space bar or Enter to finish!', image, False)
# 以ROI矩形框的左顶点为起始点,绘制对角直线
startPoint = (fold_box[0], fold_box[1]) # 直线起始点
endPoint = (fold_box[0] + fold_box[2], fold_box[1] + fold_box[3]) # 直线结束点
if isVertical is True:
cpoint = (startPoint[0] + endPoint[0]) // 2
startPoint = (cpoint, startPoint[1])
endPoint = (cpoint, endPoint[1])
if isHorizontal is True:
cpoint = (startPoint[1] + endPoint[1]) // 2
startPoint = (startPoint[0], cpoint)
endPoint = (endPoint[0], cpoint)
print(startPoint, endPoint)
cv2.line(image, startPoint, endPoint, lineColor, lineSize)
cv2.imwrite('foldLine.jpg', image)
def adjustLightAndContrast(imagePath, ALPHA=1, BETA=20):
'''
图像亮度和对比度调节
:param imagePath: 图像路径
:param ALPHA:
:param BETA:
:return:
'''
img = Image.open(imagePath)
c, r = img.size
arr = np.array(img)
for i in range(r):
for j in range(c):
for k in range(3):
temp = arr[i][j][k] * ALPHA + BETA
if temp > 255:
arr[i][j][k] = 2 * 255 - temp
else:
arr[i][j][k] = temp
return Image.fromarray(arr)
def lightCompensation(imagePath):
'''
光照补偿
:param imagePath: 图像路径
:return:
'''
img = cv2.imread(imagePath)
img = img.transpose(2, 0, 1).astype(np.uint32)
avgB = np.average(img[0])
avgG = np.average(img[1])
avgR = np.average(img[2])
avg = (avgB + avgG + avgR) / 3
img[0] = np.minimum(img[0] * (avg / avgB), 255)
img[1] = np.minimum(img[1] * (avg / avgG), 255)
img[2] = np.minimum(img[2] * (avg / avgR), 255)
image = img.transpose(1, 2, 0).astype(np.uint8)
return image
def histEqualize(imagePath):
'''
光照补偿:直方图均衡化(把原始图的直方图变换为均匀分布的形式,增强图像整体对比度)
:param imagePath: 图像路径
:return:
'''
img = cv2.imread(imagePath)
ycrcb = cv2.cvtColor(img, cv2.COLOR_BGR2YCR_CB)
channels = cv2.split(ycrcb)
cv2.equalizeHist(channels[0], channels[0]) # equalizeHist(in,out)
cv2.merge(channels, ycrcb)
img_eh = cv2.cvtColor(ycrcb, cv2.COLOR_YCR_CB2BGR)
return img_eh
def videoToFrame(videoPath, imgPath, num=5):
'''
将单个视频文件处理成帧
:param videoPath: 视频所在路径
:param imgPath: 处理后的图像保存路径
:param num: 间隔num帧数存储一张,默认5帧保留一张
:return:
'''
camera = cv2.VideoCapture(videoPath)
i = 0
while camera.isOpened():
i += 1
timer = cv2.getTickCount()
(ok, frame) = camera.read()
fps = cv2.getTickFrequency() / (cv2.getTickCount() - timer)
if ok:
if i % num == 1:
cv2.imwrite(imgPath + str(int(i / num)) + ".jpg", frame)
cv2.putText(frame, "FPS : " + str(int(fps)), (30, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.75,
(50, 170, 50), 2)
cv2.imshow("Frame", frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
else:
camera.release()
cv2.destroyAllWindows()
def videosToFrame(videosPath, imgPath, num=5):
'''
将目标路径下的(多个)视频全部处理成帧
:param videosPath: 视频所在路径
:param imgPath: 处理后的图像保存路径
:param num: 间隔num帧数存储一张,默认5帧保留一张
:return:
'''
for filename in os.listdir(videosPath):
print('开始处理:' + filename)
name = filename[0:3].lower()
name = filename[:-4]
camera = name + '_'
print(camera)
camera = cv2.VideoCapture(videosPath + filename)
i = 0
while camera.isOpened():
i += 1
timer = cv2.getTickCount()
(ok, frame) = camera.read()
fps = cv2.getTickFrequency() / (cv2.getTickCount() - timer)
if ok:
if i % num == 1:
cv2.imwrite(imgPath + name + '_' + str(int(i / num)) + ".jpg", frame)
cv2.putText(frame, "FPS : " + str(int(fps)), (30, 30), cv2.FONT_HERSHEY_SIMPLEX,
0.75, (50, 170, 50), 2)
cv2.imshow("Frame", frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
else:
camera.release()
cv2.destroyAllWindows()
def videoCut(videoPath):
'''
视频中间部分截取,按空格键(SPACE)开始保存,按q键结束保存
:param videoPath: 视频所在路径
:return:
'''
video = cv2.VideoCapture(videoPath)
imageSize = (int(video.get(3)), int(video.get(4))) # 视频尺寸
VIDEO_FPS = int(video.get(cv2.CAP_PROP_FPS)) # 视频帧率
print(imageSize)
print(video.get(cv2.CAP_PROP_FPS))
fourcc = cv2.VideoWriter_fourcc(*'XVID')
videoWriter = cv2.VideoWriter('video.avi', fourcc, VIDEO_FPS, imageSize)
flag_save = False
while video.isOpened():
(ok, frame) = video.read()
cv2.imshow("Frame", frame)
# 按空格键(SPACE)开始保存
if cv2.waitKey(1) & 0xFF == ord(' '):
flag_save = True
print("Already started saving...")
if flag_save is True:
videoWriter.write(frame)
# 按q键结束保存
if cv2.waitKey(1) & 0xFF == ord('q'):
print("Video saved successfully, has exited!")
break
video.release()
cv2.destroyAllWindows()
def videoCut2(videoPath, startSecond, endSecond):
'''
视频中间部分截取
:param videoPath: 视频所在路径
:param startSecond: 开始时间,从视频的 startSecond 秒处开始保存
:param endSecond: 结束时间,保存至 endSecond 秒结束,或者按键盘q键结束
:return:
'''
video = cv2.VideoCapture(videoPath)
imageSize = (int(video.get(3)), int(video.get(4))) # 视频尺寸
VIDEO_FPS = int(video.get(cv2.CAP_PROP_FPS)) # 视频帧率
print(imageSize)
print(video.get(cv2.CAP_PROP_FPS))
fourcc = cv2.VideoWriter_fourcc(*'XVID')
videoWriter = cv2.VideoWriter('video.avi', fourcc, VIDEO_FPS, imageSize)
i = 0
while video.isOpened():
(ok, frame) = video.read()
i += 1
# 从视频的 startSecond 秒处开始保存
if i < startSecond * VIDEO_FPS:
continue
elif i > startSecond * VIDEO_FPS:
cv2.imshow("Frame", frame)
videoWriter.write(frame)
# 保存至 endSecond 秒结束
if i > endSecond * VIDEO_FPS:
break
if cv2.waitKey(1) & 0xFF == ord('q'):
break
video.release()
cv2.destroyAllWindows()
def videoJoint(videoPath1, videoPath2, savePath, resizeVideo=0, axis=0):
'''
视频(水平/垂直)连接
:param videoPath1: 视频1的路径
:param videoPath2: 视频1的路径
:param savePath: 视频保存路径,包括保存的视频名称,文件名以 .avi 结尾
:param resizeVideo: 视频缩放比例(以视频1的像素为参照),0表示不缩放(默认值),正数表示放大,负数表示缩小,缩放倍数与resizeVideo的取值成正比
:param axis: 0表示垂直连接(默认值),1表示水平连接
:return:
'''
cam1 = cv2.VideoCapture(videoPath1)
cam2 = cv2.VideoCapture(videoPath2)
if resizeVideo == 0:
ww = int(cam1.get(3))
hh = int(cam1.get(4))
elif resizeVideo > 0:
ww = int(cam1.get(3) * (resizeVideo + 1))
hh = int(cam1.get(4) * (resizeVideo + 1))
elif resizeVideo < 0:
ww = int(cam1.get(3) // (1 - resizeVideo))
hh = int(cam1.get(4) // (1 - resizeVideo))
CAMERA_FPS = cam1.get(cv2.CAP_PROP_FPS)
fourcc = cv2.VideoWriter_fourcc(*'XVID')
if axis == 1:
videoWriter = cv2.VideoWriter(savePath, fourcc, CAMERA_FPS, (ww * 2, hh))
else:
videoWriter = cv2.VideoWriter(savePath, fourcc, CAMERA_FPS, (ww, hh * 2))
while True:
(ok1, frame1) = cam1.read()
(ok2, frame2) = cam2.read()
if ok1 and ok2:
frame1 = cv2.resize(frame1, (ww, hh))
frame2 = cv2.resize(frame2, (ww, hh))
# frame1 = addChineseToImage(frame1, "视频1", (5, 5))
# cv2.putText(frame1, "Video one", (50, 50),cv2.FONT_HERSHEY_SIMPLEX, 2.0, (0, 255, 0), 2)
image = np.concatenate([frame1, frame2], axis=axis) # axis=0时为垂直拼接;axis=1时为水平拼接
cv2.imshow("camera1", image)
videoWriter.write(image)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
else:
break
videoWriter.release()
cam1.release()
cam2.release()
cv2.destroyAllWindows()
def detectTarget_BS(frame, minArea=1000, backgroundSubtractor=None, structuringElement=None):
'''
利用背景减法获取矩形框,用于视频中动态目标的位置检测
:param frame: 待检测的视频帧
:param areaThreshold: 检测矩形框的最小面积阀值,默认1000(单位:像素)
:param backgroundSubtractor: 背景减除法算法对象
:param structuringElement: 用于形态操作的指定大小和形状的结构元素
:return:
'''
if backgroundSubtractor is None:
backgroundSubtractor = cv2.createBackgroundSubtractorKNN(detectShadows=True)
if structuringElement is None:
structuringElement = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
boxes = []
gray_L = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
fgmask = backgroundSubtractor.apply(gray_L) # 背景分割器,该函数计算了前景掩码
# 二值化阈值处理,前景掩码含有前景的白色值以及阴影的灰色值
# 在阈值化图像中,将像素差大于某一阈值(这里定为244)的标注为运动点,赋值为255(白色),其余点赋值为0(黑色)
th = cv2.threshold(fgmask.copy(), 244, 255, cv2.THRESH_BINARY)[1]
dilated = cv2.dilate(th, structuringElement, iterations=2) # 形态学膨胀
# 计算一幅图像中目标的轮廓
try:
img, contours, hierarchy = cv2.findContours(image=dilated, mode=cv2.RETR_EXTERNAL,
method=cv2.CHAIN_APPROX_SIMPLE)
except ValueError:
# print('ValueError: not enough values to unpack (expected 3, got 2), caught!')
contours, hierarchy = cv2.findContours(image=dilated, mode=cv2.RETR_EXTERNAL,
method=cv2.CHAIN_APPROX_SIMPLE)
for cnt in contours:
if cv2.contourArea(cnt) > minArea:
(x, y, w, h) = cv2.boundingRect(cnt) # 外接矩形
boxes.append((x, y, w, h))
return frame, boxes
def detectTarget_TD(imagePath1, imagePath2, minArea=500, thresh=40):
'''
利用帧间差分法获取矩形框,用于视频中动态目标的位置检测
:param imagePath1: 图像1的路径
:param imagePath2: 图像2的路径
:param minArea: 运动目标的最小面积阈值
:param thresh: 二值化处理的阈值,像素差大于该阀值的标注为运动点
:return:
'''
boxes = []
previousframe = cv2.imread(imagePath1)
currentframe = cv2.imread(imagePath2)
image_joint = np.concatenate([previousframe, currentframe], axis=1)
previousGray = cv2.cvtColor(previousframe, cv2.COLOR_BGR2GRAY)
currentGray = cv2.cvtColor(currentframe, cv2.COLOR_BGR2GRAY)
resultframe = cv2.absdiff(currentGray, previousGray)
resultframe = cv2.medianBlur(resultframe, 3)
ret, threshold_frame = cv2.threshold(resultframe, thresh, 255, cv2.THRESH_BINARY)
thresh, contours, hierarchy = cv2.findContours(threshold_frame, cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
print(len(contours))
for cnt in contours:
if cv2.contourArea(cnt) > minArea:
(x, y, w, h) = cv2.boundingRect(cnt) # 外接矩形
boxes.append((x, y, w, h))
cv2.rectangle(currentframe, (x, y), (x + w, y + h), (255, 0, 0), 3)
return image_joint, resultframe, currentframe, boxes
def drawContour_Sobel(imagePath, ksize=5, sigmaX=1.5):
'''
用Sobel算子进行图像的轮廓提取
:param imagePath: 图像路径
:param ksize: 高斯波滤中高斯核的大小,必须为正奇数或0
:param sigmaX: 高斯波滤中X方向上的高斯核标准差
:return:
'''
image = cv2.imread(imagePath)
frame_blurred = cv2.GaussianBlur(image, (ksize, ksize), sigmaX)
sobelx = cv2.Sobel(frame_blurred, cv2.CV_64F, 1, 0)
sobely = cv2.Sobel(frame_blurred, cv2.CV_64F, 0, 1)
sobelx = np.uint8(np.absolute(sobelx))
sobely = np.uint8(np.absolute(sobely))
sobelcombine = cv2.bitwise_or(sobelx, sobely)
cv2.imwrite('drawContour_Sobel.jpg', sobelcombine)
# return sobelcombine
def drawContour_Canny(imagePath, threshold1=50, threshold2=180):
'''
用Canny算子进行图像的轮廓提取
:param imagePath: 图像路径
:param threshold1: Canny算法中的低阀值(迟滞过程的第一个阈值),取值范围:0~100
:param threshold2: Canny算法中的高阀值(迟滞过程的第二个阈值),取值大于100
:return:
'''
image = cv2.imread(imagePath)
image = cv2.GaussianBlur(image, (5, 5), 1.5)
canny = cv2.Canny(image, threshold1=threshold1, threshold2=threshold2)
# 形态学:边缘检测
_, Thr_img = cv2.threshold(canny, 210, 255, cv2.THRESH_BINARY) # 设定红色通道阈值210(阈值影响梯度运算效果)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) # 定义矩形结构元素
gradient = cv2.morphologyEx(Thr_img, cv2.MORPH_GRADIENT, kernel) # 梯度
cv2.imwrite('drawContour_Canny.jpg', gradient)
# return gradient
def imageToVideo(imageDir, Frequency=3, imageWidth=640, imageHeight=480):
'''
目录下的图片转成视频文件
:param imageDir: 图片所在目录
:param Frequency: 写入频率(每秒多少帧,此处默认3)
:param imageWidth: 图像指定宽度,默认640
:param imageHeight: 图像指定高度,默认480
:return:
'''
if os.path.exists(imageDir) is False:
print('The path given does not exist, please check!')
return
imageList = os.listdir(imageDir) # 获取该目录下的所有文件名
if len(imageList) < 2:
print('Too few pictures, exit save!')
return
video_name = "imageToVideo.avi" # 导出路径
fourcc = cv2.VideoWriter_fourcc('I', '4', '2', '0')
vw = cv2.VideoWriter(video_name, fourcc, Frequency, (imageWidth, imageHeight))
imgNum = 0
for imageName in imageList:
imageFullPath = imageDir + '/' + imageName
if os.path.isdir(imageFullPath):
continue
print(imageFullPath)
imgNum += 1
img = cv2.imread(imageFullPath)
img = cv2.resize(img, (imageWidth, imageHeight))
cv2.imshow('', img)
vw.write(img) # 把图片写进视频
time.sleep(0.3)
print('Video write successful! There are {0} pictures!'.format(imgNum))
vw.release() # 释放
if __name__ == '__main__':
pass
其他功能待完善!