当前位置:   article > 正文

opencv总结(图像基本操作、直方图与模板匹配等+实战+报错记录)_opencv直方图匹配

opencv直方图匹配


一.图像处理及图像基本操作

1.颜色通道提取及合并

2.边界填充

3.图像融合

4.

二.直方图与模板匹配

1.直方图

1.1直方图

1.2直方图均衡化

1.3自适应直方图均衡化

2.模板匹配

2.1模板匹配

2.2多个匹配

三.图像特征

1.特征识别

三级目录

银行卡卡号识别

文件OCR

特征匹配

图像拼接

停车场车位识别

一.图像处理及图像基本操作

1.颜色通道提取及合并

提取函数(将图像分为三个通道,可通过索引获取对应通道的图像):cv2.split(img)
合并函数:cv2.merge((b,g,r))
获取矩阵/数组维数:.shape()

2.边界填充

当图片四周边界有空白(或者扩充边界),可以用边界填充方式进行填充。

  • BORDER_REPLICATE:复制法,也就是复制最边缘像素。
  • BORDER_REFLECT:反射法,对感兴趣的图像中的像素在两边进行复制例如:fedcba|abcdefgh|hgfedcb
  • BORDER_REFLECT_101:反射法,也就是以最边缘像素为轴,对称,gfedcb|abcdefgh|gfedcba
  • BORDER_WRAP:外包装法cdefgh|abcdefgh|abcdefg
  • BORDER_CONSTANT:常量法,常数值填充,value=0,设定填充的像素值。

传入原图,定义好整个图像的大小(四个大小),选定填充方法。
replicate = cv2.copyMakeBorder(img, top_size, bottom_size, left_size, right_size, borderType=cv2.BORDER_REPLICATE)

如图,图一为原图,其余是不同填充方式得到的结果。
居中的图片: 不同边界填充的效果

3.图像融合

进行图像融合时一定要将两张图片大小调整一致。
通过.shape()函数获其中一个图像的大小,另一张图像用resize函数调整大小,addWeighted()函数进行图像融合。

关键代码:

img_cat.shape
img_dog = cv2.resize(img_dog, (500, 414))
res = cv2.addWeighted(img_cat, 0.4, img_dog, 0.6, 0)
  • 1
  • 2
  • 3

效果展示:
猫图
狗图
融合结果:
结果

4.形态学操作

基础:膨胀、腐蚀操作。
开运算:腐蚀后膨胀;闭运算:膨胀后腐蚀。
当图片中有细小毛躁,可用腐蚀操作去掉,此时图片会有损毁,再用膨胀操作尽力恢复。
礼帽:原始输入-开运算;黑帽:闭运算-原始输入。
梯度:膨胀-腐蚀。Sobel算子、scharr算子。
Scharr算子对差异更敏感,噪音点更容易被保留。
原图:
在这里插入图片描述

开运算:

#创建一个五行五列无符号整数类型的数组,kernel的大小形状决定腐蚀或膨胀操作的强度
kernel=np.ones((5,5),np.uint8)
#kernel的大小形状决定腐蚀或膨胀操作的强度
opening=cv2.morphologyEx(img,cv2.MORPH_OPEN,kernel)
  • 1
  • 2
  • 3
  • 4

在这里插入图片描述

闭运算:

kernel=np.ones((5,5),np.uint8)
closing=cv2.morphologyEx(img,cv2.MORPH_CLOSE,kernel)
  • 1
  • 2

在这里插入图片描述

礼帽:

tophat=cv2.morphologyEx(img,cv2.MORPH_TOPHAT,kernel)
  • 1

在这里插入图片描述

黑帽:

blackhat=cv2.morphologyEx(img,cv2.MORPH_BLACKHAT,kernel)
  • 1

在这里插入图片描述

梯度:膨胀操作-腐蚀操作,梯度

kernel = np.ones((7,7),np.uint8) 
gradient = cv2.morphologyEx(pie, cv2.MORPH_GRADIENT, kernel)
  • 1
  • 2

膨胀与腐蚀操作
梯度运算后

Sobel算子:经过绝对值操作,否则两边只能保存一边(正数部分),负数部分会被截断。

sobelx = cv2.Sobel(img,cv2.CV_64F,1,0,ksize=3)
sobelx = cv2.convertScaleAbs(sobelx)
  • 1
  • 2

在这里插入图片描述

Sobel算子和scharr算子对比:

#图像梯度-sobel算子
#计算图像的梯度(相当于检测边缘)
#分为x,y方向轴,最后整合
#给出的都是关键代码(省略读取、显示)
#               ...,图像深度,1,0代表对水平方向进行处理,sobel算子大小
#x
sobelx=cv2.Sobel(img,cv2.CV_64F,1,0,ksize=3)
#y
sobely=cv2.Sobel(img,cv2.CV_64F,0,1,ksize=3)
##上面两个结果为什么会显示一半?因为在进行运算的时候,
##关于水平方向右边是黑减白,得到的值是负数;关于竖直方向下边也是黑减白,得负数。
##负数都会被截断成0,,为了解决这个问题,我们要取绝对值
##ps:1.中间部分逐渐趋向黑色就是趋近于0;2.不建议直接将水平+竖直两个方向一块计算,会更模糊
#x水平方向为例
sobelx = cv2.Sobel(img,cv2.CV_64F,1,0,ksize=3)
sobelx = cv2.convertScaleAbs(sobelx)

