一.实验内容:python
利用sift算法,实现全景拼接算法,将给定的两幅图片拼接为一幅.算法
二.实验环境:数据库
主机配置: CPU :intel core i5-7300 2.50GHZ RAM :8.0GB数组
运行环境:win10 64位操做系统app
开发环境:python3.7dom
三.核心算法原理:ide
1.SIFT算法函数
SIFT,即尺度不变特征变换(Scale-invariant feature transform,SIFT),是用于图像处理领域的一种描述。这种描述具备尺度不变性,可在图像中检测出关键点,是一种局部特征描述子。oop
特色:性能
1.SIFT特征是图像的局部特征,其对旋转、尺度缩放、亮度变化保持不变性,对视角变化、仿射变换、噪声也保持必定程度的稳定性;
2. 区分性(Distinctiveness)好,信息量丰富,适用于在海量特征数据库中进行快速、准确的匹配;
3. 多量性,即便少数的几个物体也能够产生大量的SIFT特征向量;
4.高速性,经优化的SIFT匹配算法甚至能够达到实时的要求;
5.可扩展性,能够很方便的与其余形式的特征向量进行联合。
特征检测基本步骤:
1.尺度空间极值检测:
搜索全部尺度上的图像位置。经过高斯微分函数来识别潜在的对于尺度和旋转不变的兴趣点。
2. 关键点定位
在每一个候选的位置上,经过一个拟合精细的模型来肯定位置和尺度。关键点的选择依据于它们的稳定程度。
3. 方向肯定
基于图像局部的梯度方向,分配给每一个关键点位置一个或多个方向。全部后面的对图像数据的操做都相对于关键点的方向、尺度和位置进行变换,从而提供对于这些变换的不变性。
4. 关键点描述
在每一个关键点周围的邻域内,在选定的尺度上测量图像局部的梯度。这些梯度被变换成一种表示,这种表示容许比较大的局部形状的变形和光照变化。
SIFT特征匹配阶段:
第一阶段:SIFT特征的生成,即从多幅图像中提取对尺度缩放、旋转、亮度变化无关的特征向量。
第二阶段:SIFT特征向量的匹配。
SIFT特征的生成通常包括如下几个步骤:
1. 构建尺度空间,检测极值点,得到尺度不变性。
2. 特征点过滤并进行精肯定位。
3. 为特征点分配方向值。
4. 生成特征描述子。
当两幅图像的SIFT特征向量生成之后,下一步就能够采用关键点特征向量的欧式距离来做为两幅图像中关键点的类似性断定度量。取图1的某个关键点,经过遍历找到图像2中的距离最近的两个关键点。在这两个关键点中,若是最近距离除以次近距离小于某个阈值,则断定为一对匹配点。
- Lowe’s算法:
为了进一步筛选匹配点,来获取优秀的匹配点,这就是所谓的“去粗取精”。通常会采用Lowe’s算法来进一步获取优秀匹配点。
为了排除由于图像遮挡和背景混乱而产生的无匹配关系的关键点,SIFT的做者Lowe提出了比较最近邻距离与次近邻距离的SIFT匹配方式:取一幅图像中的一个SIFT关键点,并找出其与另外一幅图像中欧式距离最近的前两个关键点,在这两个关键点中,若是最近的距离除以次近的距离获得的比率ratio少于某个阈值T,则接受这一对匹配点。由于对于错误匹配,因为特征空间的高维性,类似的距离可能有大量其余的错误匹配,从而它的ratio值比较高。显然下降这个比例阈值T,SIFT匹配点数目会减小,但更加稳定,反之亦然。
Lowe推荐ratio的阈值为0.8,但做者对大量任意存在尺度、旋转和亮度变化的两幅图片进行匹配,结果代表ratio取值在0. 4~0. 6 之间最佳,小于0. 4的不多有匹配点,大于0. 6的则存在大量错误匹配点,因此建议ratio的取值原则以下:
ratio=0. 4:对于准确度要求高的匹配;
ratio=0. 6:对于匹配点数目要求比较多的匹配;
ratio=0. 5:通常状况下。
3.RANSAC算法
随机抽样一致算法(RANdom SAmple Consensus,RANSAC),采用迭代的方式从一组包含离群的被观测数据中估算出数学模型的参数。RANSAC算法假设数据中包含正确数据和异常数据(或称为噪声)。正确数据记为内点(inliers),异常数据记为外点(outliers)。同时RANSAC也假设,给定一组正确的数据,存在能够计算出符合这些数据的模型参数的方法。该算法核心思想就是随机性和假设性,随机性是根据正确数据出现几率去随机选取抽样数据,根据大数定律,随机性模拟能够近似获得正确结果。假设性是假设选取出的抽样数据都是正确数据,而后用这些正确数据经过问题知足的模型,去计算其余点,而后对此次结果进行一个评分。
算法基本思想
(1)要获得一个直线模型,须要两个点惟一肯定一个直线方程。因此第一步随机选择两个点。
(2)经过这两个点,能够计算出这两个点所表示的模型方程y=ax+b。
(3)将全部的数据点套到这个模型中计算偏差。
(4)找到全部知足偏差阈值的点。
(5)而后咱们再重复(1)~(4)这个过程,直到达到必定迭代次数后,选出那个被支持的最多的模型,做为问题的解。
四.处理步骤:
Step1:从输入的两张图片里检测关键点、提取局部不变特征。(sift)
Step2:匹配的两幅图像之间的特征(Lowe’s算法)
Step3:使用RANSAC算法利用匹配特征向量估计单映矩阵(homography matrix)。
Step4:利用Step3获得的单映矩阵应用扭曲变换。
具体步骤分析见 六. 步骤分析
五.核心代码:
# -*- coding: utf-8 -*- """ Created on Fri Sep 27 22:29:53 2019 @author: erio """ import numpy as np import imutils import cv2 import time class Stitcher: def __init__(self): # determine if we are using OpenCV v3.X self.isv3 = imutils.is_cv3() def stitch(self, images, ratio=0.75, reprojThresh=4.0, showMatches=False): # unpack the images, then detect keypoints and extract # local invariant descriptors from them (imageB, imageA) = images start = time.time() (kpsA, featuresA) = self.detectAndDescribe(imageA) end = time.time() print('%.5f s' %(end-start)) (kpsB, featuresB) = self.detectAndDescribe(imageB) # match features between the two images start = time.time() M = self.matchKeypoints(kpsA, kpsB, featuresA, featuresB, ratio, reprojThresh) end = time.time() print('%.5f s' %(end-start)) # if the match is None, then there aren't enough matched # keypoints to create a panorama if M is None: return None # otherwise, apply a perspective warp to stitch the images # together (matches, H, status) = M start = time.time() result = cv2.warpPerspective(imageA, H, (imageA.shape[1] + imageB.shape[1], imageA.shape[0])) result[0:imageB.shape[0], 0:imageB.shape[1]] = imageB end = time.time() print('%.5f s' %(end-start)) # check to see if the keypoint matches should be visualized if showMatches: start = time.time() vis = self.drawMatches(imageA, imageB, kpsA, kpsB, matches, status) end = time.time() print('%.5f s' %(end-start)) # return a tuple of the stitched image and the # visualization return (result, vis) # return the stitched image return result #接收照片,检测关键点和提取局部不变特征 #用到了高斯差分(Difference of Gaussian (DoG))关键点检测,和SIFT特征提取 #detectAndCompute方法用来处理提取关键点和特征 #返回一系列的关键点 def detectAndDescribe(self, image): # convert the image to grayscale gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # check to see if we are using OpenCV 3.X if self.isv3: # detect and extract features from the image descriptor = cv2.xfeatures2d.SIFT_create() (kps, features) = descriptor.detectAndCompute(image, None) # otherwise, we are using OpenCV 2.4.X else: # detect keypoints in the image detector = cv2.FeatureDetector_create("SIFT") kps = detector.detect(gray) # extract features from the image extractor = cv2.DescriptorExtractor_create("SIFT") (kps, features) = extractor.compute(gray, kps) # convert the keypoints from KeyPoint objects to NumPy # arrays kps = np.float32([kp.pt for kp in kps]) # return a tuple of keypoints and features return (kps, features) #matchKeypoints方法须要四个参数,第一张图片的关键点和特征向量,第二张图片的关键点特征向量。 #David Lowe’s ratio测试变量和RANSAC重投影门限也应该被提供。 def matchKeypoints(self, kpsA, kpsB, featuresA, featuresB, ratio, reprojThresh): # compute the raw matches and initialize the list of actual # matches matcher = cv2.DescriptorMatcher_create("BruteForce") rawMatches = matcher.knnMatch(featuresA, featuresB, 2) matches = [] # loop over the raw matches for m in rawMatches: # ensure the distance is within a certain ratio of each # other (i.e. Lowe's ratio test) if len(m) == 2 and m[0].distance < m[1].distance * ratio: matches.append((m[0].trainIdx, m[0].queryIdx)) # computing a homography requires at least 4 matches if len(matches) > 4: # construct the two sets of points ptsA = np.float32([kpsA[i] for (_, i) in matches]) ptsB = np.float32([kpsB[i] for (i, _) in matches]) # compute the homography between the two sets of points (H, status) = cv2.findHomography(ptsA, ptsB, cv2.RANSAC, reprojThresh) # return the matches along with the homograpy matrix # and status of each matched point return (matches, H, status) # otherwise, no homograpy could be computed return None #连线画出两幅图的匹配 def drawMatches(self, imageA, imageB, kpsA, kpsB, matches, status): # initialize the output visualization image (hA, wA) = imageA.shape[:2] (hB, wB) = imageB.shape[:2] vis = np.zeros((max(hA, hB), wA + wB, 3), dtype="uint8") vis[0:hA, 0:wA] = imageA vis[0:hB, wA:] = imageB # loop over the matches for ((trainIdx, queryIdx), s) in zip(matches, status): # only process the match if the keypoint was successfully # matched if s == 1: # draw the match ptA = (int(kpsA[queryIdx][0]), int(kpsA[queryIdx][1])) ptB = (int(kpsB[trainIdx][0]) + wA, int(kpsB[trainIdx][1])) cv2.line(vis, ptA, ptB, (0, 255, 0), 1) # return the visualization return vis if __name__ == '__main__': # load the two images and resize them to have a width of 400 pixels # (for faster processing) imageA = cv2.imread('D:/test1.jpg') imageB = cv2.imread('D:/test2.jpg') #imageA = imutils.resize(imageA, width=400) #imageB = imutils.resize(imageB, width=400) # stitch the images together to create a panorama # showMatches=True 展现两幅图像特征的匹配,返回vis start = time.time() stitcher = Stitcher() (result, vis) = stitcher.stitch([imageA, imageB], showMatches=True) # show the images end = time.time() print('%.5f s' %(end-start)) cv2.imwrite('D:/vis1.jpg', vis) cv2.imwrite('D:/result.jpg', result)
六.步骤分析:
概述:
Sitich调用detectAndDescribe,检测两张图片里的关键点、提取局部不变特征。
有了关键点和特征,sitich调用matchKeypoints方法来匹配两张图片里的特征。若是返回匹配的M为None,就是由于现有的关键点不足以匹配生成全景图。假设M不返回None,拆包返回元组,包含关键点匹配matches、从RANSAC算法中获得的最优单映射变换矩阵H以及最后的单映计算状态列表status,用来代表那些已经成功匹配的关键点。
有了最优单映射变换矩阵H后,就可将两张图片“缝合起来”。stitch调用cv2.warpPerspective进行缝合。这样,就返回一个拼接的图片。
最后sitich调用drawMatches函数用来将两张图片关键点的匹配可视化。
函数分析:
detectAndDescribe
detectAndDescribe方法用来接收照片,检测关键点和提取局部不变特征。实现中用到了高斯差分(Difference of Gaussian (DoG))关键点检测,和SIFT特征提取。
参数为单个的图片。
返回值kps为keypoints,features为局部不变特征。
检测是否用了OpenCV 3.X,若是是,就用cv2.xfeatures2d.SIFT_create方法来实现DoG关键点检测和SIFT特征提取。detectAndCompute方法用来处理提取关键点和特征。
若是用OpenCV2.4,则cv2.FeatureDetector_create方法来实现关键点的检测(DoG)。detect方法返回一系列的关键点。用SIFT关键字来初始化cv2.DescriptorExtractor_create,设置SIFT特征提取。调用extractor的compute方法返回一组关键点周围量化检测的特征向量。
最后,关键点从KeyPoint对象转换为NumPy数组后返回给调用函数。
matchKeypoints
用于匹配两张图片的特征。
matchKeypoints方法须要六个参数,第一张图片的关键点和特征向量,第二张图片的关键点特征向量,David Lowe’s ratio测试变量ratio和RANSAC重投影门限reprojThresh。
返回值为matches, H, status。分别为匹配的关键点matches,最优单映射变换矩阵 H(3x3),单映计算的状态列表status用于表示已经成功匹配的关键点。
匹配特征过程: 循环每张图片的描述子,计算距离,最后找到每对描述子的最小距离。
由于这是计算机视觉中的一个很是广泛的作法,OpenCV已经内置了cv2.DescriptorMatcher_create方法,用来匹配特征。BruteForce参数表示咱们可以更详尽计算两张图片直接的欧式距离,以此来寻找每对描述子的最短距离。
knnMatch方法是K=2的两个特征向量的k-NN匹配(k-nearest neighbors algorithm,K近邻算法),代表每一个匹配的前两名做为特征向量返回。之因此咱们要的是匹配的前两个而不是只有第一个,是由于咱们须要用David Lowe’s ratio来测试假匹配而后作修剪。
以后,用第79行的rawMatches来计算每对描述子,可是这些描述子多是错误的,也就是这是图片不是真正的匹配。去修剪这些有误的匹配,咱们能够运用 Lowe’s ratio测试特别的来循环rawMatches,这是用来肯定高质量的特征匹配。正常的Lowe’s ratio 值在[0.7,0.8].
咱们用Lowe’s ratio 测试获得matches的值后,咱们就能够计算这两串关键点之间的单映性。 计算两串关键点的单映性须要至少四个匹配。为了得到更为可信的单映性,咱们至少须要超过四个匹配点。
调用cv2.findHomography计算H和status。
drawMatches
drawMatches用来将两张图片关键点的匹配可视化。
参数为原始图片A,B,关键点kpsA,kpsB,匹配的关键点matches以及单映的状态列表status。
返回值为将两张图片中的匹配点用直线链接起来的图片。
运用参数,咱们能够经过将两张图片匹配的关键点N和关键点M画直线链接,并返回包含这些直线的图片来实现可视化。
Stitch
参数
images。传入图片的列表,缝合在一块儿造成全景图。注意传入的图像是从左到右的顺序。若是提供的不是这样的顺序,程序仍然能够跑,可是输出全景是不正确的。ratio ,用于特征匹配时David Lowe比率测试,reprojthresh 是RANSAC算法中最大像素“回旋的余地”,最后的showMatches,是一个布尔类型的值,用于代表是否调用drawMatches进行关键点匹配可视化。
返回值,返回拼接好的图片result已经用于可视化匹配关键点的图片vis.
Sitich调用detectAndDescribe,检测两张图片里的关键点、提取局部不变特征。
调用matchKeypoints方法来匹配两张图片里的特征
调用cv2.warpPerspective进行缝合。须要三个参数:想要“缝合”上来的照片(本程序里的右边的图片);还有3*3的最优单映射转换矩阵H;最后就是塑造出要输出的照片。咱们获得输出图像的宽是两图片之和,高即为第二张图像的高度。
调用drawMatches函数用来将两张图片关键点的匹配可视化。
- 结果展现:
给定图片为老师提供的IMG_201901.jpg和IMG_201901.jpg,重命名为test1.jpg,test2.jpg。
输出拼接好的图片为rusult.jpg。
展现关键点匹配的图片vis.jpg。
传入图片
|
|
test1.jpg
|
test2.jpg
|
拼接好的图片:result.jpg
可视化关键点匹配:Vis.jpg
8.性能分析:
时间:
项目运行总时间304.79945 s
单张图片detectAndDescribe检测关键点、提取局部不变特征用时8.31044 s
matchKeypoints匹配两张图片里的特征用时292.67749 s
cv2.warpPerspective缝合图像用时0.27427 s
drawMatches创建直线关键点的匹配可视化用时0.33913 s
可见matchKeypoints计算量最大
Lowe’s ratio循环消耗时间较长,同时cv2.findHomography计算最优单映矩阵H与status占用必定时间。
时间优化:
使用比SIFT快的SURF方法,
调节它的参数,减小一些关键点,只获取64维而不是128维的向量等,加快速度。
图像拼接质量:
拼接比较流畅,肉眼判断为一张图片。
缺陷在于看到有一条像折痕同样的线条,这个就是两个图片的拼接线,主要缘由是光线的变化。
拼接质量优化:
对于衔接处存在的缝隙问题,有一个解决办法是按必定权重叠加图1和图2的重叠部分,在重叠处图2的比重是1,向着图1的方向,越远离衔接处,图1的权重愈来愈大,图2的权重愈来愈低,实现平稳过渡。
优化:
时间优化:使用比SIFT快的SURF方法,使用Hessian算法检测关键点。在使用SURF时,还能够调节它的参数,减小一些关键点,只获取64维而不是128维的向量等,加快速度。
拼接质量优化:对第一张图和它的重叠区作一些加权处理,重叠部分,离左边图近的,左边图的权重就高一些,离右边近的,右边旋转图的权重就高一些,而后二者相加,实现平滑过渡。
思路和方法
思路
提取要拼接的两张图片的特征点、特征描述符;
将两张图片中对应的位置点找到,匹配起来;
若是找到了足够多的匹配点,就能将两幅图拼接起来,拼接前,可能须要将第二幅图透视旋转一下,利用找到的关键点,将第二幅图透视旋转到一个与第一幅图相同的能够拼接的角度;
进行拼接;
进行拼接后的一些处理,让效果看上去更好。
实现方法
提取图片的特征点、描述符,可使用opencv建立一个SIFT对象,SIFT对象使用DoG方法检测关键点,并对每一个关键点周围的区域计算特征向量。在实现时,可使用比SIFT快的SURF方法,使用Hessian算法检测关键点。由于只是进行全景图拼接,在使用SURF时,还能够调节它的参数,减小一些关键点,只获取64维而不是128维的向量等,加快速度。
在分别提取好了两张图片的关键点和特征向量之后,能够利用它们进行两张图片的匹配。在拼接图片中,可使用Knn进行匹配,可是使用FLANN快速匹配库更快,图片拼接,须要用到FLANN的单应性匹配。
单应性匹配完以后能够得到透视变换H矩阵,用这个的逆矩阵来对第二幅图片进行透视变换,将其转到和第一张图同样的视角,为下一步拼接作准备。
透视变换完的图片,其大小就是最后全景图的大小,它的右边是透视变换之后的图片,左边是黑色没有信息。拼接时能够比较简单地处理,经过numpy数组选择直接把第一张图加到它的左边,覆盖掉重叠部分,获得拼接图片,这样作很是快,可是最后效果不是很好,中间有一条分割痕迹很是明显。使用opencv指南中图像金字塔的代码对拼接好的图片进行处理,整个图片平滑了,中间的缝仍是特别突兀。
直接拼效果不是很好,能够把第一张图叠在左边,可是对第一张图和它的重叠区作一些加权处理,重叠部分,离左边图近的,左边图的权重就高一些,离右边近的,右边旋转图的权重就高一些,而后二者相加,使得过渡是平滑地,这样看上去效果好一些,速度就比较慢。若是是用SURF来作,时间主要画在平滑处理上而不是特征点提取和匹配。
python_opencv中主要使用的函数
基于python 3.7和对应的python-opencv
cv2.xfeatures2d.SURF_create ([hessianThreshold[, nOctaves[, nOctaveLayers[, extended[, upright]]]]])
该函数用于生成一个SURF对象,在使用时,为提升速度,能够适当提升hessianThreshold,以减小检测的关键点的数量,能够extended=False,只生成64维的描述符而不是128维,令upright=True,不检测关键点的方向。
cv2.SURF.detectAndCompute(image, mask[, descriptors[, useProvidedKeypoints]])
该函数用于计算图片的关键点和描述符,须要对两幅图都进行计算。
flann=cv2.FlannBasedMatcher(indexParams,searchParams)
match=flann.knnMatch(descrip1,descrip2,k=2)
flann快速匹配器有两个参数,一个是indexParams,一个是searchParams,都用手册上建议的值就能够。在建立了匹配器获得匹配数组match之后,就能够参考Lowe给出的参数,对匹配进行过滤,过滤掉很差的匹配。其中返回值match包括了两张图的描述符距离distance 、训练图(第二张)的描述符索引trainIdx 、查询的图(第一张)的描述符索引queryIdx 这几个属性。
M,mask=cv2.findHomography(srcPoints, dstPoints[, method[, ransacReprojThreshold[, mask]]])
这个函数实现单应性匹配,返回的M是一个矩阵,即对关键点srcPoints作M变换能变到dstPoints的位置。
warpImg=cv2.warpPerspective(src,np.linalg.inv(M),dsize[,dst[,flags[,borderMode[,borderValue]]]])
用这个函数进行透视变换,变换视角。src是要变换的图片,np.linalg.inv(M)是④中M的逆矩阵,获得方向一致的图片。
a=b.copy() 实现深度复制,Python中默认是按引用复制,a=b是a指向b的内存。
draw_params = dict(matchColor = (0,255,0),singlePointColor = (255,0,0),matchesMask = matchMask,flags = 2),img3 = cv2.drawMatches(img1,kp1,img2,kp2,good,None,**draw_params)
使用drawMatches能够画出匹配的好的关键点,matchMask是比较好的匹配点,之间用绿色线链接起来。
核心代码
import cv2 import numpy as np from matplotlib import pyplot as plt import time MIN = 10 starttime=time.time() img1 = cv2.imread('1.jpg') #query img2 = cv2.imread('2.jpg') #train #img1gray=cv2.cvtColor(img1,cv2.COLOR_BGR2GRAY) #img2gray=cv2.cvtColor(img2,cv2.COLOR_BGR2GRAY) surf=cv2.xfeatures2d.SURF_create(10000,nOctaves=4,extended=False,upright=True) #surf=cv2.xfeatures2d.SIFT_create()#能够改成SIFT kp1,descrip1=surf.detectAndCompute(img1,None) kp2,descrip2=surf.detectAndCompute(img2,None) FLANN_INDEX_KDTREE = 0 indexParams = dict(algorithm = FLANN_INDEX_KDTREE, trees = 5) searchParams = dict(checks=50) flann=cv2.FlannBasedMatcher(indexParams,searchParams) match=flann.knnMatch(descrip1,descrip2,k=2) good=[] for i,(m,n) in enumerate(match): if(m.distance<0.75*n.distance): good.append(m) if len(good)>MIN: src_pts = np.float32([kp1[m.queryIdx].pt for m in good]).reshape(-1,1,2) ano_pts = np.float32([kp2[m.trainIdx].pt for m in good]).reshape(-1,1,2) M,mask=cv2.findHomography(src_pts,ano_pts,cv2.RANSAC,5.0) warpImg = cv2.warpPerspective(img2, np.linalg.inv(M), (img1.shape[1]+img2.shape[1], img2.shape[0])) direct=warpImg.copy() direct[0:img1.shape[0], 0:img1.shape[1]] =img1 simple=time.time() #cv2.namedWindow("Result", cv2.WINDOW_NORMAL) #cv2.imshow("Result",warpImg) rows,cols=img1.shape[:2] for col in range(0,cols): if img1[:, col].any() and warpImg[:, col].any():#开始重叠的最左端 left = col break for col in range(cols-1, 0, -1): if img1[:, col].any() and warpImg[:, col].any():#重叠的最右一列 right = col break res = np.zeros([rows, cols, 3], np.uint8) for row in range(0, rows): for col in range(0, cols): if not img1[row, col].any():#若是没有原图,用旋转的填充 res[row, col] = warpImg[row, col] elif not warpImg[row, col].any(): res[row, col] = img1[row, col] else: srcImgLen = float(abs(col - left)) testImgLen = float(abs(col - right)) alpha = srcImgLen / (srcImgLen + testImgLen) res[row, col] = np.clip(img1[row, col] * (1-alpha) + warpImg[row, col] * alpha, 0, 255) warpImg[0:img1.shape[0], 0:img1.shape[1]]=res final=time.time() img3=cv2.cvtColor(direct,cv2.COLOR_BGR2RGB) plt.imshow(img3,),plt.show() img4=cv2.cvtColor(warpImg,cv2.COLOR_BGR2RGB) plt.imshow(img4,),plt.show() print("simple stich cost %f"%(simple-starttime)) print("\ntotal cost %f"%(final-starttime)) cv2.imwrite("simplepanorma.png",direct) cv2.imwrite("bestpanorma.png",warpImg) else: print("not enough matches!")
效果图:
参考
https://www.pyimagesearch.com/
https://cloud.tencent.com/developer/article/1178958