大家好,欢迎来到IT知识分享网。
计算机视觉和图像处理
- Tensorflow入门
- 深度神经网络
- 图像分类
- 目标检测
- 图像分割
- OpenCV
- Pytorch
- NLP自然语言处理
OpenCV
一、OpenCV简介
1.1 简介
OpenCV是⼀个计算机视觉处理开源软件库,⽀持与计算机视觉和机器学习相关的众多算法。
1.2 OpenCV部署
- 创建虚拟环境
在Anaconda终端中创建虚拟环境OpenCV_env
conda create --name OpenCV_env
- 激活虚拟环境
conda activate OpenCV_env
- 安装OpenCV
安装OpenCV之前需要先安装numpy, matplotlib
pip install opencv-python -i https://pypi.tuna.tsinghua.edu.cn/simple
如果我们要利用SIFT和SURF算法进行特征提取时,还需安装:
pip install opencv-contrib-python -i https://pypi.tuna.tsinghua.edu.cn/simple
- 查看是否安装成功
import cv2 cv2.__version__
1.3 OpenCV模块
core模块实现了最核心的数据结构及其基本运算,如绘图函数、数组操作相关函数等。highgui模块实现了视频与图像的读取、显示、存储等接口。imgproc模块实现了图像处理的基础方法,包括图像滤波、图像的几何变换、平滑、阈值分割、形态学处理、边缘检测、目标检测、运动分析和对象跟踪等。features2d模块用于提取图像特征以及特征匹配,nonfree模块实现了一些专利算法,如sift特征。objdetect模块实现了一些目标检测的功能,经典的基于Haar、LBP特征的人脸检测,基于HOG的行人、汽车等目标检测,分类器使用CascadeClassification(级联分类)和Latent SVM等。
二、OpenCV基本操作
2.1 图像的基本操作
2.1.1 图像的IO操作
# 读取图像 import numpy as np import cv2 as cv import matplotlib.pyplot as plt #以灰度图的方式读取图像 img = cv.imread("dog.jpg",0)
# 显示图像 # Jupyter Notebook 是一个基于 Web 的交互式计算环境,不支持传统的 GUI 窗口显示 # cv.imshow('image',img) # cv.waitKey(0) plt.imshow(img,cmap='gray')
# 以彩色图的方式读取图像 img1 = cv.imread("dog.jpg",1) # img1[:,:,::-1]将BGR图像转换为RGB图像 plt.imshow(img1[:,:,::-1])
img.shape
# 图像保存 cv.imwrite('dog1.jpg',img)
2.1.2 绘制几何图像
# 创建图像 img = np.zeros((512,512,3),np.uint8)
# 直线的起点、终点、颜色、宽度 cv.line(img,(0,0),(512,512),(255,0,0),5) # 圆形的圆心、半径、颜色、填充、宽度 cv.circle(img,(250,250),130,(0,255,0),-1,5) # 矩形的左上角、右下角、颜色、宽度 cv.rectangle(img,(50,50),(450,450),(0,0,255),5) # 图像中添加文字 文本、位置、字体、字体大小、颜色、宽度 cv.putText(img,"OpenCV",(130,250),cv.FONT_HERSHEY_COMPLEX,2,(255,0,255),2) plt.imshow(img[:,:,::-1]) plt.show()
2.1.3 获取并修改图像的像素点
img2 = np.zeros((250,250,3),np.uint8) plt.imshow(img2[:,:,::-1])
# 获取位置(50,200)的像素点 img2[50,200]
#获取位置(50,100)蓝色通道的强度值,0表示蓝色,1表示绿色,2表示红色 img2[50,100,0]
# 修改位置(50,100)的像素值 img2[50,100] = [255,0,0]
plt.imshow(img2[:,:,::-1])
2.1.4 获取图像的属性
img2.shape
img2.size
img2.dtype
2.1.5 图像通道的拆分和合并
# 通道拆分 b,g,r = cv.split(img1)
plt.imshow(b,cmap='gray')
# 通道合并 img3 = cv.merge((b,g,r))
plt.imshow(img3[:,:,::-1])
2.1.6 色彩空间的改变
# 将BGR通道图像转变为HSV通道图像 img3 = cv.cvtColor(img1,cv.COLOR_BGR2HSV)
plt.imshow(img3[:,:,::-1])
# 将BGR通道图像变为GRAY通道图像 img4 = cv.cvtColor(img1,cv.COLOR_BGR2GRAY)
plt.imshow(img4,cmap='gray')
2.2 算数操作
2.2.1 图像的加法
# 读取图像 img_sun = cv.imread('sun.jpg') img_tree = cv.imread('tree.jpg')
plt.imshow(img_tree[:,:,::-1])
plt.imshow(img_sun[:,:,::-1])
# 查看图像形状 img_sun.shape,img_tree.shape
形状相同才能进行相加
# 缩小图形形状 img_sun = cv.resize(img_sun,(350,251))
img_sun.shape
# 加法操作 img_cvadd = cv.add(img_sun,img_tree) img_add = img_sun+img_tree
fig,axes = plt.subplots(1,2,figsize=(10,8)) axes[0].imshow(img_cvadd[:,:,::-1]) axes[0].set_title("cv中的加法") axes[1].imshow(img_add[:,:,::-1]) axes[1].set_title("直接相加") plt.show()
2.2.2 图像的混合
# 读取图像 img_sun = cv.imread('sun.jpg') img_tree = cv.imread('tree.jpg') # 缩小图形形状 img_sun = cv.resize(img_sun,(350,251)) # 图像混合,gamma参数会影响最终图像的亮度 img5 = cv.addWeighted(img_sun,0.3,img_tree,0.7,0) # 图像的显示 plt.imshow(img5[:,:,::-1]) plt.show()
三、OpenCV图像处理
3.1 图像的几何变换
import cv2 as cv import matplotlib.pyplot as plt
- 图像缩放
# 读取图片 img = cv.imread('dog.jpg') # 图像缩放 # 绝对尺寸 rows,cols = img.shape[:2] res = cv.resize(img,(2*cols,2*rows)) # 相对尺寸 res1 = cv.resize(img,None,fx=0.5,fy=0.5) # 图像显示 fig,axes = plt.subplots(1,3,figsize=(10,8)) axes[0].imshow(img[:,:,::-1]) axes[0].set_title("原始图像") axes[1].imshow(res[:,:,::-1]) axes[1].set_title("绝对尺寸") axes[2].imshow(res1[:,:,::-1]) axes[2].set_title("相对尺寸") plt.show()
- 图像平移
import numpy as np img = cv.imread('dog.jpg') # 像素点平移(50,100) rows,cols = img.shape[:2] # 平移矩阵 m = np.float32([[1,0,50],[0,1,100]]) res = cv.warpAffine(img,m,(cols,rows)) fig,axes = plt.subplots(1,2,figsize=(10,8)) axes[0].imshow(img[:,:,::-1]) axes[0].set_title("原始图像") axes[1].imshow(res[:,:,::-1]) axes[1].set_title("平移后图像") plt.show()
- 图像旋转
img = cv.imread("dog.jpg") rows,cols = img.shape[:2] # 旋转矩阵 m = cv.getRotationMatrix2D((cols//2,rows//2),90,0.5) res = cv.warpAffine(img,m,(cols,rows)) fig,axes = plt.subplots(1,2,figsize=(10,8)) axes[0].imshow(img[:,:,::-1]) axes[0].set_title("原始图像") axes[1].imshow(res[:,:,::-1]) axes[1].set_title("旋转后图像") plt.show()
- 仿射变换
rows,cols = img.shape[:2] # 原点集 pts1 = np.float32([[50,50],[200,50],[50,200]]) # 目标点集 pts2 = np.float32([[100,100],[200,50],[100,250]]) # 仿射变化矩阵 m = cv.getAffineTransform(pts1,pts2) res = cv.warpAffine(img,m,(cols,rows)) fig,axes = plt.subplots(1,2,figsize=(10,8)) axes[0].imshow(img[:,:,::-1]) axes[0].set_title("原始图像") axes[1].imshow(res[:,:,::-1]) axes[1].set_title("仿射后图像") plt.show()
- 投射变换
img = cv.imread("dog.jpg") rows,cols = img.shape[:2] # 投射变换矩阵 pts1 = np.float32([[56,65],[368,52],[28,387],[389,390]]) pts2 = np.float32([[100,145],[300,100],[80,290],[310,300]]) m = cv.getPerspectiveTransform(pts1,pts2) # 进行变换 res = cv.warpPerspective(img,m,(cols,rows)) fig,axes = plt.subplots(1,2,figsize=(10,8)) axes[0].imshow(img[:,:,::-1]) axes[0].set_title("原图像") axes[1].imshow(res[:,:,::-1]) axes[1].set_title("仿射后图像") plt.show()
- 图像金字塔
img = cv.imread("dog.jpg") # 上采样 img_up = cv.pyrUp(img) # 下采样 img_down = cv.pyrDown(img) fig,axes = plt.subplots(1,3,figsize=(10,8)) axes[0].imshow(img[:,:,::-1]) axes[0].set_title("原图像") axes[1].imshow(img_up[:,:,::-1]) axes[1].set_title("上采样图像") axes[2].imshow(img_down[:,:,::-1]) axes[2].set_title("下采样图像") plt.show()
3.2 图像的形态学操作
- 膨胀和腐蚀
img = cv.imread("img_five.jpg") # 创建核结构 kernel = np.ones((5,5),np.uint8) # 腐蚀 erode = cv.erode(img,kernel) # 膨胀 dilate = cv.dilate(img,kernel) fig,axes = plt.subplots(1,3,figsize=(10,8)) axes[0].imshow(img[:,:,::-1]) axes[0].set_title("原图像") axes[1].imshow(erode[:,:,::-1]) axes[1].set_title("腐蚀后图像") axes[2].imshow(dilate[:,:,::-1]) axes[2].set_title("膨胀后图像") plt.show()
- 开闭运算
img_1 = cv.imread('img1.png') img_2 = cv.imread('img2.png') kernel = np.ones((10,10),np.uint8) Open = cv.morphologyEx(img_1,cv.MORPH_OPEN,kernel) Close = cv.morphologyEx(img_2,cv.MORPH_CLOSE,kernel) fig,axes = plt.subplots(2,2,figsize=(10,8)) axes[0][0].imshow(img_1[:,:,::-1]) axes[0][0].set_title("原图像") axes[0,1].imshow(Open[:,:,::-1]) axes[0,1].set_title("开运算图像") axes[1,0].imshow(img_2[:,:,::-1]) axes[1,0].set_title("原图像") axes[1,1].imshow(Close[:,:,::-1]) axes[1,1].set_title("闭运算图像") plt.show()
- 礼貌和黑帽
img_1 = cv.imread("img1.png") img_2 = cv.imread("img2.png") kernel = np.ones((10,10),np.uint8) Open = cv.morphologyEx(img_1,cv.MORPH_TOPHAT,kernel) Close = cv.morphologyEx(img_2,cv.MORPH_BLACKHAT,kernel) fig,axes = plt.subplots(2,2,figsize=(10,8)) axes[0][0].imshow(img_1[:,:,::-1]) axes[0][0].set_title("原图像") axes[0,1].imshow(Open[:,:,::-1]) axes[0,1].set_title("礼帽运算结果") axes[1,0].imshow(img_2[:,:,::-1]) axes[1,0].set_title("原图像") axes[1,1].imshow(Close[:,:,::-1]) axes[1,1].set_title("黑帽运算结果") plt.show()
3.3 图像的平滑
img_girl= cv.imread("girl_img.png") # 均值滤波 blur = cv.blur(img_girl,(7,7)) # 高斯滤波 gaublur = cv.GaussianBlur(img_girl,(9,9),0) # 中值滤波 medblur = cv.medianBlur(img_girl,5) fig,axes = plt.subplots(2,2,figsize=(10,8)) axes[0][0].imshow(img_girl[:,:,::-1]) axes[0][0].set_title("原图像") axes[0,1].imshow(blur[:,:,::-1]) axes[0,1].set_title("均值滤波运算结果") axes[1,0].imshow(gaublur[:,:,::-1]) axes[1,0].set_title("高斯滤波结果") axes[1,1].imshow(medblur[:,:,::-1]) axes[1,1].set_title("中值滤波结果") plt.show()
3.4 直方图
3.1.4 灰度直方图
- 直方图的计算和绘制
img_dog = cv.imread("dog.jpg",0) # 统计灰度图 histr = cv.calcHist(img_dog,[0],None,[256],[0,256]) fig,axes = plt.subplots(1,2,figsize=(18,6)) axes[0].imshow(img_dog,cmap='gray') axes[1].plot(histr) axes[1].grid() axes[1].set_xlabel('Pixel Value') axes[1].set_ylabel('Frequency') # 调整布局 plt.tight_layout() plt.show()
- 掩码的应用
img_dog1 = cv.imread("dog.jpg",0) # 创建遮挡 mask = np.zeros(img_dog1.shape[:2],np.uint8) mask[50:130,150:230] = 255 # 进行按位与运算 mask_img = cv.bitwise_and(img_dog1,img_dog1,mask=mask) mask_histr = cv.calcHist([img_dog1],[0],mask,[256],[0,256]) fig,axes = plt.subplots(2,2,figsize=(10,8)) axes[0,0].imshow(img_dog1,cmap='gray') axes[0,0].set_title("原图") axes[0,1].imshow(mask,cmap='gray') axes[0,1].set_title("遮挡") axes[1,0].imshow(mask_img,cmap="gray") axes[1,0].set_title("遮挡后数据") axes[1,1].plot(mask_histr) axes[1,1].set_title("灰度直方图") plt.show()
3.1.5 直方图均衡化
- 应用
img_dog = cv.imread("dog.jpg",0) dst = cv.equalizeHist(img_dog) fig,axes = plt.subplots(1,2,figsize=(18,6)) axes[0].imshow(img_dog,cmap='gray') axes[0].set_title("原图") axes[1].imshow(dst,cmap='gray') axes[1].set_title("均衡化后的结果") # plt.tight_layout() plt.show()
- 自适应的直方图均衡化
img_dog = cv.imread("dog.jpg",0) clahe = cv.createCLAHE(clipLimit=2,tileGridSize=(12,12)) cl1 = clahe.apply(img_dog) fig,axes = plt.subplots(1,2,figsize=(10,8)) axes[0].imshow(img_dog,cmap="gray") axes[0].set_title("原图") axes[1].imshow(cl1,cmap="gray") axes[1].set_title("自适应后图像") plt.show()
3.5 边缘检测
3.5.1 Sobel检测算子
import cv2 as cv import matplotlib.pyplot as plt import numpy as np
img_horse = cv.imread("horse.jpg",0) # 计算Sobel卷积结果(边缘检测) x = cv.Sobel(img_horse,cv.CV_16S,1,0) y = cv.Sobel(img_horse,cv.CV_16S,0,1) # 数据转换(将其缩放到uint8) scale_x = cv.convertScaleAbs(x) scale_y = cv.convertScaleAbs(y) # 结果合成 gamma为正亮度增加,0亮度不变,负数亮度降低 res = cv.addWeighted(scale_x,0.5,scale_y,0.5,0) fig,axes = plt.subplots(1,2,figsize=(9,10)) axes[0].imshow(img_horse,cmap='gray') axes[0].set_title("原图") axes[1].imshow(res,cmap=plt.cm.gray) axes[1].set_title("Sobel滤波后结果") plt.show()
# ksize为-1时,使用3x3的Scharr滤波器 x = cv.Sobel(img_horse,cv.CV_16S,1,0,ksize=-1) y = cv.Sobel(img_horse,cv.CV_16S,0,1,ksize=-1) scale_x = cv.convertScaleAbs(x) scale_y = cv.convertScaleAbs(y) res = cv.addWeighted(scale_x,0.5,scale_y,0.5,0) fig,axes = plt.subplots(1,2,figsize=(9,10)) axes[0].imshow(img_horse,cmap='gray') axes[0].set_title("原图") axes[1].imshow(res,cmap=plt.cm.gray) axes[1].set_title("Scharr滤波后结果") plt.show()
3.5.2 Laplacian算子
img_horse = cv.imread("horse.jpg",0) res = cv.Laplacian(img_horse,cv.CV_16S,ksize=3) scale_res = cv.convertScaleAbs(res) fig,axes = plt.subplots(1,2,figsize=(9,10)) axes[0].imshow(img_horse,cmap='gray') axes[0].set_title("原图") axes[1].imshow(scale_res,cmap=plt.cm.gray) axes[1].set_title("Laplacian滤波后结果") plt.show()
3.5.3 Canny边缘检测
img_horse = cv.imread("horse.jpg",0) # Canny边缘检测,min_threshold最小阈值 min_threshold = 20 max_threshold = 100 res = cv.Canny(img_horse,min_threshold,max_threshold) fig,axes = plt.subplots(1,2,figsize=(9,10)) axes[0].imshow(img_horse,cmap='gray') axes[0].set_title("原图") axes[1].imshow(res,cmap=plt.cm.gray) axes[1].set_title("Canny滤波后结果") plt.show()
3.6 模板匹配和霍夫变换的应用
3.6.1 模板匹配
img_party = cv.imread("party.jpg") template = cv.imread("template.jpg") h,w = template.shape[:2] # 模板匹配 res = cv.matchTemplate(img_party,template,cv.TM_SQDIFF) # 返回图像最佳匹配位置,确定左上角的坐标 min_val,max_val,min_loc,max_loc = cv.minMaxLoc(res) # 使用平方差时(cv.TM_SQDIFF)最小值为最佳匹配位置 top_left = min_loc bottom_right = (top_left[0]+h,top_left[1]+w) cv.rectangle(img_party,top_left,bottom_right,(0,255,0),2) fig,axes = plt.subplots(1,2,figsize=(10,8),gridspec_kw={
'width_ratios': [1, 6]}) axes[0].imshow(template[:,:,::-1]) axes[0].set_title("匹配模板") axes[1].imshow(img_party[:,:,::-1]) axes[1].set_title("匹配结果") plt.tight_layout() plt.show()
3.6.2 霍夫变换
# 读取图像 img_rili = cv.imread("rili.jpg") # 使用Canny转换为二值图 gray = cv.cvtColor(img_rili,cv.COLOR_BGR2GRAY) edges = cv.Canny(gray,100,200) # 霍夫直线变换 lines = cv.HoughLines(edges,0.8,np.pi/180,150) # 检查是否检测到直线 for line in lines: rho, theta = line[0] a = np.cos(theta) b = np.sin(theta) x0 = a * rho y0 = b * rho x1 = int(x0 - 1000 * b) y1 = int(y0 + 1000 * a) x2 = int(x0 + 1000 * b) y2 = int(y0 - 1000 * a) cv.line(img_rili, (x1, y1), (x2, y2), (0, 0, 255), 2) # 使用matplotlib显示图像 plt.imshow(img_rili[:, :, ::-1]) plt.xticks([]), plt.yticks([]) plt.title("霍夫变换直线检测") plt.show()
3.7 图像变化
3.7.1 傅里叶变换
读取图像: 读取图像并转换为灰度图像。 傅里叶变换: 正变换:将图像转换为频域表示。 频谱中心化:将频谱的低频部分移到中心,高频部分移到四周。 计算频谱和相位谱:将复数形式的频谱转换为幅度和相位,并对幅度谱进行对数变换。 傅里叶逆变换: 反变换:将频域表示转换回时域(或空域)。 计算灰度值:计算逆变换后的图像的幅度。 显示结果: 使用 matplotlib 显示原始图像、频谱图和逆变换后的图像。
img_dog= cv.imread("dog.jpg",0) # 傅里叶正变换,cv.DFT_COMPLEX_OUTPUT:指定输出为复数形式 dft = cv.dft(np.float32(img_dog),flags=cv.DFT_COMPLEX_OUTPUT) # 频谱中心化 dft_shift = np.fft.fftshift(dft) # 计算频谱和相位谱 mag,angle = cv.cartToPolar(dft_shift[:,:,0],dft_shift[:,:,-1],angleInDegrees=True) # 对幅度谱进行对数变换,以便更好地可视化。对数变换可以压缩动态范围,使图像的细节更明显。 mag = 20 * np.log(mag) # 傅里叶反变换 img_back = cv.idft(dft) img_back = cv.magnitude(img_back[:,:,0],img_back[:,:,1]) fig,axes = plt.subplots(2,2,figsize=(10,8)) axes[0,0].imshow(img_dog,cmap='gray') axes[0,0].set_title("原图") axes[0,1].imshow(mag,cmap='gray') axes[0,1].set_title("频谱") axes[1,0].imshow(angle,cmap='gray') axes[1,0].set_title("相位谱") axes[1,1].imshow(img_back,cmap='gray') axes[1,1].set_title("逆变换结果") plt.show()
3.7.2 高通和低通滤波
# 高通滤波 dog_img = cv.imread("dog.jpg",0) rows,cols = dog_img.shape mask = np.ones((rows,cols,2),np.uint8) mask[int(rows/2)-30:int(rows/2)+30,int(cols/2)-30:int(cols/2)+30,:] = 0 # 正变换 dft = cv.dft(np.float32(dog_img),flags=cv.DFT_COMPLEX_OUTPUT) # 频谱中心化 dft_shift = np.fft.fftshift(dft) # 滤波(移除低频) dft_shift = dft_shift * mask # 频谱中心化 dft_shift = np.fft.fftshift(dft_shift) # 反变化 img_back = cv.idft(dft_shift) # 计算灰度值 img_back = cv.magnitude(img_back[:,:,0],img_back[:,:,1]) fig,axes = plt.subplots(1,2,figsize=(10,8)) axes[0].imshow(dog_img, cmap = 'gray') axes[0].set_title('原图') axes[1].imshow(img_back, cmap = 'gray') axes[1].set_title('高通滤波结果') plt.show()
# 低通滤波 dog_img = cv.imread("dog.jpg",0) rows,cols = dog_img.shape mask = np.zeros((rows,cols,2),np.uint8) mask[int(rows/2)-30:int(rows/2)+30,int(cols/2)-30:int(cols/2)+30,:] = 1 # 正变换 dft = cv.dft(np.float32(dog_img),flags=cv.DFT_COMPLEX_OUTPUT) # 频谱中心化 dft_shift = np.fft.fftshift(dft) # 滤波(移除低频) dft_shift = dft_shift * mask # 频谱中心化 dft_shift = np.fft.fftshift(dft_shift) # 反变化 img_back = cv.idft(dft_shift) # 计算灰度值 img_back = cv.magnitude(img_back[:,:,0],img_back[:,:,1]) fig,axes = plt.subplots(1,2,figsize=(10,8)) axes[0].imshow(dog_img, cmap = 'gray') axes[0].set_title('原图') axes[1].imshow(img_back, cmap = 'gray') axes[1].set_title('低通滤波结果') plt.show()
3.7.3 带通和带阻滤波
# 带通滤波 dog_img = cv.imread("dog.jpg",0) rows,cols = dog_img.shape mask1 = np.ones((rows,cols,2),np.uint8) mask1[int(rows/2)-8:int(rows/2)+8,int(cols/2)-8:int(cols/2)+8] = 0 mask2 = np.zeros((rows,cols,2),np.uint8) mask2[int(rows/2)-80:int(rows/2)+80,int(cols/2)-80:int(cols/2)+80] = 1 mask = mask1*mask2 # 正变换 dft = cv.dft(np.float32(dog_img),flags=cv.DFT_COMPLEX_OUTPUT) # 频谱中心化 dft_shift = np.fft.fftshift(dft) # 滤波(移除低频) dft_shift = dft_shift * mask # 频谱中心化 dft_shift = np.fft.fftshift(dft_shift) # 反变化 img_back = cv.idft(dft_shift) # 计算灰度值 img_back = cv.magnitude(img_back[:,:,0],img_back[:,:,1]) fig,axes = plt.subplots(1,2,figsize=(10,8)) axes[0].imshow(dog_img, cmap = 'gray') axes[0].set_title('原图') axes[1].imshow(img_back, cmap = 'gray') axes[1].set_title('带通滤波结果') plt.show()
# 带阻滤波 dog_img = cv.imread("dog.jpg",0) rows,cols = dog_img.shape mask = np.ones((rows,cols,2),np.uint8) mask[int(rows/2)+60:int(rows/2)+130,int(cols/2)-130:int(cols/2)+130] = 0 mask[int(rows/2)-130:int(rows/2)-60,int(cols/2)-130:int(cols/2)+130] = 0 mask[int(rows/2)-130:int(rows/2)+130,int(cols/2)+60:int(cols/2)+130] = 0 mask[int(rows/2)-130:int(rows/2)+130,int(cols/2)-130:int(cols/2)-60] = 0 # 正变换 dft = cv.dft(np.float32(dog_img),flags=cv.DFT_COMPLEX_OUTPUT) # 频谱中心化 dft_shift = np.fft.fftshift(dft) # 滤波(移除低频) dft_shift = dft_shift * mask # 频谱中心化 dft_shift = np.fft.fftshift(dft_shift) # 反变化 img_back = cv.idft(dft_shift) # 计算灰度值 img_back = cv.magnitude(img_back[:,:,0],img_back[:,:,1]) fig,axes = plt.subplots(1,2,figsize=(10,8)) axes[0].imshow(dog_img, cmap = 'gray') axes[0].set_title('原图') axes[1].imshow(img_back, cmap = 'gray') axes[1].set_title('带阻滤波结果') plt.show()
3.8 轮廓检测与轮廓特征
3.8.1 轮廓检测
import cv2 as cv import matplotlib.pyplot as plt import numpy as np
ditu = cv.imread("ditu.jpg") # 复制原图 img_ditu = ditu.copy() imggray = cv.cvtColor(ditu,cv.COLOR_BGR2GRAY) canny = cv.Canny(imggray,120,255) # 轮廓提取 contours,hierarchy = cv.findContours(canny,cv.RETR_TREE,cv.CHAIN_APPROX_SIMPLE) # 将轮廓绘制在图形上 img = cv.drawContours(ditu,contours,-1,(0,0,255),2) fig,axes = plt.subplots(1,2,figsize=(10,8)) axes[0].imshow(img_ditu[:,:,::-1]) axes[0].set_title("原图") axes[1].imshow(img[:,:,::-1]) axes[1].set_title("轮廓") plt.show()
3.8.2 轮廓特征
- 轮廓面积
area = sum(cv.contourArea(cnt) for cnt in contours) area
- 轮廓周长
perimeter = sum(cv.arcLength(cnt,True) for cnt in contours) perimeter
- 轮廓近似
# 读取图像 img = cv.imread("jinsi.jpg") # 灰度转换 imggray = cv.cvtColor(img, cv.COLOR_BGR2GRAY) # 二值化处理 ret, thresh = cv.threshold(imggray, 127, 255, cv.THRESH_BINARY) # 轮廓提取 contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE) # 初始化绘制图像 img_contours = img.copy() img_approx = img.copy() # 处理所有轮廓 for cnt in contours: # 计算轮廓的弧长 arc_length = cv.arcLength(cnt, True) # 计算近似多边形 epsilon = 0.1 * arc_length approx = cv.approxPolyDP(cnt, epsilon, True) # 绘制原始轮廓和近似多边形(-1表示绘制所有轮廓) img_contours = cv.drawContours(img_contours, [cnt], -1, (0, 255, 0), 2) img_approx = cv.drawContours(img_approx, [approx], -1, (0, 255, 0), 2) # 显示图像 fig, axes = plt.subplots(1, 3, figsize=(10, 13)) axes[0].imshow(ditu[:, :, ::-1]) axes[0].set_title("原图") axes[1].imshow(img_contours[:, :, ::-1]) axes[1].set_title("原始轮廓") axes[2].imshow(img_approx[:, :, ::-1]) axes[2].set_title("近似多边形") plt.show()
- 凸包
star = cv.imread("star.jpg") # 灰度处理 imggray = cv.cvtColor(star,cv.COLOR_BGR2GRAY) # 边缘检测 canny = cv.Canny(imggray,50,200) # 获取轮廓 contours,hierarchy = cv.findContours(canny,cv.RETR_TREE,cv.CHAIN_APPROX_SIMPLE) # 图像复制 img_star = star.copy() #凸包检测 Hulls = [] for cnt in contours: hull = cv.convexHull(cnt) Hulls.append(hull) # 绘制图形 img_contour = cv.drawContours(star,contours,-1,(255,0,0),2) img_hull = cv.drawContours(img_star,Hulls,-1,(255,0,0),2) fig,axes = plt.subplots(1,2,figsize=(10,8)) axes[0].imshow(img_contour[:,:,::-1]) axes[0].set_title("轮廓检测结果") axes[1].imshow(img_hull[:,:,::-1]) axes[1].set_title("凸包结果") plt.show()
- 边界矩形
rect = cv.imread("rect.jpg") # 灰度处理 imggray = cv.cvtColor(rect,cv.COLOR_BGR2GRAY) # 转换为二值化 ret,thresh = cv.threshold(imggray,127,255,0) # 轮廓提取 contours,hierarchy = cv.findContours(thresh,cv.RETR_TREE,cv.CHAIN_APPROX_SIMPLE) # 图像复制 rect_img = rect.copy() # 直边界矩形 bounding_box = [] for cnt in contours: x,y,w,h = cv.boundingRect(cnt) bounding_box.append((x,y,w,h)) for x,y,w,h in bounding_box: cv.rectangle(rect,(x,y),(x+w,y+h),(0,255,0),2) # 旋转边界矩形 min_area_rects = [] for cnt in contours: rect_min = cv.minAreaRect(cnt) box = cv.boxPoints(rect_min) box = np.int0(box) min_area_rects.append(box) for box in min_area_rects: cv.polylines(rect_img,[box],True,(0,255,0),2) fig,axes = plt.subplots(1,2,figsize=(10,8)) axes[0].imshow(rect[:,:,::-1]) axes[0].set_title("直边界矩形") axes[0].set_xticks([]),axes[0].set_yticks([]) axes[1].imshow(rect_img[:,:,::-1]) axes[1].set_title("旋转边界矩形") axes[1].set_xticks([]),axes[1].set_yticks([]) plt.show()
- 最小外接圆
img = cv.imread("rect.jpg") imggray = cv.cvtColor(img,cv.COLOR_BGR2GRAY) ret,thresh = cv.threshold(imggray,127,255,0) contours,hierarchy = cv.findContours(thresh,cv.RETR_TREE,cv.CHAIN_APPROX_SIMPLE) boundings = [] for cnt in contours: (x,y),radius = cv.minEnclosingCircle(cnt) center = (int(x),int(y)) radius = int(radius) boundings.append((center,radius)) for center,radius in boundings: cv.circle(img,center,radius,(0,255,0),2) plt.imshow(img[:,:,::-1]) plt.xticks([]) plt.yticks([]) plt.show()
- 椭圆拟合
img = cv.imread("rect.jpg") imggray = cv.cvtColor(img,cv.COLOR_BGR2GRAY) ret,thresh = cv.threshold(imggray,127,255,0) contours,hierarchy = cv.findContours(thresh,cv.RETR_TREE,cv.CHAIN_APPROX_SIMPLE) boundings = [] for cnt in contours: # 过滤掉点数少于5个和面积过小的轮廓 if len(cnt) >= 5 and cv.contourArea(cnt) > 100: ellipse = cv.fitEllipse(cnt) boundings.append(ellipse) for ellipse in boundings: cv.ellipse(img,ellipse,(0,255,0),2) plt.imshow(img[:,:,::-1]) plt.xticks([]) plt.yticks([]) plt.show()
3.9 图像分割
3.9.1 全阈值分割
img = cv.imread("gradient.jpg",0) ret,thresh1 = cv.threshold(img,127,255,cv.THRESH_BINARY) ret,thresh2 = cv.threshold(img,127,255,cv.THRESH_BINARY_INV) ret,thresh3 = cv.threshold(img,127,255,cv.THRESH_TRUNC) ret,thresh4 = cv.threshold(img,127,255,cv.THRESH_TOZERO) ret,thresh5 = cv.threshold(img,127,255,cv.THRESH_TOZERO_INV) titles = ["原图","阈值二值化","阈值反二值化","截断","阈值取零","阈值反取零"] images = [img,thresh1,thresh2,thresh3,thresh4,thresh5] plt.figure(figsize=(10,8)) for i in range(6): plt.subplot(2,3,i+1) plt.imshow(images[i],cmap='gray') plt.title(titles[i]) plt.show()
3.9.2 自适应阈值分割
fruit = cv.imread("fruit.jpg",0) # 固定阈值 ret,threshold = cv.threshold(fruit,127,225,cv.THRESH_BINARY) # 自适应阈值 # 领域内求平均值 th1 = cv.adaptiveThreshold(fruit,255,cv.ADAPTIVE_THRESH_MEAN_C,cv.THRESH_BINARY,11,4) # 领域内高斯加权 th2 = cv.adaptiveThreshold(fruit,255,cv.ADAPTIVE_THRESH_GAUSSIAN_C,cv.THRESH_BINARY,17,6) title = ["原图","固定阈值","自适应阈值(求均值)","自适应阈值(高斯加权)"] images = [fruit,threshold,th1,th2] plt.figure(figsize=(10,8)) for i in range(4): plt.subplot(2,2,i+1) plt.imshow(images[i],cmap="gray") plt.title(title[i]) plt.show()
3.9.3 Ostu阈值(大律法)
littledog = cv.imread("littledog.jpg",0) # 固定阈值 ret,thresh = cv.threshold(littledog,40,255,cv.THRESH_BINARY) # ostu分割 ret1,thresh2 = cv.threshold(littledog,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU) fig,axes = plt.subplots(1,3,figsize=(10,8)) axes[0].imshow(littledog,cmap="gray") axes[0].set_title("原图") axes[1].imshow(thresh,cmap="gray") axes[1].set_title("全阈值分割") axes[2].imshow(thresh2,cmap="gray") axes[2].set_title("OStu分割") plt.show()
3.9.4 分水岭算法
img = cv.imread("horse.jpg") imggray = cv.cvtColor(img,cv.COLOR_BGR2GRAY) # 边缘检测 canny = cv.Canny(imggray,127,255) # 轮廓检测 contours,hierarchy = cv.findContours(canny,cv.RETR_TREE,cv.CHAIN_APPROX_SIMPLE) # 初始化标记图像 marks = np.zeros(img.shape[:2],np.int32) # 绘制轮廓 for index in range(len(contours)): # index:-1 绘制所有的轮廓,非负数:要绘制的轮廓的索引 marks = cv.drawContours(marks,contours,index,(index,index,index),1,8,hierarchy) # 使用分水岭算法 # img必须是8位无符号整数的三通道图像,marks必须是32位有符号整数的单通道图像 # 返回值:正整数:表示该像素属于某个特定区域,-1表示该像素是区域的边界 marks = cv.watershed(img,marks) # 生成随机颜色 colours = np.zeros((np.max(marks) + 1,3)) for i in range(len(colours)): aa = np.random.uniform(0,255) bb = np.random.uniform(0,255) cc = np.random.uniform(0,255) colours[i] = np.array([aa,bb,cc],np.uint8) # 对每个区域进行颜色填充 bgrimg = np.zeros(img.shape,np.uint8) index = 0 for i in range(marks.shape[0]): for j in range(marks.shape[1]): index = marks[i][j] if index == -1: bgrimg[i][j] = np.array([255,255,255]) else: bgrimg[i][j] = colours[index] plt.imshow(bgrimg[:,:,::-1]) plt.title("图像分割结果") plt.show()
3.9.5 GrabCut算法
img = cv.imread("liying.jpg") masks = np.zeros(img.shape[:2],np.uint8) # 矩形窗口,指定前景区域 rect = [260,50,740,730] # 前景和背景分割 cv.grabCut(img,masks,tuple(rect),None,None,30,cv.GC_INIT_WITH_RECT) # 抠取图像 mask2 = np.where((masks==2)|(masks==0),0,1).astype("uint8") img_show = img * mask2[:,:,np.newaxis] # 将矩形绘制在图像上 cv.rectangle(img,(260,50),(1000,780),(255,0,0),2) fig,axes = plt.subplots(1,2,figsize=(10,8)) axes[0].imshow(img[:,:,::-1]) axes[0].set_title("矩形框选位置") axes[1].imshow(img_show[:,:,::-1]) axes[1].set_title("扣取结果") plt.show()
四、图像的特征提取与描述
- 图像特征要有区分性,容易被比较。一般认为角点、斑点等是比较好的图像特征
- 特征检测:找到图像中的特征
- 特征描述:对特征及其周围的区域描述
4.1 Harris角点检测
Harris角点检测的思想是通过图像的局部的小窗口观察图像,角点的特征是窗口沿任意方向移动都会导致图像灰度明显变化。
import cv2 as cv import matplotlib.pyplot as plt import numpy as np
# 对棋盘进行角点检测 qipan = cv.imread("qipan.jpg") gray = cv.cvtColor(qipan,cv.COLOR_BGR2GRAY) gray = np.float32(gray) # Harris角点检测 # 2,blockSize计算每个像素的角点响应时考虑的窗口大小 # 3,Sobel 算子用于计算图像的梯度,进而用于计算角点响应。 # 0.04,用于平衡角点检测的灵敏度 dst = cv.cornerHarris(gray,2,3,0.04) # 设置阈值,选择 dst 中角点响应值大于阈值的像素位置 qipan[dst > 0.001*dst.max()] = [0,255,0] plt.figure(figsize=(10,8)) plt.imshow(qipan[:,:,::-1]) plt.show()
4.2 Shi-Tomasi角点检测
tv = cv.imread("tv.jpg") gray = cv.cvtColor(tv,cv.COLOR_BGR2GRAY) # 提取角坐标 conners = cv.goodFeaturesToTrack(gray,1000,0.01,10) # 绘制角点 for i in conners: # 将坐标展平为一维 x,y = i.ravel() x,y = int(x),int(y) cv.circle(tv,(x,y),3,(0,255,0),-1) plt.figure(figsize=(10,8)) plt.imshow(tv[:,:,::-1]) plt.show()
4.3 sift算法
通过多尺度空间检测极值点作为关键点,确保尺度不变性;再对关键点邻域进行方向赋值,确保旋转不变性,从而实现对图像特征的稳定提取。
tv = cv.imread("tv.jpg") gray = cv.cvtColor(tv,cv.COLOR_BGR2GRAY) # 实例化shif sift = cv.xfeatures2d.SIFT_create() # 角点检测:kp角点的信息包括坐标,方向等 des角点描述 kp, des = sift.detectAndCompute(gray,None) # 绘制角点 cv.drawKeypoints(tv,kp,tv,flags=cv.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS) plt.figure(figsize=(10,8)) plt.imshow(tv[:,:,::-1]) plt.show()
4.4 fast检测算法
若一个像素周围有一定数量的像素与该点像素值不同,则认为其为角点
tv = cv.imread("tv.jpg") # 实例化fast对象 fast = cv.FastFeatureDetector_create(threshold=30) # 检测关键的 kp = fast.detect(tv,None) # 绘制关键点,默认开启非极大值抑制,抑制候选角点的重叠 img1 = cv.drawKeypoints(tv,kp,None,(0,0,255)) # 关闭非极大值抑制 fast.setNonmaxSuppression(0) kp = fast.detect(tv,None) img2 = cv.drawKeypoints(tv,kp,None,(0,0,255)) plt.figure(figsize=(10,8)) plt.subplot(121) plt.imshow(img1[:,:,::-1]) plt.title("开启非极大值抑制") plt.subplot(122) plt.imshow(img2[:,:,::-1]) plt.title("关闭非极大值抑制") plt.show()
4.5 orb角点检测
tv = cv.imread("tv.jpg") # 实例化ORB # nfeatures设置要检测的最大特征点数量 orb = cv.ORB_create(nfeatures=200) # 角点检测 kp,des = orb.detectAndCompute(tv,None) # 绘制角点 cv.drawKeypoints(tv,kp,tv,(0,255,0)) plt.figure(figsize=(5,4)) plt.imshow(tv[:,:,::-1]) plt.show()
五、视频操作
import cv2 as cv import numpy as np import matplotlib.pyplot as plt
5.1 视频读写
video_ying = cv.VideoCapture("赵丽颖.mp4")
# 查看是否捕获成功 video_ying.isOpened()
# 判断是否读取成功 while(video_ying.isOpened()): # 获取每一帧图像 ret,frame = video_ying.read() if ret == True: cv.imshow("frame",frame) # 每一帧间隔50秒,按q退出 if cv.waitKey(50) & 0xFF == ord('q'): break # 释放VideoCapture对象,关闭视频文件 video_ying.release() # 关闭OpenCV所创建的窗口 cv.destroyAllWindows()
5.2 视频保存
# 读取视频 cap = cv.VideoCapture("赵丽颖.mp4") # 获取图像的属性(宽度和高度),并将其转换为整数 frame_width = int(cap.get(3)) frame_height = int(cap.get(4)) # 创建保存视频的对象,设置编码格式,帧率,图像的宽度和高度 out = cv.VideoWriter('outpy.avi', cv.VideoWriter_fourcc('M', 'J', 'P', 'G'), 10, (frame_width, frame_height)) # 循环读取视频中的每一帧图像 while True: # 获取视频中的每一帧图像 ret, frame = cap.read() # 如果读取成功,将每一帧图像写入到输出文件中 if ret == True: out.write(frame) else: break # 释放资源 cap.release() out.release() cv.destroyAllWindows()
5.3 视频追踪
目标:追踪视频中的小柯基
5.3.1 meanshift算法
简单,迭代次数少。但不能适应运动目标的形状和大小的变化
import cv2 as cv import numpy as np # 视频捕获 video_dog = cv.VideoCapture('dog.mp4') # 获取第一帧图像,并指定目标位置 ret, frame = video_dog.read() # 目标位置(行,高,列,宽) r, h, c, w = 140, 230, 300, 150 track_window = (c, r, w, h) # 指定目标的感兴趣区域 roi = frame[r:r+h, c:c+w] # 计算直方图 # 转换色彩空间(HSV) hsv_roi = cv.cvtColor(roi, cv.COLOR_BGR2HSV) # 去除低亮度的值(色调,饱和度,亮度) mask = cv.inRange(hsv_roi, np.array((0, 100, 52)), np.array((180, 255, 255))) # 计算直方图 # [0] 只计算第一个通道,即色调(hue)通道的直方图 # [180] 定义直方图bin数量 # [0,180] 计算从0到180之间的色调值的直方图 roi_hist = cv.calcHist([hsv_roi],[0],mask,[180],[0,180]) # 归一化 cv.normalize(roi_hist, roi_hist, 0, 255, cv.NORM_MINMAX) # 目标追踪 # 设置窗口搜索终止条件:最大迭代次数,窗口中心漂移最小值 term_crit = (cv.TERM_CRITERIA_EPS | cv.TERM_CRITERIA_COUNT, 10, 1) while True: # 获取每一帧图像 ret, frame = video_dog.read() if ret: # 算直方图的反向投影,生成一个概率图 dst,指示当前帧中哪些区域最可能包含目标。 hsv = cv.cvtColor(frame, cv.COLOR_BGR2HSV) dst = cv.calcBackProject([hsv], [0], roi_hist, [0, 180], 1) # 进行Meanshift追踪 ret, track_window = cv.meanShift(dst, track_window, term_crit) # 追踪的位置绘制在视频上,并进行显示 x, y, w, h = track_window img2 = cv.rectangle(frame, (x, y), (x + w, y + h), 255, 2) cv.imshow('frame', img2) if cv.waitKey(65) & 0xFF == ord('q'): break else: break video_dog.release() cv.destroyAllWindows()
5.3.2 camshift算法
camshift算法可以适应目标大小形状的改变,具有较好的追踪效果。但当背景色和目标颜色接近时,容易使目标的区域变大。
import cv2 as cv import numpy as np video_dog = cv.VideoCapture('dog.mp4') # 获取第一帧图像,并指定目标位置 ret, frame = video_dog.read() # 目标位置 r, h, c, w = 170, 150, 350, 90 track_window = (c, r, w, h) # 指定目标区域 roi = frame[r:r+h, c:c+w] # 计算直方图 # 转换色彩空间(HSV) hsv_roi = cv.cvtColor(roi, cv.COLOR_BGR2HSV) # 去除低亮度的值(色调,饱和度,亮度) mask = cv.inRange(hsv_roi, np.array((0, 100, 50)), np.array((180, 255, 255))) # 计算直方图 # [0] 只计算第一个通道,即色调(hue)通道的直方图 # [180] 定义直方图bin数量 # [0,180] 计算从0到180之间的色调值的直方图 roi_hist = cv.calcHist([hsv_roi],[0],mask,[180],[0,180]) # 归一化 cv.normalize(roi_hist, roi_hist, 0, 255, cv.NORM_MINMAX) # 目标追踪 # 设置窗口搜索终止条件:最大迭代次数,窗口中心漂移最小值 term_crit = (cv.TERM_CRITERIA_EPS | cv.TERM_CRITERIA_COUNT, 10, 1) while True: ret, frame = video_dog.read() if ret: # 计算直方图的反向投影,生成一个概率图 dst,指示当前帧中哪些区域最可能包含目标。 hsv = cv.cvtColor(frame, cv.COLOR_BGR2HSV) dst = cv.calcBackProject([hsv], [0], roi_hist, [0, 180], 1) # CamShift追踪 ret, track_window = cv.CamShift(dst, track_window, term_crit) # 将追踪的位置绘制在视频上,并进行显示 # pst矩形框的四个坐标 pts = cv.boxPoints(ret) pts = np.intp(pts) img2 = cv.polylines(frame,[pts],True, 255,2) # 显示图像 cv.imshow('frame', img2) if cv.waitKey(60) & 0xFF == ord('q'): break else: break video_dog.release() cv.destroyAllWindows()
六、人脸、眼睛检测案例
OpenCV中自带已训练好的检测器,包括面部、眼睛等,都保存在XML中,我们可以通过以下程序找到他们:
import cv2 as cv print(cv.__file__)
所有的XML文件都在D:\Anaconda\Lib\site-packages\cv2\data\中
6.1 人脸以及眼睛检测(图片)
img = cv.imread("liying2.jpg") gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY) # 定义训练器路径 cascade_path = "D:\\Anaconda\\Lib\\site-packages\\cv2\\data\\haarcascade_frontalface_default.xml" eye_cascade_path = "D:\\Anaconda\\Lib\\site-packages\\cv2\\data\\haarcascade_eye.xml" # 实例化人脸级联分类器 face_cas = cv.CascadeClassifier(cascade_path) # 实例化眼睛级联分类器 eyes_cas = cv.CascadeClassifier(eye_cascade_path) # 人脸检测 faceRects = face_cas.detectMultiScale(gray,scaleFactor=1.05,minNeighbors=5,minSize=(20,20)) # 遍历人脸 for faceRect in faceRects: x,y,w,h = faceRect cv.rectangle(img,(x,y),(x+w,y+h),(0,255,0),2) roi_color = img[x:x+w,y:y+h] roi_gray = gray[x:x+w,y:y+h] eyes = eyes_cas.detectMultiScale(roi_gray,scaleFactor=1.2, minNeighbors=5, minSize=(30, 30), maxSize=(100, 100)) for x,y,w,h in eyes: cv.rectangle(roi_color,(x,y),(x+w,y+h),(255,0,0),2) plt.figure(figsize=(7,4)) plt.imshow(img[:,:,::-1]) plt.title("检测结果") plt.show()
6.2 人脸以及眼睛检测(视频)
import cv2 as cv import matplotlib.pyplot as plt video = cv.VideoCapture("赵丽颖.mp4") cascade_path = "D:\\Anaconda\\Lib\\site-packages\\cv2\\data\\haarcascade_frontalface_default.xml" eyes_cascade_path = "D:\\Anaconda\\Lib\\site-packages\\cv2\\data\\haarcascade_eye.xml" while True: ret,frame = video.read() if ret==True: gray = cv.cvtColor(frame,cv.COLOR_BGR2GRAY) # 实例化人脸级联分类器 face_cas = cv.CascadeClassifier(cascade_path) # 实例化眼睛级联分类器 eyes_cas = cv.CascadeClassifier(eyes_cascade_path) # 检测人脸 faceRects = face_cas.detectMultiScale(gray,scaleFactor=1.2,minNeighbors=3,minSize=(60,60)) for faceRect in faceRects: x,y,w,h = faceRect cv.rectangle(frame,(x,y),(x+w,y+h),(0,255,0),2) roi_color = frame[y:y+h,x:x+w] # 定位人脸上半部分 roi_gray = gray[y:y+int(h*0.7),x:x+w] eyes = eyes_cas.detectMultiScale(roi_gray,scaleFactor=1.3,minNeighbors=5, minSize=(20,20), maxSize=(100, 100)) for x,y,w,h in eyes: cv.rectangle(roi_color,(x,y),(x+w,y+h),(255,0,0),2) cv.imshow("frame",frame) if cv.waitKey(20) & 0xFF==ord('q'): break else: break video.release() cv.destroyAllWindows()
由于运行结果是视频所以无法展示
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/113916.html






































