##求和(将水平、竖直两个方向相加)
sobelxy=cv2.addWeighted(sobelx,0.5,sobely,0.5,0)
cv_show(sobelxy,'sobelsy')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
##图像梯度--scharr算子(结果差异更明,对差异更敏感)
##图像梯度--laplacian算子(二阶导:体现一阶导的变换,更敏感,对噪音点敏感,
##!但噪音点不一定是边界)
#三种方法对比
#sobel算子
img=cv2.imread('lena.jpg',cv2.IMREAD_GRAYSCALE)
sobelx=cv2.Sobel(img,cv2.CV_64F,1,0,ksize=3)
sobely=cv2.Sobel(img,cv2.CV_64F,0,1,ksize=3)
sobelx=cv2.convertScaleAbs(sobelx)
sobely=cv2.convertScaleAbs(sobely)
sobelxy=cv2.addWeighted(sobelx,0.5,sobely,0.5,0)

#scharr算子
scharrx=cv2.Scharr(img,cv2.CV_64F,1,0)
scharry=cv2.Scharr(img,cv2.CV_64F,0,1)
scharrx=cv2.convertScaleAbs(scharrx)
scharry=cv2.convertScaleAbs(scharry)
scharrxy=cv2.addWeighted(scharrx,0.5,scharry,0.5,0)

##laplacian
laplacian=cv2.Laplacian(img,cv2.CV_64F)
laplacian=cv2.convertScaleAbs(laplacian)

res=np.hstack((sobelxy,scharrxy,laplacian))
cv_show(res,'res')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

sobel算子、scharr算子

5.图像平滑

为解决噪音点问题。
原图:
在这里插入图片描述
常用滤波:均值滤波、方框滤波、高斯滤波、中值滤波。
均值滤波:顾名思义就是取像素点求和取平均值。

blur=cv2.blur(img,(3,3))
  • 1

均值滤波
方框滤波:当参数normallize为TRUE时,结果和均值一样;为False时,超过255的部分当作255处理,显示白色。

box = cv2.boxFilter(img,-1,(3,3), normalize=True)
  • 1

方框滤波
高斯滤波:高斯模糊的卷积核里的数值满足高斯分布,相当于更重视中间的(矩阵)xy方向标准差都是1。

aussian=cv2.GaussianBlur(img,(5,5),1)
  • 1

高斯滤波
中值滤波:相当于用中值代替(更加平滑了甚至有点模糊)。

median=cv2.medianBlur(img,5)
  • 1

中值滤波

6.图像阈值

给图像找一个上下边界,边界内称为阈值,根据type对图像处理。

ret, dst = cv2.threshold(src, thresh, maxval, type)
  • 1
  • src: 输入图,只能输入单通道图像,通常来说为灰度图

  • dst: 输出图

  • thresh: 阈值

  • maxval: 当像素值超过了阈值(或者小于阈值,根据type来决定),所赋予的值

  • type:二值化操作的类型,包含以下5种类型: cv2.THRESH_BINARY; cv2.THRESH_BINARY_INV; cv2.THRESH_TRUNC; cv2.THRESH_TOZERO;cv2.THRESH_TOZERO_INV

  • cv2.THRESH_BINARY 超过阈值部分取maxval(最大值),否则取0

  • cv2.THRESH_BINARY_INV THRESH_BINARY的反转

  • cv2.THRESH_TRUNC 大于阈值部分设为阈值,否则不变

  • cv2.THRESH_TOZERO 大于阈值部分不改变,否则设为0

  • cv2.THRESH_TOZERO_INV THRESH_TOZERO的反转
    结果:
    不同图像阈值处理的结果

7.图像轮廓

需要先处理为灰度图。

gray=cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
ret,thresh=cv2.threshold(gray,127,255,cv2.THRESH_BINARY)
#获取所有轮廓
contours, hierarchy = cv2.findContours(thresh,cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
#通过索引获取其中某个轮廓
contours[0]
#轮廓绘制:
#函数中的参数:传入绘制图像,轮廓,轮廓索引#retr_tree(-1代表所有),颜色模式(三个通道),#线条厚度
res=cv2.drawContours(draw_img,contours,-1,(0,0,255),2)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

原图
绘制所有轮廓
计算轮廓面积、周长

cv2.contourArea(cnt)
cv2.arcLength(cnt,True)
#绘制边界矩形
gray=cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
ret,thresh=cv2.threshold(gray,127,255,cv2.THRESH_BINARY)
contours,hierarchy=cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
cnt=contours[0]
x,y,w,h=cv2.boundingRect(cnt)
img=cv2.rectangle(img,(x,y),(x+w,y+h),(0,255,0),2)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

以边界矩形为例,结果如图:
将左下角三角形的外接矩形

8.图像金字塔

高斯金字塔
向下采样,缩小;向上采样,放大。
两个操作先后执行仍然会有精度的缺失。
原图:
原图
向上采样:

up=cv2.pyrUp(img)
  • 1

向上采样,图像扩大了
向下采样:

down=cv2.pyrDown(img)
  • 1

向下采样
拉普拉斯金字塔
为实现高斯金字塔图像重建,将高斯金字塔与其上一层通过采样扩大后的差值图像。

down=cv2.pyrDown(img)
down_up=cv2.pyrUp(down)
l_1=img-down_up
  • 1
  • 2
  • 3

拉普拉斯金字塔

二.直方图与模板匹配

1.直方图

统计不同像素值在一幅图像中的个数,并以直方图的方式显示。

hist = cv2.calcHist([img],[0],None,[256],[0,256])
  • 1

要处理的图像(需要先灰度处理):
在这里插入图片描述
统计所得直方图:

cv2.calcHist(images,channels,mask,histSize,ranges)

- images: 原图像图像格式为 uint8 或 float32。当传入函数时应 用中括号 [] 括来例如[img]
- channels: 同样用中括号括来它会告函数我们统幅图 像的直方图。如果入图像是灰度图它的值就是 [0]如果是彩色图像 的传入的参数可以是 [0][1][2] 它们分别对应着 BGR。 
- mask: 掩模图像。统整幅图像的直方图就把它为 None。但是如 果你想统图像某一分的直方图的你就制作一个掩模图像并 使用它。
- histSize:BIN 的数目。也应用中括号括来
- ranges: 像素值范围常为 [0256] 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

直方图
1.1直方图均衡化
可以增强局部的对比度,但不影响整体的对比度。

equ = cv2.equalizeHist(img) 
  • 1

直方图均衡化
1.2自适应直方图均衡化
第一个参数控制对比度,第二个参数确定被分割的区域,若图像被分割的区域越多,图像就越平滑。Apply方法将图像的对比度进行拉伸。

clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8)) 
res_clahe = clahe.apply(img)
  • 1
  • 2

结果图:
自适应直方图均衡化

1.3Mask操作

填充选中区域(mask),可以过滤掉不需要的部分(与操作)。

创建mask

#mask填充的图
mask = np.zeros(img.shape[:2], np.uint8)
#与mask图与操作后的图
masked_img = cv2.bitwise_and(img, img, mask=mask)#与操作
  • 1
  • 2
  • 3
  • 4

在这里插入图片描述

2.模板匹配

模板匹配和卷积原理很像,模板在原图像上从原点开始滑动,计算模板与(图像被模板覆盖的地方)的差别程度。

#第二个参数0,表示取为灰度图像
img=cv2.imread('lena.jpg',0)
#五种模板匹配的方法:
#- TM_SQDIFF:计算平方不同,计算出来的值越小,越相关        
#- TM_CCORR:计算相关性,计算出来的值越大,越相关
#- TM_CCOEFF:计算相关系数,计算出来的值越大,越相关
#- TM_SQDIFF_NORMED:计算归一化平方不同,计算出来的值越接近0,越相关
#- TM_CCORR_NORMED:计算归一化相关性,计算出来的值越接近1,越相关
#- TM_CCOEFF_NORMED:计算归一化相关系数,计算出来的值越接近1,越相关
#在函数matchTemplate第三个参数中填入所选的参数方法:
res = cv2.matchTemplate(img, template, cv2.TM_SQDIFF)
#通过函数minMaxLoc()获取矩阵中的最大最小值以及它们的位置,后续可以利用得到的位置绘制所匹配到的位置的矩形框。
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

给出两个方法的结果。
在这里插入图片描述

匹配多个对象:

res = cv2.matchTemplate(img_gray, template, cv2.TM_CCOEFF_NORMED)
threshold = 0.8

# 取匹配程度大于%80的坐标
loc = np.where(res >= threshold)
for pt in zip(*loc[::-1]):  # *号表示可选参数
    bottom_right = (pt[0] + w, pt[1] + h)
    cv2.rectangle(img_rgb, pt, bottom_right, (0, 0, 255), 2)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

结果:要匹配的图像是金币。
在这里插入图片描述

三.图像特征

# 实例化,绘制关键点
sift = cv2.xfeatures2d.SIFT_create()
kp = sift.detect(gray, None)
img=cv2.drawKeypoints(gray,kp,img)
  • 1
  • 2
  • 3
  • 4

在这里插入图片描述

四.实战部分

1.银行卡卡号识别

1.读取模板图像
2.将模板图像中的对应数字图像分割对应上表示的数字,通过数据结构元组表示
2.1(2具体)灰度处理–二值图像(边缘明显)(一般情况下,做轮廓检测输入的都是二值图像)–计算轮廓
3.读取银行卡图像
4.将亮的部分处理得更亮,而后得到一组一组(一个长方形条)的四位银行卡号
4.1预处理–灰度处理–礼帽操作–梯度–绝对值+归一化(归一化使图像像素值在一个可控的范围,方便二值操作)
4.2闭运算(先膨胀载腐蚀,将数字连接在一起)–闭运算(填补空缺)–计算轮廓
5.循环处理每一组长方形,将长方形中的四位数字分割开,与模板图像元组对应进行对比,得到相对应的数字

需要识别的银行卡:
在这里插入图片描述
结果:
在这里插入图片描述
代码:轮廓提取,裁剪每一位卡号,匹配:

# 符合区域大小的留下来
for(i,c) in enumerate(cnts):
    (x,y,w,h)=cv2.boundingRect(c)
    ar=w/float(h)
    if ar>2.5 and ar<4.0:
        if(w>40 and w<55) and (h>10 and h<20):
            locs.append((x,y,w,h))

locs=sorted(locs,key=lambda x:x[0])
output=[]

for(i,(gX,gY,gW,gH)) in enumerate(locs):
    groupOutput=[]
    # 随便取,往外扩一点区域
    group = gray[gY - 5:gY + gH + 5, gX - 5:gX + gW + 5]
    cv_show('group',group)
    group = cv2.threshold(group, 0, 255,
		cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
    cv_show('group',group)
    digitCnts,hierarchy = cv2.findContours(group.copy(), cv2.RETR_EXTERNAL,
		cv2.CHAIN_APPROX_SIMPLE)
    digitCnts = contours.sort_contours(digitCnts,method="left-to-right")[0]
    
    for c in digitCnts:
        # 计算点集或轮廓形成的最小外接矩形
        (x,y,w,h)=cv2.boundingRect(c)
        roi=group[y:y+h,x:x+w]
        roi=cv2.resize(roi,(57,88))
        cv_show('roi',roi)

        scores=[]

        # 计算得分,找到数字,匹配
        for (digit,digitROI) in digits.items():
            result=cv2.matchTemplate(roi,digitROI,cv2.TM_CCOEFF)
            (_,score,_,_)=cv2.minMaxLoc(result)
            scores.append(score)

        groupOutput.append(str(np.argmax(scores)))

    cv2.rectangle(image, (gX - 5, gY - 5),
		(gX + gW + 5, gY + gH + 5), (0, 0, 255), 1)
    cv2.putText(image, "".join(groupOutput), (gX, gY - 15),
		cv2.FONT_HERSHEY_SIMPLEX, 0.65, (0, 0, 255), 2)
    
    output.extend(groupOutput)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

2.文件OCR

预处理–遍历轮廓–透视变换–二值处理–利用OCR进行文字识别。
难点:
透视变换。

#透视变换
def order_points(pts):
	# 一共4个坐标点
	rect = np.zeros((4, 2), dtype = "float32")

	# 按顺序找到对应坐标0123分别是 左上,右上,右下,左下
	# 计算左上,右下
	s = pts.sum(axis = 1)
	rect[0] = pts[np.argmin(s)]
	rect[2] = pts[np.argmax(s)]

	# 计算右上和左下
	diff = np.diff(pts, axis = 1)
	rect[1] = pts[np.argmin(diff)]
	rect[3] = pts[np.argmax(diff)]

	return rect

def four_point_transform(image, pts):
	# 获取输入坐标点
	rect = order_points(pts)
	(tl, tr, br, bl) = rect

	# 计算输入的w和h值
	widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))
	widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))
	maxWidth = max(int(widthA), int(widthB))

	heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
	heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
	maxHeight = max(int(heightA), int(heightB))

	# 变换后对应坐标位置
	dst = np.array([
		[0, 0],
		[maxWidth - 1, 0],
		[maxWidth - 1, maxHeight - 1],
		[0, maxHeight - 1]], dtype = "float32")

	# 计算变换矩阵
	M = cv2.getPerspectiveTransform(rect, dst)
	warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))

	# 返回变换后结果
	return warped
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

要处理的图:
在这里插入图片描述
图像处理后:
在这里插入图片描述
得到的文本结果:
在这里插入图片描述
3.特征匹配
预处理–特征点检测–特征点匹配–显示匹配结果
需要匹配的图:
在这里插入图片描述
在这里插入图片描述
匹配结果:
在这里插入图片描述

kp1, des1 = sift.detectAndCompute(img1, None)
kp2, des2 = sift.detectAndCompute(img2, None)

# crossCheck表示两个特征点要互相匹,例如A中的第i个特征点与B中的第j个特征点最近的,并且B中的第j个特征点到A中的第i个特征点也是 
#NORM_L2: 归一化数组的(欧几里德距离),如果其他特征计算方法需要考虑不同的匹配计算方式
bf = cv2.BFMatcher(crossCheck=True)

matches = bf.match(des1, des2)
# 根据距离排序。key定义了排序的依据--distance
matches = sorted(matches, key=lambda x: x.distance)

# 前十个关键点
img3 = cv2.drawMatches(img1, kp1, img2, kp2, matches[:10], None,flags=2)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

k对最佳匹配,一个点对应两个最近的点(匹配的点更多了):

bf = cv2.BFMatcher()
# 一个点对应两个最近的
matches = bf.knnMatch(des1, des2, k=2)

#保留距离小于0.75的
good = []
for m, n in matches:
    if m.distance < 0.75 * n.distance:
        good.append([m])
# good:保留全部关键点(倒数第二个参数决定保存的关键点个数)
img3 = cv2.drawMatchesKnn(img1,kp1,img2,kp2,good,None,flags=2)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

在这里插入图片描述
4.图像拼接
需要拼接的图像:
在这里插入图片描述
在这里插入图片描述
结果:
在这里插入图片描述
关键代码,图像拼接:stitcher

class Stitcher:

    #拼接函数
    def stitch(self, images, ratio=0.75, reprojThresh=4.0,showMatches=False):
        #获取输入图片
        (imageB, imageA) = images
        #检测A、B图片的SIFT关键特征点,并计算特征描述子
        (kpsA, featuresA) = self.detectAndDescribe(imageA)
        (kpsB, featuresB) = self.detectAndDescribe(imageB)

        # 匹配两张图片的所有特征点,返回匹配结果
        M = self.matchKeypoints(kpsA, kpsB, featuresA, featuresB, ratio, reprojThresh)

        # 如果返回结果为空,没有匹配成功的特征点,退出算法
        if M is None:
            return None

        # 否则,提取匹配结果
        # H是3x3视角变换矩阵   
        #只需要知道可以通过一个M矩阵将A图片形状变换为B图片即可,目前不需要知道原理   
        (matches, H, status) = M
        # 将图片A进行视角变换,result是变换后图片
        result = cv2.warpPerspective(imageA, H, (imageA.shape[1] + imageB.shape[1], imageA.shape[0]))
        self.cv_show('result', result)
        # 将图片B传入result图片最左端,采用切片
        result[0:imageB.shape[0], 0:imageB.shape[1]] = imageB
        self.cv_show('result', result)
        # 检测是否需要显示图片匹配
        if showMatches:
            # 生成匹配图片,特征点连线图
            vis = self.drawMatches(imageA, imageB, kpsA, kpsB, matches, status)
            # 返回结果
            return (result, vis)

        # 返回匹配结果
        return result
        # 建立SIFT生成器
        descriptor = cv2.xfeatures2d.SIFT_create()
        # 检测SIFT特征点,并计算描述子
        (kps, features) = descriptor.detectAndCompute(image, None)

        # 将结果转换成NumPy数组
        kps = np.float32([kp.pt for kp in kps])

        # 返回特征点集,及对应的描述特征
        return (kps, features)

    def matchKeypoints(self, kpsA, kpsB, featuresA, featuresB, ratio, reprojThresh):
        # 建立暴力匹配器
        matcher = cv2.BFMatcher()
  
        # 使用KNN检测来自A、B图的SIFT特征匹配对,K=2
        rawMatches = matcher.knnMatch(featuresA, featuresB, 2)

        matches = []
        for m in rawMatches:
            # 当最近距离跟次近距离的比值小于ratio值时,保留此匹配对
            if len(m) == 2 and m[0].distance < m[1].distance * ratio:
            # 存储两个点在featuresA, featuresB中的索引值
                matches.append((m[0].trainIdx, m[0].queryIdx))

        # 当筛选后的匹配对大于4时,计算视角变换矩阵
        if len(matches) > 4:
            # 获取匹配对的点坐标
            ptsA = np.float32([kpsA[i] for (_, i) in matches])
            ptsB = np.float32([kpsB[i] for (i, _) in matches])

            # 计算视角变换矩阵
            (H, status) = cv2.findHomography(ptsA, ptsB, cv2.RANSAC, reprojThresh)

            # 返回结果
            return (matches, H, status)

        # 如果匹配对小于4时,返回None
        return None

    def drawMatches(self, imageA, imageB, kpsA, kpsB, matches, status):
        # 初始化可视化图片,将A、B图左右连接到一起
        (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

        # 联合遍历,画出匹配对
        for ((trainIdx, queryIdx), s) in zip(matches, status):
            # 当点对匹配成功时,画到可视化图上
            if s == 1:
                # 画出匹配对
                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 vis
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95

5.停车场车位识别
大步骤:将图像预处理,选出目标区域–训练训练集,用该算法识别目标区域中的停车场空位。
–读取图像
–得到图像的二值(两种方法,一种canny边缘检测/一种阈值(二值)),也叫背景过滤
–灰度
–边缘(灰度图相对于二值图得到的图像边缘效果更好)
–剔除无用的区域:获取关键点(手动调整标记),将关键点相连(利用边缘点获取的方式吗),mask填充(对选定的图像进行遮挡),剔除像素值不为255的地方。
–获得停车位:霍夫变换,获得横线,调整横线,过滤不符合要求的线(如斜线)。注意特殊列(第一和最后),数字标列(字典表示),调整,将图片分割出来保存(利用xy的坐标及车位之间的距离估计)(难,有关数学)。
–训练集训练得到结果并应用:标记图中的空余车位及计算两种车位的总数。
原图:
在这里插入图片描述
车位标识:
在这里插入图片描述

class Parking:

    def cv_show(self,name,img):
        cv2.imshow(name, img)
        cv2.waitKey(0)
        cv2.destroyAllWindows()
    
    # △
    def show_images(self, images, cmap=None):
        cols = 2
        rows = (len(images)+1)//cols
        
        plt.figure(figsize=(15, 12))
        for i, image in enumerate(images):
            plt.subplot(rows, cols, i+1)
            cmap = 'gray' if len(image.shape)==2 else cmap
            plt.imshow(image, cmap=cmap)
            plt.xticks([])
            plt.yticks([])
        plt.tight_layout(pad=0, h_pad=0, w_pad=0)
        plt.show()
    
   
    def select_rgb_white_yellow(self,image): 
        #过滤掉背景(只是过滤背景,并没有把无关的部分去掉,取边缘)
        lower = np.uint8([120, 120, 120])
        upper = np.uint8([255, 255, 255])
        # lower_red和高于upper_red的部分分别变成0,lower_red~upper_red之间的值变成255,相当于过滤背景
        white_mask = cv2.inRange(image, lower, upper)
        self.cv_show('white_mask',white_mask)
        
        masked = cv2.bitwise_and(image, image, mask = white_mask)
        self.cv_show('masked',masked)
        return masked
    
    def convert_gray_scale(self,image):
        return cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
    
    def detect_edges(self,image, low_threshold=50, high_threshold=200):
        return cv2.Canny(image, low_threshold, high_threshold)
    
    # 剔除操作,只保存像素值为255的地方
    def filter_region(self,image, vertices):
        """
                剔除掉不需要的地方
        """ 
        mask = np.zeros_like(image)
        if len(mask.shape)==2:
            cv2.fillPoly(mask, vertices, 255)
            self.cv_show('mask', mask)    
        return cv2.bitwise_and(image, mask)
    
    def select_region(self,image):
        """
                手动选择区域
        """
        # first, define the polygon by vertices
        rows, cols = image.shape[:2]
        pt_1  = [cols*0.05, rows*0.90]
        pt_2 = [cols*0.05, rows*0.70]
        pt_3 = [cols*0.30, rows*0.55]
        pt_4 = [cols*0.6, rows*0.15]
        pt_5 = [cols*0.90, rows*0.15] 
        pt_6 = [cols*0.90, rows*0.90]

        vertices = np.array([[pt_1, pt_2, pt_3, pt_4, pt_5, pt_6]], dtype=np.int32) 
        point_img = image.copy()       
        point_img = cv2.cvtColor(point_img, cv2.COLOR_GRAY2RGB)
        for point in vertices[0]:
            cv2.circle(point_img, (point[0],point[1]), 10, (0,0,255), 4)
        self.cv_show('point_img',point_img)
        
        return self.filter_region(image, vertices)
    
    def hough_lines(self,image):
        #输入的图像需要是边缘检测后的结果
        #minLineLengh(线的最短长度,比这个短的都被忽略)和MaxLineCap(两条直线之间的最大间隔,小于此值,认为是一条直线)
        #rho距离精度,theta角度精度,threshod超过设定阈值才被检测出线段
        return cv2.HoughLinesP(image, rho=0.1, theta=np.pi/10, threshold=15, minLineLength=9, maxLineGap=4)
        
    def draw_lines(self,image, lines, color=[255, 0, 0], thickness=2, make_copy=True):
        # 过滤霍夫变换检测到直线(通过线与线之间xy坐标的差值,过滤斜线等不符合要求的线)
        if make_copy:
            image = np.copy(image) 
        cleaned = []
        for line in lines:
            for x1,y1,x2,y2 in line:
                if abs(y2-y1) <=1 and abs(x2-x1) >=25 and abs(x2-x1) <= 55:
                    cleaned.append((x1,y1,x2,y2))
                    cv2.line(image, (x1, y1), (x2, y2), color, thickness)
        print(" No lines detected: ", len(cleaned))
        return image
    def identify_blocks(self,image, lines, make_copy=True):
        if make_copy:
            new_image = np.copy(image)
        #Step 1: 过滤部分直线
        cleaned = []
        for line in lines:
            for x1,y1,x2,y2 in line:
                if abs(y2-y1) <=1 and abs(x2-x1) >=25 and abs(x2-x1) <= 55:
                    cleaned.append((x1,y1,x2,y2))
        
        #Step 2: 对直线按照x1进行排序
        import operator
        list1 = sorted(cleaned, key=operator.itemgetter(0, 1))
        
        #Step 3: 找到多个列,相当于每列是一排车
        # cluster簇,代表一排车,作用为给车位标号
        clusters = {}
        dIndex = 0
        clus_dist = 10
    
        # 距离较近,为一簇,距离较远则不是一簇
        for i in range(len(list1) - 1):
            distance = abs(list1[i+1][0] - list1[i][0])
            if distance <= clus_dist:
                if not dIndex in clusters.keys(): clusters[dIndex] = []
                clusters[dIndex].append(list1[i])
                clusters[dIndex].append(list1[i + 1]) 
    
            else:
                dIndex += 1
        
        #Step 4: 得到坐标
        rects = {}
        i = 0
        for key in clusters:
            all_list = clusters[key]
            cleaned = list(set(all_list))
            if len(cleaned) > 5:
                cleaned = sorted(cleaned, key=lambda tup: tup[1])
                avg_y1 = cleaned[0][1]
                avg_y2 = cleaned[-1][1]
                avg_x1 = 0
                avg_x2 = 0
                for tup in cleaned:
                    avg_x1 += tup[0]
                    avg_x2 += tup[2]
                avg_x1 = avg_x1/len(cleaned)
                avg_x2 = avg_x2/len(cleaned)
                rects[i] = (avg_x1, avg_y1, avg_x2, avg_y2)
                i += 1
        
        print("Num Parking Lanes: ", len(rects))
        #Step 5: 把列矩形画出来
        buff = 7
        for key in rects:
            tup_topLeft = (int(rects[key][0] - buff), int(rects[key][1]))
            tup_botRight = (int(rects[key][2] + buff), int(rects[key][3]))
            cv2.rectangle(new_image, tup_topLeft,tup_botRight,(0,255,0),3)
        return new_image, rects
    
    def draw_parking(self,image, rects, make_copy = True, color=[255, 0, 0], thickness=2, save = True):
        if make_copy:
            new_image = np.copy(image)
        gap = 15.5
        spot_dict = {} # 字典:一个车位对应一个位置
        tot_spots = 0
        #微调
        adj_y1 = {0: 20, 1:-10, 2:0, 3:-11, 4:28, 5:5, 6:-15, 7:-15, 8:-10, 9:-30, 10:9, 11:-32}
        adj_y2 = {0: 30, 1: 50, 2:15, 3:10, 4:-15, 5:15, 6:15, 7:-20, 8:15, 9:15, 10:0, 11:30}
        
        adj_x1 = {0: -8, 1:-15, 2:-15, 3:-15, 4:-15, 5:-15, 6:-15, 7:-15, 8:-10, 9:-10, 10:-10, 11:0}
        adj_x2 = {0: 0, 1: 15, 2:15, 3:15, 4:15, 5:15, 6:15, 7:15, 8:10, 9:10, 10:10, 11:0}
        for key in rects:
            tup = rects[key]
            x1 = int(tup[0]+ adj_x1[key])
            x2 = int(tup[2]+ adj_x2[key])
            y1 = int(tup[1] + adj_y1[key])
            y2 = int(tup[3] + adj_y2[key])
            cv2.rectangle(new_image, (x1, y1),(x2,y2),(0,255,0),2)
            num_splits = int(abs(y2-y1)//gap)
            for i in range(0, num_splits+1):
                y = int(y1 + i*gap)
                cv2.line(new_image, (x1, y), (x2, y), color, thickness)
            if key > 0 and key < len(rects) -1 :        
                #竖直线
                x = int((x1 + x2)/2)
                cv2.line(new_image, (x, y1), (x, y2), color, thickness)
            # 计算数量
            if key == 0 or key == (len(rects) -1):
                tot_spots += num_splits +1
            else:
                tot_spots += 2*(num_splits +1)
                
            # 字典对应好
            if key == 0 or key == (len(rects) -1):
                for i in range(0, num_splits+1):
                    cur_len = len(spot_dict)
                    y = int(y1 + i*gap)
                    spot_dict[(x1, y, x2, y+gap)] = cur_len +1        
            else:
                for i in range(0, num_splits+1):
                    cur_len = len(spot_dict)
                    y = int(y1 + i*gap)
                    x = int((x1 + x2)/2)
                    spot_dict[(x1, y, x, y+gap)] = cur_len +1
                    spot_dict[(x, y, x2, y+gap)] = cur_len +2   
        
        print("total parking spaces: ", tot_spots, cur_len)
        if save:
            filename = 'with_parking.jpg'
            cv2.imwrite(filename, new_image)
        return new_image, spot_dict
    
    def assign_spots_map(self,image, spot_dict, make_copy = True, color=[255, 0, 0], thickness=2):
        if make_copy:
            new_image = np.copy(image)
        for spot in spot_dict.keys():
            (x1, y1, x2, y2) = spot
            cv2.rectangle(new_image, (int(x1),int(y1)), (int(x2),int(y2)), color, thickness)
        return new_image
    
    def save_images_for_cnn(self,image, spot_dict, folder_name ='cnn_data'):
        for spot in spot_dict.keys():
            (x1, y1, x2, y2) = spot
            (x1, y1, x2, y2) = (int(x1), int(y1), int(x2), int(y2))
            #裁剪
            spot_img = image[y1:y2, x1:x2]
            spot_img = cv2.resize(spot_img, (0,0), fx=2.0, fy=2.0) 
            spot_id = spot_dict[spot]
            
            filename = 'spot' + str(spot_id) +'.jpg'
            print(spot_img.shape, filename, (x1,x2,y1,y2))
            
            cv2.imwrite(os.path.join(folder_name, filename), spot_img)
    def make_prediction(self,image,model,class_dictionary):
        #预处理
        img = image/255.
    
        #转换成4D tensor
        image = np.expand_dims(img, axis=0)
    
        # 用训练好的模型进行训练
        class_predicted = model.predict(image)
        inID = np.argmax(class_predicted[0])
        label = class_dictionary[inID]
        return label
    def predict_on_image(self,image, spot_dict , model,class_dictionary,make_copy=True, color = [0, 255, 0], alpha=0.5):
        if make_copy:
            new_image = np.copy(image)
            overlay = np.copy(image)
        self.cv_show('new_image',new_image)
        cnt_empty = 0
        all_spots = 0
        for spot in spot_dict.keys():
            all_spots += 1
            (x1, y1, x2, y2) = spot
            (x1, y1, x2, y2) = (int(x1), int(y1), int(x2), int(y2))
            spot_img = image[y1:y2, x1:x2]
            spot_img = cv2.resize(spot_img, (48, 48)) 
            
            label = self.make_prediction(spot_img,model,class_dictionary)
            if label == 'empty':
                cv2.rectangle(overlay, (int(x1),int(y1)), (int(x2),int(y2)), color, -1)
                cnt_empty += 1
                
        cv2.addWeighted(overlay, alpha, new_image, 1 - alpha, 0, new_image)
                
        cv2.putText(new_image, "Available: %d spots" %cnt_empty, (30, 95),
        cv2.FONT_HERSHEY_SIMPLEX,
        0.7, (255, 255, 255), 2)
        
        cv2.putText(new_image, "Total: %d spots" %all_spots, (30, 125),
        cv2.FONT_HERSHEY_SIMPLEX,
        0.7, (255, 255, 255), 2)
        save = False
        
        if save:
            filename = 'with_marking.jpg'
            cv2.imwrite(filename, new_image)
        self.cv_show('new_image',new_image)
        
        return new_image
        
    def predict_on_video(self,video_name,final_spot_dict, model,class_dictionary,ret=True):   
        cap = cv2.VideoCapture(video_name)
        count = 0
        while ret:
            ret, image = cap.read()
            count += 1
            if count == 5:
                count = 0
                
                new_image = np.copy(image)
                overlay = np.copy(image)
                cnt_empty = 0
                all_spots = 0
                color = [0, 255, 0] 
                alpha=0.5
                for spot in final_spot_dict.keys():
                    all_spots += 1
                    (x1, y1, x2, y2) = spot
                    (x1, y1, x2, y2) = (int(x1), int(y1), int(x2), int(y2))
                    spot_img = image[y1:y2, x1:x2]
                    spot_img = cv2.resize(spot_img, (48,48)) 
    
                    label = self.make_prediction(spot_img,model,class_dictionary)
                    if label == 'empty':
                        cv2.rectangle(overlay, (int(x1),int(y1)), (int(x2),int(y2)), color, -1)
                        cnt_empty += 1
    
                cv2.addWeighted(overlay, alpha, new_image, 1 - alpha, 0, new_image)
    
                cv2.putText(new_image, "Available: %d spots" %cnt_empty, (30, 95),
                cv2.FONT_HERSHEY_SIMPLEX,
                0.7, (255, 255, 255), 2)
    
                cv2.putText(new_image, "Total: %d spots" %all_spots, (30, 125),
                cv2.FONT_HERSHEY_SIMPLEX,
                0.7, (255, 255, 255), 2)
                cv2.imshow('frame', new_image)
                if cv2.waitKey(10) & 0xFF == ord('q'):
                    break

        cv2.destroyAllWindows()
        cap.release()

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292
  • 293
  • 294
  • 295
  • 296
  • 297
  • 298
  • 299
  • 300
  • 301
  • 302
  • 303
  • 304
  • 305
  • 306
  • 307
  • 308
  • 309
  • 310
  • 311
  • 312
  • 313
  • 314
  • 315
  • 316
  • 317
  • 318

三.报错记录+知识点

1.Jupyter编辑器代码单元格处为In[*],代表当前单元格失效,代码无法执行。解决:查看右上角Python3状态栏右侧图标,实心圆代表连接忙,线性圆代表正常连接。重启kernel内核/将虚拟环境pyenv安装好。
2.查找(修改)jupyter所在的文件夹:
第一:打开Anaconda Prompt,输入指令jupyter notebook --generate-config
保存生成的文件路径,找到配置文件。
第二:找到.py为后缀的文件,用记事本格式打开。
第三:用ctrl+f查找c.NotebookApp.notebook_dir,在单引号后写入新路径。(需要全英文)
第四:右击jupyter图标-属性-目标,将尾部字符串“%…(省略)”删除-应用-确认。
最后检查:进入jupyter,输入以下代码,可得到所在文件夹。
import os
print(os.path.abspath(‘.’))
3.error: (-215:Assertion failed) size.width>0 && size.height>0 in fun
imshow(‘a’,b)中b的路径不对。可能是此处文件名不对,也可能是上一步imread中的路径不对(必须要用英文斜杠 / ,一个斜杠不行用两个)。
4.知识点:
什么是API?
Application Programming Interface程序之间的接口–程序之间的合约。
5.在Python的matplotlib库中,plt.subplot()函数用于添加子图到当前图形中。函数中的参数231表示一个三位数,每一位数都代表了不同的含义。百位数(2)表示子图的行数,十位数(3)表示子图的列数,个位数(1)表示当前正在创建或激活的子图的索引。因此,plt.subplot(231)意味着在一个2行3列的子图网格中,激活或创建第1个子图。
6.腐蚀操作+膨胀操作:当图片中有细小毛躁,可用腐蚀操作去掉,此时图片会有损毁,再用膨胀操作尽力恢复。
7.not enough values to unpack (expected 3, got 2):
计算轮廓的函数已经有更新,现在只能返回两个结果:
digitCnts,hierarchy = cv2.findContours(group.copy(), cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
8.在边缘检测之前执行滤波操作主要有以下几个原因:
降噪:边缘检测往往会受到图像中噪声的干扰,通过滤波可以降低图像中的噪声,提高边缘检测的准确性。
平滑图像:滤波操作可以使图像变得更为平滑,这样在进行边缘检测时,不会受到图像本身纹理和细节的影响,提高边缘检测的精度。
改进检测性能:滤波操作可以抑制边缘检测中可能出现的伪峰和毛刺现象,提高边缘检测的稳定性和性能。
滤波操作对于边缘检测来说是重要的前置处理步骤,它可以帮助我们更好地提取出图像中的边缘信息,提高边缘检测的精度和稳定性。
9.读取图像的路径中不要有中文
10.cnts=cv2.findContours(edged.copy(),cv2.RETR_LIST,cv2.CHAIN_APPROX_SIMPLE)[0]
该函数下标索引更新为0:[0]
11.vscode打开新窗口:左上角 Files -> open new window 然后在新的窗口里把工程再打开一次。
12.归一化的目的:为使结果更加均衡。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/141049
推荐阅读
相关标签
  

闽ICP备14008679号