gordonchan commited on
Commit
cc30486
·
verified ·
1 Parent(s): 5916d45

Upload 51 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. beautyPlugin/GrindSkin.py +43 -0
  2. beautyPlugin/MakeBeautiful.py +45 -0
  3. beautyPlugin/MakeWhiter.py +108 -0
  4. beautyPlugin/ThinFace.py +267 -0
  5. beautyPlugin/__init__.py +4 -0
  6. beautyPlugin/lut_image/1.png +0 -0
  7. beautyPlugin/lut_image/3.png +0 -0
  8. beautyPlugin/lut_image/lutOrigin.png +0 -0
  9. hivisionai/__init__.py +0 -0
  10. hivisionai/app.py +452 -0
  11. hivisionai/hyService/__init__.py +0 -0
  12. hivisionai/hyService/cloudService.py +406 -0
  13. hivisionai/hyService/dbTools.py +337 -0
  14. hivisionai/hyService/error.py +20 -0
  15. hivisionai/hyService/serviceTest.py +34 -0
  16. hivisionai/hyService/utils.py +92 -0
  17. hivisionai/hyTrain/APIs.py +197 -0
  18. hivisionai/hyTrain/DataProcessing.py +37 -0
  19. hivisionai/hyTrain/__init__.py +0 -0
  20. hivisionai/hycv/FaceDetection68/__init__.py +8 -0
  21. hivisionai/hycv/FaceDetection68/faceDetection68.py +443 -0
  22. hivisionai/hycv/__init__.py +1 -0
  23. hivisionai/hycv/error.py +16 -0
  24. hivisionai/hycv/face_tools.py +427 -0
  25. hivisionai/hycv/idphoto.py +2 -0
  26. hivisionai/hycv/idphotoTool/__init__.py +0 -0
  27. hivisionai/hycv/idphotoTool/cuny_tools.py +593 -0
  28. hivisionai/hycv/idphotoTool/idphoto_change_cloth.py +271 -0
  29. hivisionai/hycv/idphotoTool/idphoto_cut.py +420 -0
  30. hivisionai/hycv/idphotoTool/move_image.py +121 -0
  31. hivisionai/hycv/idphotoTool/neck_processing.py +320 -0
  32. hivisionai/hycv/matting_tools.py +39 -0
  33. hivisionai/hycv/mtcnn_onnx/__init__.py +2 -0
  34. hivisionai/hycv/mtcnn_onnx/box_utils.py +238 -0
  35. hivisionai/hycv/mtcnn_onnx/detector.py +166 -0
  36. hivisionai/hycv/mtcnn_onnx/first_stage.py +97 -0
  37. hivisionai/hycv/mtcnn_onnx/visualization_utils.py +31 -0
  38. hivisionai/hycv/tensor2numpy.py +63 -0
  39. hivisionai/hycv/utils.py +452 -0
  40. hivisionai/hycv/vision.py +446 -0
  41. images/test.jpg +0 -0
  42. images/test2.jpg +0 -0
  43. images/test3.jpg +0 -0
  44. images/test4.jpg +0 -0
  45. src/EulerZ.py +51 -0
  46. src/cuny_tools.py +621 -0
  47. src/error.py +27 -0
  48. src/face_judgement_align.py +576 -0
  49. src/imageTransform.py +218 -0
  50. src/layoutCreate.py +113 -0
beautyPlugin/GrindSkin.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ @author: cuny
3
+ @file: GrindSkin.py
4
+ @time: 2022/7/2 14:44
5
+ @description:
6
+ 磨皮算法
7
+ """
8
+ import cv2
9
+ import numpy as np
10
+
11
+
12
+ def grindSkin(src, grindDegree: int = 3, detailDegree: int = 1, strength: int = 9):
13
+ """
14
+ Dest =(Src * (100 - Opacity) + (Src + 2 * GaussBlur(EPFFilter(Src) - Src)) * Opacity) /100
15
+ 人像磨皮方案,后续会考虑使用一些皮肤区域检测算法来实现仅皮肤区域磨皮,增加算法的精细程度——或者使用人脸关键点
16
+ https://www.cnblogs.com/Imageshop/p/4709710.html
17
+ Args:
18
+ src: 原图
19
+ grindDegree: 磨皮程度调节参数
20
+ detailDegree: 细节程度调节参数
21
+ strength: 融合程度,作为磨皮强度(0 - 10)
22
+
23
+ Returns:
24
+ 磨皮后的图像
25
+ """
26
+ if strength <= 0:
27
+ return src
28
+ dst = src.copy()
29
+ opacity = min(10., strength) / 10.
30
+ dx = grindDegree * 5 # 双边滤波参数之一
31
+ fc = grindDegree * 12.5 # 双边滤波参数之一
32
+ temp1 = cv2.bilateralFilter(src[:, :, :3], dx, fc, fc)
33
+ temp2 = cv2.subtract(temp1, src[:, :, :3])
34
+ temp3 = cv2.GaussianBlur(temp2, (2 * detailDegree - 1, 2 * detailDegree - 1), 0)
35
+ temp4 = cv2.add(cv2.add(temp3, temp3), src[:, :, :3])
36
+ dst[:, :, :3] = cv2.addWeighted(temp4, opacity, src[:, :, :3], 1 - opacity, 0.0)
37
+ return dst
38
+
39
+
40
+ if __name__ == "__main__":
41
+ input_image = cv2.imread("test_image/7.jpg")
42
+ output_image = grindSkin(src=input_image)
43
+ cv2.imwrite("grindSkinCompare.png", np.hstack((input_image, output_image)))
beautyPlugin/MakeBeautiful.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ @author: cuny
3
+ @file: MakeBeautiful.py
4
+ @time: 2022/7/7 20:23
5
+ @description:
6
+ 美颜工具集合文件,作为暴露在外的插件接口
7
+ """
8
+ from .GrindSkin import grindSkin
9
+ from .MakeWhiter import MakeWhiter
10
+ from .ThinFace import thinFace
11
+ import numpy as np
12
+
13
+
14
+ def makeBeautiful(input_image: np.ndarray,
15
+ landmark,
16
+ thinStrength: int,
17
+ thinPlace: int,
18
+ grindStrength: int,
19
+ whiterStrength: int
20
+ ) -> np.ndarray:
21
+ """
22
+ 美颜工具的接口函数,用于实现美颜效果
23
+ Args:
24
+ input_image: 输入的图像
25
+ landmark: 瘦脸需要的人脸关键点信息,为fd68返回的第二个参数
26
+ thinStrength: 瘦脸强度,为0-10(如果更高其实也没什么问题),当强度为0或者更低时,则不瘦脸
27
+ thinPlace: 选择瘦脸区域,为0-2之间的值,越大瘦脸的点越靠下
28
+ grindStrength: 磨皮强度,为0-10(如果更高其实也没什么问题),当强度为0或者更低时,则不磨皮
29
+ whiterStrength: 美白强度,为0-10(如果更高其实也没什么问题),当强度为0或者更低时,则不美白
30
+ Returns:
31
+ output_image 输出图像
32
+ """
33
+ try:
34
+ _, _, _ = input_image.shape
35
+ except ValueError:
36
+ raise TypeError("输入图像必须为3通道或者4通道!")
37
+ # 三通道或者四通道图像
38
+ # 首先进行瘦脸
39
+ input_image = thinFace(input_image, landmark, place=thinPlace, strength=thinStrength)
40
+ # 其次进行磨皮
41
+ input_image = grindSkin(src=input_image, strength=grindStrength)
42
+ # 最后进行美白
43
+ makeWhiter = MakeWhiter()
44
+ input_image = makeWhiter.run(input_image, strength=whiterStrength)
45
+ return input_image
beautyPlugin/MakeWhiter.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ @author: cuny
3
+ @file: MakeWhiter.py
4
+ @time: 2022/7/2 14:28
5
+ @description:
6
+ 美白算法
7
+ """
8
+ import os
9
+ import cv2
10
+ import math
11
+ import numpy as np
12
+ local_path = os.path.dirname(__file__)
13
+
14
+
15
+ class MakeWhiter(object):
16
+ class __LutWhite:
17
+ """
18
+ 美白的内部类
19
+ """
20
+
21
+ def __init__(self, lut):
22
+ cube64rows = 8
23
+ cube64size = 64
24
+ cube256size = 256
25
+ cubeScale = int(cube256size / cube64size) # 4
26
+
27
+ reshapeLut = np.zeros((cube256size, cube256size, cube256size, 3))
28
+ for i in range(cube64size):
29
+ tmp = math.floor(i / cube64rows)
30
+ cx = int((i - tmp * cube64rows) * cube64size)
31
+ cy = int(tmp * cube64size)
32
+ cube64 = lut[cy:cy + cube64size, cx:cx + cube64size] # cube64 in lut(512*512 (512=8*64))
33
+ _rows, _cols, _ = cube64.shape
34
+ if _rows == 0 or _cols == 0:
35
+ continue
36
+ cube256 = cv2.resize(cube64, (cube256size, cube256size))
37
+ i = i * cubeScale
38
+ for k in range(cubeScale):
39
+ reshapeLut[i + k] = cube256
40
+ self.lut = reshapeLut
41
+
42
+ def imageInLut(self, src):
43
+ arr = src.copy()
44
+ bs = arr[:, :, 0]
45
+ gs = arr[:, :, 1]
46
+ rs = arr[:, :, 2]
47
+ arr[:, :] = self.lut[bs, gs, rs]
48
+ return arr
49
+
50
+ def __init__(self, lutImage: np.ndarray = None):
51
+ self.__lutWhiten = None
52
+ if lutImage is not None:
53
+ self.__lutWhiten = self.__LutWhite(lutImage)
54
+
55
+ def setLut(self, lutImage: np.ndarray):
56
+ self.__lutWhiten = self.__LutWhite(lutImage)
57
+
58
+ @staticmethod
59
+ def generate_identify_color_matrix(size: int = 512, channel: int = 3) -> np.ndarray:
60
+ """
61
+ 用于生成一张初始的查找表
62
+ Args:
63
+ size: 查找表尺寸,默认为512
64
+ channel: 查找表通道数,默认为3
65
+
66
+ Returns:
67
+ 返回生成的查找表图像
68
+ """
69
+ img = np.zeros((size, size, channel), dtype=np.uint8)
70
+ for by in range(size // 64):
71
+ for bx in range(size // 64):
72
+ for g in range(64):
73
+ for r in range(64):
74
+ x = r + bx * 64
75
+ y = g + by * 64
76
+ img[y][x][0] = int(r * 255.0 / 63.0 + 0.5)
77
+ img[y][x][1] = int(g * 255.0 / 63.0 + 0.5)
78
+ img[y][x][2] = int((bx + by * 8.0) * 255.0 / 63.0 + 0.5)
79
+ return cv2.cvtColor(img, cv2.COLOR_RGB2BGR).clip(0, 255).astype('uint8')
80
+
81
+ def run(self, src: np.ndarray, strength: int) -> np.ndarray:
82
+ """
83
+ 美白图像
84
+ Args:
85
+ src: 原图
86
+ strength: 美白强度,0 - 10
87
+ Returns:
88
+ 美白后的图像
89
+ """
90
+ dst = src.copy()
91
+ strength = min(10, int(strength)) / 10.
92
+ if strength <= 0:
93
+ return dst
94
+ self.setLut(cv2.imread(f"{local_path}/lut_image/3.png", -1))
95
+ _, _, c = src.shape
96
+ img = self.__lutWhiten.imageInLut(src[:, :, :3])
97
+ dst[:, :, :3] = cv2.addWeighted(src[:, :, :3], 1 - strength, img, strength, 0)
98
+ return dst
99
+
100
+
101
+ if __name__ == "__main__":
102
+ # makeLut = MakeWhiter()
103
+ # cv2.imwrite("lutOrigin.png", makeLut.generate_identify_color_matrix())
104
+ input_image = cv2.imread("test_image/7.jpg", -1)
105
+ lut_image = cv2.imread("lut_image/3.png")
106
+ makeWhiter = MakeWhiter(lut_image)
107
+ output_image = makeWhiter.run(input_image, 10)
108
+ cv2.imwrite("makeWhiterCompare.png", np.hstack((input_image, output_image)))
beautyPlugin/ThinFace.py ADDED
@@ -0,0 +1,267 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ @author: cuny
3
+ @file: ThinFace.py
4
+ @time: 2022/7/2 15:50
5
+ @description:
6
+ 瘦脸算法,用到了图像局部平移法
7
+ 先使用人脸关键点检测,然后再使用图像局部平移法
8
+ 需要注意的是,这部分不会包含dlib人脸关键点检测,因为考虑到模型载入的问题
9
+ """
10
+ import cv2
11
+ import math
12
+ import numpy as np
13
+
14
+
15
+ class TranslationWarp(object):
16
+ """
17
+ 本类包含瘦脸算法,由于瘦脸算法包含了很多个版本,所以以类的方式呈现
18
+ 前两个算法没什么好讲的,网上资料很多
19
+ 第三个采用numpy内部的自定义函数处理,在处理速度上有一些提升
20
+ 最后采用cv2.map算法,处理速度大幅度提升
21
+ """
22
+
23
+ # 瘦脸
24
+ @staticmethod
25
+ def localTranslationWarp(srcImg, startX, startY, endX, endY, radius):
26
+ # 双线性插值法
27
+ def BilinearInsert(src, ux, uy):
28
+ w, h, c = src.shape
29
+ if c == 3:
30
+ x1 = int(ux)
31
+ x2 = x1 + 1
32
+ y1 = int(uy)
33
+ y2 = y1 + 1
34
+ part1 = src[y1, x1].astype(np.float64) * (float(x2) - ux) * (float(y2) - uy)
35
+ part2 = src[y1, x2].astype(np.float64) * (ux - float(x1)) * (float(y2) - uy)
36
+ part3 = src[y2, x1].astype(np.float64) * (float(x2) - ux) * (uy - float(y1))
37
+ part4 = src[y2, x2].astype(np.float64) * (ux - float(x1)) * (uy - float(y1))
38
+ insertValue = part1 + part2 + part3 + part4
39
+ return insertValue.astype(np.int8)
40
+
41
+ ddradius = float(radius * radius) # 圆的半径
42
+ copyImg = srcImg.copy() # copy后的图像矩阵
43
+ # 计算公式中的|m-c|^2
44
+ ddmc = (endX - startX) * (endX - startX) + (endY - startY) * (endY - startY)
45
+ H, W, C = srcImg.shape # 获取图像的形状
46
+ for i in range(W):
47
+ for j in range(H):
48
+ # # 计算该点是否在形变圆的范围之内
49
+ # # 优化,第一步,直接判断是会在(startX,startY)的矩阵框中
50
+ if math.fabs(i - startX) > radius and math.fabs(j - startY) > radius:
51
+ continue
52
+ distance = (i - startX) * (i - startX) + (j - startY) * (j - startY)
53
+ if distance < ddradius:
54
+ # 计算出(i,j)坐标的原坐标
55
+ # 计算公式中右边平方号里的部分
56
+ ratio = (ddradius - distance) / (ddradius - distance + ddmc)
57
+ ratio = ratio * ratio
58
+ # 映射原位置
59
+ UX = i - ratio * (endX - startX)
60
+ UY = j - ratio * (endY - startY)
61
+
62
+ # 根据双线性插值法得到UX,UY的值
63
+ # start_ = time.time()
64
+ value = BilinearInsert(srcImg, UX, UY)
65
+ # print(f"双线性插值耗时;{time.time() - start_}")
66
+ # 改变当前 i ,j的值
67
+ copyImg[j, i] = value
68
+ return copyImg
69
+
70
+ # 瘦脸pro1, 限制了for循环的遍历次数
71
+ @staticmethod
72
+ def localTranslationWarpLimitFor(srcImg, startP: np.matrix, endP: np.matrix, radius: float):
73
+ startX, startY = startP[0, 0], startP[0, 1]
74
+ endX, endY = endP[0, 0], endP[0, 1]
75
+
76
+ # 双线性插值法
77
+ def BilinearInsert(src, ux, uy):
78
+ w, h, c = src.shape
79
+ if c == 3:
80
+ x1 = int(ux)
81
+ x2 = x1 + 1
82
+ y1 = int(uy)
83
+ y2 = y1 + 1
84
+ part1 = src[y1, x1].astype(np.float64) * (float(x2) - ux) * (float(y2) - uy)
85
+ part2 = src[y1, x2].astype(np.float64) * (ux - float(x1)) * (float(y2) - uy)
86
+ part3 = src[y2, x1].astype(np.float64) * (float(x2) - ux) * (uy - float(y1))
87
+ part4 = src[y2, x2].astype(np.float64) * (ux - float(x1)) * (uy - float(y1))
88
+ insertValue = part1 + part2 + part3 + part4
89
+ return insertValue.astype(np.int8)
90
+
91
+ ddradius = float(radius * radius) # 圆的半径
92
+ copyImg = srcImg.copy() # copy后的图像矩阵
93
+ # 计算公式中的|m-c|^2
94
+ ddmc = (endX - startX) ** 2 + (endY - startY) ** 2
95
+ # 计算正方形的左上角起始点
96
+ startTX, startTY = (startX - math.floor(radius + 1), startY - math.floor((radius + 1)))
97
+ # 计算正方形的右下角的结束点
98
+ endTX, endTY = (startX + math.floor(radius + 1), startY + math.floor((radius + 1)))
99
+ # 剪切srcImg
100
+ srcImg = srcImg[startTY: endTY + 1, startTX: endTX + 1, :]
101
+ # db.cv_show(srcImg)
102
+ # 裁剪后的图像相当于在x,y都减少了startX - math.floor(radius + 1)
103
+ # 原本的endX, endY在切后的坐标点
104
+ endX, endY = (endX - startX + math.floor(radius + 1), endY - startY + math.floor(radius + 1))
105
+ # 原���的startX, startY剪切后的坐标点
106
+ startX, startY = (math.floor(radius + 1), math.floor(radius + 1))
107
+ H, W, C = srcImg.shape # 获取图像的形状
108
+ for i in range(W):
109
+ for j in range(H):
110
+ # 计算该点是否在形变圆的范围之内
111
+ # 优化,第一步,直接判断是会在(startX,startY)的矩阵框中
112
+ # if math.fabs(i - startX) > radius and math.fabs(j - startY) > radius:
113
+ # continue
114
+ distance = (i - startX) * (i - startX) + (j - startY) * (j - startY)
115
+ if distance < ddradius:
116
+ # 计算出(i,j)坐标的原坐标
117
+ # 计算公式中右边平方号里的部分
118
+ ratio = (ddradius - distance) / (ddradius - distance + ddmc)
119
+ ratio = ratio * ratio
120
+ # 映射原位置
121
+ UX = i - ratio * (endX - startX)
122
+ UY = j - ratio * (endY - startY)
123
+
124
+ # 根据双线性插值法得到UX,UY的值
125
+ # start_ = time.time()
126
+ value = BilinearInsert(srcImg, UX, UY)
127
+ # print(f"双线性插值耗时;{time.time() - start_}")
128
+ # 改变当前 i ,j的值
129
+ copyImg[j + startTY, i + startTX] = value
130
+ return copyImg
131
+
132
+ # # 瘦脸pro2,采用了numpy自定义函数做处理
133
+ # def localTranslationWarpNumpy(self, srcImg, startP: np.matrix, endP: np.matrix, radius: float):
134
+ # startX , startY = startP[0, 0], startP[0, 1]
135
+ # endX, endY = endP[0, 0], endP[0, 1]
136
+ # ddradius = float(radius * radius) # 圆的半径
137
+ # copyImg = srcImg.copy() # copy后的图像矩阵
138
+ # # 计算公式中的|m-c|^2
139
+ # ddmc = (endX - startX)**2 + (endY - startY)**2
140
+ # # 计算正方形的左上角起始点
141
+ # startTX, startTY = (startX - math.floor(radius + 1), startY - math.floor((radius + 1)))
142
+ # # 计算正方形的右下角的结束点
143
+ # endTX, endTY = (startX + math.floor(radius + 1), startY + math.floor((radius + 1)))
144
+ # # 剪切srcImg
145
+ # self.thinImage = srcImg[startTY : endTY + 1, startTX : endTX + 1, :]
146
+ # # s = self.thinImage
147
+ # # db.cv_show(srcImg)
148
+ # # 裁剪后的图像相当于在x,y都减少了startX - math.floor(radius + 1)
149
+ # # 原本的endX, endY在切后的坐标点
150
+ # endX, endY = (endX - startX + math.floor(radius + 1), endY - startY + math.floor(radius + 1))
151
+ # # 原本的startX, startY剪切后的坐标点
152
+ # startX ,startY = (math.floor(radius + 1), math.floor(radius + 1))
153
+ # H, W, C = self.thinImage.shape # 获取图像的形状
154
+ # index_m = np.arange(H * W).reshape((H, W))
155
+ # triangle_ufunc = np.frompyfunc(self.process, 9, 3)
156
+ # # start_ = time.time()
157
+ # finalImgB, finalImgG, finalImgR = triangle_ufunc(index_m, self, W, ddradius, ddmc, startX, startY, endX, endY)
158
+ # finaleImg = np.dstack((finalImgB, finalImgG, finalImgR)).astype(np.uint8)
159
+ # finaleImg = np.fliplr(np.rot90(finaleImg, -1))
160
+ # copyImg[startTY: endTY + 1, startTX: endTX + 1, :] = finaleImg
161
+ # # print(f"图像处理耗时;{time.time() - start_}")
162
+ # # db.cv_show(copyImg)
163
+ # return copyImg
164
+
165
+ # 瘦脸pro3,采用opencv内置函数
166
+ @staticmethod
167
+ def localTranslationWarpFastWithStrength(srcImg, startP: np.matrix, endP: np.matrix, radius, strength: float = 100.):
168
+ """
169
+ 采用opencv内置函数
170
+ Args:
171
+ srcImg: 源图像
172
+ startP: 起点位置
173
+ endP: 终点位置
174
+ radius: 处理半径
175
+ strength: 瘦脸强度,一般取100以上
176
+
177
+ Returns:
178
+
179
+ """
180
+ startX, startY = startP[0, 0], startP[0, 1]
181
+ endX, endY = endP[0, 0], endP[0, 1]
182
+ ddradius = float(radius * radius)
183
+ # copyImg = np.zeros(srcImg.shape, np.uint8)
184
+ # copyImg = srcImg.copy()
185
+
186
+ maskImg = np.zeros(srcImg.shape[:2], np.uint8)
187
+ cv2.circle(maskImg, (startX, startY), math.ceil(radius), (255, 255, 255), -1)
188
+
189
+ K0 = 100 / strength
190
+
191
+ # 计算公式中的|m-c|^2
192
+ ddmc_x = (endX - startX) * (endX - startX)
193
+ ddmc_y = (endY - startY) * (endY - startY)
194
+ H, W, C = srcImg.shape
195
+
196
+ mapX = np.vstack([np.arange(W).astype(np.float32).reshape(1, -1)] * H)
197
+ mapY = np.hstack([np.arange(H).astype(np.float32).reshape(-1, 1)] * W)
198
+
199
+ distance_x = (mapX - startX) * (mapX - startX)
200
+ distance_y = (mapY - startY) * (mapY - startY)
201
+ distance = distance_x + distance_y
202
+ K1 = np.sqrt(distance)
203
+ ratio_x = (ddradius - distance_x) / (ddradius - distance_x + K0 * ddmc_x)
204
+ ratio_y = (ddradius - distance_y) / (ddradius - distance_y + K0 * ddmc_y)
205
+ ratio_x = ratio_x * ratio_x
206
+ ratio_y = ratio_y * ratio_y
207
+
208
+ UX = mapX - ratio_x * (endX - startX) * (1 - K1 / radius)
209
+ UY = mapY - ratio_y * (endY - startY) * (1 - K1 / radius)
210
+
211
+ np.copyto(UX, mapX, where=maskImg == 0)
212
+ np.copyto(UY, mapY, where=maskImg == 0)
213
+ UX = UX.astype(np.float32)
214
+ UY = UY.astype(np.float32)
215
+ copyImg = cv2.remap(srcImg, UX, UY, interpolation=cv2.INTER_LINEAR)
216
+ return copyImg
217
+
218
+
219
+ def thinFace(src, landmark, place: int = 0, strength=30.):
220
+ """
221
+ 瘦脸程序接口,输入人脸关键点信息和强度,即可实现瘦脸
222
+ 注意处理四通道图像
223
+ Args:
224
+ src: 原图
225
+ landmark: 关键点信息
226
+ place: 选择瘦脸区域,为0-4之间的值
227
+ strength: 瘦脸强度,输入值在0-10之间,如果小于或者等于0,则不瘦脸
228
+
229
+ Returns:
230
+ 瘦脸后的图像
231
+ """
232
+ strength = min(100., strength * 10.)
233
+ if strength <= 0.:
234
+ return src
235
+ # 也可以设置瘦脸区域
236
+ place = max(0, min(4, int(place)))
237
+ left_landmark = landmark[4 + place]
238
+ left_landmark_down = landmark[6 + place]
239
+ right_landmark = landmark[13 + place]
240
+ right_landmark_down = landmark[15 + place]
241
+ endPt = landmark[58]
242
+ # 计算第4个点到第6个点的距离作为瘦脸距离
243
+ r_left = math.sqrt(
244
+ (left_landmark[0, 0] - left_landmark_down[0, 0]) ** 2 +
245
+ (left_landmark[0, 1] - left_landmark_down[0, 1]) ** 2
246
+ )
247
+
248
+ # 计算第14个点到第16个点的距离作为瘦脸距离
249
+ r_right = math.sqrt((right_landmark[0, 0] - right_landmark_down[0, 0]) ** 2 +
250
+ (right_landmark[0, 1] - right_landmark_down[0, 1]) ** 2)
251
+ # 瘦左边脸
252
+ thin_image = TranslationWarp.localTranslationWarpFastWithStrength(src, left_landmark[0], endPt[0], r_left, strength)
253
+ # 瘦右边脸
254
+ thin_image = TranslationWarp.localTranslationWarpFastWithStrength(thin_image, right_landmark[0], endPt[0], r_right, strength)
255
+ return thin_image
256
+
257
+
258
+ if __name__ == "__main__":
259
+ import os
260
+ from hycv.FaceDetection68.faceDetection68 import FaceDetection68
261
+ local_file = os.path.dirname(__file__)
262
+ PREDICTOR_PATH = f"{local_file}/weights/shape_predictor_68_face_landmarks.dat" # 关键点检测模型路径
263
+ fd68 = FaceDetection68(model_path=PREDICTOR_PATH)
264
+ input_image = cv2.imread("test_image/4.jpg", -1)
265
+ _, landmark_, _ = fd68.facePoints(input_image)
266
+ output_image = thinFace(input_image, landmark_, strength=30.2)
267
+ cv2.imwrite("thinFaceCompare.png", np.hstack((input_image, output_image)))
beautyPlugin/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ from .MakeBeautiful import makeBeautiful
2
+
3
+
4
+
beautyPlugin/lut_image/1.png ADDED
beautyPlugin/lut_image/3.png ADDED
beautyPlugin/lut_image/lutOrigin.png ADDED
hivisionai/__init__.py ADDED
File without changes
hivisionai/app.py ADDED
@@ -0,0 +1,452 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+
3
+ """
4
+ @Time : 2022/8/27 14:17
5
+ @Author : cuny
6
+ @File : app.py
7
+ @Software : PyCharm
8
+ @Introduce:
9
+ 查看包版本等一系列操作
10
+ """
11
+ import os
12
+ import sys
13
+ import json
14
+ import shutil
15
+ import zipfile
16
+ import requests
17
+ from argparse import ArgumentParser
18
+ from importlib.metadata import version
19
+ try: # 加上这个try的原因在于本地环境和云函数端的import形式有所不同
20
+ from qcloud_cos import CosConfig
21
+ from qcloud_cos import CosS3Client
22
+ except ImportError:
23
+ try:
24
+ from qcloud_cos_v5 import CosConfig
25
+ from qcloud_cos_v5 import CosS3Client
26
+ from qcloud_cos.cos_exception import CosServiceError
27
+ except ImportError:
28
+ raise ImportError("请下载腾讯云COS相关代码包:pip install cos-python-sdk-v5")
29
+
30
+
31
+ class HivisionaiParams(object):
32
+ """
33
+ 定义一些基本常量
34
+ """
35
+ # 文件所在路径
36
+ # 包名称
37
+ package_name = "HY-sdk"
38
+ # 腾讯云相关变量
39
+ region = "ap-beijing"
40
+ zip_key = "HY-sdk/" # zip存储的云端文件夹路径,这里改了publish.yml也需要更改
41
+ # 云端用户配置,如果在cloud_config_save不存在,就需要下载此文件
42
+ user_url = "https://hy-sdk-config-1305323352.cos.ap-beijing.myqcloud.com/sdk-user/user_config.json"
43
+ bucket = "cloud-public-static-1306602019"
44
+ # 压缩包类型
45
+ file_format = ".zip"
46
+ # 下载路径(.hivisionai文件夹路径)
47
+ download_path = os.path.expandvars('$HOME')
48
+ # zip文件、zip解压缩文件的存放路径
49
+ save_folder = f"{os.path.expandvars('$HOME')}/.hivisionai/sdk"
50
+ # 腾讯云配置文件存放路径
51
+ cloud_config_save = f"{os.path.expandvars('$HOME')}/.hivisionai/user_config.json"
52
+ # 项目路径
53
+ hivisionai_path = os.path.dirname(os.path.dirname(__file__))
54
+ # 使用hivisionai的路径
55
+ getcwd = os.getcwd()
56
+ # HY-func的依赖配置
57
+ # 每个依赖会包含三个参数,保存路径(save_path,相对于HY_func的路径)、下载url(url)
58
+ functionDependence = {
59
+ "configs": [
60
+ # --------- 配置文件部分
61
+ # _lib
62
+ {
63
+ "url": "https://hy-sdk-config-1305323352.cos.ap-beijing.myqcloud.com/hy-func/_lib/config/aliyun-human-matting-api.json",
64
+ "save_path": "_lib/config/aliyun-human-matting-api.json"
65
+ },
66
+ {
67
+ "url": "https://hy-sdk-config-1305323352.cos.ap-beijing.myqcloud.com/hy-func/_lib/config/megvii-face-plus-api.json",
68
+ "save_path": "_lib/config/megvii-face-plus-api.json"
69
+ },
70
+ {
71
+ "url": "https://hy-sdk-config-1305323352.cos.ap-beijing.myqcloud.com/hy-func/_lib/config/volcano-face-change-api.json",
72
+ "save_path": "_lib/config/volcano-face-change-api.json"
73
+ },
74
+ # _service
75
+ {
76
+ "url": "https://hy-sdk-config-1305323352.cos.ap-beijing.myqcloud.com/hy-func/_service/config/func_error_conf.json",
77
+ "save_path": "_service/utils/config/func_error_conf.json"
78
+ },
79
+ {
80
+ "url": "https://hy-sdk-config-1305323352.cos.ap-beijing.myqcloud.com/hy-func/_service/config/service_config.json",
81
+ "save_path": "_service/utils/config/service_config.json"
82
+ },
83
+ # --------- 模型部分
84
+ # 模型部分存储在Notion文档当中
85
+ # https://www.notion.so/HY-func-cc6cc41ba6e94b36b8fa5f5d67d1683f
86
+ ],
87
+ "weights": "https://www.notion.so/HY-func-cc6cc41ba6e94b36b8fa5f5d67d1683f"
88
+ }
89
+
90
+
91
+ class HivisionaiUtils(object):
92
+ """
93
+ 本类为一些基本工具类,包含代码复用相关内容
94
+ """
95
+ @staticmethod
96
+ def get_client():
97
+ """获取cos客户端对象"""
98
+ def get_secret():
99
+ # 首先判断cloud_config_save下是否存在
100
+ if not os.path.exists(HivisionaiParams.cloud_config_save):
101
+ print("Downloading user_config...")
102
+ resp = requests.get(HivisionaiParams.user_url)
103
+ open(HivisionaiParams.cloud_config_save, "wb").write(resp.content)
104
+ config = json.load(open(HivisionaiParams.cloud_config_save, "r"))
105
+ return config["secret_id"], config["secret_key"]
106
+ # todo 接入HY-Auth-Sync
107
+ secret_id, secret_key = get_secret()
108
+ return CosS3Client(CosConfig(Region=HivisionaiParams.region, Secret_id=secret_id, Secret_key=secret_key))
109
+
110
+ def get_all_versions(self):
111
+ """获取云端的所有版本号"""
112
+ def getAllVersion_base():
113
+ """
114
+ 返回cos存储桶内部的某个文件夹的内部名称
115
+ ps:如果需要修改默认的存储桶配置,请在代码运行的时候加入代码 s.bucket = 存储桶名称 (s是对象实例)
116
+ 返回的内容存储在response["Content"],不过返回的数据大小是有限制的,具体内容还是请看官方文档。
117
+ Returns:
118
+ [版本列表]
119
+ """
120
+ resp = client.list_objects(
121
+ Bucket=HivisionaiParams.bucket,
122
+ Prefix=HivisionaiParams.zip_key,
123
+ Marker=marker
124
+ )
125
+ versions_list.extend([x["Key"].split("/")[-1].split(HivisionaiParams.file_format)[0] for x in resp["Contents"] if int(x["Size"]) > 0])
126
+ if resp['IsTruncated'] == 'false': # 接下来没有数据了,就退出
127
+ return ""
128
+ else:
129
+ return resp['NextMarker']
130
+ client = self.get_client()
131
+ marker = ""
132
+ versions_list = []
133
+ while True: # 轮询
134
+ try:
135
+ marker = getAllVersion_base()
136
+ except KeyError as e:
137
+ print(e)
138
+ raise
139
+ if len(marker) == 0: # 没有数据了
140
+ break
141
+ return versions_list
142
+
143
+ def get_newest_version(self):
144
+ """获取最新的版本号"""
145
+ versions_list = self.get_all_versions()
146
+ # reverse=True,降序
147
+ versions_list.sort(key=lambda x: int(x.split(".")[-1]), reverse=True)
148
+ versions_list.sort(key=lambda x: int(x.split(".")[-2]), reverse=True)
149
+ versions_list.sort(key=lambda x: int(x.split(".")[-3]), reverse=True)
150
+ return versions_list[0]
151
+
152
+ def download_version(self, v):
153
+ """
154
+ 在存储桶中下载文件,将下载好的文件解压至本地
155
+ Args:
156
+ v: 版本号,x.x.x
157
+
158
+ Returns:
159
+ None
160
+ """
161
+ file_name = v + HivisionaiParams.file_format
162
+ client = self.get_client()
163
+ print(f"Download to {HivisionaiParams.save_folder}...")
164
+ try:
165
+ resp = client.get_object(HivisionaiParams.bucket, HivisionaiParams.zip_key + "/" + file_name)
166
+ contents = resp["Body"].get_raw_stream().read()
167
+ except CosServiceError:
168
+ print(f"[{file_name}.zip] does not exist, please check your version!")
169
+ sys.exit()
170
+ if not os.path.exists(HivisionaiParams.save_folder):
171
+ os.makedirs(HivisionaiParams.save_folder)
172
+ open(os.path.join(HivisionaiParams.save_folder, file_name), "wb").write(contents)
173
+ print("Download success!")
174
+
175
+ @staticmethod
176
+ def download_dependence(path=None):
177
+ """
178
+ 一键下载HY-sdk所需要的所有依赖,需要注意的是,本方法必须在运行pip install之后使用(运行完pip install之后才会出现hivisionai文件夹)
179
+ Args:
180
+ path: 文件路径,精确到hivisionai文件夹的上一个目录,如果为None,则默认下载到python环境下hivisionai安装的目录
181
+
182
+ Returns:
183
+ 下载相应内容到指定位置
184
+ """
185
+ # print("指定的下载路径:", path) # 此时在path路径下必然存在一个hivisionai文件夹
186
+ # print("系统安装的hivisionai库的路径:", HivisionaiParams.hivisionai_path)
187
+ print("Dependence downloading...")
188
+ if path is None:
189
+ path = HivisionaiParams.hivisionai_path
190
+ # ----------------下载mtcnn模型文件
191
+ mtcnn_path = os.path.join(path, "hivisionai/hycv/mtcnn_onnx/weights")
192
+ base_url = "https://linimages.oss-cn-beijing.aliyuncs.com/"
193
+ onnx_files = ["pnet.onnx", "rnet.onnx", "onet.onnx"]
194
+ print(f"Downloading mtcnn model in {mtcnn_path}")
195
+ if not os.path.exists(mtcnn_path):
196
+ os.mkdir(mtcnn_path)
197
+ for onnx_file in onnx_files:
198
+ if not os.path.exists(os.path.join(mtcnn_path, onnx_file)):
199
+ # download onnx model
200
+ onnx_url = base_url + onnx_file
201
+ print("Downloading Onnx Model in:", onnx_url)
202
+ r = requests.get(onnx_url, stream=True)
203
+ if r.status_code == 200:
204
+ open(os.path.join(mtcnn_path, onnx_file), 'wb').write(r.content) # 将内容写入文件
205
+ print(f"Download finished -- {onnx_file}")
206
+ del r
207
+ # ----------------
208
+ print("Dependence download finished...")
209
+
210
+
211
+ class HivisionaiApps(object):
212
+ """
213
+ 本类为app对外暴露的接口,为了代码规整性,这里使用类来对暴露接口进行调整
214
+ """
215
+ @staticmethod
216
+ def show_cloud_version():
217
+ """查看在cos中的所有HY-sdk版本"""
218
+ print("Connect to COS...")
219
+ versions_list = hivisionai_utils.get_all_versions()
220
+ # reverse=True,降序
221
+ versions_list.sort(key=lambda x: int(x.split(".")[-1]), reverse=True)
222
+ versions_list.sort(key=lambda x: int(x.split(".")[-2]), reverse=True)
223
+ versions_list.sort(key=lambda x: int(x.split(".")[-3]), reverse=True)
224
+ if len(versions_list) == 0:
225
+ print("There is no version currently, please release it first!")
226
+ sys.exit()
227
+ versions = "The currently existing versions (Keep 10): \n"
228
+ for i, v in enumerate(versions_list):
229
+ versions += str(v) + " "
230
+ if i == 9:
231
+ break
232
+ print(versions)
233
+
234
+ @staticmethod
235
+ def upgrade(v: str, enforce: bool = False, save_cached: bool = False):
236
+ """
237
+ 自动升级HY-sdk到指定版本
238
+ Args:
239
+ v: 指定的版本号,格式为x.x.x
240
+ enforce: 是否需要强制执行更新命令
241
+ save_cached: 是否保存下载的wheel文件,默认为否
242
+ Returns:
243
+ None
244
+ """
245
+ def check_format():
246
+ # noinspection PyBroadException
247
+ try:
248
+ major, minor, patch = v.split(".")
249
+ int(major)
250
+ int(minor)
251
+ int(patch)
252
+ except Exception as e:
253
+ print(f"Illegal version number!\n{e}")
254
+ pass
255
+ print("Upgrading, please wait a moment...")
256
+ if v == "-1":
257
+ v = hivisionai_utils.get_newest_version()
258
+ # 检查format的格式
259
+ check_format()
260
+ if v == version(HivisionaiParams.package_name) and not enforce:
261
+ print(f"Current version: {v} already exists, skip installation.")
262
+ sys.exit()
263
+ hivisionai_utils.download_version(v)
264
+ # 下载完毕(下载至save_folder),解压文件
265
+ target_zip = os.path.join(HivisionaiParams.save_folder, f"{v}.zip")
266
+ assert zipfile.is_zipfile(target_zip), "Decompression failed, and the target was not a zip file."
267
+ new_dir = target_zip.replace('.zip', '') # 解压的文件名
268
+ if os.path.exists(new_dir): # 判断文件夹是否存在
269
+ shutil.rmtree(new_dir)
270
+ os.mkdir(new_dir) # 新建文件夹
271
+ f = zipfile.ZipFile(target_zip)
272
+ f.extractall(new_dir) # 提取zip文件
273
+ print("Decompressed, begin to install...")
274
+ os.system(f'pip3 install {os.path.join(new_dir, "**.whl")}')
275
+ # 开始自动下载必要的模型依赖
276
+ hivisionai_utils.download_dependence()
277
+ # 安装完毕,如果save_cached为真,删除"$HOME/.hivisionai/sdk"内部的所有文件元素
278
+ if save_cached is True:
279
+ os.system(f'rm -rf {HivisionaiParams.save_folder}/**')
280
+
281
+ @staticmethod
282
+ def export(path):
283
+ """
284
+ 输出最新版本的文件到命令运行的path目录
285
+ Args:
286
+ path: 用户输入的路径
287
+
288
+ Returns:
289
+ 输出最新的hivisionai到path目录
290
+ """
291
+ # print(f"当前路径: {os.path.join(HivisionaiParams.getcwd, path)}")
292
+ # print(f"文件路径: {os.path.dirname(__file__)}")
293
+ export_path = os.path.join(HivisionaiParams.getcwd, path)
294
+ # 判断输出路径存不存在,如果不存在,就报错
295
+ assert os.path.exists(export_path), f"{export_path} dose not Exists!"
296
+ v = hivisionai_utils.get_newest_version()
297
+ # 下载文件到.hivisionai/sdk当中
298
+ hivisionai_utils.download_version(v)
299
+ # 下载完毕(下载至save_folder),解压文件
300
+ target_zip = os.path.join(HivisionaiParams.save_folder, f"{v}.zip")
301
+ assert zipfile.is_zipfile(target_zip), "Decompression failed, and the target was not a zip file."
302
+ new_dir = os.path.basename(target_zip.replace('.zip', '')) # 解压的文件名
303
+ new_dir = os.path.join(export_path, new_dir) # 解压的文件路径
304
+ if os.path.exists(new_dir): # 判断文件夹是否存在
305
+ shutil.rmtree(new_dir)
306
+ os.mkdir(new_dir) # 新建文件夹
307
+ f = zipfile.ZipFile(target_zip)
308
+ f.extractall(new_dir) # 提取zip文件
309
+ print("Decompressed, begin to export...")
310
+ # 强制删除bin/hivisionai和hivisionai/以及HY_sdk-**
311
+ bin_path = os.path.join(export_path, "bin")
312
+ hivisionai_path = os.path.join(export_path, "hivisionai")
313
+ sdk_path = os.path.join(export_path, "HY_sdk-**")
314
+ os.system(f"rm -rf {bin_path} {hivisionai_path} {sdk_path}")
315
+ # 删除完毕,开始export
316
+ os.system(f'pip3 install {os.path.join(new_dir, "**.whl")} -t {export_path}')
317
+ hivisionai_utils.download_dependence(export_path)
318
+ # 将下载下来的文件夹删除
319
+ os.system(f'rm -rf {target_zip} && rm -rf {new_dir}')
320
+ print("Done.")
321
+
322
+ @staticmethod
323
+ def hy_func_init(force):
324
+ """
325
+ 在HY-func目录下使用hivisionai --init,可以自动将需要的依赖下载到指定位置
326
+ 不过对于比较大的模型——修复模型而言,需要手动下载
327
+ Args:
328
+ force: 如果force为True,则会强制重新下载所有的内容,包括修复模型这种比较大的模型
329
+ Returns:
330
+ 程序执行完毕,会将一些必要的依��也下载完毕
331
+ """
332
+ cwd = HivisionaiParams.getcwd
333
+ # 判断当前文件夹是否是HY-func
334
+ dirName = os.path.basename(cwd)
335
+ assert dirName == "HY-func", "请在正确的文件目录下初始化HY-func!"
336
+ # 需要下载的内容会存放在HivisionaiParams的functionDependence变量下
337
+ functionDependence = HivisionaiParams.functionDependence
338
+ # 下载配置文件
339
+ configs = functionDependence["configs"]
340
+ print("正在下载配置文件...")
341
+ for config in configs:
342
+ if not force and os.path.exists(config['save_path']):
343
+ print(f"[pass]: {os.path.basename(config['url'])}")
344
+ continue
345
+ print(f"[Download]: {config['url']}")
346
+ resp = requests.get(config['url'])
347
+ # json文件存储在text区域,但是其他的不一定
348
+ open(os.path.join(cwd, config['save_path']), 'w').write(resp.text)
349
+ # 其他文件,提示访问notion文档
350
+ print(f"[NOTICE]: 一切准备就绪,请访问下面的文档下载剩下的模型文件:\n{functionDependence['weights']}")
351
+
352
+ @staticmethod
353
+ def hy_func_deploy(functionName: str = None, functionPath: str = None):
354
+ """
355
+ 在HY-func目录下使用此命令,并且随附功能函数的名称,就可以将HY-func的部署版放到桌面上
356
+ 但是需要注意的是,本方式不适合修复功能使用,修复功能依旧需要手动制作镜像
357
+ Args:
358
+ functionName: 功能函数名称
359
+ functionPath: 需要注册的HY-func路径
360
+
361
+ Returns:
362
+ 程序执行完毕,桌面会出现一个同名文件夹
363
+ """
364
+ # 为了代码撰写的方便,这里仅仅把模型文件删除,其余配置文件保留
365
+ # 为了实现在任意位置输入hivisionai --deploy funcName都能成功,在使用前需要在.hivisionai/user_config.json中注册
366
+ # print(functionName, functionPath)
367
+ if functionPath is not None:
368
+ # 更新/添加路径
369
+ # functionPath为相对于使用路径的路径
370
+ assert os.path.basename(functionPath) == "HY-func", "所指向路径非HY-func!"
371
+ func_path = os.path.join(HivisionaiParams.getcwd, functionPath)
372
+ assert os.path.join(func_path), f"路径不存在: {func_path}"
373
+ # functionPath的路径写到user_config当中
374
+ user_config = json.load(open(HivisionaiParams.cloud_config_save, 'rb'))
375
+ user_config["func_path"] = func_path
376
+ open(HivisionaiParams.cloud_config_save, 'w').write(json.dumps(user_config))
377
+ print("HY-func全局路径保存成功!")
378
+ try:
379
+ user_config = json.load(open(HivisionaiParams.cloud_config_save, 'rb'))
380
+ func_path = user_config['func_path']
381
+ except KeyError:
382
+ return print("请先使用-p命令注册全局HY-func路径!")
383
+ # 此时func_path必然存在
384
+ # print(os.listdir(func_path))
385
+ assert functionName in os.listdir(func_path), functionName + "功能不存在!"
386
+ func_path_deploy = os.path.join(func_path, functionName)
387
+ # 开始复制文件到指定目录
388
+ # 我们默认移动到Desktop目录下,如果没有此目录,需要先创建一个
389
+ target_dir = os.path.join(HivisionaiParams.download_path, "Desktop")
390
+ assert os.path.exists(target_dir), target_dir + "文件路径不存在,你需要先创建一下!"
391
+ # 开始移动
392
+ target_dir = os.path.join(target_dir, functionName)
393
+ print("正在复制需要部署的文件...")
394
+ os.system(f"rm -rf {target_dir}")
395
+ os.system(f'cp -rf {func_path_deploy} {target_dir}')
396
+ os.system(f"cp -rf {os.path.join(func_path, '_lib')} {target_dir}")
397
+ os.system(f"cp -rf {os.path.join(func_path, '_service')} {target_dir}")
398
+ # 生成最新的hivisionai
399
+ print("正在生成hivisionai代码包...")
400
+ os.system(f'hivisionai -t {target_dir}')
401
+ # 移动完毕,删除模型文件
402
+ print("移动完毕,正在删除不需要的文件...")
403
+ # 模型文件
404
+ os.system(f"rm -rf {os.path.join(target_dir, '_lib', 'weights', '**')}")
405
+ # hivisionai生成时的多余文件
406
+ os.system(f"rm -rf {os.path.join(target_dir, 'bin')} {os.path.join(target_dir, 'HY_sdk**')}")
407
+ print("部署文件生成成功,你可以开始部署了!")
408
+
409
+
410
+ hivisionai_utils = HivisionaiUtils()
411
+
412
+
413
+ def entry_point():
414
+ parser = ArgumentParser()
415
+ # 查看版本号
416
+ parser.add_argument("-v", "--version", action="store_true", help="View the current HY-sdk version, which does not represent the final cloud version.")
417
+ # 自动更新
418
+ parser.add_argument("-u", "--upgrade", nargs='?', const="-1", type=str, help="Automatically update HY-sdk to the latest version")
419
+ # 查找云端的HY-sdk版本
420
+ parser.add_argument("-l", "--list", action="store_true", help="Find HY-sdk versions of the cloud, and keep up to ten")
421
+ # 下载云端的版本到本地路径
422
+ parser.add_argument("-t", "--export", nargs='?', const="./", help="Add a path parameter to automatically download the latest version of sdk to this path. If there are no parameters, the default is the current path")
423
+ # 强制更新附带参数,当一个功能需要强制执行一遍的时候,需要附带此参数
424
+ parser.add_argument("-f", "--force", action="store_true", help="Enforcement of other functions, execution of a single parameter is meaningless")
425
+ # 初始化HY-func
426
+ parser.add_argument("--init", action="store_true", help="Initialization HY-func")
427
+ # 部署HY-func
428
+ parser.add_argument("-d", "--deploy", nargs='?', const="-1", type=str, help="Deploy HY-func")
429
+ # 涉及注册一些自定义内容的时候,需要附带此参数,并写上自定义内容
430
+ parser.add_argument("-p", "--param", nargs='?', const="-1", type=str, help="When registering some custom content, you need to attach this parameter and write the custom content.")
431
+ args = parser.parse_args()
432
+ if args.version:
433
+ print(version(HivisionaiParams.package_name))
434
+ sys.exit()
435
+ if args.upgrade:
436
+ HivisionaiApps.upgrade(args.upgrade, args.force)
437
+ sys.exit()
438
+ if args.list:
439
+ HivisionaiApps.show_cloud_version()
440
+ sys.exit()
441
+ if args.export:
442
+ HivisionaiApps.export(args.export)
443
+ sys.exit()
444
+ if args.init:
445
+ HivisionaiApps.hy_func_init(args.force)
446
+ sys.exit()
447
+ if args.deploy:
448
+ HivisionaiApps.hy_func_deploy(args.deploy, args.param)
449
+
450
+
451
+ if __name__ == "__main__":
452
+ entry_point()
hivisionai/hyService/__init__.py ADDED
File without changes
hivisionai/hyService/cloudService.py ADDED
@@ -0,0 +1,406 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 焕影小程序功能服务端的基本工具函数,以类的形式封装
3
+ """
4
+ try: # 加上这个try的原因在于本地环境和云函数端的import形式有所不同
5
+ from qcloud_cos import CosConfig
6
+ from qcloud_cos import CosS3Client
7
+ except ImportError:
8
+ try:
9
+ from qcloud_cos_v5 import CosConfig
10
+ from qcloud_cos_v5 import CosS3Client
11
+ except ImportError:
12
+ raise ImportError("请下载腾讯云COS相关代码包:pip install cos-python-sdk-v5")
13
+ import requests
14
+ import datetime
15
+ import json
16
+ from .error import ProcessError
17
+ import os
18
+ local_path_ = os.path.dirname(__file__)
19
+
20
+
21
+ class GetConfig(object):
22
+ @staticmethod
23
+ def hy_sdk_client(Id:str, Key:str):
24
+ # 从cos中寻找文件
25
+ REGION: str = 'ap-beijing'
26
+ TOKEN = None
27
+ SCHEME: str = 'https'
28
+ BUCKET: str = 'hy-sdk-config-1305323352'
29
+ client_config = CosConfig(Region=REGION,
30
+ SecretId=Id,
31
+ SecretKey=Key,
32
+ Token=TOKEN,
33
+ Scheme=SCHEME)
34
+ return CosS3Client(client_config), BUCKET
35
+
36
+ def load_json(self, path:str, default_download=False):
37
+ try:
38
+ if os.path.isdir(path):
39
+ raise ProcessError("请输入具体的配置文件路径,而非文件夹!")
40
+ if default_download is True:
41
+ print(f"\033[34m 默认强制重新下载配置文件...\033[0m")
42
+ raise FileNotFoundError
43
+ with open(path) as f:
44
+ config = json.load(f)
45
+ return config
46
+ except FileNotFoundError:
47
+ dir_name = os.path.dirname(path)
48
+ try:
49
+ os.makedirs(dir_name)
50
+ except FileExistsError:
51
+ pass
52
+ base_name = os.path.basename(path)
53
+ print(f"\033[34m 正在从COS中下载配置文件...\033[0m")
54
+ print(f"\033[31m 请注意,接下来会在{dir_name}路径下生成文件{base_name}...\033[0m")
55
+ Id = input("请输入SecretId:")
56
+ Key = input("请输入SecretKey:")
57
+ client, bucket = self.hy_sdk_client(Id, Key)
58
+ data_bytes = client.get_object(Bucket=bucket,Key=base_name)["Body"].get_raw_stream().read()
59
+ data = json.loads(data_bytes.decode("utf-8"))
60
+ # data["SecretId"] = Id # 未来可以把这个加上
61
+ # data["SecretKey"] = Key
62
+ with open(path, "w") as f:
63
+ data_str = json.dumps(data, ensure_ascii=False)
64
+ # 如果 ensure_ascii 是 true (即默认值),输出保证将所有输入的非 ASCII 字符转义。
65
+ # 如果 ensure_ascii 是 false,这些字符会原样输出。
66
+ f.write(data_str)
67
+ f.close()
68
+ print(f"\033[32m 配置文件保存成功\033[0m")
69
+ return data
70
+ except json.decoder.JSONDecodeError:
71
+ print(f"\033[31m WARNING: 配置文件为空!\033[0m")
72
+ return {}
73
+
74
+ def load_file(self, cloud_path:str, local_path:str):
75
+ """
76
+ 从COS中下载文件到本地,本函数将会被默认执行的,在使用的时候建议加一些限制.
77
+ :param cloud_path: 云端的文件路径
78
+ :param local_path: 将云端文件保存在本地的路径
79
+ """
80
+ if os.path.isdir(cloud_path):
81
+ raise ProcessError("请输入具体的云端文件路径,而非文件夹!")
82
+ if os.path.isdir(local_path):
83
+ raise ProcessError("请输入具体的本地文件路径,而非文件夹!")
84
+ dir_name = os.path.dirname(local_path)
85
+ base_name = os.path.basename(local_path)
86
+ try:
87
+ os.makedirs(dir_name)
88
+ except FileExistsError:
89
+ pass
90
+ cloud_name = os.path.basename(cloud_path)
91
+ print(f"\033[31m 请注意,接下来会在{dir_name}路径下生成文件{base_name}\033[0m")
92
+ Id = input("请输入SecretId:")
93
+ Key = input("请输入SecretKey:")
94
+ client, bucket = self.hy_sdk_client(Id, Key)
95
+ print(f"\033[34m 正在从COS中下载文件: {cloud_name}, 此过程可能耗费一些时间...\033[0m")
96
+ data_bytes = client.get_object(Bucket=bucket,Key=cloud_path)["Body"].get_raw_stream().read()
97
+ # data["SecretId"] = Id # 未来可以把这个加上
98
+ # data["SecretKey"] = Key
99
+ with open(local_path, "wb") as f:
100
+ # 如果 ensure_ascii 是 true (即默认值),输出保证将所有输入的非 ASCII 字符转义。
101
+ # 如果 ensure_ascii 是 false,这些字符会原样输出。
102
+ f.write(data_bytes)
103
+ f.close()
104
+ print(f"\033[32m 文件保存成功\033[0m")
105
+
106
+
107
+ class CosConf(GetConfig):
108
+ """
109
+ 从安全的角度出发,将一些默认配置文件上传至COS中,接下来使用COS和它的子类���时候,在第一次使用时需要输入Cuny给的id和key
110
+ 用于连接cos存储桶,下载配置文件.
111
+ 当然,在service_default_download = False的时候,如果在运行路径下已经有conf/service_config.json文件了,
112
+ 那么就不用再次下载了,也不用输入id和key
113
+ 事实上这只需要运行一次,因为配置文件将会被下载至源码文件夹中
114
+ 如果要自定义路径,请在继承的子类中编写__init__函数,将service_path定向到指定路径
115
+ """
116
+ def __init__(self) -> None:
117
+ # 下面这些参数是类的共享参数
118
+ self.__SECRET_ID: str = None # 服务的id
119
+ self.__SECRET_KEY: str = None # 服务的key
120
+ self.__REGION: str = None # 服务的存储桶地区
121
+ self.__TOKEN: str = None # 服务的token,目前一直是None
122
+ self.__SCHEME: str = None # 服务的访问协议,默认实际上是https
123
+ self.__BUCKET: str = None # 服务的存储桶
124
+ self.__SERVICE_CONFIG: dict = None # 服务的配置文件
125
+ self.service_path: str = f"{local_path_}/conf/service_config.json"
126
+ # 配置文件路径,默认是函数运行的路径下的conf文件夹
127
+ self.service_default_download = False # 是否在每次访问配置的时候都重新下载文件
128
+
129
+ @property
130
+ def service_config(self):
131
+ if self.__SERVICE_CONFIG is None or self.service_default_download is True:
132
+ self.__SERVICE_CONFIG = self.load_json(self.service_path, self.service_default_download)
133
+ return self.__SERVICE_CONFIG
134
+
135
+ @property
136
+ def client(self):
137
+ client_config = CosConfig(Region=self.region,
138
+ SecretId=self.secret_id,
139
+ SecretKey=self.secret_key,
140
+ Token=self.token,
141
+ Scheme=self.scheme)
142
+ return CosS3Client(client_config)
143
+
144
+ def get_key(self, key:str):
145
+ try:
146
+ data = self.service_config[key]
147
+ if data == "None":
148
+ return None
149
+ else:
150
+ return data
151
+ except KeyError:
152
+ print(f"\033[31m没有对应键值{key},默认返回None\033[0m")
153
+ return None
154
+
155
+ @property
156
+ def secret_id(self):
157
+ if self.__SECRET_ID is None:
158
+ self.__SECRET_ID = self.get_key("SECRET_ID")
159
+ return self.__SECRET_ID
160
+
161
+ @secret_id.setter
162
+ def secret_id(self, value:str):
163
+ self.__SECRET_ID = value
164
+
165
+ @property
166
+ def secret_key(self):
167
+ if self.__SECRET_KEY is None:
168
+ self.__SECRET_KEY = self.get_key("SECRET_KEY")
169
+ return self.__SECRET_KEY
170
+
171
+ @secret_key.setter
172
+ def secret_key(self, value:str):
173
+ self.__SECRET_KEY = value
174
+
175
+ @property
176
+ def region(self):
177
+ if self.__REGION is None:
178
+ self.__REGION = self.get_key("REGION")
179
+ return self.__REGION
180
+
181
+ @region.setter
182
+ def region(self, value:str):
183
+ self.__REGION = value
184
+
185
+ @property
186
+ def token(self):
187
+ # if self.__TOKEN is None:
188
+ # self.__TOKEN = self.get_key("TOKEN")
189
+ # 这里可以注释掉
190
+ return self.__TOKEN
191
+
192
+ @token.setter
193
+ def token(self, value:str):
194
+ self.__TOKEN= value
195
+
196
+ @property
197
+ def scheme(self):
198
+ if self.__SCHEME is None:
199
+ self.__SCHEME = self.get_key("SCHEME")
200
+ return self.__SCHEME
201
+
202
+ @scheme.setter
203
+ def scheme(self, value:str):
204
+ self.__SCHEME = value
205
+
206
+ @property
207
+ def bucket(self):
208
+ if self.__BUCKET is None:
209
+ self.__BUCKET = self.get_key("BUCKET")
210
+ return self.__BUCKET
211
+
212
+ @bucket.setter
213
+ def bucket(self, value):
214
+ self.__BUCKET = value
215
+
216
+ def downloadFile_COS(self, key, bucket:str=None, if_read:bool=False):
217
+ """
218
+ 从COS下载对象(二进制数据), 如果下载失败就返回None
219
+ """
220
+ CosBucket = self.bucket if bucket is None else bucket
221
+ try:
222
+ # 将本类的Debug继承给抛弃了
223
+ # self.debug_print(f"Download from {CosBucket}", font_color="blue")
224
+ obj = self.client.get_object(
225
+ Bucket=CosBucket,
226
+ Key=key
227
+ )
228
+ if if_read is True:
229
+ data = obj["Body"].get_raw_stream().read() # byte
230
+ return data
231
+ else:
232
+ return obj
233
+ except Exception as e:
234
+ print(f"\033[31m下载失败! 错误描述:{e}\033[0m")
235
+ return None
236
+
237
+ def showFileList_COS_base(self, key, bucket, marker:str=""):
238
+ """
239
+ 返回cos存储桶内部的某个文件夹的内部名称
240
+ :param key: cos云端的存储路径
241
+ :param bucket: cos存储桶名称,如果没指定名称(None)就会寻找默认的存储桶
242
+ :param marker: 标记,用于记录上次查询到哪里了
243
+ ps:如果需要修改默认的存储桶配置,请在代码运行的时候加入代码 s.bucket = 存储桶名称 (s是对象实例)
244
+ 返回的内容存储在response["Content"],不过返回的数据大小是有限制的,具体内容还是请看官方文档。
245
+ """
246
+ response = self.client.list_objects(
247
+ Bucket=bucket,
248
+ Prefix=key,
249
+ Marker=marker
250
+ )
251
+ return response
252
+
253
+ def showFileList_COS(self, key, bucket:str=None)->list:
254
+ """
255
+ 实现查询存储桶中所有对象的操作,因为cos的sdk有返回数据包大小的限制,所以我们需要进行一定的改动
256
+ """
257
+ marker = ""
258
+ file_list = []
259
+ CosBucket = self.bucket if bucket is None else bucket
260
+ while True: # 轮询
261
+ response = self.showFileList_COS_base(key, CosBucket, marker)
262
+ try:
263
+ file_list.extend(response["Contents"])
264
+ except KeyError as e:
265
+ print(e)
266
+ raise
267
+ if response['IsTruncated'] == 'false': # 接下来没有数据了,就退出
268
+ break
269
+ marker = response['NextMarker']
270
+ return file_list
271
+
272
+ def uploadFile_COS(self, buffer, key, bucket:str=None):
273
+ """
274
+ 从COS上传数据,需要注意的是必须得是二进制文件
275
+ """
276
+ CosBucket = self.bucket if bucket is None else bucket
277
+ try:
278
+ self.client.put_object(
279
+ Bucket=CosBucket,
280
+ Body=buffer,
281
+ Key=key
282
+ )
283
+ return True
284
+ except Exception as e:
285
+ print(e)
286
+ return False
287
+
288
+
289
+ class FuncDiary(CosConf):
290
+ filter_dict = {"60a5e13da00e6e0001fd53c8": "Cuny",
291
+ "612c290f3a9af4000170faad": "守望平凡",
292
+ "614de96e1259260001506d6c": "林泽毅-焕影一新"}
293
+
294
+ def __init__(self, func_name: str, uid: str, error_conf_path: str = f"{local_path_}/conf/func_error_conf.json"):
295
+ """
296
+ 日志类的实例化
297
+ Args:
298
+ func_name: 功能名称,影响了日志投递的路径
299
+ """
300
+ super().__init__()
301
+ # 配置文件路径,默认是函数运行的路径下的conf文件夹
302
+ self.service_path: str = os.path.join(os.path.dirname(error_conf_path), "service_config.json")
303
+ self.error_dict = self.load_json(path=error_conf_path)
304
+ self.__up: str = f"wx/invokeFunction_c/{datetime.datetime.now().strftime('%Y/%m/%d/%H')}/{func_name}/"
305
+ self.func_name: str = func_name
306
+ # 下面这个属性是的日志名称的前缀
307
+ self.__start_time = datetime.datetime.now().timestamp()
308
+ h_point = datetime.datetime.strptime(datetime.datetime.now().strftime('%Y/%m/%d/%H'), '%Y/%m/%d/%H')
309
+ h_point_timestamp = h_point.timestamp()
310
+ self.__prefix = int(self.__start_time - h_point_timestamp).__str__() + "_"
311
+ self.__uid = uid
312
+ self.__diary = None
313
+
314
+ def __str__(self):
315
+ return f"<{self.func_name}> DIARY for {self.__uid}"
316
+
317
+ @property
318
+ def content(self):
319
+ return self.__diary
320
+
321
+ @content.setter
322
+ def content(self, value: str):
323
+ if not isinstance(value, dict):
324
+ raise TypeError("content 只能是字典!")
325
+ if "status" in value:
326
+ raise KeyError("status字段已被默认占用,请在日志信息中更换字段名称!")
327
+ if self.__diary is None:
328
+ self.__diary = value
329
+ else:
330
+ raise PermissionError("为了减小日志对整体代码的影响,<content>只能被覆写一次!")
331
+
332
+ def uploadDiary_COS(self, status_id: str, suffix: str = "", bucket: str = "hy-hcy-data-logs-1306602019"):
333
+ if self.__diary is None:
334
+ self.__diary = {"status": self.error_dict[status_id]}
335
+ if status_id == "0000":
336
+ self.__up += f"True/{self.__uid}/"
337
+ else:
338
+ self.__up += f"False/{self.__uid}/"
339
+ interval = int(10 * (datetime.datetime.now().timestamp() - self.__start_time))
340
+ prefix = self.__prefix + status_id + "_" + interval.__str__()
341
+ self.__diary["status"] = self.error_dict[status_id]
342
+ name = prefix + "_" + suffix if len(suffix) != 0 else prefix
343
+ self.uploadFile_COS(buffer=json.dumps(self.__diary), key=self.__up + name, bucket=bucket)
344
+ print(f"{self}上传成功.")
345
+
346
+
347
+ class ResponseWebSocket(CosConf):
348
+ # 网关推送地址
349
+ __HOST:str = None
350
+ @property
351
+ def sendBackHost(self):
352
+ if self.__HOST is None:
353
+ self.__HOST = self.get_key("HOST")
354
+ return self.__HOST
355
+
356
+ @sendBackHost.setter
357
+ def sendBackHost(self, value):
358
+ self.__HOST = value
359
+
360
+ def sendMsg_toWebSocket(self, message,connectionID:str = None):
361
+ if connectionID is not None:
362
+ retmsg = {'websocket': {}}
363
+ retmsg['websocket']['action'] = "data send"
364
+ retmsg['websocket']['secConnectionID'] = connectionID
365
+ retmsg['websocket']['dataType'] = 'text'
366
+ retmsg['websocket']['data'] = json.dumps(message)
367
+ requests.post(self.sendBackHost, json=retmsg)
368
+ print("send success!")
369
+ else:
370
+ pass
371
+
372
+ @staticmethod
373
+ def create_Msg(status, msg):
374
+ """
375
+ 本方法用于创建一个用于发送到WebSocket客户端的数据
376
+ 输入的信息部分,需要有如下几个参数:
377
+ 1. id,固定为"return-result"
378
+ 2. status,如果输入为1则status=true, 如果输入为-1则status=false
379
+ 3. obj_key, 图片的云端路径, 这是输入的msg本身自带的
380
+ """
381
+ msg['status'] = "false" if status == -1 else 'true' # 其实最好还是用bool
382
+ msg['id'] = "async-back-msg"
383
+ msg['type'] = "funcType"
384
+ msg["format"] = "imageType"
385
+ return msg
386
+
387
+
388
+ # 功能服务类
389
+ class Service(ResponseWebSocket):
390
+ """
391
+ 服务的主函数,封装了cos上传/下载功能以及与api网关的一键通讯
392
+ 将类的实例变成一个可被调用的对象,在服务运行的时候,只需要运行该对象即可
393
+ 当然,因为是类,所以支持继承和修改
394
+ """
395
+ @classmethod
396
+ def process(cls, *args, **kwargs):
397
+ """
398
+ 处理函数,在使用的时候请将之重构
399
+ """
400
+ pass
401
+
402
+ @classmethod
403
+ def __call__(cls, *args, **kwargs):
404
+ pass
405
+
406
+
hivisionai/hyService/dbTools.py ADDED
@@ -0,0 +1,337 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import pymongo
3
+ import datetime
4
+ import time
5
+ from .cloudService import GetConfig
6
+ local_path = os.path.dirname(__file__)
7
+
8
+
9
+ class DBUtils(GetConfig):
10
+ """
11
+ 从安全的角度出发,将一些默认配置文件上传至COS中,接下来使用COS和它的子类的时候,在第一次使用时需要输入Cuny给的id和key
12
+ 用于连接数据库等对象
13
+ 当然,在db_default_download = False的时候,如果在运行路径下已经有配置文件了,
14
+ 那么就不用再次下载了,也不用输入id和key
15
+ 事实上这只需要运行一次,因为配置文件将会被下载至源码文件夹中
16
+ 如果要自定义路径,请在继承的子类中编写__init__函数,将service_path定向到指定路径
17
+ """
18
+ __BASE_DIR: dict = None
19
+ __PARAMS_DIR: dict = None
20
+ db_base_path: str = f"{local_path}/conf/base_config.json"
21
+ db_params_path: str = f"{local_path}/conf/params.json"
22
+ db_default_download: bool = False
23
+
24
+ @property
25
+ def base_config(self):
26
+ if self.__BASE_DIR is None:
27
+ self.__BASE_DIR = self.load_json(self.db_base_path, self.db_default_download)
28
+ return self.__BASE_DIR
29
+
30
+ @property
31
+ def db_config(self):
32
+ return self.base_config["database_config"]
33
+
34
+ @property
35
+ def params_config(self):
36
+ if self.__PARAMS_DIR is None:
37
+ self.__PARAMS_DIR = self.load_json(self.db_params_path, self.db_default_download)
38
+ return self.__PARAMS_DIR
39
+
40
+ @property
41
+ def size_dir(self):
42
+ return self.params_config["size_config"]
43
+
44
+ @property
45
+ def func_dir(self):
46
+ return self.params_config["func_config"]
47
+
48
+ @property
49
+ def wx_config(self):
50
+ return self.base_config["wx_config"]
51
+
52
+ def get_dbClient(self):
53
+ return pymongo.MongoClient(self.db_config["connect_url"])
54
+
55
+ @staticmethod
56
+ def get_time(yyyymmdd=None, delta_date=0):
57
+ """
58
+ 给出当前的时间
59
+ :param yyyymmdd: 以yyyymmdd给出的日期时间
60
+ :param delta_date: 获取减去delta_day后的时间,默认为0就是当天
61
+ 时间格式:yyyy_mm_dd
62
+ """
63
+ if yyyymmdd is None:
64
+ now_time = (datetime.datetime.now() - datetime.timedelta(delta_date)).strftime("%Y-%m-%d")
65
+ return now_time
66
+ # 输入了yyyymmdd的数据和delta_date,通过这两个数据返回距离yyyymmdd delta_date天的时间
67
+ pre_time = datetime.datetime(int(yyyymmdd[0:4]), int(yyyymmdd[4:6]), int(yyyymmdd[6:8]))
68
+ return (pre_time - datetime.timedelta(delta_date)).strftime("%Y-%m-%d")
69
+
70
+ # 获得时间戳
71
+ def get_timestamp(self, date_time:str=None) -> int:
72
+ """
73
+ 输入的日期形式为:"2021-11-29 16:39:45.999"
74
+ 真正必须输入的是前十个字符,及精确到日期,后面的时间可以不输入,不输入则默认置零
75
+ """
76
+ def standardDateTime(dt:str) -> str:
77
+ """
78
+ 规范化时间字符串
79
+ """
80
+ if len(dt) < 10:
81
+ raise ValueError("你必须至少输入准确到天的日期!比如:2021-11-29")
82
+ elif len(dt) == 10:
83
+ return dt + " 00:00:00.0"
84
+ else:
85
+ try:
86
+ date, time = dt.split(" ")
87
+ except ValueError:
88
+ raise ValueError("你只能也必须在日期与具体时间之间增加一个空格,其他地方不能出现空格!")
89
+ while len(time) < 10:
90
+ if len(time) in (2, 5):
91
+ time += ":"
92
+ elif len(time) == 8:
93
+ time += "."
94
+ else:
95
+ time += "0"
96
+ return date + " " + time
97
+ if date_time is None:
98
+ # 默认返回当前时间(str), date_time精确到毫秒
99
+ date_time = datetime.datetime.now()
100
+ # 转换成时间戳
101
+ else:
102
+ date_time = standardDateTime(dt=date_time)
103
+ date_time = datetime.datetime.strptime(date_time, "%Y-%m-%d %H:%M:%S.%f")
104
+ timestamp_ms = int(time.mktime(date_time.timetuple()) * 1000.0 + date_time.microsecond / 1000.0)
105
+ return timestamp_ms
106
+
107
+ @staticmethod
108
+ def get_standardTime(yyyy_mm_dd: str):
109
+ return yyyy_mm_dd[0:4] + yyyy_mm_dd[5:7] + yyyy_mm_dd[8:10]
110
+
111
+ def find_oneDay_data(self, db_name: str, collection_name: str, date: str = None) -> dict:
112
+ """
113
+ 获取指定天数的数据,如果date is None,就自动寻找距今最近的有数据的那一天的数据
114
+ """
115
+ df = None # 应该被返回的数据
116
+ collection = self.get_dbClient()[db_name][collection_name]
117
+ if date is None: # 自动寻找前几天的数据,最多三十天
118
+ for delta_date in range(1, 31):
119
+ date_yyyymmdd = self.get_standardTime(self.get_time(delta_date=delta_date))
120
+ filter_ = {"date": date_yyyymmdd}
121
+ df = collection.find_one(filter=filter_)
122
+ if df is not None:
123
+ del df["_id"]
124
+ break
125
+ else:
126
+ filter_ = {"date": date}
127
+ df = collection.find_one(filter=filter_)
128
+ if df is not None:
129
+ del df["_id"]
130
+ return df
131
+
132
+ def find_daysData_byPeriod(self, date_period: tuple, db_name: str, col_name: str):
133
+ # 给出一个指定的范围日期,返回相应的数据(日期的两头都会被寻找)
134
+ # 这个函数我们默认数据库中的数据是连续的,即不会出现在 20211221 到 20211229 之间有一天没有数据的情况
135
+ if len(date_period) != 2:
136
+ raise ValueError("date_period数据结构:(开始日期,截止日期)")
137
+ start, end = date_period # yyyymmdd
138
+ delta_date = int(end) - int(start)
139
+ if delta_date < 0:
140
+ raise ValueError("传入的日期有误!")
141
+ collection = self.get_dbClient()[db_name][col_name]
142
+ date = start
143
+ while int(date) <= int(end):
144
+ yield collection.find_one(filter={"date": date})
145
+ date = self.get_standardTime(self.get_time(date, -1))
146
+
147
+ @staticmethod
148
+ def find_biggest_valueDict(dict_: dict):
149
+ # 寻找字典中数值最大的字段,要求输入的字典的字段值全为数字
150
+ while len(dict_) > 0:
151
+ max_value = 0
152
+ p = None
153
+ for key in dict_:
154
+ if dict_[key] > max_value:
155
+ p = key
156
+ max_value = dict_[key]
157
+ yield p, max_value
158
+ del dict_[p]
159
+
160
+ def copy_andAdd_dict(self, dict_base, dict_):
161
+ # 深度拷贝字典,将后者赋值给前者
162
+ # 如果后者的键名在前者已经存在,则直接相加。这就要求两者的数据是数值型
163
+ for key in dict_:
164
+ if key not in dict_base:
165
+ dict_base[key] = dict_[key]
166
+ else:
167
+ if isinstance(dict_[key], int) or isinstance(dict_[key], float):
168
+ dict_base[key] = round(dict_[key] + dict_base[key], 2)
169
+ else:
170
+ dict_base[key] = self.copy_andAdd_dict(dict_base[key], dict_[key])
171
+ return dict_base
172
+
173
+ @staticmethod
174
+ def compare_data(dict1: dict, dict2: dict, suffix: str, save: int, **kwargs):
175
+ """
176
+ 有两个字典,并且通过kwargs会传输一个新的字典,根据字典中的键值我们进行比对,处理成相应的数据格式
177
+ 并且在dict1中,生成一个新的键值,为kwargs中的元素+suffix
178
+ save:保留几位小数
179
+ """
180
+ new_dict = dict1.copy()
181
+ for key in kwargs:
182
+ try:
183
+ if kwargs[key] not in dict2 or int(dict2[kwargs[key]]) == -1 or float(dict1[kwargs[key]]) <= 0.0:
184
+ # 数据不存在
185
+ data_new = 5002
186
+ else:
187
+ try:
188
+ data_new = round(
189
+ ((float(dict1[kwargs[key]]) - float(dict2[kwargs[key]])) / float(dict2[kwargs[key]])) * 100
190
+ , save)
191
+ except ZeroDivisionError:
192
+ data_new = 5002
193
+ if data_new == 0.0:
194
+ data_new = 0
195
+ except TypeError as e:
196
+ print(e)
197
+ data_new = 5002 # 如果没有之前的数据,默认返回0
198
+ new_dict[kwargs[key] + suffix] = data_new
199
+ return new_dict
200
+
201
+ @staticmethod
202
+ def sum_dictList_byKey(dictList: list, **kwargs) -> dict:
203
+ """
204
+ 有一个列表,列表中的元素为字典,并且所有字典都有一个键值为key的字段,字段值为数字
205
+ 我们将每一个字典的key字段提取后相加,得到该字段值之和.
206
+ """
207
+ sum_num = {}
208
+ if kwargs is None:
209
+ raise ImportError("Please input at least ONE key")
210
+ for key in kwargs:
211
+ sum_num[kwargs[key]] = 0
212
+ for dict_ in dictList:
213
+ if not isinstance(dict_, dict):
214
+ raise TypeError("object is not DICT!")
215
+ for key in kwargs:
216
+ sum_num[kwargs[key]] += dict_[kwargs[key]]
217
+ return sum_num
218
+
219
+ @staticmethod
220
+ def sum_2ListDict(list_dict1: list, list_dict2: list, key_name, data_name):
221
+ """
222
+ 有两个列表,列表内的元素为字典,我们根据key所对应的键值寻找列表中键值相同的两个元素,将他们的data对应的键值相加
223
+ 生成新的列表字典(其余键值被删除)
224
+ key仅在一个列表中存在,则直接加入新的列表字典
225
+ """
226
+ sum_list = []
227
+
228
+ def find_sameKey(kn, key_, ld: list) -> int:
229
+ for dic_ in ld:
230
+ if dic_[kn] == key_:
231
+ post_ = ld.index(dic_)
232
+ return post_
233
+ return -1
234
+
235
+ for dic in list_dict1:
236
+ key = dic[key_name] # 键名
237
+ post = find_sameKey(key_name, key, list_dict2) # 在list2中寻找相同的位置
238
+ data = dic[data_name] + list_dict2[post][data_name] if post != -1 else dic[data_name]
239
+ sum_list.append({key_name: key, data_name: data})
240
+ return sum_list
241
+
242
+ @staticmethod
243
+ def find_biggest_dictList(dictList: list, key: str = "key", data: str = "value"):
244
+ """
245
+ 有一个列表,里面每一个元素都是一个字典
246
+ 这些字典有一些共通性质,那就是里面都有一个key键名和一个data键名,后者的键值必须是数字
247
+ 我们根据data键值的大小进行生成,每一次返回列表中data键值最大的数和它的key键值
248
+ """
249
+ while len(dictList) > 0:
250
+ point = 0
251
+ biggest_num = int(dictList[0][data])
252
+ biggest_key = dictList[0][key]
253
+ for i in range(len(dictList)):
254
+ num = int(dictList[i][data])
255
+ if num > biggest_num:
256
+ point = i
257
+ biggest_num = int(dictList[i][data])
258
+ biggest_key = dictList[i][key]
259
+ yield str(biggest_key), biggest_num
260
+ del dictList[point]
261
+
262
+ def get_share_data(self, date_yyyymmdd: str):
263
+ # 获得用户界面情况
264
+ visitPage = self.find_oneDay_data(date=date_yyyymmdd,
265
+ db_name="cuny-user-analysis",
266
+ collection_name="daily-userVisitPage")
267
+ if visitPage is not None:
268
+ # 这一部分没有得到数据是可以容忍的.不用抛出模态框错误
269
+ # 获得昨日用户分享情况
270
+ sum_num = self.sum_dictList_byKey(dictList=visitPage["data_list"],
271
+ key1="page_share_pv",
272
+ key2="page_share_uv")
273
+ else:
274
+ # 此时将分享次数等置为-1
275
+ sum_num = {"page_share_pv": -1, "page_share_uv": -1}
276
+ return sum_num
277
+
278
+ @staticmethod
279
+ def compare_date(date1_yyyymmdd: str, date2_yyyymmdd: str):
280
+ # 如果date1是date2的昨天,那么就返回True
281
+ date1 = int(date1_yyyymmdd)
282
+ date2 = int(date2_yyyymmdd)
283
+ return True if date2 - date1 == 1 else False
284
+
285
+ def change_time(self, date_yyyymmdd: str, mode: int):
286
+ # 将yyyymmdd的数据分开为相应的数据形式
287
+ if mode == 1:
288
+ if self.compare_date(date_yyyymmdd, self.get_standardTime(self.get_time(delta_date=0))) is False:
289
+ return date_yyyymmdd[0:4] + "年" + date_yyyymmdd[4:6] + "月" + date_yyyymmdd[6:8] + "日"
290
+ else:
291
+ return "昨日"
292
+ elif mode == 2:
293
+ date = date_yyyymmdd[0:4] + "." + date_yyyymmdd[4:6] + "." + date_yyyymmdd[6:8]
294
+ if self.compare_date(date_yyyymmdd, self.get_standardTime(self.get_time(delta_date=0))) is True:
295
+ return date + "~" + date + " | 昨日"
296
+ else:
297
+ return date + "~" + date
298
+
299
+ @staticmethod
300
+ def changeList_dict2List_list(dl: list, order: list):
301
+ """
302
+ 列表内是一个个字典,本函数将字典拆解,以order的形式排列键值为列表
303
+ 考虑到一些格式的问题,这里我采用生成器的形式封装
304
+ """
305
+ for dic in dl:
306
+ # dic是列表内的字典元素
307
+ tmp = []
308
+ for key_name in order:
309
+ key = dic[key_name]
310
+ tmp.append(key)
311
+ yield tmp
312
+
313
+ def dict_mapping(self, dict_name: str, id_: str):
314
+ """
315
+ 进行字典映射,输入字典名称和键名,返回具体的键值
316
+ 如果不存在,则原路返回键名
317
+ """
318
+ try:
319
+ return getattr(self, dict_name)[id_]
320
+ except KeyError:
321
+ return id_
322
+ except AttributeError:
323
+ print(f"[WARNING]: 本对象内部不存在{dict_name}!")
324
+ return id_
325
+
326
+ @staticmethod
327
+ def dictAddKey(dic: dict, dic_tmp: dict, **kwargs):
328
+ """
329
+ 往字典中加入参数,可迭代
330
+ """
331
+ for key in kwargs:
332
+ dic[key] = dic_tmp[key]
333
+ return dic
334
+
335
+
336
+ if __name__ == "__main__":
337
+ dbu = DBUtils()
hivisionai/hyService/error.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ @author: cuny
3
+ @fileName: error.py
4
+ @create_time: 2022/03/10 下午3:14
5
+ @introduce:
6
+ 保存一些定义的错误类型
7
+ """
8
+ class ProcessError(Exception):
9
+ def __init__(self, err):
10
+ super().__init__(err)
11
+ self.err = err
12
+ def __str__(self):
13
+ return self.err
14
+
15
+ class WrongImageType(TypeError):
16
+ def __init__(self, err):
17
+ super().__init__(err)
18
+ self.err = err
19
+ def __str__(self):
20
+ return self.err
hivisionai/hyService/serviceTest.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 用于测试云端或者本地服务的运行是否成功
3
+ """
4
+ import requests
5
+ import functools
6
+ import cv2
7
+ import time
8
+
9
+ def httpPostTest(url, msg:dict):
10
+ """
11
+ 以post请求访问api,携带msg(dict)信息
12
+ """
13
+ re = requests.post(url=url, json=msg)
14
+ print(re.text)
15
+ return re
16
+
17
+
18
+ def localTestImageFunc(path):
19
+ """
20
+ 在本地端测试算法,需要注意的是本装饰器只支持测试和图像相关算法
21
+ path代表测试图像的路径,其余参数请写入被装饰的函数中,并且只支持标签形式输入
22
+ 被测试的函数的第一个输入参数必须为图像矩阵(以cv2读入)
23
+ """
24
+ def decorator(func):
25
+ @functools.wraps(func)
26
+ def wrapper(**kwargs):
27
+ start = time.time()
28
+ image = cv2.imread(path)
29
+ image_out = func(image) if len(kwargs) == 0 else func(image, kwargs)
30
+ print("END.\n处理时间(不计算加载模型时间){}秒:".format(round(time.time()-start, 2)))
31
+ cv2.imshow("test", image_out)
32
+ cv2.waitKey(0)
33
+ return wrapper
34
+ return decorator
hivisionai/hyService/utils.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ @author: cuny
3
+ @fileName: utils.py
4
+ @create_time: 2021/12/29 下午1:29
5
+ @introduce:
6
+ 焕影服务的一些工具函数,涉及两类:
7
+ 1. 开发debug时候的工具函数
8
+ 2. 初始化COS配置时的工具函数
9
+ """
10
+ import cv2
11
+ from .error import WrongImageType
12
+ import numpy as np
13
+
14
+ class Debug(object):
15
+ color_dir:dict = {
16
+ "red":"31m",
17
+ "green":"32m",
18
+ "yellow":"33m",
19
+ "blue":"34m",
20
+ "common":"38m"
21
+ } # 颜色值
22
+ __DEBUG:bool = True
23
+
24
+ @property
25
+ def debug(self):
26
+ return self.__DEBUG
27
+
28
+ @debug.setter
29
+ def debug(self, value):
30
+ if not isinstance(value, bool):
31
+ raise TypeError("你必须设定debug的值为bool的True或者False")
32
+ print(f"设置debug为: {value}")
33
+ self.__DEBUG = value
34
+
35
+ def debug_print(self, text, **kwargs):
36
+ if self.debug is True:
37
+ key = self.color_dir["common"] if "font_color" not in kwargs else self.color_dir[kwargs["font_color"]]
38
+ print(f"\033[{key}{text}\033[0m")
39
+
40
+ @staticmethod
41
+ def resize_image_esp(input_image, esp=2000):
42
+ """
43
+ 输入:
44
+ input_path:numpy图片
45
+ esp:限制的最大边长
46
+ """
47
+ # resize函数=>可以让原图压缩到最大边为esp的尺寸(不改变比例)
48
+ width = input_image.shape[0]
49
+ length = input_image.shape[1]
50
+ max_num = max(width, length)
51
+
52
+ if max_num > esp:
53
+ print("Image resizing...")
54
+ if width == max_num:
55
+ length = int((esp / width) * length)
56
+ width = esp
57
+
58
+ else:
59
+ width = int((esp / length) * width)
60
+ length = esp
61
+ print(length, width)
62
+ im_resize = cv2.resize(input_image, (length, width), interpolation=cv2.INTER_AREA)
63
+ return im_resize
64
+ else:
65
+ return input_image
66
+
67
+ def cv_show(self, *args, **kwargs):
68
+ def check_images(img):
69
+ # 判断是否是矩阵类型
70
+ if not isinstance(img, np.ndarray):
71
+ raise WrongImageType("输入的图像必须是 np.ndarray 类型!")
72
+ if self.debug is True:
73
+ size = 500 if "size" not in kwargs else kwargs["size"] # 默认缩放尺寸为最大边500像素点
74
+ if len(args) == 0:
75
+ raise ProcessError("你必须传入若干图像信息!")
76
+ flag = False
77
+ base = None
78
+ for image in args:
79
+ check_images(image)
80
+ if flag is False:
81
+ image = self.resize_image_esp(image, size)
82
+ h, w = image.shape[0], image.shape[1]
83
+ flag = (w, h)
84
+ base = image
85
+ else:
86
+ image = cv2.resize(image, flag)
87
+ base = np.hstack((base, image))
88
+ title = "cv_show" if "winname" not in kwargs else kwargs["winname"]
89
+ cv2.imshow(title, base)
90
+ cv2.waitKey(0)
91
+ else:
92
+ pass
hivisionai/hyTrain/APIs.py ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests, os
2
+ import json
3
+ import hashlib, base64, hmac
4
+ import sys
5
+ import oss2
6
+ from aliyunsdkimageseg.request.v20191230.SegmentBodyRequest import SegmentBodyRequest
7
+ from aliyunsdkimageseg.request.v20191230.SegmentSkinRequest import SegmentSkinRequest
8
+ from aliyunsdkfacebody.request.v20191230.DetectFaceRequest import DetectFaceRequest
9
+ from aliyunsdkcore.client import AcsClient
10
+
11
+ # 头像抠图参数配置
12
+ def params_of_head(photo_base64, photo_type):
13
+ print ('测试头像抠图接口 ...')
14
+ host = 'https://person.market.alicloudapi.com'
15
+ uri = '/segment/person/headrgba' # 头像抠图返回透明PNG图
16
+ # uri = '/segment/person/head' # 头像抠图返回alpha图
17
+ # uri = '/segment/person/headborder' # 头像抠图返回带白边的透明PNG图
18
+ return host, uri, {
19
+ 'photo': photo_base64,
20
+ 'type': photo_type,
21
+ 'face_required': 0, # 可选,检测是否必须带有人脸才进行抠图处理,0为检测,1为不检测,默认为0
22
+ 'border_ratio': 0.3, # 可选,仅带白边接口可用,
23
+ # 在头像边缘增加白边(或者其他颜色)宽度,取值为0-0.5,
24
+ # 这个宽度是相对于图片宽度和高度最大值的比例,
25
+ # 比如原图尺寸为640x480,border_ratio为0.2,
26
+ # 则添加的白边的宽度为:max(640,480) * 0.2 = 96个像素
27
+ 'margin_color': '#ff0000' # 可选,仅带白边接口可用,
28
+ # 在头像边缘增加边框的颜色,默认为白色
29
+
30
+ }
31
+
32
+ # 头像抠图API
33
+ def wanxing_get_head_api(file_name='/home/parallels/Desktop/change_cloth/input_image/03.jpg',
34
+ output_path="./head.png",
35
+ app_key='204014294',
36
+ secret="pI2uo7AhCFjnaZWYrCCAEjmsZJbK6vzy",
37
+ stage='RELEASE'):
38
+ info = sys.version_info
39
+ if info[0] < 3:
40
+ is_python3 = False
41
+ else:
42
+ is_python3 = True
43
+
44
+ with open(file_name, 'rb') as fp:
45
+ photo_base64 = base64.b64encode(fp.read())
46
+ if is_python3:
47
+ photo_base64 = photo_base64.decode('utf8')
48
+
49
+ _, photo_type = os.path.splitext(file_name)
50
+ photo_type = photo_type.lstrip('.')
51
+ # print(photo_type)
52
+ # print(photo_base64)
53
+
54
+ # host, uri, body_json = params_of_portrait_matting(photo_base64, photo_type)
55
+ # host, uri, body_json = params_of_object_matting(photo_base64)
56
+ # host, uri, body_json = params_of_idphoto(photo_base64, photo_type)
57
+ host, uri, body_json = params_of_head(photo_base64, photo_type)
58
+ # host, uri, body_json = params_of_crop(photo_base64)
59
+ api = host + uri
60
+
61
+ body = json.dumps(body_json)
62
+ md5lib = hashlib.md5()
63
+ if is_python3:
64
+ md5lib.update(body.encode('utf8'))
65
+ else:
66
+ md5lib.update(body)
67
+ body_md5 = md5lib.digest()
68
+ body_md5 = base64.b64encode(body_md5)
69
+ if is_python3:
70
+ body_md5 = body_md5.decode('utf8')
71
+
72
+ method = 'POST'
73
+ accept = 'application/json'
74
+ content_type = 'application/octet-stream; charset=utf-8'
75
+ date_str = ''
76
+ headers = ''
77
+
78
+ string_to_sign = method + '\n' \
79
+ + accept + '\n' \
80
+ + body_md5 + '\n' \
81
+ + content_type + '\n' \
82
+ + date_str + '\n' \
83
+ + headers \
84
+ + uri
85
+ if is_python3:
86
+ signed = hmac.new(secret.encode('utf8'),
87
+ string_to_sign.encode('utf8'),
88
+ digestmod=hashlib.sha256).digest()
89
+ else:
90
+ signed = hmac.new(secret, string_to_sign, digestmod=hashlib.sha256).digest()
91
+ signed = base64.b64encode(signed)
92
+ if is_python3:
93
+ signed = signed.decode('utf8')
94
+
95
+ headers = {
96
+ 'Accept': accept,
97
+ 'Content-MD5': body_md5,
98
+ 'Content-Type': content_type,
99
+ 'X-Ca-Key': app_key,
100
+ 'X-Ca-Stage': stage,
101
+ 'X-Ca-Signature': signed
102
+ }
103
+ #print signed
104
+
105
+
106
+ resp = requests.post(api, data=body, headers=headers)
107
+ # for u,v in resp.headers.items():
108
+ # print(u+": " + v)
109
+ try:
110
+ res = resp.content
111
+ res = json.loads(res)
112
+ # print ('res:', res)
113
+ if str(res['status']) == '0':
114
+ # print ('成功!')
115
+ file_object = requests.get(res["data"]["result"])
116
+ # print(file_object)
117
+ with open(output_path, 'wb') as local_file:
118
+ local_file.write(file_object.content)
119
+
120
+ # image = cv2.imread("./test_head.png", -1)
121
+ # return image
122
+ else:
123
+ pass
124
+ # print ('失败!')
125
+ except:
126
+ print('failed parse:', resp)
127
+
128
+ # 阿里云抠图API
129
+ def aliyun_human_matting_api(input_path, output_path, type="human"):
130
+ auth = oss2.Auth('LTAI5tP2NxdzSFfpKYxZFCuJ', 'VzbGdUbRawuMAitekP3ORfrw0i3NEX')
131
+ bucket = oss2.Bucket(auth, 'https://oss-cn-shanghai.aliyuncs.com', 'huanying-api')
132
+ key = os.path.basename(input_path)
133
+ origin_image = input_path
134
+ try:
135
+ bucket.put_object_from_file(key, origin_image, headers={"Connection":"close"})
136
+ except Exception as e:
137
+ print(e)
138
+
139
+ url = bucket.sign_url('GET', key, 10 * 60)
140
+ client = AcsClient('LTAI5tP2NxdzSFfpKYxZFCuJ', 'VzbGdUbRawuMAitekP3ORfrw0i3NEX', 'cn-shanghai')
141
+ if type == "human":
142
+ request = SegmentBodyRequest()
143
+ elif type == "skin":
144
+ request = SegmentSkinRequest()
145
+ request.set_accept_format('json')
146
+ request.set_ImageURL(url)
147
+
148
+ try:
149
+ response = client.do_action_with_exception(request)
150
+ response_dict = eval(str(response, encoding='utf-8'))
151
+ if type == "human":
152
+ output_url = response_dict['Data']['ImageURL']
153
+ elif type == "skin":
154
+ output_url = response_dict['Data']['Elements'][0]['URL']
155
+ file_object = requests.get(output_url)
156
+ with open(output_path, 'wb') as local_file:
157
+ local_file.write(file_object.content)
158
+ bucket.delete_object(key)
159
+ except Exception as e:
160
+ print(e)
161
+ response = client.do_action_with_exception(request)
162
+ response_dict = eval(str(response, encoding='utf-8'))
163
+ print(response_dict)
164
+ output_url = response_dict['Data']['ImageURL']
165
+ file_object = requests.get(output_url)
166
+ with open(output_path, 'wb') as local_file:
167
+ local_file.write(file_object.content)
168
+ bucket.delete_object(key)
169
+
170
+ # 阿里云人脸检测API
171
+ def aliyun_face_detect_api(input_path, type="human"):
172
+ auth = oss2.Auth('LTAI5tP2NxdzSFfpKYxZFCuJ', 'VzbGdUbRawuMAitekP3ORfrw0i3NEX')
173
+ bucket = oss2.Bucket(auth, 'https://oss-cn-shanghai.aliyuncs.com', 'huanying-api')
174
+ key = os.path.basename(input_path)
175
+ origin_image = input_path
176
+ try:
177
+ bucket.put_object_from_file(key, origin_image, headers={"Connection":"close"})
178
+ except Exception as e:
179
+ print(e)
180
+
181
+ url = bucket.sign_url('GET', key, 10 * 60)
182
+ client = AcsClient('LTAI5tP2NxdzSFfpKYxZFCuJ', 'VzbGdUbRawuMAitekP3ORfrw0i3NEX', 'cn-shanghai')
183
+ if type == "human":
184
+ request = DetectFaceRequest()
185
+ request.set_accept_format('json')
186
+ request.set_ImageURL(url)
187
+ try:
188
+ response = client.do_action_with_exception(request)
189
+ response_json = json.loads(str(response, encoding='utf-8'))
190
+ print(response_json["Data"]["PoseList"][-1])
191
+ bucket.delete_object(key)
192
+ return response_json["Data"]["PoseList"][-1]
193
+ except Exception as e:
194
+ print(e)
195
+
196
+ if __name__ == "__main__":
197
+ wanxing_get_head_api()
hivisionai/hyTrain/DataProcessing.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import random
3
+ from scipy.ndimage import grey_erosion, grey_dilation
4
+ import numpy as np
5
+ from glob import glob
6
+ import random
7
+
8
+
9
+ def make_a_and_trimaps(input_image, resize=(512, 512)):
10
+ image = cv2.resize(input_image, resize)
11
+ b, g, r, a = cv2.split(image)
12
+
13
+ a_scale_resize = a / 255
14
+ trimap = (a_scale_resize >= 0.95).astype("float32")
15
+ not_bg = (a_scale_resize > 0).astype("float32")
16
+ d_size = a.shape[0] // 256 * random.randint(10, 20)
17
+ e_size = a.shape[0] // 256 * random.randint(10, 20)
18
+ trimap[np.where((grey_dilation(not_bg, size=(d_size, d_size))
19
+ - grey_erosion(trimap, size=(e_size, e_size))) != 0)] = 0.5
20
+
21
+ return a, trimap*255
22
+
23
+
24
+ def get_filedir_filelist(input_path):
25
+ return glob(input_path+"/*")
26
+
27
+
28
+ def extChange(filedir, ext="png"):
29
+ ext_origin = str(filedir).split(".")[-1]
30
+ return filedir.replace(ext_origin, ext)
31
+
32
+ def random_image_crop(input_image:np.array, crop_size=(512,512)):
33
+ height, width = input_image.shape[0], input_image.shape[1]
34
+ crop_height, crop_width = crop_size[0], crop_size[1]
35
+ x = random.randint(0, width-crop_width)
36
+ y = random.randint(0, height-crop_height)
37
+ return input_image[y:y+crop_height, x:x+crop_width]
hivisionai/hyTrain/__init__.py ADDED
File without changes
hivisionai/hycv/FaceDetection68/__init__.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ """
2
+ @author: cuny
3
+ @fileName: __init__.py
4
+ @create_time: 2022/01/03 下午9:39
5
+ @introduce:
6
+ 人脸68关键点检测sdk的__init__包,实际上是对dlib的封装
7
+ """
8
+ from .faceDetection68 import FaceDetection68, PoseEstimator68
hivisionai/hycv/FaceDetection68/faceDetection68.py ADDED
@@ -0,0 +1,443 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ @author: cuny
3
+ @fileName: faceDetection68.py
4
+ @create_time: 2022/01/03 下午10:20
5
+ @introduce:
6
+ 人脸68关键点检测主文件,以类的形式封装
7
+ """
8
+ from hivisionai.hyService.cloudService import GetConfig
9
+ import os
10
+ import cv2
11
+ import dlib
12
+ import numpy as np
13
+ local_file = os.path.dirname(__file__)
14
+ PREDICTOR_PATH = f"{local_file}/weights/shape_predictor_68_face_landmarks.dat" # 关键点检测模型路径
15
+ MODULE3D_PATH = f"{local_file}/weights/68_points_3D_model.txt" # 3d的68点配置文件路径
16
+
17
+ # 定义一个人脸检测错误的错误类
18
+ class FaceError(Exception):
19
+ def __init__(self, err):
20
+ super().__init__(err)
21
+ self.err = err
22
+ def __str__(self):
23
+ return self.err
24
+
25
+ class FaceConfig68(object):
26
+ face_area:list = None # 一些其他的参数,在本类中实际没啥用
27
+ FACE_POINTS = list(range(17, 68)) # 人脸轮廓点索引
28
+ MOUTH_POINTS = list(range(48, 61)) # 嘴巴点索引
29
+ RIGHT_BROW_POINTS = list(range(17, 22)) # 右眉毛索引
30
+ LEFT_BROW_POINTS = list(range(22, 27)) # 左眉毛索引
31
+ RIGHT_EYE_POINTS = list(range(36, 42)) # 右眼索引
32
+ LEFT_EYE_POINTS = list(range(42, 48)) # 左眼索引
33
+ NOSE_POINTS = list(range(27, 35)) # 鼻子索引
34
+ JAW_POINTS = list(range(0, 17)) # 下巴索引
35
+ LEFT_FACE = list(range(42, 48)) + list(range(22, 27)) # 左半边脸索引
36
+ RIGHT_FACE = list(range(36, 42)) + list(range(17, 22)) # 右半边脸索引
37
+ JAW_END = 17 # 下巴结束点
38
+ FACE_START = 0 # 人脸识别开始
39
+ FACE_END = 68 # 人脸识别结束
40
+ # 下面这个是整张脸的mark点,可以用:
41
+ # for group in self.OVERLAY_POINTS:
42
+ # cv2.fillConvexPoly(face_mask, cv2.convexHull(dst_matrix[group]), (255, 255, 255))
43
+ # 来形成人脸蒙版
44
+ OVERLAY_POINTS = [
45
+ JAW_POINTS,
46
+ LEFT_FACE,
47
+ RIGHT_FACE
48
+ ]
49
+
50
+ class FaceDetection68(FaceConfig68):
51
+ """
52
+ 人脸68关键点检测主类,当然使用的是dlib开源包
53
+ """
54
+ def __init__(self, model_path:str=None, default_download:bool=False, *args, **kwargs):
55
+ # 初始化,检查并下载模型
56
+ self.model_path = PREDICTOR_PATH if model_path is None else model_path
57
+ if not os.path.exists(self.model_path) or default_download: # 下载配置
58
+ gc = GetConfig()
59
+ gc.load_file(cloud_path="weights/shape_predictor_68_face_landmarks.dat",
60
+ local_path=self.model_path)
61
+ self.__detector = None
62
+ self.__predictor = None
63
+
64
+ @property
65
+ def detector(self):
66
+ if self.__detector is None:
67
+ self.__detector = dlib.get_frontal_face_detector() # 获取人脸分类器
68
+ return self.__detector
69
+ @property
70
+ def predictor(self):
71
+ if self.__predictor is None:
72
+ self.__predictor = dlib.shape_predictor(self.model_path) # 输入模型,构建特征提取器
73
+ return self.__predictor
74
+
75
+ @staticmethod
76
+ def draw_face(img:np.ndarray, dets:dlib.rectangles, *args, **kwargs):
77
+ # 画人脸检测框, 为了一些兼容操作我没有设置默认显示,可以在运行完本函数后将返回值进行self.cv_show()
78
+ tmp = img.copy()
79
+ for face in dets:
80
+ # 左上角(x1,y1),右下角(x2,y2)
81
+ x1, y1, x2, y2 = face.left(), face.top(), face.right(), face.bottom()
82
+ # print(x1, y1, x2, y2)
83
+ cv2.rectangle(tmp, (x1, y1), (x2, y2), (0, 255, 0), 2)
84
+ return tmp
85
+
86
+ @staticmethod
87
+ def draw_points(img:np.ndarray, landmarks:np.matrix, if_num:int=False, *args, **kwargs):
88
+ """
89
+ 画人脸关键点, 为了一些兼容操作我没有设置默认显示,可以在运行完本函数后将返回值进行self.cv_show()
90
+ :param img: 输入的是人脸检测的图,必须是3通道或者灰度图
91
+ :param if_num: 是否在画关键点的同时画上编号
92
+ :param landmarks: 输入的关键点矩阵信息
93
+ """
94
+ tmp = img.copy()
95
+ h, w, c = tmp.shape
96
+ r = int(h / 100) - 2 if h > w else int(w / 100) - 2
97
+ for idx, point in enumerate(landmarks):
98
+ # 68点的坐标
99
+ pos = (point[0, 0], point[0, 1])
100
+ # 利用cv2.circle给每个特征点画一个圈,共68个
101
+ cv2.circle(tmp, pos, r, color=(0, 0, 255), thickness=-1) # bgr
102
+ if if_num is True:
103
+ # 利用cv2.putText输出1-68
104
+ font = cv2.FONT_HERSHEY_SIMPLEX
105
+ cv2.putText(tmp, str(idx + 1), pos, font, 0.8, (0, 0, 255), 1, cv2.LINE_AA)
106
+ return tmp
107
+
108
+ @staticmethod
109
+ def resize_image_esp(input_image_, esp=2000):
110
+ """
111
+ 输入:
112
+ input_path:numpy图片
113
+ esp:限制的最大边长
114
+ """
115
+ # resize函数=>可以让原图压缩到最大边为esp的尺寸(不改变比例)
116
+ width = input_image_.shape[0]
117
+
118
+ length = input_image_.shape[1]
119
+ max_num = max(width, length)
120
+
121
+ if max_num > esp:
122
+ print("Image resizing...")
123
+ if width == max_num:
124
+ length = int((esp / width) * length)
125
+ width = esp
126
+
127
+ else:
128
+ width = int((esp / length) * width)
129
+ length = esp
130
+ print(length, width)
131
+ im_resize = cv2.resize(input_image_, (length, width), interpolation=cv2.INTER_AREA)
132
+ return im_resize
133
+ else:
134
+ return input_image_
135
+
136
+ def facesPoints(self, img:np.ndarray, esp:int=None, det_num:int=1,*args, **kwargs):
137
+ """
138
+ :param img: 输入的是人脸检测的图,必须是3通道或者灰度图
139
+ :param esp: 如果输入了具体数值,会将图片的最大边长缩放至esp,另一边等比例缩放
140
+ :param det_num: 人脸检测的迭代次数, 采样次数越多,越有利于检测到更多的人脸
141
+ :return
142
+ 返回人脸检测框对象dets, 人脸关键点矩阵列表(列表中每个元素为一个人脸的关键点矩阵), 人脸关键点元组列表(列表中每个元素为一个人脸的关键点列表)
143
+ """
144
+ # win = dlib.image_window()
145
+ # win.clear_overlay()
146
+ # win.set_image(img)
147
+ # dlib的人脸检测装置
148
+ if esp is not None:
149
+ img = self.resize_image_esp(input_image_=img, esp=esp)
150
+ dets = self.detector(img, det_num)
151
+ # self.draw_face(img, dets)
152
+ # font_color = "green" if len(dets) == 1 else "red"
153
+ # dg.debug_print("Number of faces detected: {}".format(len(dets)), font_color=font_color)
154
+ landmarkList = []
155
+ pointsList = []
156
+ for d in dets:
157
+ shape = self.predictor(img, d)
158
+ landmark = np.matrix([[p.x, p.y] for p in shape.parts()])
159
+ landmarkList.append(landmark)
160
+ point_list = []
161
+ for p in landmark.tolist():
162
+ point_list.append((p[0], p[1]))
163
+ pointsList.append(point_list)
164
+ # dg.debug_print("Key point detection SUCCESS.", font_color="green")
165
+ return dets, landmarkList, pointsList
166
+
167
+ def facePoints(self, img:np.ndarray, esp:int=None, det_num:int=1, *args, **kwargs):
168
+ """
169
+ 本函数与facesPoints大致类似,主要区别在于本函数默认只能返回一个人脸关键点参数
170
+ """
171
+ # win = dlib.image_window()
172
+ # win.clear_overlay()
173
+ # win.set_image(img)
174
+ # dlib的人脸检测装置, 参数1表示对图片进行上采样一次,采样次数越多,越有利于检测到更多的人脸
175
+ if esp is not None:
176
+ img = self.resize_image_esp(input_image_=img, esp=esp)
177
+ dets = self.detector(img, det_num)
178
+ # self.draw_face(img, dets)
179
+ font_color = "green" if len(dets) == 1 else "red"
180
+ # dg.debug_print("Number of faces detected: {}".format(len(dets)), font_color=font_color)
181
+ if font_color=="red":
182
+ # 本检测函数必然只能检测出一张人脸
183
+ raise FaceError("Face detection error!!!")
184
+ d = dets[0] # 唯一人脸
185
+ shape = self.predictor(img, d)
186
+ landmark = np.matrix([[p.x, p.y] for p in shape.parts()])
187
+ # print("face_landmark:", landmark) # 打印关键点矩阵
188
+ # shape = predictor(img, )
189
+ # dlib.hit_enter_to_continue()
190
+ # 返回关键点矩阵,关键点,
191
+ point_list = []
192
+ for p in landmark.tolist():
193
+ point_list.append((p[0], p[1]))
194
+ # dg.debug_print("Key point detection SUCCESS.", font_color="green")
195
+ # 最后的一个返回参数只会被计算一次,用于标明脸部框的位置
196
+ # [人脸框左上角纵坐标(top),左上角横坐标(left),人脸框宽度(width),人脸框高度(height)]
197
+ return dets, landmark, point_list
198
+
199
+ class PoseEstimator68(object):
200
+ """
201
+ Estimate head pose according to the facial landmarks
202
+ 本类将实现但输入图的人脸姿态检测
203
+ """
204
+ def __init__(self, img:np.ndarray, params_path:str=None, default_download:bool=False):
205
+ self.params_path = MODULE3D_PATH if params_path is None else params_path
206
+ if not os.path.exists(self.params_path) or default_download:
207
+ gc = GetConfig()
208
+ gc.load_file(cloud_path="weights/68_points_3D_model.txt",
209
+ local_path=self.params_path)
210
+ h, w, c = img.shape
211
+ self.size = (h, w)
212
+ # 3D model points.
213
+ self.model_points = np.array([
214
+ (0.0, 0.0, 0.0), # Nose tip
215
+ (0.0, -330.0, -65.0), # Chin
216
+ (-225.0, 170.0, -135.0), # Left eye left corner
217
+ (225.0, 170.0, -135.0), # Right eye right corner
218
+ (-150.0, -150.0, -125.0), # Mouth left corner
219
+ (150.0, -150.0, -125.0) # Mouth right corner
220
+ ]) / 4.5
221
+ self.model_points_68 = self._get_full_model_points()
222
+
223
+ # Camera internals
224
+ self.focal_length = self.size[1]
225
+ self.camera_center = (self.size[1] / 2, self.size[0] / 2)
226
+ self.camera_matrix = np.array(
227
+ [[self.focal_length, 0, self.camera_center[0]],
228
+ [0, self.focal_length, self.camera_center[1]],
229
+ [0, 0, 1]], dtype="double")
230
+
231
+ # Assuming no lens distortion
232
+ self.dist_coeefs = np.zeros((4, 1))
233
+
234
+ # Rotation vector and translation vector
235
+ self.r_vec = np.array([[0.01891013], [0.08560084], [-3.14392813]])
236
+ self.t_vec = np.array(
237
+ [[-14.97821226], [-10.62040383], [-2053.03596872]])
238
+ # self.r_vec = None
239
+ # self.t_vec = None
240
+
241
+ def _get_full_model_points(self):
242
+ """Get all 68 3D model points from file"""
243
+ raw_value = []
244
+ with open(self.params_path) as file:
245
+ for line in file:
246
+ raw_value.append(line)
247
+ model_points = np.array(raw_value, dtype=np.float32)
248
+ model_points = np.reshape(model_points, (3, -1)).T
249
+
250
+ # Transform the model into a front view.
251
+ # model_points[:, 0] *= -1
252
+ model_points[:, 1] *= -1
253
+ model_points[:, 2] *= -1
254
+ return model_points
255
+
256
+ def show_3d_model(self):
257
+ from matplotlib import pyplot
258
+ from mpl_toolkits.mplot3d import Axes3D
259
+ fig = pyplot.figure()
260
+ ax = Axes3D(fig)
261
+
262
+ x = self.model_points_68[:, 0]
263
+ y = self.model_points_68[:, 1]
264
+ z = self.model_points_68[:, 2]
265
+
266
+ ax.scatter(x, y, z)
267
+ ax.axis('auto')
268
+ pyplot.xlabel('x')
269
+ pyplot.ylabel('y')
270
+ pyplot.show()
271
+
272
+ def solve_pose(self, image_points):
273
+ """
274
+ Solve pose from image points
275
+ Return (rotation_vector, translation_vector) as pose.
276
+ """
277
+ assert image_points.shape[0] == self.model_points_68.shape[0], "3D points and 2D points should be of same number."
278
+ (_, rotation_vector, translation_vector) = cv2.solvePnP(
279
+ self.model_points, image_points, self.camera_matrix, self.dist_coeefs)
280
+
281
+ # (success, rotation_vector, translation_vector) = cv2.solvePnP(
282
+ # self.model_points,
283
+ # image_points,
284
+ # self.camera_matrix,
285
+ # self.dist_coeefs,
286
+ # rvec=self.r_vec,
287
+ # tvec=self.t_vec,
288
+ # useExtrinsicGuess=True)
289
+ return rotation_vector, translation_vector
290
+
291
+ def solve_pose_by_68_points(self, image_points):
292
+ """
293
+ Solve pose from all the 68 image points
294
+ Return (rotation_vector, translation_vector) as pose.
295
+ """
296
+ if self.r_vec is None:
297
+ (_, rotation_vector, translation_vector) = cv2.solvePnP(
298
+ self.model_points_68, image_points, self.camera_matrix, self.dist_coeefs)
299
+ self.r_vec = rotation_vector
300
+ self.t_vec = translation_vector
301
+
302
+ (_, rotation_vector, translation_vector) = cv2.solvePnP(
303
+ self.model_points_68,
304
+ image_points,
305
+ self.camera_matrix,
306
+ self.dist_coeefs,
307
+ rvec=self.r_vec,
308
+ tvec=self.t_vec,
309
+ useExtrinsicGuess=True)
310
+
311
+ return rotation_vector, translation_vector
312
+
313
+ # def draw_annotation_box(self, image, rotation_vector, translation_vector, color=(255, 255, 255), line_width=2):
314
+ # """Draw a 3D box as annotation of pose"""
315
+ # point_3d = []
316
+ # rear_size = 75
317
+ # rear_depth = 0
318
+ # point_3d.append((-rear_size, -rear_size, rear_depth))
319
+ # point_3d.append((-rear_size, rear_size, rear_depth))
320
+ # point_3d.append((rear_size, rear_size, rear_depth))
321
+ # point_3d.append((rear_size, -rear_size, rear_depth))
322
+ # point_3d.append((-rear_size, -rear_size, rear_depth))
323
+ #
324
+ # front_size = 100
325
+ # front_depth = 100
326
+ # point_3d.append((-front_size, -front_size, front_depth))
327
+ # point_3d.append((-front_size, front_size, front_depth))
328
+ # point_3d.append((front_size, front_size, front_depth))
329
+ # point_3d.append((front_size, -front_size, front_depth))
330
+ # point_3d.append((-front_size, -front_size, front_depth))
331
+ # point_3d = np.array(point_3d, dtype=np.float64).reshape(-1, 3)
332
+ #
333
+ # # Map to 2d image points
334
+ # (point_2d, _) = cv2.projectPoints(point_3d,
335
+ # rotation_vector,
336
+ # translation_vector,
337
+ # self.camera_matrix,
338
+ # self.dist_coeefs)
339
+ # point_2d = np.int32(point_2d.reshape(-1, 2))
340
+ #
341
+ # # Draw all the lines
342
+ # cv2.polylines(image, [point_2d], True, color, line_width, cv2.LINE_AA)
343
+ # cv2.line(image, tuple(point_2d[1]), tuple(
344
+ # point_2d[6]), color, line_width, cv2.LINE_AA)
345
+ # cv2.line(image, tuple(point_2d[2]), tuple(
346
+ # point_2d[7]), color, line_width, cv2.LINE_AA)
347
+ # cv2.line(image, tuple(point_2d[3]), tuple(
348
+ # point_2d[8]), color, line_width, cv2.LINE_AA)
349
+ #
350
+ # def draw_axis(self, img, R, t):
351
+ # points = np.float32(
352
+ # [[30, 0, 0], [0, 30, 0], [0, 0, 30], [0, 0, 0]]).reshape(-1, 3)
353
+ #
354
+ # axisPoints, _ = cv2.projectPoints(
355
+ # points, R, t, self.camera_matrix, self.dist_coeefs)
356
+ #
357
+ # img = cv2.line(img, tuple(axisPoints[3].ravel()), tuple(
358
+ # axisPoints[0].ravel()), (255, 0, 0), 3)
359
+ # img = cv2.line(img, tuple(axisPoints[3].ravel()), tuple(
360
+ # axisPoints[1].ravel()), (0, 255, 0), 3)
361
+ # img = cv2.line(img, tuple(axisPoints[3].ravel()), tuple(
362
+ # axisPoints[2].ravel()), (0, 0, 255), 3)
363
+
364
+ def draw_axes(self, img, R, t):
365
+ """
366
+ OX is drawn in red, OY in green and OZ in blue.
367
+ """
368
+ return cv2.drawFrameAxes(img, self.camera_matrix, self.dist_coeefs, R, t, 30)
369
+
370
+ @staticmethod
371
+ def get_pose_marks(marks):
372
+ """Get marks ready for pose estimation from 68 marks"""
373
+ pose_marks = [marks[30], marks[8], marks[36], marks[45], marks[48], marks[54]]
374
+ return pose_marks
375
+
376
+ @staticmethod
377
+ def rot_params_rm(R):
378
+ from math import pi,atan2,asin, fabs
379
+ # x轴
380
+ pitch = (180 * atan2(-R[2][1], R[2][2]) / pi)
381
+ f = (0 > pitch) - (0 < pitch)
382
+ pitch = f * (180 - fabs(pitch))
383
+ # y轴
384
+ yaw = -(180 * asin(R[2][0]) / pi)
385
+ # z轴
386
+ roll = (180 * atan2(-R[1][0], R[0][0]) / pi)
387
+ f = (0 > roll) - (0 < roll)
388
+ roll = f * (180 - fabs(roll))
389
+ if not fabs(roll) < 90.0:
390
+ roll = f * (180 - fabs(roll))
391
+ rot_params = [pitch, yaw, roll]
392
+ return rot_params
393
+
394
+ @staticmethod
395
+ def rot_params_rv(rvec_):
396
+ from math import pi, atan2, asin, fabs
397
+ R = cv2.Rodrigues(rvec_)[0]
398
+ # x轴
399
+ pitch = (180 * atan2(-R[2][1], R[2][2]) / pi)
400
+ f = (0 > pitch) - (0 < pitch)
401
+ pitch = f * (180 - fabs(pitch))
402
+ # y轴
403
+ yaw = -(180 * asin(R[2][0]) / pi)
404
+ # z轴
405
+ roll = (180 * atan2(-R[1][0], R[0][0]) / pi)
406
+ f = (0 > roll) - (0 < roll)
407
+ roll = f * (180 - fabs(roll))
408
+ rot_params = [pitch, yaw, roll]
409
+ return rot_params
410
+
411
+ def imageEulerAngle(self, img_points):
412
+ # 这里的img_points对应的是facePoints的第三个返回值,注意是facePoints而非facesPoints
413
+ # 对于facesPoints而言,需要把第三个返回值逐一取出再输入
414
+ # 把列表转为矩阵,且编码形式为float64
415
+ img_points = np.array(img_points, dtype=np.float64)
416
+ rvec, tvec = self.solve_pose_by_68_points(img_points)
417
+ # 旋转向量转旋转矩阵
418
+ R = cv2.Rodrigues(rvec)[0]
419
+ # theta = np.linalg.norm(rvec)
420
+ # r = rvec / theta
421
+ # R_ = np.array([[0, -r[2][0], r[1][0]],
422
+ # [r[2][0], 0, -r[0][0]],
423
+ # [-r[1][0], r[0][0], 0]])
424
+ # R = np.cos(theta) * np.eye(3) + (1 - np.cos(theta)) * r * r.T + np.sin(theta) * R_
425
+ # 旋转矩阵转欧拉角
426
+ eulerAngle = self.rot_params_rm(R)
427
+ # 返回一个元组和欧拉角列表
428
+ return (rvec, tvec, R), eulerAngle
429
+
430
+
431
+ # if __name__ == "__main__":
432
+ # # 示例
433
+ # from hyService.utils import Debug
434
+ # dg = Debug()
435
+ # image_input = cv2.imread("./test.jpg") # 读取一张图片, 必须是三通道或者灰度图
436
+ # fd68 = FaceDetection68() # 初始化人脸关键点检测类
437
+ # dets_, landmark_, point_list_ = fd68.facePoints(image_input) # 输入图片. 检测单张人脸
438
+ # # dets_, landmark_, point_list_ = fd68.facesPoints(input_image) # 输入图片. 检测多张人脸
439
+ # img = fd68.draw_points(image_input, landmark_)
440
+ # dg.cv_show(img)
441
+ # pe = PoseEstimator68(image_input)
442
+ # _, ea = pe.imageEulerAngle(point_list_) # 输入关键点列表, 如果要使用facesPoints,则输入的是point_list_[i]
443
+ # print(ea) # 结果
hivisionai/hycv/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from .utils import cover_mask, get_box, get_box_pro, filtering, cut, zoom_image_without_change_size
hivisionai/hycv/error.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 定义hycv的一些错误类型,其实和hyService大致相同
3
+ """
4
+ class ProcessError(Exception):
5
+ def __init__(self, err):
6
+ super().__init__(err)
7
+ self.err = err
8
+ def __str__(self):
9
+ return self.err
10
+
11
+ class WrongImageType(TypeError):
12
+ def __init__(self, err):
13
+ super().__init__(err)
14
+ self.err = err
15
+ def __str__(self):
16
+ return self.err
hivisionai/hycv/face_tools.py ADDED
@@ -0,0 +1,427 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import os
3
+ import onnxruntime
4
+ from .mtcnn_onnx.detector import detect_faces
5
+ from .tensor2numpy import *
6
+ from PIL import Image
7
+ import requests
8
+ from os.path import exists
9
+
10
+
11
+ def download_img(img_url, base_dir):
12
+ print("Downloading Onnx Model in:", img_url)
13
+ r = requests.get(img_url, stream=True)
14
+ filename = img_url.split("/")[-1]
15
+ # print(r.status_code) # 返回状态码
16
+ if r.status_code == 200:
17
+ open(f'{base_dir}/{filename}', 'wb').write(r.content) # 将内容写入图片
18
+ print(f"Download Finshed -- {filename}")
19
+ del r
20
+
21
+ class BBox(object):
22
+ # bbox is a list of [left, right, top, bottom]
23
+ def __init__(self, bbox):
24
+ self.left = bbox[0]
25
+ self.right = bbox[1]
26
+ self.top = bbox[2]
27
+ self.bottom = bbox[3]
28
+ self.x = bbox[0]
29
+ self.y = bbox[2]
30
+ self.w = bbox[1] - bbox[0]
31
+ self.h = bbox[3] - bbox[2]
32
+
33
+ # scale to [0,1]
34
+ def projectLandmark(self, landmark):
35
+ landmark_= np.asarray(np.zeros(landmark.shape))
36
+ for i, point in enumerate(landmark):
37
+ landmark_[i] = ((point[0]-self.x)/self.w, (point[1]-self.y)/self.h)
38
+ return landmark_
39
+
40
+ # landmark of (5L, 2L) from [0,1] to real range
41
+ def reprojectLandmark(self, landmark):
42
+ landmark_= np.asarray(np.zeros(landmark.shape))
43
+ for i, point in enumerate(landmark):
44
+ x = point[0] * self.w + self.x
45
+ y = point[1] * self.h + self.y
46
+ landmark_[i] = (x, y)
47
+ return landmark_
48
+
49
+
50
+ def face_detect_mtcnn(input_image, color_key=None, filter=None):
51
+ """
52
+ Inputs:
53
+ - input_image: OpenCV Numpy.array
54
+ - color_key: 当color_key等于"RGB"时,将不进行转换操作
55
+ - filter:当filter等于True时,将抛弃掉置信度小于0.98或人脸框面积小于3600的人脸
56
+ return:
57
+ - faces: 带有人脸信息的变量
58
+ - landmarks: face alignment
59
+ """
60
+ if color_key != "RGB":
61
+ input_image = cv2.cvtColor(input_image, cv2.COLOR_BGR2RGB)
62
+
63
+ input_image = Image.fromarray(input_image)
64
+ faces, landmarks = detect_faces(input_image)
65
+
66
+ if filter:
67
+ face_clean = []
68
+ for face in faces:
69
+ confidence = face[-1]
70
+ x1 = face[0]
71
+ y1 = face[1]
72
+ x2 = face[2]
73
+ y2 = face[3]
74
+ w = x2 - x1 + 1
75
+ h = y2 - y1 + 1
76
+ measure = w * h
77
+ if confidence >= 0.98 and measure > 3600:
78
+ # 如果检测到的人脸置信度小于0.98或人脸框面积小于3600,则抛弃该人脸
79
+ face_clean.append(face)
80
+ faces = face_clean
81
+
82
+ return faces, landmarks
83
+
84
+
85
+ def mtcnn_bbox(face, width, height):
86
+ x1 = face[0]
87
+ y1 = face[1]
88
+ x2 = face[2]
89
+ y2 = face[3]
90
+ w = x2 - x1 + 1
91
+ h = y2 - y1 + 1
92
+
93
+ size = int(max([w, h]) * 1.1)
94
+ cx = x1 + w // 2
95
+ cy = y1 + h // 2
96
+ x1 = cx - size // 2
97
+ x2 = x1 + size
98
+ y1 = cy - size // 2
99
+ y2 = y1 + size
100
+
101
+ dx = max(0, -x1)
102
+ dy = max(0, -y1)
103
+ x1 = max(0, x1)
104
+ y1 = max(0, y1)
105
+
106
+ edx = max(0, x2 - width)
107
+ edy = max(0, y2 - height)
108
+ x2 = min(width, x2)
109
+ y2 = min(height, y2)
110
+
111
+ return x1, x2, y1, y2, dx, dy, edx, edy
112
+
113
+
114
+ def mtcnn_cropped_face(face_box, image, width, height):
115
+ x1, x2, y1, y2, dx, dy, edx, edy = mtcnn_bbox(face_box, width, height)
116
+ new_bbox = list(map(int, [x1, x2, y1, y2]))
117
+ new_bbox = BBox(new_bbox)
118
+ cropped = image[new_bbox.top:new_bbox.bottom, new_bbox.left:new_bbox.right]
119
+ if (dx > 0 or dy > 0 or edx > 0 or edy > 0):
120
+ cropped = cv2.copyMakeBorder(cropped, int(dy), int(edy), int(dx), int(edx), cv2.BORDER_CONSTANT, 0)
121
+ return cropped, new_bbox
122
+
123
+
124
+ def face_landmark_56(input_image, faces_box=None):
125
+ basedir = os.path.dirname(os.path.realpath(__file__)).split("mtcnn.py")[0]
126
+ mean = np.asarray([0.485, 0.456, 0.406])
127
+ std = np.asarray([0.229, 0.224, 0.225])
128
+ base_url = "https://linimages.oss-cn-beijing.aliyuncs.com/"
129
+
130
+ if not exists(f"{basedir}/mtcnn_onnx/weights/landmark_detection_56_se_external.onnx"):
131
+ # download onnx model
132
+ download_img(img_url=base_url + "landmark_detection_56_se_external.onnx",
133
+ base_dir=f"{basedir}/mtcnn_onnx/weights")
134
+
135
+ ort_session = onnxruntime.InferenceSession(f"{basedir}/mtcnn_onnx/weights/landmark_detection_56_se_external.onnx")
136
+ out_size = 56
137
+
138
+ height, width, _ = input_image.shape
139
+ if faces_box is None:
140
+ faces_box, _ = face_detect_mtcnn(input_image)
141
+
142
+ if len(faces_box) == 0:
143
+ print('NO face is detected!')
144
+ return None
145
+ else:
146
+ landmarks = []
147
+ for face_box in faces_box:
148
+ cropped, new_bbox = mtcnn_cropped_face(face_box, input_image, width, height)
149
+ cropped_face = cv2.resize(cropped, (out_size, out_size))
150
+
151
+ test_face = NNormalize(cropped_face, mean=mean, std=std)
152
+ test_face = NTo_Tensor(test_face)
153
+ test_face = NUnsqueeze(test_face)
154
+
155
+ ort_inputs = {ort_session.get_inputs()[0].name: test_face}
156
+ ort_outs = ort_session.run(None, ort_inputs)
157
+
158
+ landmark = ort_outs[0]
159
+
160
+ landmark = landmark.reshape(-1, 2)
161
+ landmark = new_bbox.reprojectLandmark(landmark)
162
+ landmarks.append(landmark)
163
+
164
+ return landmarks
165
+
166
+
167
+
168
+ REFERENCE_FACIAL_POINTS = [
169
+ [30.29459953, 51.69630051],
170
+ [65.53179932, 51.50139999],
171
+ [48.02519989, 71.73660278],
172
+ [33.54930115, 92.3655014],
173
+ [62.72990036, 92.20410156]
174
+ ]
175
+
176
+ DEFAULT_CROP_SIZE = (96, 112)
177
+
178
+
179
+ def _umeyama(src, dst, estimate_scale=True, scale=1.0):
180
+ """Estimate N-D similarity transformation with or without scaling.
181
+ Parameters
182
+ ----------
183
+ src : (M, N) array
184
+ Source coordinates.
185
+ dst : (M, N) array
186
+ Destination coordinates.
187
+ estimate_scale : bool
188
+ Whether to estimate scaling factor.
189
+ Returns
190
+ -------
191
+ T : (N + 1, N + 1)
192
+ The homogeneous similarity transformation matrix. The matrix contains
193
+ NaN values only if the problem is not well-conditioned.
194
+ References
195
+ ----------
196
+ .. [1] "Least-squares estimation of transformation parameters between two
197
+ point patterns", Shinji Umeyama, PAMI 1991, :DOI:`10.1109/34.88573`
198
+ """
199
+
200
+ num = src.shape[0]
201
+ dim = src.shape[1]
202
+
203
+ # Compute mean of src and dst.
204
+ src_mean = src.mean(axis=0)
205
+ dst_mean = dst.mean(axis=0)
206
+
207
+ # Subtract mean from src and dst.
208
+ src_demean = src - src_mean
209
+ dst_demean = dst - dst_mean
210
+
211
+ # Eq. (38).
212
+ A = dst_demean.T @ src_demean / num
213
+
214
+ # Eq. (39).
215
+ d = np.ones((dim,), dtype=np.double)
216
+ if np.linalg.det(A) < 0:
217
+ d[dim - 1] = -1
218
+
219
+ T = np.eye(dim + 1, dtype=np.double)
220
+
221
+ U, S, V = np.linalg.svd(A)
222
+
223
+ # Eq. (40) and (43).
224
+ rank = np.linalg.matrix_rank(A)
225
+ if rank == 0:
226
+ return np.nan * T
227
+ elif rank == dim - 1:
228
+ if np.linalg.det(U) * np.linalg.det(V) > 0:
229
+ T[:dim, :dim] = U @ V
230
+ else:
231
+ s = d[dim - 1]
232
+ d[dim - 1] = -1
233
+ T[:dim, :dim] = U @ np.diag(d) @ V
234
+ d[dim - 1] = s
235
+ else:
236
+ T[:dim, :dim] = U @ np.diag(d) @ V
237
+
238
+ if estimate_scale:
239
+ # Eq. (41) and (42).
240
+ scale = 1.0 / src_demean.var(axis=0).sum() * (S @ d)
241
+ else:
242
+ scale = scale
243
+
244
+ T[:dim, dim] = dst_mean - scale * (T[:dim, :dim] @ src_mean.T)
245
+ T[:dim, :dim] *= scale
246
+
247
+ return T, scale
248
+
249
+
250
+ class FaceWarpException(Exception):
251
+ def __str__(self):
252
+ return 'In File {}:{}'.format(
253
+ __file__, super.__str__(self))
254
+
255
+
256
+ def get_reference_facial_points_5(output_size=None,
257
+ inner_padding_factor=0.0,
258
+ outer_padding=(0, 0),
259
+ default_square=False):
260
+ tmp_5pts = np.array(REFERENCE_FACIAL_POINTS)
261
+ tmp_crop_size = np.array(DEFAULT_CROP_SIZE)
262
+
263
+ # 0) make the inner region a square
264
+ if default_square:
265
+ size_diff = max(tmp_crop_size) - tmp_crop_size
266
+ tmp_5pts += size_diff / 2
267
+ tmp_crop_size += size_diff
268
+
269
+ if (output_size and
270
+ output_size[0] == tmp_crop_size[0] and
271
+ output_size[1] == tmp_crop_size[1]):
272
+ print('output_size == DEFAULT_CROP_SIZE {}: return default reference points'.format(tmp_crop_size))
273
+ return tmp_5pts
274
+
275
+ if (inner_padding_factor == 0 and
276
+ outer_padding == (0, 0)):
277
+ if output_size is None:
278
+ print('No paddings to do: return default reference points')
279
+ return tmp_5pts
280
+ else:
281
+ raise FaceWarpException(
282
+ 'No paddings to do, output_size must be None or {}'.format(tmp_crop_size))
283
+
284
+ # check output size
285
+ if not (0 <= inner_padding_factor <= 1.0):
286
+ raise FaceWarpException('Not (0 <= inner_padding_factor <= 1.0)')
287
+
288
+ if ((inner_padding_factor > 0 or outer_padding[0] > 0 or outer_padding[1] > 0)
289
+ and output_size is None):
290
+ output_size = tmp_crop_size * \
291
+ (1 + inner_padding_factor * 2).astype(np.int32)
292
+ output_size += np.array(outer_padding)
293
+ print(' deduced from paddings, output_size = ', output_size)
294
+
295
+ if not (outer_padding[0] < output_size[0]
296
+ and outer_padding[1] < output_size[1]):
297
+ raise FaceWarpException('Not (outer_padding[0] < output_size[0]'
298
+ 'and outer_padding[1] < output_size[1])')
299
+
300
+ # 1) pad the inner region according inner_padding_factor
301
+ # print('---> STEP1: pad the inner region according inner_padding_factor')
302
+ if inner_padding_factor > 0:
303
+ size_diff = tmp_crop_size * inner_padding_factor * 2
304
+ tmp_5pts += size_diff / 2
305
+ tmp_crop_size += np.round(size_diff).astype(np.int32)
306
+
307
+ # print(' crop_size = ', tmp_crop_size)
308
+ # print(' reference_5pts = ', tmp_5pts)
309
+
310
+ # 2) resize the padded inner region
311
+ # print('---> STEP2: resize the padded inner region')
312
+ size_bf_outer_pad = np.array(output_size) - np.array(outer_padding) * 2
313
+ # print(' crop_size = ', tmp_crop_size)
314
+ # print(' size_bf_outer_pad = ', size_bf_outer_pad)
315
+
316
+ if size_bf_outer_pad[0] * tmp_crop_size[1] != size_bf_outer_pad[1] * tmp_crop_size[0]:
317
+ raise FaceWarpException('Must have (output_size - outer_padding)'
318
+ '= some_scale * (crop_size * (1.0 + inner_padding_factor)')
319
+
320
+ scale_factor = size_bf_outer_pad[0].astype(np.float32) / tmp_crop_size[0]
321
+ # print(' resize scale_factor = ', scale_factor)
322
+ tmp_5pts = tmp_5pts * scale_factor
323
+ # size_diff = tmp_crop_size * (scale_factor - min(scale_factor))
324
+ # tmp_5pts = tmp_5pts + size_diff / 2
325
+ tmp_crop_size = size_bf_outer_pad
326
+ # print(' crop_size = ', tmp_crop_size)
327
+ # print(' reference_5pts = ', tmp_5pts)
328
+
329
+ # 3) add outer_padding to make output_size
330
+ reference_5point = tmp_5pts + np.array(outer_padding)
331
+ tmp_crop_size = output_size
332
+ # print('---> STEP3: add outer_padding to make output_size')
333
+ # print(' crop_size = ', tmp_crop_size)
334
+ # print(' reference_5pts = ', tmp_5pts)
335
+ #
336
+ # print('===> end get_reference_facial_points\n')
337
+
338
+ return reference_5point
339
+
340
+
341
+ def get_affine_transform_matrix(src_pts, dst_pts):
342
+ tfm = np.float32([[1, 0, 0], [0, 1, 0]])
343
+ n_pts = src_pts.shape[0]
344
+ ones = np.ones((n_pts, 1), src_pts.dtype)
345
+ src_pts_ = np.hstack([src_pts, ones])
346
+ dst_pts_ = np.hstack([dst_pts, ones])
347
+
348
+ A, res, rank, s = np.linalg.lstsq(src_pts_, dst_pts_)
349
+
350
+ if rank == 3:
351
+ tfm = np.float32([
352
+ [A[0, 0], A[1, 0], A[2, 0]],
353
+ [A[0, 1], A[1, 1], A[2, 1]]
354
+ ])
355
+ elif rank == 2:
356
+ tfm = np.float32([
357
+ [A[0, 0], A[1, 0], 0],
358
+ [A[0, 1], A[1, 1], 0]
359
+ ])
360
+
361
+ return tfm
362
+
363
+
364
+ def warp_and_crop_face(src_img,
365
+ facial_pts,
366
+ reference_pts=None,
367
+ crop_size=(96, 112),
368
+ align_type='smilarity'): #smilarity cv2_affine affine
369
+ if reference_pts is None:
370
+ if crop_size[0] == 96 and crop_size[1] == 112:
371
+ reference_pts = REFERENCE_FACIAL_POINTS
372
+ else:
373
+ default_square = False
374
+ inner_padding_factor = 0
375
+ outer_padding = (0, 0)
376
+ output_size = crop_size
377
+
378
+ reference_pts = get_reference_facial_points_5(output_size,
379
+ inner_padding_factor,
380
+ outer_padding,
381
+ default_square)
382
+
383
+ ref_pts = np.float32(reference_pts)
384
+ ref_pts_shp = ref_pts.shape
385
+ if max(ref_pts_shp) < 3 or min(ref_pts_shp) != 2:
386
+ raise FaceWarpException(
387
+ 'reference_pts.shape must be (K,2) or (2,K) and K>2')
388
+
389
+ if ref_pts_shp[0] == 2:
390
+ ref_pts = ref_pts.T
391
+
392
+ src_pts = np.float32(facial_pts)
393
+ src_pts_shp = src_pts.shape
394
+ if max(src_pts_shp) < 3 or min(src_pts_shp) != 2:
395
+ raise FaceWarpException(
396
+ 'facial_pts.shape must be (K,2) or (2,K) and K>2')
397
+
398
+ if src_pts_shp[0] == 2:
399
+ src_pts = src_pts.T
400
+
401
+ if src_pts.shape != ref_pts.shape:
402
+ raise FaceWarpException(
403
+ 'facial_pts and reference_pts must have the same shape')
404
+
405
+ if align_type == 'cv2_affine':
406
+ tfm = cv2.getAffineTransform(src_pts[0:3], ref_pts[0:3])
407
+ tfm_inv = cv2.getAffineTransform(ref_pts[0:3], src_pts[0:3])
408
+ elif align_type == 'affine':
409
+ tfm = get_affine_transform_matrix(src_pts, ref_pts)
410
+ tfm_inv = get_affine_transform_matrix(ref_pts, src_pts)
411
+ else:
412
+ params, scale = _umeyama(src_pts, ref_pts)
413
+ tfm = params[:2, :]
414
+
415
+ params, _ = _umeyama(ref_pts, src_pts, False, scale=1.0/scale)
416
+ tfm_inv = params[:2, :]
417
+
418
+ face_img = cv2.warpAffine(src_img, tfm, (crop_size[0], crop_size[1]), flags=3)
419
+
420
+ return face_img, tfm_inv
421
+
422
+
423
+ if __name__ == "__main__":
424
+ image = cv2.imread("/home/parallels/Desktop/IDPhotos/input_image/03.jpg")
425
+ face_detect_mtcnn(image)
426
+
427
+
hivisionai/hycv/idphoto.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ from .idphotoTool.idphoto_cut import IDphotos_create
2
+ from .idphotoTool.idphoto_change_cloth import change_cloth
hivisionai/hycv/idphotoTool/__init__.py ADDED
File without changes
hivisionai/hycv/idphotoTool/cuny_tools.py ADDED
@@ -0,0 +1,593 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+ from ..utils import get_box_pro
4
+ from ..vision import cover_image, draw_picture_dots
5
+
6
+
7
+ def transformationNeck2(image:np.ndarray, per_to_side:float=0.8)->np.ndarray:
8
+ """
9
+ 透视变换脖子函数,输入图像和四个点(矩形框)
10
+ 矩形框内的图像可能是不完整的(边角有透明区域)
11
+ 我们将根据透视变换将矩形框内的图像拉伸成和矩形框一样的形状.
12
+ 算法分为几个步骤: 选择脖子的四个点 -> 选定这四个点拉伸后的坐标 -> 透视变换 -> 覆盖原图
13
+ """
14
+ _, _, _, a = cv2.split(image) # 这应该是一个四通道的图像
15
+ height, width = a.shape
16
+ def locate_side(image_:np.ndarray, x_:int, y_max:int) -> int:
17
+ # 寻找x=y, 且 y <= y_max 上从下往上第一个非0的点,如果没找到就返回0
18
+ y_ = 0
19
+ for y_ in range(y_max - 1, -1, -1):
20
+ if image_[y_][x_] != 0:
21
+ break
22
+ return y_
23
+ def locate_width(image_:np.ndarray, y_:int, mode, left_or_right:int=None):
24
+ # 从y=y这个水平线上寻找两边的非零点
25
+ # 增加left_or_right的原因在于为下面check_jaw服务
26
+ if mode==1: # 左往右
27
+ x_ = 0
28
+ if left_or_right is None:
29
+ left_or_right = 0
30
+ for x_ in range(left_or_right, width):
31
+ if image_[y_][x_] != 0:
32
+ break
33
+ else: # 右往左
34
+ x_ = width
35
+ if left_or_right is None:
36
+ left_or_right = width - 1
37
+ for x_ in range(left_or_right, -1, -1):
38
+ if image_[y_][x_] != 0:
39
+ break
40
+ return x_
41
+ def check_jaw(image_:np.ndarray, left_, right_):
42
+ """
43
+ 检查选择的点是否与截到下巴,如果截到了,就往下平移一个单位
44
+ """
45
+ f= True # True代表没截到下巴
46
+ # [x, y]
47
+ for x_cell in range(left_[0] + 1, right_[0]):
48
+ if image_[left_[1]][x_cell] == 0:
49
+ f = False
50
+ break
51
+ if f is True:
52
+ return left_, right_
53
+ else:
54
+ y_ = left_[1] + 2
55
+ x_left_ = locate_width(image_, y_, mode=1, left_or_right=left_[0])
56
+ x_right_ = locate_width(image_, y_, mode=2, left_or_right=right_[0])
57
+ left_, right_ = check_jaw(image_, [x_left_, y_], [x_right_, y_])
58
+ return left_, right_
59
+ # 选择脖子的四个点,核心在于选择上面的两个点,这两个点的确定的位置应该是"宽出来的"两个点
60
+ _, _ ,_, a = cv2.split(image) # 这应该是一个四通道的图像
61
+ ret,a_thresh = cv2.threshold(a,127,255,cv2.THRESH_BINARY)
62
+ y_high, y_low, x_left, x_right = get_box_pro(image=image, model=1) # 直接返回矩阵信息
63
+ y_left_side = locate_side(image_=a_thresh, x_=x_left, y_max=y_low) # 左边的点的y轴坐标
64
+ y_right_side = locate_side(image_=a_thresh, x_=x_right, y_max=y_low) # 右边的点的y轴坐标
65
+ y = min(y_left_side, y_right_side) # 将两点的坐标保持相同
66
+ cell_left_above, cell_right_above = check_jaw(a_thresh,[x_left, y], [x_right, y])
67
+ x_left, x_right = cell_left_above[0], cell_right_above[0]
68
+ # 此时我们寻找到了脖子的"宽出来的"两个点,这两个点作为上面的两个点, 接下来寻找下面的两个点
69
+ if per_to_side >1:
70
+ assert ValueError("per_to_side 必须小于1!")
71
+ # 在后面的透视变换中我会把它拉成矩形, 在这里我先获取四个点的高和宽
72
+ height_ = 150 # 这个值应该是个变化的值,与拉伸的长度有关,但是现在先规定为150
73
+ width_ = x_right - x_left # 其实也就是 cell_right_above[1] - cell_left_above[1]
74
+ y = int((y_low - y)*per_to_side + y) # 定位y轴坐标
75
+ cell_left_below, cell_right_bellow = ([locate_width(a_thresh, y_=y, mode=1), y], [locate_width(a_thresh, y_=y, mode=2), y])
76
+ # 四个点全齐,开始透视变换
77
+ # 寻找透视变换后的四个点,只需要变换below的两个点即可
78
+ # cell_left_below_final, cell_right_bellow_final = ([cell_left_above[1], y_low], [cell_right_above[1], y_low])
79
+ # 需要变换的四个点为 cell_left_above, cell_right_above, cell_left_below, cell_right_bellow
80
+ rect = np.array([cell_left_above, cell_right_above, cell_left_below, cell_right_bellow],
81
+ dtype='float32')
82
+ # 变化后的坐标点
83
+ dst = np.array([[0, 0], [width_, 0], [0 , height_], [width_, height_]],
84
+ dtype='float32')
85
+ # 计算变换矩阵
86
+ M = cv2.getPerspectiveTransform(rect, dst)
87
+ warped = cv2.warpPerspective(image, M, (width_, height_))
88
+ final = cover_image(image=warped, background=image, mode=3, x=cell_left_above[0], y=cell_left_above[1])
89
+ # tmp = np.zeros(image.shape)
90
+ # final = cover_image(image=warped, background=tmp, mode=3, x=cell_left_above[0], y=cell_left_above[1])
91
+ # final = cover_image(image=image, background=final, mode=3, x=0, y=0)
92
+ return final
93
+
94
+ def transformationNeck(image:np.ndarray, cutNeckHeight:int, neckBelow:int,
95
+ toHeight:int,per_to_side:float=0.75) -> np.ndarray:
96
+ """
97
+ 脖子扩充算法, 其实需要输入的只是脖子扣出来的部分以及需要被扩充的高度/需要被扩充成的高度.
98
+ """
99
+ height, width, channels = image.shape
100
+ _, _, _, a = cv2.split(image) # 这应该是一个四通道的图像
101
+ ret, a_thresh = cv2.threshold(a, 127, 255, cv2.THRESH_BINARY) # 将透明图层二值化
102
+ def locate_width(image_:np.ndarray, y_:int, mode, left_or_right:int=None):
103
+ # 从y=y这个水平线上寻找两边的非零点
104
+ # 增加left_or_right的原因在于为下面check_jaw服务
105
+ if mode==1: # 左往右
106
+ x_ = 0
107
+ if left_or_right is None:
108
+ left_or_right = 0
109
+ for x_ in range(left_or_right, width):
110
+ if image_[y_][x_] != 0:
111
+ break
112
+ else: # 右往左
113
+ x_ = width
114
+ if left_or_right is None:
115
+ left_or_right = width - 1
116
+ for x_ in range(left_or_right, -1, -1):
117
+ if image_[y_][x_] != 0:
118
+ break
119
+ return x_
120
+ def check_jaw(image_:np.ndarray, left_, right_):
121
+ """
122
+ 检查选择的点是否与截到下巴,如果截到了,就往下平移一个单位
123
+ """
124
+ f= True # True代表没截到下巴
125
+ # [x, y]
126
+ for x_cell in range(left_[0] + 1, right_[0]):
127
+ if image_[left_[1]][x_cell] == 0:
128
+ f = False
129
+ break
130
+ if f is True:
131
+ return left_, right_
132
+ else:
133
+ y_ = left_[1] + 2
134
+ x_left_ = locate_width(image_, y_, mode=1, left_or_right=left_[0])
135
+ x_right_ = locate_width(image_, y_, mode=2, left_or_right=right_[0])
136
+ left_, right_ = check_jaw(image_, [x_left_, y_], [x_right_, y_])
137
+ return left_, right_
138
+ x_left = locate_width(image_=a_thresh, mode=1, y_=cutNeckHeight)
139
+ x_right = locate_width(image_=a_thresh, mode=2, y_=cutNeckHeight)
140
+ # 在这里我们取消了对下巴的检查,原因在于输入的imageHeight并不能改变
141
+ # cell_left_above, cell_right_above = check_jaw(a_thresh, [x_left, imageHeight], [x_right, imageHeight])
142
+ cell_left_above, cell_right_above = [x_left, cutNeckHeight], [x_right, cutNeckHeight]
143
+ toWidth = x_right - x_left # 矩形宽
144
+ # 此时我们寻找到了脖子的"宽出来的"两个点,这两个点作为上面的两个点, 接下来寻找下面的两个点
145
+ if per_to_side >1:
146
+ assert ValueError("per_to_side 必须小于1!")
147
+ y_below = int((neckBelow - cutNeckHeight) * per_to_side + cutNeckHeight) # 定位y轴坐标
148
+ cell_left_below = [locate_width(a_thresh, y_=y_below, mode=1), y_below]
149
+ cell_right_bellow = [locate_width(a_thresh, y_=y_below, mode=2), y_below]
150
+ # 四个点全齐,开始透视变换
151
+ # 需要变换的四个点为 cell_left_above, cell_right_above, cell_left_below, cell_right_bellow
152
+ rect = np.array([cell_left_above, cell_right_above, cell_left_below, cell_right_bellow],
153
+ dtype='float32')
154
+ # 变化后的坐标点
155
+ dst = np.array([[0, 0], [toWidth, 0], [0 , toHeight], [toWidth, toHeight]],
156
+ dtype='float32')
157
+ M = cv2.getPerspectiveTransform(rect, dst)
158
+ warped = cv2.warpPerspective(image, M, (toWidth, toHeight))
159
+ # 将变换后的图像覆盖到原图上
160
+ final = cover_image(image=warped, background=image, mode=3, x=cell_left_above[0], y=cell_left_above[1])
161
+ return final
162
+
163
+ def bestJunctionCheck_beta(image:np.ndarray, stepSize:int=4, if_per:bool=False):
164
+ """
165
+ 最优衔接点检测算法, 去寻找脖子的"拐点"
166
+ """
167
+ point_k = 1
168
+ _, _, _, a = cv2.split(image) # 这应该是一个四通道的图像
169
+ height, width = a.shape
170
+ ret, a_thresh = cv2.threshold(a, 127, 255, cv2.THRESH_BINARY) # 将透明图层二值化
171
+ y_high, y_low, x_left, x_right = get_box_pro(image=image, model=1) # 直接返回矩阵信息
172
+ def scan(y_:int, max_num:int=2):
173
+ num = 0
174
+ left = False
175
+ right = False
176
+ for x_ in range(width):
177
+ if a_thresh[y_][x_] != 0:
178
+ if x_ < width // 2 and left is False:
179
+ num += 1
180
+ left = True
181
+ elif x_ > width // 2 and right is False:
182
+ num += 1
183
+ right = True
184
+ return True if num >= max_num else False
185
+ def locate_neck_above():
186
+ """
187
+ 定位脖子的尖尖脚
188
+ """
189
+ for y_ in range( y_high - 2, height):
190
+ if scan(y_):
191
+ return y_, y_
192
+ y_high_left, y_high_right = locate_neck_above()
193
+ def locate_width_pro(image_:np.ndarray, y_:int, mode):
194
+ """
195
+ 这会是一个生成器,用于生成脖子两边的轮廓
196
+ x_, y_ 是启始点的坐标,每一次寻找都会让y_+1
197
+ mode==1说明是找左边的边,即,image_[y_][x_] == 0 且image_[y_][x_ + 1] !=0 时跳出;
198
+ 否则 当image_[y_][x_] != 0 时, x_ - 1; 当image_[y_][x_] == 0 且 image_[y_][x_ + 1] ==0 时x_ + 1
199
+ mode==2说明是找右边的边,即,image_[y_][x_] == 0 且image_[y_][x_ - 1] !=0 时跳出
200
+ 否则 当image_[y_][x_] != 0 时, x_ + 1; 当image_[y_][x_] == 0 且 image_[y_][x_ - 1] ==0 时x_ - 1
201
+ """
202
+ y_ += 1
203
+ if mode == 1:
204
+ x_ = 0
205
+ while 0 <= y_ < height and 0 <= x_ < width:
206
+ while image_[y_][x_] != 0 and x_ >= 0:
207
+ x_ -= 1
208
+ while image_[y_][x_] == 0 and image_[y_][x_ + 1] == 0 and x_ < width - 2:
209
+ x_ += 1
210
+ yield [y_, x_]
211
+ y_ += 1
212
+ elif mode == 2:
213
+ x_ = width-1
214
+ while 0 <= y_ < height and 0 <= x_ < width:
215
+ while image_[y_][x_] != 0 and x_ < width - 2: x_ += 1
216
+ while image_[y_][x_] == 0 and image_[y_][x_ - 1] == 0 and x_ >= 0: x_ -= 1
217
+ yield [y_, x_]
218
+ y_ += 1
219
+ yield False
220
+ def kGenerator(image_:np.ndarray, mode):
221
+ """
222
+ 导数生成器,用来生成每一个点对应的导数
223
+ """
224
+ y_ = y_high_left if mode == 1 else y_high_right
225
+ c_generator = locate_width_pro(image_=image_, y_=y_, mode=mode)
226
+ for cell in c_generator:
227
+ nc = locate_width_pro(image_=image_, y_=cell[0] + stepSize, mode=mode)
228
+ nextCell = next(nc)
229
+ if nextCell is False:
230
+ yield False, False
231
+ else:
232
+ k = (cell[1] - nextCell[1]) / stepSize
233
+ yield k, cell
234
+ def findPt(image_:np.ndarray, mode):
235
+ k_generator = kGenerator(image_=image_, mode=mode)
236
+ k, cell = next(k_generator)
237
+ k_next, cell_next = next(k_generator)
238
+ if k is False:
239
+ raise ValueError("无法找到拐点!")
240
+ while k_next is not False:
241
+ k_next, cell_next = next(k_generator)
242
+ if (k_next < - 1 / stepSize) or k_next > point_k:
243
+ break
244
+ cell = cell_next
245
+ # return int(cell[0] + stepSize / 2)
246
+ return cell[0]
247
+ # 先找左边的拐点:
248
+ pointY_left = findPt(image_=a_thresh, mode=1)
249
+ # 再找右边的拐点:
250
+ pointY_right = findPt(image_=a_thresh, mode=2)
251
+ point = (pointY_left + pointY_right) // 2
252
+ if if_per is True:
253
+ point = (pointY_left + pointY_right) // 2
254
+ return point / (y_low - y_high)
255
+ pointX_left = next(locate_width_pro(image_=a_thresh, y_= point - 1, mode=1))[1]
256
+ pointX_right = next(locate_width_pro(image_=a_thresh, y_=point- 1, mode=2))[1]
257
+ return [pointX_left, point], [pointX_right, point]
258
+
259
+
260
+ def bestJunctionCheck(image:np.ndarray, offset:int, stepSize:int=4):
261
+ """
262
+ 最优点检测算算法输入一张脖子图片(无论这张图片是否已经被二值化,我都认为没有被二值化),输出一个小数(脖子最上方与衔接点位置/脖子图像长度)
263
+ 与beta版不同的是它新增了一个阈值限定内容.
264
+ 对于脖子而言,我我们首先可以定位到上面的部分,然后根据上面的这个点向下进行遍历检测.
265
+ 与beta版类似,我们使用一个stepSize来用作斜率的检测
266
+ 但是对于遍历检测而言,与beta版不同的是,我们需要对遍历的地方进行一定的限制.
267
+ 限制的标准是,如果当前遍历的点的横坐标和起始点横坐标的插值超过了某个阈值,则认为是越界.
268
+ """
269
+ point_k = 1
270
+ _, _, _, a = cv2.split(image) # 这应该是一个四通道的图像
271
+ height, width = a.shape
272
+ ret, a_thresh = cv2.threshold(a, 127, 255, cv2.THRESH_BINARY) # 将透明图层二值化
273
+ # 直接返回脖子的位置信息, 修正系数为0, get_box_pro内部也封装了二值化,所以直接输入原图
274
+ y_high, y_low, _, _ = get_box_pro(image=image, model=1, correction_factor=0)
275
+ # 真正有用的只有上下y轴的两个值...
276
+ # 首先当然是确定起始点的位置,我们用同样的scan扫描函数进行行遍历.
277
+ def scan(y_:int, max_num:int=2):
278
+ num = 0
279
+ # 设定两个值,分别代表脖子的左边和右边
280
+ left = False
281
+ right = False
282
+ for x_ in range(width):
283
+ if a_thresh[y_][x_] != 0:
284
+ # 检测左边
285
+ if x_ < width // 2 and left is False:
286
+ num += 1
287
+ left = True
288
+ # 检测右边
289
+ elif x_ > width // 2 and right is False:
290
+ num += 1
291
+ right = True
292
+ return True if num >= max_num else False
293
+ def locate_neck_above():
294
+ """
295
+ 定位脖子的尖尖脚
296
+ """
297
+ # y_high就是脖子的最高点
298
+ for y_ in range(y_high, height):
299
+ if scan(y_):
300
+ return y_
301
+ y_start = locate_neck_above() # 得到遍历的初始高度
302
+ if y_low - y_start < stepSize: assert ValueError("脖子太小!")
303
+ # 然后获取一下初始的坐标点
304
+ x_left, x_right = 0, width
305
+ for x_left_ in range(0, width):
306
+ if a_thresh[y_start][x_left_] != 0:
307
+ x_left = x_left_
308
+ break
309
+ for x_right_ in range(width -1 , -1, -1):
310
+ if a_thresh[y_start][x_right_] != 0:
311
+ x_right = x_right_
312
+ break
313
+ # 接下来我定义两个生成器,首先是脖子轮廓(向下寻找的)生成器,每进行一次next,生成器会返回y+1的脖子轮廓点
314
+ def contoursGenerator(image_:np.ndarray, y_:int, mode):
315
+ """
316
+ 这会是一个生成器,用于生成脖子两边的轮廓
317
+ y_ 是启始点的y坐标,每一次寻找都会让y_+1
318
+ mode==1说明是找左边的边,即,image_[y_][x_] == 0 且image_[y_][x_ + 1] !=0 时跳出;
319
+ 否则 当image_[y_][x_] != 0 时, x_ - 1; 当image_[y_][x_] == 0 且 image_[y_][x_ + 1] ==0 时x_ + 1
320
+ mode==2说明是找右边的边,即,image_[y_][x_] == 0 且image_[y_][x_ - 1] !=0 时跳出
321
+ 否则 当image_[y_][x_] != 0 时, x_ + 1; 当image_[y_][x_] == 0 且 image_[y_][x_ - 1] ==0 时x_ - 1
322
+ """
323
+ y_ += 1
324
+ try:
325
+ if mode == 1:
326
+ x_ = 0
327
+ while 0 <= y_ < height and 0 <= x_ < width:
328
+ while image_[y_][x_] != 0 and x_ >= 0: x_ -= 1
329
+ # 这里其实会有bug,不过可以不管
330
+ while x_ < width and image_[y_][x_] == 0 and image_[y_][x_ + 1] == 0: x_ += 1
331
+ yield [y_, x_]
332
+ y_ += 1
333
+ elif mode == 2:
334
+ x_ = width-1
335
+ while 0 <= y_ < height and 0 <= x_ < width:
336
+ while x_ < width and image_[y_][x_] != 0: x_ += 1
337
+ while x_ >= 0 and image_[y_][x_] == 0 and image_[y_][x_ - 1] == 0: x_ -= 1
338
+ yield [y_, x_]
339
+ y_ += 1
340
+ # 当处理失败则返回False
341
+ except IndexError:
342
+ yield False
343
+ # 然后是斜率生成器,这个生成器依赖子轮廓生成器,每一次生成轮廓后会计算斜率,另一个点的选取和stepSize有关
344
+ def kGenerator(image_: np.ndarray, mode):
345
+ """
346
+ 导数生成器,用来生成每一个点对应的导数
347
+ """
348
+ y_ = y_start
349
+ # 对起始点建立一个生成器, mode=1时是左边轮廓,mode=2时是右边轮廓
350
+ c_generator = contoursGenerator(image_=image_, y_=y_, mode=mode)
351
+ for cell in c_generator:
352
+ # 寻找距离当前cell距离为stepSize的轮廓点
353
+ kc = contoursGenerator(image_=image_, y_=cell[0] + stepSize, mode=mode)
354
+ kCell = next(kc)
355
+ if kCell is False:
356
+ # 寻找失败
357
+ yield False, False
358
+ else:
359
+ # 寻找成功,返回当坐标点和斜率值
360
+ # 对于左边而言,斜率必然是前一个点的坐标减去后一个点的坐标
361
+ # 对于右边而言,斜率必然是后一个点的坐标减去前一个点的坐标
362
+ k = (cell[1] - kCell[1]) / stepSize if mode == 1 else (kCell[1] - cell[1]) / stepSize
363
+ yield k, cell
364
+ # 接着开始写寻找算法,需要注意的是我们是分两边选择的
365
+ def findPt(image_:np.ndarray, mode):
366
+ x_base = x_left if mode == 1 else x_right
367
+ k_generator = kGenerator(image_=image_, mode=mode)
368
+ k, cell = k_generator.__next__()
369
+ if k is False:
370
+ raise ValueError("无法找到拐点!")
371
+ k_next, cell_next = k_generator.__next__()
372
+ while k_next is not False:
373
+ cell = cell_next
374
+ if cell[1] > x_base and mode == 2:
375
+ x_base = cell[1]
376
+ elif cell[1] < x_base and mode == 1:
377
+ x_base = cell[1]
378
+ # 跳出循环的方式一:斜率超过了某个值
379
+ if k_next > point_k:
380
+ print("K out")
381
+ break
382
+ # 跳出循环的方式二:超出阈值
383
+ elif abs(cell[1] - x_base) > offset:
384
+ print("O out")
385
+ break
386
+ k_next, cell_next = k_generator.__next__()
387
+ if abs(cell[1] - x_base) > offset:
388
+ cell[0] = cell[0] - offset - 1
389
+ return cell[0]
390
+ # 先找左边的拐点:
391
+ pointY_left = findPt(image_=a_thresh, mode=1)
392
+ # 再找右边的拐点:
393
+ pointY_right = findPt(image_=a_thresh, mode=2)
394
+ point = min(pointY_right, pointY_left)
395
+ per = (point - y_high) / (y_low - y_high)
396
+ # pointX_left = next(contoursGenerator(image_=a_thresh, y_= point- 1, mode=1))[1]
397
+ # pointX_right = next(contoursGenerator(image_=a_thresh, y_=point - 1, mode=2))[1]
398
+ # return [pointX_left, point], [pointX_right, point]
399
+ return per
400
+
401
+
402
+ def checkSharpCorner(image:np.ndarray):
403
+ _, _, _, a = cv2.split(image) # 这应该是一个四通道的图像
404
+ height, width = a.shape
405
+ ret, a_thresh = cv2.threshold(a, 127, 255, cv2.THRESH_BINARY) # 将透明图层二值化
406
+ # 直接返回脖子的位置信息, 修正系数为0, get_box_pro内部也封装了二值化,所以直接输入原图
407
+ y_high, y_low, _, _ = get_box_pro(image=image, model=1, correction_factor=0)
408
+ def scan(y_:int, max_num:int=2):
409
+ num = 0
410
+ # 设定两个值,分别代表脖子的左边和右边
411
+ left = False
412
+ right = False
413
+ for x_ in range(width):
414
+ if a_thresh[y_][x_] != 0:
415
+ # 检测左边
416
+ if x_ < width // 2 and left is False:
417
+ num += 1
418
+ left = True
419
+ # 检测右边
420
+ elif x_ > width // 2 and right is False:
421
+ num += 1
422
+ right = True
423
+ return True if num >= max_num else False
424
+ def locate_neck_above():
425
+ """
426
+ 定位脖子的尖尖脚
427
+ """
428
+ # y_high就是脖子的最高点
429
+ for y_ in range(y_high, height):
430
+ if scan(y_):
431
+ return y_
432
+ y_start = locate_neck_above()
433
+ return y_start
434
+
435
+ def checkJaw(image:np.ndarray, y_start:int):
436
+ # 寻找"马鞍点"
437
+ _, _, _, a = cv2.split(image) # 这应该是一个四通道的图像
438
+ height, width = a.shape
439
+ ret, a_thresh = cv2.threshold(a, 127, 255, cv2.THRESH_BINARY) # 将透明图层二值化
440
+ if width <=1: raise TypeError("图像太小!")
441
+ x_left, x_right = 0, width - 1
442
+ for x_left in range(width):
443
+ if a_thresh[y_start][x_left] != 0:
444
+ while a_thresh[y_start][x_left] != 0: x_left += 1
445
+ break
446
+ for x_right in range(width-1, -1, -1):
447
+ if a_thresh[y_start][x_right] != 0:
448
+ while a_thresh[y_start][x_right] != 0: x_right -= 1
449
+ break
450
+ point_list_y = []
451
+ point_list_x = []
452
+ for x in range(x_left, x_right):
453
+ y = y_start
454
+ while a_thresh[y][x] == 0: y += 1
455
+ point_list_y.append(y)
456
+ point_list_x.append(x)
457
+ y = max(point_list_y)
458
+ x = point_list_x[point_list_y.index(y)]
459
+ return x, y
460
+
461
+
462
+ def checkHairLOrR(cloth_image_input_cut,
463
+ input_a,
464
+ neck_a,
465
+ cloth_image_input_top_y,
466
+ cutbar_top=0.4,
467
+ cutbar_bottom=0.5,
468
+ threshold=0.3):
469
+ """
470
+ 本函数用于检测衣服是否被头发遮挡,当前只考虑左右是否被遮挡,即"一刀切"
471
+ 返回int
472
+ 0代表没有被遮挡
473
+ 1代表左边被遮挡
474
+ 2代表右边被遮挡
475
+ 3代表全被遮挡了
476
+ 约定,输入的图像是一张灰度图,且被二值化过.
477
+ """
478
+ def per_darkPoint(img:np.ndarray) -> int:
479
+ """
480
+ 用于遍历相加图像上的黑点.
481
+ 然后返回黑点数/图像面积
482
+ """
483
+ h, w = img.shape
484
+ sum_darkPoint = 0
485
+ for y in range(h):
486
+ for x in range(w):
487
+ if img[y][x] == 0:
488
+ sum_darkPoint += 1
489
+ return sum_darkPoint / (h * w)
490
+
491
+ if threshold < 0 or threshold > 1: raise TypeError("阈值设置必须在0和1之间!")
492
+
493
+ # 裁出cloth_image_input_cut按高度40%~50%的区域-cloth_image_input_cutbar,并转换为A矩阵,做二值化
494
+ cloth_image_input_height = cloth_image_input_cut.shape[0]
495
+ _, _, _, cloth_image_input_cutbar = cv2.split(cloth_image_input_cut[
496
+ int(cloth_image_input_height * cutbar_top):int(
497
+ cloth_image_input_height * cutbar_bottom), :])
498
+ _, cloth_image_input_cutbar = cv2.threshold(cloth_image_input_cutbar, 127, 255, cv2.THRESH_BINARY)
499
+
500
+ # 裁出input_image、neck_image的A矩阵的对应区域,并做二值化
501
+ input_a_cutbar = input_a[cloth_image_input_top_y + int(cloth_image_input_height * cutbar_top):
502
+ cloth_image_input_top_y + int(cloth_image_input_height * cutbar_bottom), :]
503
+ _, input_a_cutbar = cv2.threshold(input_a_cutbar, 127, 255, cv2.THRESH_BINARY)
504
+ neck_a_cutbar = neck_a[cloth_image_input_top_y + int(cloth_image_input_height * cutbar_top):
505
+ cloth_image_input_top_y + int(cloth_image_input_height * cutbar_bottom), :]
506
+ _, neck_a_cutbar = cv2.threshold(neck_a_cutbar, 50, 255, cv2.THRESH_BINARY)
507
+
508
+ # 将三个cutbar合到一起-result_a_cutbar
509
+ input_a_cutbar = np.uint8(255 - input_a_cutbar)
510
+ result_a_cutbar = cv2.add(input_a_cutbar, cloth_image_input_cutbar)
511
+ result_a_cutbar = cv2.add(result_a_cutbar, neck_a_cutbar)
512
+
513
+ if_mask = 0
514
+ # 我们将图像 一刀切,分为左边和右边
515
+ height, width = result_a_cutbar.shape # 一通道图像
516
+ left_image = result_a_cutbar[:, :width//2]
517
+ right_image = result_a_cutbar[:, width//2:]
518
+ if per_darkPoint(left_image) > threshold:
519
+ if_mask = 1
520
+ if per_darkPoint(right_image) > threshold:
521
+ if_mask = 3 if if_mask == 1 else 2
522
+ return if_mask
523
+
524
+
525
+ if __name__ == "__main__":
526
+ for i in range(1, 8):
527
+ img = cv2.imread(f"./neck_temp/neck_image{i}.png", cv2.IMREAD_UNCHANGED)
528
+ # new = transformationNeck(image=img, cutNeckHeight=419,neckBelow=472, toHeight=150)
529
+ # point_list = bestJunctionCheck(img, offset=5, stepSize=3)
530
+ # per = bestJunctionCheck(img, offset=5, stepSize=3)
531
+ # # 返回一个小数的形式, 接下来我将它处理为两个点
532
+ point_list = []
533
+ # y_high_, y_low_, _, _ = get_box_pro(image=img, model=1, conreection_factor=0)
534
+ # _y = y_high_ + int((y_low_ - y_high_) * per)
535
+ # _, _, _, a_ = cv2.split(img) # 这应该是一个四通道的图像
536
+ # h, w = a_.shape
537
+ # r, a_t = cv2.threshold(a_, 127, 255, cv2.THRESH_BINARY) # 将透明图层二值化
538
+ # _x = 0
539
+ # for _x in range(w):
540
+ # if a_t[_y][_x] != 0:
541
+ # break
542
+ # point_list.append([_x, _y])
543
+ # for _x in range(w - 1, -1, -1):
544
+ # if a_t[_y][_x] != 0:
545
+ # break
546
+ # point_list.append([_x, _y])
547
+ y = checkSharpCorner(img)
548
+ point = checkJaw(image=img, y_start=y)
549
+ point_list.append(point)
550
+ new = draw_picture_dots(img, point_list, pen_size=2)
551
+ cv2.imshow(f"{i}", new)
552
+ cv2.waitKey(0)
553
+
554
+ def find_black(image):
555
+ """
556
+ 找黑色点函数,遇到输入矩阵中的第一个黑点,返回它的y值
557
+ """
558
+ height, width = image.shape[0], image.shape[1]
559
+ for i in range(height):
560
+ for j in range(width):
561
+ if image[i, j] < 127:
562
+ return i
563
+ return None
564
+
565
+ def convert_black_array(image):
566
+ height, width = image.shape[0], image.shape[1]
567
+ mask = np.zeros([height, width])
568
+ for j in range(width):
569
+ for i in range(height):
570
+ if image[i, j] > 127:
571
+ mask[i:, j] = 1
572
+ break
573
+ return mask
574
+
575
+ def checkLongHair(neck_image, head_bottom_y, neck_top_y):
576
+ """
577
+ 长发检测函数,输入为head/neck图像,通过下巴是否为最低点,来判断是否为长发
578
+ :return 0 : 短发
579
+ :return 1 : 长发
580
+ """
581
+ jaw_y = neck_top_y + checkJaw(neck_image, y_start=checkSharpCorner(neck_image))[1]
582
+ if jaw_y >= head_bottom_y-3:
583
+ return 0
584
+ else:
585
+ return 1
586
+
587
+ def checkLongHair2(head_bottom_y, cloth_top_y):
588
+ if head_bottom_y > cloth_top_y+10:
589
+ return 1
590
+ else:
591
+ return 0
592
+
593
+
hivisionai/hycv/idphotoTool/idphoto_change_cloth.py ADDED
@@ -0,0 +1,271 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+ from ..utils import get_box_pro, cut_BiggestAreas, locate_neck, get_cutbox_image
4
+ from .move_image import move
5
+ from ..vision import add_background, cover_image
6
+ from ..matting_tools import get_modnet_matting
7
+ from .neck_processing import transformationNeck
8
+ from .cuny_tools import checkSharpCorner, checkJaw, checkHairLOrR,\
9
+ checkLongHair, checkLongHair2, convert_black_array, find_black
10
+
11
+ test_image_path = "./supple_image/"
12
+
13
+ def change_cloth(input_image:np.array,
14
+ cloth_model,
15
+ CLOTH_WIDTH,
16
+ CLOTH_X,
17
+ CLOTH_WIDTH_CHANGE,
18
+ CLOTH_X_CHANGE,
19
+ CLOTH_Y,
20
+ neck_ratio=0.2,
21
+ space_adjust=None,
22
+ hair_front=True
23
+ ):
24
+
25
+ # ============= 1. 得到头脖子图、纯头图、纯脖子图的相关信息 =============== #
26
+ # 1.1 获取原图input_image属性
27
+ input_height, input_width = input_image.shape[0], input_image.shape[1]
28
+ # print("input_height:", input_height)
29
+ # print("input_width", input_width)
30
+ b, g, r, input_a = cv2.split(input_image)
31
+
32
+ # 1.2 得到头脖子图headneck_image、纯头图head_image
33
+ input_image = add_background(input_image, bgr=(255, 255, 255))
34
+ headneck_image = get_modnet_matting(input_image, checkpoint_path="./checkpoint/huanying_headneck3.onnx")
35
+ head_image = get_modnet_matting(input_image, checkpoint_path="./checkpoint/huanying_head3.onnx")
36
+
37
+ # 1.3 得到优化后的脖子图neck_threshold_image
38
+ _, _, _, headneck_a = cv2.split(headneck_image)
39
+ _, _, _, head_a = cv2.split(head_image)
40
+ neck_a = cv2.subtract(headneck_a, head_a)
41
+ _, neck_threshold_a = cv2.threshold(neck_a, 180, 255, cv2.THRESH_BINARY)
42
+ neck_threshold_image = cut_BiggestAreas(cv2.merge(
43
+ (np.uint8(b), np.uint8(g), np.uint8(r), np.uint8(neck_threshold_a))))
44
+
45
+ # 1.4 得到优化后的头脖子图headneck_threshold_image
46
+ _, headneck_threshold_a = cv2.threshold(headneck_a, 180, 255, cv2.THRESH_BINARY)
47
+ headneck_threshold_image = cut_BiggestAreas(
48
+ cv2.merge((np.uint8(b), np.uint8(g), np.uint8(r), np.uint8(headneck_threshold_a))))
49
+
50
+ # 1.5 获取脖子图、头脖子图的A矩阵
51
+ _, _, _, neck_threshold_a2 = cv2.split(neck_threshold_image)
52
+ _, _, _, headneck_threshold_a2 = cv2.split(headneck_threshold_image)
53
+
54
+ # 1.6 获取头发的底部坐标信息,以及头的左右坐标信息
55
+ _, headneck_y_bottom, headneck_x_left, headneck_x_right = get_box_pro(headneck_threshold_image,
56
+ model=2, correction_factor=0)
57
+ headneck_y_bottom = input_height-headneck_y_bottom
58
+ headneck_x_right = input_width-headneck_x_right
59
+
60
+
61
+
62
+ # ============= 2. 得到原来的衣服的相关信息 =============== #
63
+ # 2.1 抠出原来衣服cloth_image_input
64
+ cloth_origin_image_a = cv2.subtract(np.uint8(input_a), np.uint8(headneck_a))
65
+ _, cloth_origin_image_a = cv2.threshold(cloth_origin_image_a, 180, 255, cv2.THRESH_BINARY)
66
+ cloth_image_input = cut_BiggestAreas(cv2.merge((np.uint8(b), np.uint8(g), np.uint8(r), np.uint8(cloth_origin_image_a))))
67
+
68
+ # 2.2 对cloth_image_input做裁剪(减去上面的大片透明区域)
69
+ cloth_image_input_top_y, _, _, _ = get_box_pro(cloth_image_input, model=2)
70
+ cloth_image_input_cut = cloth_image_input[cloth_image_input_top_y:, :]
71
+
72
+
73
+
74
+ # ============= 3.计算脖子的衔接点信息,为新服装拼接作准备 ===============#
75
+ # 3.1 得到裁剪透明区域后的脖子图neck_cut_image,以及它的坐标信息
76
+ neck_y_top, neck_y_bottom, neck_x_left, neck_x_right = get_box_pro(neck_threshold_image, model=2)
77
+ neck_cut_image = get_cutbox_image(neck_threshold_image)
78
+ neck_height = input_height - (neck_y_top + neck_y_bottom)
79
+ neck_width = input_width - (neck_x_right + neck_x_left)
80
+
81
+ # 3.2 对neck_cut_image做“尖尖”检测,得到较低的“尖尖”对于脖子高度的比率y_neck_corner_ratio
82
+ y_neck_corner = checkSharpCorner(neck_cut_image)
83
+ y_neck_corner_ratio = y_neck_corner / neck_height
84
+
85
+ # 3.3 取y_neck_corner_ratio与新衣服预先设定好的neck_ratio的最大值,作为最终的neck_ratio
86
+ neck_ratio = max(neck_ratio, y_neck_corner_ratio)
87
+
88
+ # 3.4 计算在neck_ratio下的脖子左衔接点坐标neck_left_x_byRatio,neck_left_y_byRatio、宽度neck_width_byRatio
89
+ neck_coordinate1, neck_coordinate2, neck_width_byRatio = locate_neck(neck_cut_image, float(neck_ratio))
90
+ neck_width_byRatio = neck_width_byRatio + CLOTH_WIDTH_CHANGE
91
+ neck_left_x_byRatio = neck_x_left + neck_coordinate1[1] + CLOTH_X_CHANGE
92
+ neck_left_y_byRatio = neck_y_top + neck_coordinate1[0]
93
+
94
+
95
+
96
+ # ============= 4.读取新衣服图,调整大小 =============== #
97
+ # 4.1 得到新衣服图片的拼贴坐标x, y以及脖子最底部的坐标y_neckline
98
+ CLOTH_HEIGHT = CLOTH_Y
99
+ RESIZE_RATIO = neck_width_byRatio / CLOTH_WIDTH
100
+ x, y = int(neck_left_x_byRatio - CLOTH_X * RESIZE_RATIO), neck_left_y_byRatio
101
+ y_neckline = y + int(CLOTH_HEIGHT * RESIZE_RATIO)
102
+
103
+ # 4.2 读取新衣服,并进行缩放
104
+ cloth = cv2.imread(cloth_model, -1)
105
+ cloth_height, cloth_width = cloth.shape[0], cloth.shape[1]
106
+ cloth = cv2.resize(cloth, (int(cloth_width * RESIZE_RATIO),
107
+ int(cloth_height * RESIZE_RATIO)), interpolation=cv2.INTER_AREA)
108
+
109
+ # 4.3 获得新衣服的A矩阵
110
+ _, _, _, cloth_a = cv2.split(cloth)
111
+
112
+
113
+
114
+ # ============= 5. 判断头发的前后关系,以及对于长发的背景填充、判定是否为长发等 =============== #
115
+ # 5.1 根据hair_number, 判断是0:头发披在后面、1:左前右后、2:左后右前还是3:都在前面
116
+ hair_number = checkHairLOrR(cloth_image_input_cut, input_a, neck_a, cloth_image_input_top_y)
117
+
118
+ # 5.2 对于长发的背景填充-将原衣服区域的部分变成黑色,并填充到最终图片作为背景
119
+ cloth_image_input_save = cloth_origin_image_a[:int(y+cloth_height*RESIZE_RATIO),
120
+ max(0, headneck_x_left-1):min(headneck_x_right+1, input_width)]
121
+ headneck_threshold_a_save = headneck_a[:int(y+cloth_height*RESIZE_RATIO),
122
+ max(0, headneck_x_left-1):min(headneck_x_right+1, input_width)]
123
+ headneck_mask = convert_black_array(headneck_threshold_a_save)
124
+ kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 15))
125
+ cloth_image_input_save = cv2.dilate(cloth_image_input_save, kernel)
126
+ cloth_image_input_save = np.uint8(cloth_image_input_save*headneck_mask)
127
+
128
+ # 5.3 检测是否为长发
129
+ head_bottom_y = input_height - get_box_pro(head_image, model=2, correction_factor=0)[1]
130
+ isLongHair01 = checkLongHair(neck_cut_image, head_bottom_y, neck_top_y=neck_y_top)
131
+ isLongHair02 = checkLongHair2(head_bottom_y, cloth_image_input_top_y)
132
+ isLongHair = isLongHair01 and isLongHair02
133
+
134
+
135
+
136
+ # ============= 6.第一轮服装拼贴 =============== #
137
+ # 6.1 创建一个空白背景background
138
+ background = np.uint8((np.zeros([input_height, input_width, 4])))
139
+
140
+ # 6.2 盖上headneck_image
141
+ result_headneck_image = cover_image(headneck_image, background, 0, 0, mode=3)
142
+
143
+ # 6.3 如果space_adjust开启的话,background的底部将增加一些行数
144
+ if space_adjust is not None:
145
+ insert_array = np.uint8(np.zeros((space_adjust, input_width, 4)))
146
+ result_headneck_image = np.r_[result_headneck_image, insert_array]
147
+
148
+ # 6.4 盖上新衣服
149
+ result_cloth_image = cover_image(cloth, result_headneck_image, x, y, mode=3)
150
+
151
+ # 6.5 截出脖子与衣服交接的区域neck_cloth_image,以及它的A矩阵neck_cloth_a
152
+ neck_cloth_image = result_cloth_image[y:y_neckline,
153
+ neck_left_x_byRatio:neck_left_x_byRatio+neck_width_byRatio]
154
+ _, _, _, neck_cloth_a = cv2.split(neck_cloth_image)
155
+ _, neck_cloth_a = cv2.threshold(neck_cloth_a, 127, 255, cv2.THRESH_BINARY)
156
+
157
+
158
+
159
+ # ============= 7.第二轮服装拼贴 =============== #
160
+ # 7.1 检测neck_cloth_a中是否有黑点(即镂空区域)
161
+ # 如果black_dots_y不为None,说明存在镂空区域——需要进行脖子拉伸;反而则不存在,不需要
162
+ black_dots_y = find_black(neck_cloth_a)
163
+ # cv2.imwrite(test_image_path+"neck_cloth_a.jpg", neck_cloth_a)
164
+
165
+ # flag: 用于指示是否进行拉伸
166
+ flag = 0
167
+
168
+ # 7.2 如果存在镂空区域,则进行拉伸
169
+ if black_dots_y != None:
170
+ flag = 1
171
+ # cutNeckHeight:要拉伸的区域的顶部y值
172
+ # neckBelow:脖子底部的y值
173
+ # toHeight:拉伸区域的高度
174
+ cutNeckHeight = black_dots_y + y - 6
175
+ # if cutNeckHeight < neck_y_top+checkJaw(neck_cut_image, y_start=checkSharpCorner(neck_cut_image))[1]:
176
+ # print("拒绝!!!!!!")
177
+ # return 0, 0, 0, 0, 0
178
+
179
+ neckBelow = input_height-neck_y_bottom
180
+ toHeight = y_neckline-cutNeckHeight
181
+ print("cutNeckHeight:", cutNeckHeight)
182
+ print("toHeight:", toHeight)
183
+ print("neckBelow:", neckBelow)
184
+ # cv2.imwrite(test_image_path+"neck_image.png", neck_threshold_image)
185
+
186
+ # 对原有的脖子做拉伸,得到new_neck_image
187
+ new_neck_image = transformationNeck(neck_threshold_image,
188
+ cutNeckHeight=cutNeckHeight,
189
+ neckBelow=neckBelow,
190
+ toHeight=toHeight)
191
+ # cv2.imwrite(test_image_path+"new_neck_image.png", new_neck_image)
192
+
193
+
194
+ # 重新进行拼贴
195
+ result_headneck_image = cover_image(new_neck_image, result_headneck_image, 0, 0, mode=3)
196
+ result_headneck_image = cover_image(head_image, result_headneck_image, 0, 0, mode=3)
197
+ result_cloth_image = cover_image(cloth, result_headneck_image, x, y, mode=3)
198
+
199
+ _, _, _, neck_a = cv2.split(new_neck_image)
200
+
201
+
202
+ # 7.3 下面是对最终图的A矩阵进行处理
203
+ # 首先将neck_a与新衣服衔接点的左边两边区域删去,得到neck_a_leftright
204
+ neck_a_copy = neck_a.copy()
205
+ neck_a_copy[neck_left_y_byRatio:, :max(0, neck_left_x_byRatio-4)] = 0
206
+ neck_a_copy[neck_left_y_byRatio:,
207
+ min(input_width, neck_left_x_byRatio + neck_width_byRatio - CLOTH_X_CHANGE+4):] = 0
208
+ n_a_leftright = cv2.subtract(neck_a, neck_a_copy)
209
+
210
+ # 7.4 如果存在镂空区域,则对headneck_a做进一步处理
211
+ if black_dots_y != None:
212
+ neck_a = cv2.subtract(neck_a, n_a_leftright)
213
+ # 得到去掉脖子两翼的新的headneck_a
214
+ headneck_a = cv2.subtract(headneck_a, n_a_leftright)
215
+ # 将headneck_a覆盖上拉伸后的脖子A矩阵
216
+ headneck_a = np.uint8(cover_image(neck_a, headneck_a, 0, 0, mode=2))
217
+ else:
218
+ headneck_a = cv2.subtract(headneck_a, n_a_leftright)
219
+
220
+
221
+
222
+ # 7.5 如果是长发
223
+ if isLongHair:
224
+ # 在背景加入黑色矩形,填充抠头模型可能会出现的,部分长发没有抠全的部分
225
+ black_background_x1 = int(neck_left_x_byRatio - neck_width_byRatio * 0.1)
226
+ black_background_x2 = int(neck_left_x_byRatio + neck_width_byRatio * 1.1)
227
+ black_background_y1 = int(neck_y_top - neck_height * 0.1)
228
+ black_background_y2 = min(input_height - neck_y_bottom - 3, head_bottom_y)
229
+ headneck_a[black_background_y1:black_background_y2, black_background_x1:black_background_x2] = 255
230
+
231
+ # 在背景中,将原本衣服区域置为黑色
232
+ headneck_a = cover_image(cloth_image_input_save, headneck_a, max(0, headneck_x_left-1), 0, mode=2)
233
+
234
+ # 7.6 如果space_adjust开启的话,headneck_a的底部将增加一些行数
235
+ if space_adjust is not None:
236
+ insert_array = np.uint8(np.zeros((space_adjust, input_width)))
237
+ headneck_a = np.r_[headneck_a, insert_array]
238
+
239
+ # 7.7 盖上新衣服
240
+ new_a = np.uint8(cover_image(cloth_a, headneck_a, x, y, mode=2))
241
+
242
+ # neck_cloth_a = new_a[y:y_neckline, neck_left_x_byRatio:neck_left_x_byRatio + neck_width_byRatio]
243
+ # _, neck_cloth_a = cv2.threshold(neck_cloth_a, 127, 255, cv2.THRESH_BINARY)
244
+ # cv2.imwrite(test_image_path + "neck_cloth_a2.jpg", neck_cloth_a)
245
+ #
246
+ # if find_black(neck_cloth_a) != None:
247
+ # print("拒绝!!!!")
248
+ # return "拒绝"
249
+
250
+ # 7.8 如果有头发披在前面
251
+ if hair_front:
252
+ # 如果头发披在左边
253
+ if hair_number == 1:
254
+ result_cloth_image = cover_image(head_image[:, :head_image.shape[1] // 2], result_cloth_image, 0, 0, mode=3)
255
+ # 如果头发披在右边
256
+ elif hair_number == 2:
257
+ result_cloth_image = cover_image(head_image[:, head_image.shape[1] // 2:], result_cloth_image, head_image.shape[1] // 2, 0, mode=3)
258
+ # 如果头发披在两边
259
+ elif hair_number == 3:
260
+ result_cloth_image = cover_image(head_image, result_cloth_image, 0, 0, mode=3)
261
+
262
+ # 7.9 合成最终图片,并做底部空隙的移动
263
+ r, g, b, _ = cv2.split(result_cloth_image)
264
+ result_image = move(cv2.merge((r, g, b, new_a)))
265
+
266
+ # 7.10 返回:结果图、是否拉伸、头发前披状态、是否长发
267
+ return 1, result_image, flag, hair_number, isLongHair
268
+
269
+
270
+ if __name__ == "__main__":
271
+ pass
hivisionai/hycv/idphotoTool/idphoto_cut.py ADDED
@@ -0,0 +1,420 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import math
3
+ from ..utils import get_box_pro
4
+ from ..face_tools import face_detect_mtcnn
5
+ from ..vision import IDphotos_cut, detect_distance, resize_image_esp, draw_picture_dots
6
+ from ..matting_tools import get_modnet_matting
7
+ from .move_image import move
8
+ from src.hivisionai.hyTrain.APIs import aliyun_face_detect_api
9
+ import numpy as np
10
+ import json
11
+
12
+
13
+ def get_max(height, width, d1, d2, d3, d4, rotation_flag):
14
+ if rotation_flag:
15
+ height1 = height
16
+ height2 = height - int(d1.y) # d2
17
+ height3 = int(d4.y) # d3
18
+ height4 = int(d4.y) - int(d1.x)
19
+
20
+ width1 = width
21
+ width2 = width - int(d3.x)
22
+ width3 = int(d2.x)
23
+ width4 = int(d2.x) - int(d3.x)
24
+
25
+ else:
26
+ height1 = height
27
+ height2 = height - int(d2.y)
28
+ height3 = int(d3.y)
29
+ height4 = int(d3.y) - int(d2.y)
30
+
31
+ width1 = width
32
+ width2 = width - int(d1.x)
33
+ width3 = int(d4.x)
34
+ width4 = int(d4.x) - int(d1.x)
35
+
36
+ height_list = [height1, height2, height3, height4]
37
+ width_list = [width1, width2, width3, width4]
38
+
39
+ background_height = max(height_list)
40
+ status_height = height_list.index(background_height)
41
+
42
+ background_width = max(width_list)
43
+ status_width = width_list.index(background_width)
44
+
45
+ height_change = 0
46
+ width_change = 0
47
+ height_change2 = 0
48
+ width_change2 = 0
49
+ if status_height == 1 or status_height == 3:
50
+ if rotation_flag:
51
+ height_change = abs(d1.y)
52
+ height_change2 = d1.y
53
+ else:
54
+ height_change = abs(d2.y)
55
+ height_change2 = d2.y
56
+
57
+ if status_width == 1 or status_width == 3:
58
+ if rotation_flag:
59
+ width_change = abs(d3.x)
60
+ width_change2 = d3.x
61
+ else:
62
+ width_change = abs(d1.x)
63
+ width_change2 = d1.x
64
+
65
+ return background_height, status_height, background_width, status_width, height_change, width_change,\
66
+ height_change2, width_change2
67
+
68
+ class LinearFunction_TwoDots(object):
69
+ """
70
+ 通过两个坐标点构建线性函数
71
+ """
72
+ def __init__(self, dot1, dot2):
73
+ self.d1 = dot1
74
+ self.d2 = dot2
75
+ self.k = (self.d2.y - self.d1.y) / (self.d2.x - self.d1.x)
76
+ self.b = self.d2.y - self.k * self.d2.x
77
+
78
+ def forward(self, input, mode="x"):
79
+ if mode == "x":
80
+ return self.k * input + self.b
81
+ elif mode == "y":
82
+ return (input - self.b) / self.k
83
+
84
+ def forward_x(self, x):
85
+ return self.k * x + self.b
86
+
87
+ def forward_y(self, y):
88
+ return (y - self.b) / self.k
89
+
90
+ class Coordinate(object):
91
+ def __init__(self, x, y):
92
+ self.x = x
93
+ self.y = y
94
+
95
+ def __str__(self):
96
+ return "({}, {})".format(self.x, self.y)
97
+
98
+ def IDphotos_create(input_image, size=(413, 295), head_measure_ratio=0.2, head_height_ratio=0.45,
99
+ checkpoint_path="checkpoint/ModNet1.0.onnx", align=True):
100
+ """
101
+ input_path: 输入图像路径
102
+ output_path: 输出图像路径
103
+ size: 裁剪尺寸,格式应该如(413,295),竖直距离在前,水平距离在后
104
+ head_measure_ratio: 人头面积占照片面积的head_ratio
105
+ head_height_ratio: 人头中心处于照片从上到下的head_height
106
+ align: 是否进行人脸矫正
107
+ """
108
+
109
+ input_image = resize_image_esp(input_image, 2000) # 将输入图片压缩到最大边长为2000
110
+ # cv2.imwrite("./temp_input_image.jpg", input_image)
111
+ origin_png_image = get_modnet_matting(input_image, checkpoint_path)
112
+ # cv2.imwrite("./test_image/origin_png_image.png", origin_png_image)
113
+ _, _, _, a = cv2.split(origin_png_image)
114
+ width_length_ratio = size[0]/size[1] # 长宽比
115
+ rotation = aliyun_face_detect_api("./temp_input_image.jpg")
116
+
117
+ # 如果旋转角过小,则不进行矫正
118
+ if abs(rotation) < 0.025:
119
+ align=False
120
+
121
+ if align:
122
+ print("开始align")
123
+ if rotation > 0:
124
+ rotation_flag = 0 # 逆时针旋转
125
+ else:
126
+ rotation_flag = 1 # 顺时针旋转
127
+ width, height, channels = input_image.shape
128
+
129
+ p_list = [(0, 0), (0, height), (width, 0), (width, height)]
130
+ rotate_list = []
131
+ rotate = cv2.getRotationMatrix2D((height * 0.5, width * 0.5), rotation, 0.75)
132
+ for p in p_list:
133
+ p_m = np.array([[p[1]], [p[0]], [1]])
134
+ rotate_list.append(np.dot(rotate[:2], p_m))
135
+ # print("旋转角的四个顶点", rotate_list)
136
+
137
+ input_image = cv2.warpAffine(input_image, rotate, (height, width), flags=cv2.INTER_AREA)
138
+ new_a = cv2.warpAffine(a, rotate, (height, width), flags=cv2.INTER_AREA)
139
+ # cv2.imwrite("./test_image/rotation.jpg", input_image)
140
+
141
+ # ===================== 开始人脸检测 ===================== #
142
+ faces, _ = face_detect_mtcnn(input_image, filter=True)
143
+ face_num = len(faces)
144
+ print("检测到的人脸数目为:", len(faces))
145
+ # ===================== 人脸检测结束 ===================== #
146
+
147
+ if face_num == 1:
148
+ face_rect = faces[0]
149
+ x, y = face_rect[0], face_rect[1]
150
+ w, h = face_rect[2] - x + 1, face_rect[3] - y + 1
151
+ elif face_num == 0:
152
+ print("无人脸,返回0!!!")
153
+ return 0
154
+ else:
155
+ print("太多人脸,返回2!!!")
156
+ return 2
157
+
158
+ d1, d2, d3, d4 = rotate_list[0], rotate_list[1], rotate_list[2], rotate_list[3]
159
+ d1 = Coordinate(int(d1[0]), int(d1[1]))
160
+ d2 = Coordinate(int(d2[0]), int(d2[1]))
161
+ d3 = Coordinate(int(d3[0]), int(d3[1]))
162
+ d4 = Coordinate(int(d4[0]), int(d4[1]))
163
+ print("d1:", d1)
164
+ print("d2:", d2)
165
+ print("d3:", d3)
166
+ print("d4:", d4)
167
+
168
+ background_height, status_height, background_width, status_width,\
169
+ height_change, width_change, height_change2, width_change2 = get_max(width, height, d1, d2, d3, d4, rotation_flag)
170
+
171
+ print("background_height:", background_height)
172
+ print("background_width:", background_width)
173
+ print("status_height:", status_height)
174
+ print("status_width:", status_width)
175
+ print("height_change:", height_change)
176
+ print("width_change:", width_change)
177
+
178
+ background = np.zeros([background_height, background_width, 3])
179
+ background_a = np.zeros([background_height, background_width])
180
+
181
+ background[height_change:height_change+width, width_change:width_change+height] = input_image
182
+ background_a[height_change:height_change+width, width_change:width_change+height] = new_a
183
+ d1 = Coordinate(int(d1.x)-width_change2, int(d1.y)-height_change2)
184
+ d2 = Coordinate(int(d2.x)-width_change2, int(d2.y)-height_change2)
185
+ d3 = Coordinate(int(d3.x)-width_change2, int(d3.y)-height_change2)
186
+ d4 = Coordinate(int(d4.x)-width_change2, int(d4.y)-height_change2)
187
+ print("d1:", d1)
188
+ print("d2:", d2)
189
+ print("d3:", d3)
190
+ print("d4:", d4)
191
+
192
+ if rotation_flag:
193
+ f13 = LinearFunction_TwoDots(d1, d3)
194
+ d5 = Coordinate(max(0, d3.x), f13.forward_x(max(0, d3.x)))
195
+ print("d5:", d5)
196
+
197
+ f42 = LinearFunction_TwoDots(d4, d2)
198
+ d7 = Coordinate(f42.forward_y(d5.y), d5.y)
199
+ print("d7", d7)
200
+
201
+ background_draw = draw_picture_dots(background, dots=[(d1.x, d1.y),
202
+ (d2.x, d2.y),
203
+ (d3.x, d3.y),
204
+ (d4.x, d4.y),
205
+ (d5.x, d5.y),
206
+ (d7.x, d7.y)])
207
+ # cv2.imwrite("./test_image/rotation_background.jpg", background_draw)
208
+
209
+ if x<d5.x or x+w>d7.x:
210
+ print("return 6")
211
+ return 6
212
+
213
+ background_output = background[:int(d5.y), int(d5.x):int(d7.x)]
214
+ background_a_output = background_a[:int(d5.y), int(d5.x):int(d7.x)]
215
+ # cv2.imwrite("./test_image/rotation_background_cut.jpg", background_output)
216
+
217
+ else:
218
+ f34 = LinearFunction_TwoDots(d3, d4)
219
+ d5 = Coordinate(min(width_change+height, d4.x), f34.forward_x(min(width_change+height, d4.x)))
220
+ print("d5:", d5)
221
+
222
+ f13 = LinearFunction_TwoDots(d1, d3)
223
+ d7 = Coordinate(f13.forward_y(d5.y), d5.y)
224
+ print("d7", d7)
225
+
226
+ if x<d7.x or x+w>d5.x:
227
+ print("return 6")
228
+ return 6
229
+
230
+ background_draw = draw_picture_dots(background, dots=[(d1.x, d1.y),
231
+ (d2.x, d2.y),
232
+ (d3.x, d3.y),
233
+ (d4.x, d4.y),
234
+ (d5.x, d5.y),
235
+ (d7.x, d7.y)])
236
+
237
+ # cv2.imwrite("./test_image/rotation_background.jpg", background_draw)
238
+
239
+ background_output = background[:int(d5.y), int(d7.x):int(d5.x)]
240
+ background_a_output = background_a[:int(d5.y), int(d7.x):int(d5.x)]
241
+ # cv2.imwrite("./test_image/rotation_background_cut.jpg", background_output)
242
+
243
+ input_image = np.uint8(background_output)
244
+ b, g, r = cv2.split(input_image)
245
+ origin_png_image = cv2.merge((b, g, r, np.uint8(background_a_output)))
246
+
247
+ # ===================== 开始人脸检测 ===================== #
248
+ width, length = input_image.shape[0], input_image.shape[1]
249
+ faces, _ = face_detect_mtcnn(input_image, filter=True)
250
+ face_num = len(faces)
251
+ print("检测到的人脸数目为:", len(faces))
252
+ # ===================== 人脸检测结束 ===================== #
253
+
254
+ if face_num == 1:
255
+
256
+ face_rect = faces[0]
257
+ x, y = face_rect[0], face_rect[1]
258
+ w, h = face_rect[2] - x + 1, face_rect[3] - y + 1
259
+
260
+ # x,y,w,h代表人脸框的左上角坐标和宽高
261
+
262
+ # 检测头顶下方空隙,如果头顶下方空隙过小,则拒绝
263
+ if y+h >= 0.85*width:
264
+ # print("face bottom too short! y+h={} width={}".format(y+h, width))
265
+ print("在人脸下方的空间太少,返回值3!!!")
266
+ return 3
267
+
268
+ # 第一次裁剪
269
+ # 确定裁剪的基本参数
270
+ face_center = (x+w/2, y+h/2) # 面部中心坐标
271
+ face_measure = w*h # 面部面积
272
+ crop_measure = face_measure/head_measure_ratio # 裁剪框面积:为面部面积的5倍
273
+ resize_ratio = crop_measure/(size[0]*size[1]) # 裁剪框缩放率(以输入尺寸为标准)
274
+ resize_ratio_single = math.sqrt(resize_ratio)
275
+ crop_size = (int(size[0]*resize_ratio_single), int(size[1]*resize_ratio_single)) # 裁剪框大小
276
+ print("crop_size:", crop_size)
277
+
278
+ # 裁剪规则:x1和y1为裁剪的起始坐标,x2和y2为裁剪的最终坐标
279
+ # y的确定由人脸中心在照片的45%位置决定
280
+ x1 = int(face_center[0]-crop_size[1]/2)
281
+ y1 = int(face_center[1]-crop_size[0]*head_height_ratio)
282
+ y2 = y1+crop_size[0]
283
+ x2 = x1+crop_size[1]
284
+
285
+ # 对原图进行抠图,得到透明图img
286
+ print("开始进行抠图")
287
+ # origin_png_image => 对原图的抠图结果
288
+ # cut_image => 第一次裁剪后的图片
289
+ # result_image => 第二次裁剪后的图片/输出图片
290
+ # origin_png_image = get_human_matting(input_image, get_file_dir(checkpoint_path))
291
+
292
+ cut_image = IDphotos_cut(x1, y1, x2, y2, origin_png_image)
293
+ # cv2.imwrite("./temp.png", cut_image)
294
+ # 对裁剪得到的图片temp_path,我们将image=temp_path resize为裁剪框大小,这样方便进行后续计算
295
+ cut_image = cv2.resize(cut_image, (crop_size[1], crop_size[0]))
296
+ y_top, y_bottom, x_left, x_right = get_box_pro(cut_image, model=2) # 得到透明图中人像的上下左右距离信息
297
+ print("y_top:{}, y_bottom:{}, x_left:{}, x_right:{}".format(y_top, y_bottom, x_left, x_right))
298
+
299
+ # 判断左右是否有间隙
300
+ if x_left > 0 or x_right > 0:
301
+ # 左右有空隙, 我们需要减掉它
302
+ print("左右有空隙!")
303
+ status_left_right = 1
304
+ cut_value_top = int(((x_left + x_right) * width_length_ratio) / 2) # 减去左右,为了保持比例,上下也要相应减少cut_value_top
305
+ print("cut_value_top:", cut_value_top)
306
+
307
+ else:
308
+ # 左右没有空隙, 则不管
309
+ status_left_right = 0
310
+ cut_value_top = 0
311
+ print("cut_value_top:", cut_value_top)
312
+
313
+ # 检测人头顶与照片的顶部是否在合适的距离内
314
+ print("y_top:", y_top)
315
+ status_top, move_value = detect_distance(y_top-int((x_left+x_right)*width_length_ratio/2), crop_size[0])
316
+ # status=0 => 距离合适, 无需移动
317
+ # status=1 => 距离过大, 人像应向上移动
318
+ # status=2 => 距离过小, 人像应向下移动
319
+ # move_value => 上下移动的距离
320
+ print("status_top:", status_top)
321
+ print("move_value:", move_value)
322
+
323
+ # 开始第二次裁剪
324
+ if status_top == 0:
325
+ # 如果上下距离合适,则无需移动
326
+ if status_left_right:
327
+ # 如果左右有空隙,则需要用到cut_value_top
328
+ result_image = IDphotos_cut(x1 + x_left,
329
+ y1 + cut_value_top,
330
+ x2 - x_right,
331
+ y2 - cut_value_top,
332
+ origin_png_image)
333
+
334
+ else:
335
+ # 如果左右没有空隙,那么则无需改动
336
+ result_image = cut_image
337
+
338
+ elif status_top == 1:
339
+ # 如果头顶离照片顶部距离过大,需要人像向上移动,则需要用到move_value
340
+ if status_left_right:
341
+ # 左右存在距离,则需要cut_value_top
342
+ result_image = IDphotos_cut(x1 + x_left,
343
+ y1 + cut_value_top + move_value,
344
+ x2 - x_right,
345
+ y2 - cut_value_top + move_value,
346
+ origin_png_image)
347
+ else:
348
+ # 左右不存在距离
349
+ result_image = IDphotos_cut(x1 + x_left,
350
+ y1 + move_value,
351
+ x2 - x_right,
352
+ y2 + move_value,
353
+ origin_png_image)
354
+
355
+ else:
356
+ # 如果头顶离照片顶部距离过小,则需要人像向下移动,则需要用到move_value
357
+ if status_left_right:
358
+ # 左右存在距离,则需要cut_value_top
359
+ result_image = IDphotos_cut(x1 + x_left,
360
+ y1 + cut_value_top - move_value,
361
+ x2 - x_right,
362
+ y2 - cut_value_top - move_value,
363
+ origin_png_image)
364
+ else:
365
+ # 左右不存在距离
366
+ result_image = IDphotos_cut(x1 + x_left,
367
+ y1 - move_value,
368
+ x2 - x_right,
369
+ y2 - move_value,
370
+ origin_png_image)
371
+
372
+ # 调节头顶位置————防止底部空一块儿
373
+ result_image = move(result_image)
374
+
375
+ # 高清保存
376
+ # cv2.imwrite(output_path.replace(".png", "_HD.png"), result_image)
377
+
378
+ # 普清保存
379
+ result_image2 = cv2.resize(result_image, (size[1], size[0]), interpolation=cv2.INTER_AREA)
380
+ # cv2.imwrite("./output_image.png", result_image)
381
+ print("完成.返回1")
382
+ return 1, result_image, result_image2
383
+
384
+ elif face_num == 0:
385
+ print("无人脸,返回0!!!")
386
+ return 0
387
+ else:
388
+ print("太多人脸,返回2!!!")
389
+ return 2
390
+
391
+
392
+ if __name__ == "__main__":
393
+ with open("./Setting.json") as json_file:
394
+ # file_list = get_filedir_filelist("./input_image")
395
+ setting = json.load(json_file)
396
+ filedir = "../IDPhotos/input_image/linzeyi.jpg"
397
+ file_list = [filedir]
398
+ for filedir in file_list:
399
+ print(filedir)
400
+ # try:
401
+ status_id, result_image, result_image2 = IDphotos_create(cv2.imread(filedir),
402
+ size=(setting["size_height"], setting["size_width"]),
403
+ head_height_ratio=setting["head_height_ratio"],
404
+ head_measure_ratio=setting["head_measure_ratio"],
405
+ checkpoint_path=setting["checkpoint_path"],
406
+ align=True)
407
+ # cv2.imwrite("./result_image.png", result_image)
408
+
409
+ if status_id == 1:
410
+ print("处理完毕!")
411
+ elif status_id == 0:
412
+ print("没有人脸!请重新上传有人脸的照片.")
413
+ elif status_id == 2:
414
+ print("人脸不只一张!请重新上传单独人脸的照片.")
415
+ elif status_id == 3:
416
+ print("人头下方空隙不足!")
417
+ elif status_id == 4:
418
+ print("此照片不能制作该规格!")
419
+ # except Exception as e:
420
+ # print(e)
hivisionai/hycv/idphotoTool/move_image.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 有一些png图像下部也会有一些透明的区域,使得图像无法对其底部边框
3
+ 本程序实现移动图像,使其下部与png图像实际大小相对齐
4
+ """
5
+ import os
6
+ import cv2
7
+ import numpy as np
8
+ from ..utils import get_box_pro
9
+
10
+ path_pre = os.path.join(os.getcwd(), 'pre')
11
+ path_final = os.path.join(os.getcwd(), 'final')
12
+
13
+
14
+ def merge(boxes):
15
+ """
16
+ 生成的边框可能不止只有一个,需要将边框合并
17
+ """
18
+ x, y, h, w = boxes[0]
19
+ # x和y应该是整个boxes里面最小的值
20
+ if len(boxes) > 1:
21
+ for tmp in boxes:
22
+ x_tmp, y_tmp, h_tmp, w_tmp = tmp
23
+ if x > x_tmp:
24
+ x_max = x_tmp + w_tmp if x_tmp + w_tmp > x + w else x + w
25
+ x = x_tmp
26
+ w = x_max - x
27
+ if y > y_tmp:
28
+ y_max = y_tmp + h_tmp if y_tmp + h_tmp > y + h else y + h
29
+ y = y_tmp
30
+ h = y_max - y
31
+ return tuple((x, y, h, w))
32
+
33
+
34
+ def get_box(png_img):
35
+ """
36
+ 获取矩形边框最终返回一个元组(x,y,h,w),分别对应矩形左上角的坐标和矩形的高和宽
37
+ """
38
+ r, g, b , a = cv2.split(png_img)
39
+ gray_img = a
40
+ th, binary = cv2.threshold(gray_img, 127 , 255, cv2.THRESH_BINARY) # 二值化
41
+ # cv2.imshow("name", binary)
42
+ # cv2.waitKey(0)
43
+ contours, hierarchy = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 得到轮廓列表contours
44
+ bounding_boxes = merge([cv2.boundingRect(cnt) for cnt in contours]) # 轮廓合并
45
+ # print(bounding_boxes)
46
+ return bounding_boxes
47
+
48
+ def get_box_2(png_img):
49
+ """
50
+ 不用opencv内置算法生成矩形了,改用自己的算法(for循环)
51
+ """
52
+ _, _, _, a = cv2.split(png_img)
53
+ _, a = cv2.threshold(a, 127, 255, cv2.THRESH_BINARY)
54
+ # 将r,g,b通道丢弃,只留下透明度通道
55
+ # cv2.imshow("name", a)
56
+ # cv2.waitKey(0)
57
+ # 在透明度矩阵中,0代表完全透明
58
+ height,width=a.shape # 高和宽
59
+ f=0
60
+ tmp1 = 0
61
+
62
+ """
63
+ 获取上下
64
+ """
65
+ for tmp1 in range(0,height):
66
+ tmp_a_high= a[tmp1:tmp1+1,:][0]
67
+ for tmp2 in range(width):
68
+ # a = tmp_a_low[tmp2]
69
+ if tmp_a_high[tmp2]!=0:
70
+ f=1
71
+ if f == 1:
72
+ break
73
+ delta_y_high = tmp1 + 1
74
+ f = 0
75
+ for tmp1 in range(height,-1, -1):
76
+ tmp_a_low= a[tmp1-1:tmp1+1,:][0]
77
+ for tmp2 in range(width):
78
+ # a = tmp_a_low[tmp2]
79
+ if tmp_a_low[tmp2]!=0:
80
+ f=1
81
+ if f == 1:
82
+ break
83
+ delta_y_bottom = height - tmp1 + 3
84
+ """
85
+ 获取左右
86
+ """
87
+ f = 0
88
+ for tmp1 in range(width):
89
+ tmp_a_left = a[:, tmp1:tmp1+1]
90
+ for tmp2 in range(height):
91
+ if tmp_a_left[tmp2] != 0:
92
+ f = 1
93
+ if f==1:
94
+ break
95
+ delta_x_left = tmp1 + 1
96
+ f = 0
97
+ for tmp1 in range(width, -1, -1):
98
+ tmp_a_left = a[:, tmp1-1:tmp1]
99
+ for tmp2 in range(height):
100
+ if tmp_a_left[tmp2] != 0:
101
+ f = 1
102
+ if f==1:
103
+ break
104
+ delta_x_right = width - tmp1 + 1
105
+ return delta_y_high, delta_y_bottom, delta_x_left, delta_x_right
106
+
107
+ def move(input_image):
108
+ """
109
+ 裁剪主函数,输入一张png图像,该图像周围是透明的
110
+ """
111
+ png_img = input_image # 获取图像
112
+
113
+ height, width, channels = png_img.shape # 高y、宽x
114
+ y_low,y_high, _, _ = get_box_pro(png_img, model=2) # for循环
115
+ base = np.zeros((y_high, width, channels),dtype=np.uint8) # for循环
116
+ png_img = png_img[0:height - y_high, :, :] # for循环
117
+ png_img = np.concatenate((base, png_img), axis=0)
118
+ return png_img
119
+
120
+ if __name__ == "__main__":
121
+ pass
hivisionai/hycv/idphotoTool/neck_processing.py ADDED
@@ -0,0 +1,320 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+ from ..utils import get_box_pro
4
+ from ..vision import cover_image
5
+
6
+
7
+ def transformationNeck(image:np.ndarray, cutNeckHeight:int, neckBelow:int,
8
+ toHeight:int,per_to_side:float=0.75) -> np.ndarray:
9
+ """
10
+ 脖子扩充算法, 其实需要输入的只是脖子扣出来的部分以及需要被扩充的高度/需要被扩充成的高度.
11
+ """
12
+ height, width, channels = image.shape
13
+ _, _, _, a = cv2.split(image) # 这应该是一个四通道的图像
14
+ ret, a_thresh = cv2.threshold(a, 20, 255, cv2.THRESH_BINARY) # 将透明图层二值化
15
+ def locate_width(image_:np.ndarray, y_:int, mode, left_or_right:int=None):
16
+ # 从y=y这个水平线上寻找两边的非零点
17
+ # 增加left_or_right的原因在于为下面check_jaw服务
18
+ if mode==1: # 左往右
19
+ x_ = 0
20
+ if left_or_right is None:
21
+ left_or_right = 0
22
+ for x_ in range(left_or_right, width):
23
+ if image_[y_][x_] != 0:
24
+ break
25
+ else: # 右往左
26
+ x_ = width
27
+ if left_or_right is None:
28
+ left_or_right = width - 1
29
+ for x_ in range(left_or_right, -1, -1):
30
+ if image_[y_][x_] != 0:
31
+ break
32
+ return x_
33
+ def check_jaw(image_:np.ndarray, left_, right_):
34
+ """
35
+ 检查选择的点是否与截到下巴,如果截到了,就往下平移一个单位
36
+ """
37
+ f= True # True代表没截到下巴
38
+ # [x, y]
39
+ for x_cell in range(left_[0] + 1, right_[0]):
40
+ if image_[left_[1]][x_cell] == 0:
41
+ f = False
42
+ break
43
+ if f is True:
44
+ return left_, right_
45
+ else:
46
+ y_ = left_[1] + 2
47
+ x_left_ = locate_width(image_, y_, mode=1, left_or_right=left_[0])
48
+ x_right_ = locate_width(image_, y_, mode=2, left_or_right=right_[0])
49
+ left_, right_ = check_jaw(image_, [x_left_, y_], [x_right_, y_])
50
+ return left_, right_
51
+ x_left = locate_width(image_=a_thresh, mode=1, y_=cutNeckHeight)
52
+ x_right = locate_width(image_=a_thresh, mode=2, y_=cutNeckHeight)
53
+ # 在这里我们取消了对下巴的检查,原因在于输入的imageHeight并不能改变
54
+ # cell_left_above, cell_right_above = check_jaw(a_thresh, [x_left, imageHeight], [x_right, imageHeight])
55
+ cell_left_above, cell_right_above = [x_left, cutNeckHeight], [x_right, cutNeckHeight]
56
+ toWidth = x_right - x_left # 矩形宽
57
+ # 此时我们寻找到了脖子的"宽出来的"两个点,这两个点作为上面的两个点, 接下来寻找下面的两个点
58
+ if per_to_side >1:
59
+ assert ValueError("per_to_side 必须小于1!")
60
+ y_below = int((neckBelow - cutNeckHeight) * per_to_side + cutNeckHeight) # 定位y轴坐标
61
+ cell_left_below = [locate_width(a_thresh, y_=y_below, mode=1), y_below]
62
+ cell_right_bellow = [locate_width(a_thresh, y_=y_below, mode=2), y_below]
63
+ # 四个点全齐,开始透视变换
64
+ # 需要变换的四个点为 cell_left_above, cell_right_above, cell_left_below, cell_right_bellow
65
+ rect = np.array([cell_left_above, cell_right_above, cell_left_below, cell_right_bellow],
66
+ dtype='float32')
67
+ # 变化后的坐标点
68
+ dst = np.array([[0, 0], [toWidth, 0], [0 , toHeight], [toWidth, toHeight]],
69
+ dtype='float32')
70
+ M = cv2.getPerspectiveTransform(rect, dst)
71
+ warped = cv2.warpPerspective(image, M, (toWidth, toHeight))
72
+ # 将变换后的图像覆盖到原图上
73
+ final = cover_image(image=warped, background=image, mode=3, x=cell_left_above[0], y=cell_left_above[1])
74
+ return final
75
+
76
+
77
+ def transformationNeck2(image:np.ndarray, per_to_side:float=0.8)->np.ndarray:
78
+ """
79
+ 透视变换脖子函数,输入图像和四个点(矩形框)
80
+ 矩形框内的图像可能是不完整的(边角有透明区域)
81
+ 我们将根据透视变换将矩形框内的图像拉伸成和矩形框一样的形状.
82
+ 算法分为几个步骤: 选择脖子的四个点 -> 选定这四个点拉伸后的坐标 -> 透视变换 -> 覆盖原图
83
+ """
84
+ b, g, r, a = cv2.split(image) # 这应该是一个四通道的图像
85
+ height, width = a.shape
86
+ def locate_side(image_:np.ndarray, x_:int, y_max:int) -> int:
87
+ # 寻找x=y, 且 y <= y_max 上从下往上第一个非0的点,如果没找到就返回0
88
+ y_ = 0
89
+ for y_ in range(y_max - 1, -1, -1):
90
+ if image_[y_][x_] != 0:
91
+ break
92
+ return y_
93
+ def locate_width(image_:np.ndarray, y_:int, mode, left_or_right:int=None):
94
+ # 从y=y这个水平线上寻找两边的非零点
95
+ # 增加left_or_right的原因在于为下面check_jaw服务
96
+ if mode==1: # 左往右
97
+ x_ = 0
98
+ if left_or_right is None:
99
+ left_or_right = 0
100
+ for x_ in range(left_or_right, width):
101
+ if image_[y_][x_] != 0:
102
+ break
103
+ else: # 右往左
104
+ x_ = width
105
+ if left_or_right is None:
106
+ left_or_right = width - 1
107
+ for x_ in range(left_or_right, -1, -1):
108
+ if image_[y_][x_] != 0:
109
+ break
110
+ return x_
111
+ def check_jaw(image_:np.ndarray, left_, right_):
112
+ """
113
+ 检查选择的点是否与截到下巴,如果截到了,就往下平移一个单位
114
+ """
115
+ f= True # True代表没截到下巴
116
+ # [x, y]
117
+ for x_cell in range(left_[0] + 1, right_[0]):
118
+ if image_[left_[1]][x_cell] == 0:
119
+ f = False
120
+ break
121
+ if f is True:
122
+ return left_, right_
123
+ else:
124
+ y_ = left_[1] + 2
125
+ x_left_ = locate_width(image_, y_, mode=1, left_or_right=left_[0])
126
+ x_right_ = locate_width(image_, y_, mode=2, left_or_right=right_[0])
127
+ left_, right_ = check_jaw(image_, [x_left_, y_], [x_right_, y_])
128
+ return left_, right_
129
+ # 选择脖子的四个点,核心在于选择上面的两个点,这两个点的确定的位置应该是"宽出来的"两个点
130
+ _, _ ,_, a = cv2.split(image) # 这应该是一个四通道的图像
131
+ ret,a_thresh = cv2.threshold(a,127,255,cv2.THRESH_BINARY)
132
+ y_high, y_low, x_left, x_right = get_box_pro(image=image, model=1) # 直接返回矩阵信息
133
+ y_left_side = locate_side(image_=a_thresh, x_=x_left, y_max=y_low) # 左边的点的y轴坐标
134
+ y_right_side = locate_side(image_=a_thresh, x_=x_right, y_max=y_low) # 右边的点的y轴坐标
135
+ y = min(y_left_side, y_right_side) # 将两点的坐标保持相同
136
+ cell_left_above, cell_right_above = check_jaw(a_thresh,[x_left, y], [x_right, y])
137
+ x_left, x_right = cell_left_above[0], cell_right_above[0]
138
+ # 此时我们寻找到了脖子的"宽出来的"两个点,这两个点作为上面的两个点, 接下来寻找下面的两个点
139
+ if per_to_side >1:
140
+ assert ValueError("per_to_side 必须小于1!")
141
+ # 在后面的透视变换中我会把它拉成矩形, 在这里我先获取四个点的高和宽
142
+ height_ = 100 # 这个值应该是个变化的值,与拉伸的长度有关,但是现在先规定为150
143
+ width_ = x_right - x_left # 其实也就是 cell_right_above[1] - cell_left_above[1]
144
+ y = int((y_low - y)*per_to_side + y) # 定位y轴坐标
145
+ cell_left_below, cell_right_bellow = ([locate_width(a_thresh, y_=y, mode=1), y], [locate_width(a_thresh, y_=y, mode=2), y])
146
+ # 四个点全齐,开始透视变换
147
+ # 寻找透视变换后的四个点,只需要变换below的两个点即可
148
+ # cell_left_below_final, cell_right_bellow_final = ([cell_left_above[1], y_low], [cell_right_above[1], y_low])
149
+ # 需要变换的四个点为 cell_left_above, cell_right_above, cell_left_below, cell_right_bellow
150
+ rect = np.array([cell_left_above, cell_right_above, cell_left_below, cell_right_bellow],
151
+ dtype='float32')
152
+ # 变化后的坐标点
153
+ dst = np.array([[0, 0], [width_, 0], [0 , height_], [width_, height_]],
154
+ dtype='float32')
155
+ # 计算变换矩阵
156
+ M = cv2.getPerspectiveTransform(rect, dst)
157
+ warped = cv2.warpPerspective(image, M, (width_, height_))
158
+
159
+ # a = cv2.erode(a, (10, 10))
160
+ # image = cv2.merge((r, g, b, a))
161
+ final = cover_image(image=warped, background=image, mode=3, x=cell_left_above[0], y=cell_left_above[1])
162
+ # tmp = np.zeros(image.shape)
163
+ # final = cover_image(image=warped, background=tmp, mode=3, x=cell_left_above[0], y=cell_left_above[1])
164
+ # final = cover_image(image=image, background=final, mode=3, x=0, y=0)
165
+ return final
166
+
167
+
168
+ def bestJunctionCheck(image:np.ndarray, offset:int, stepSize:int=2):
169
+ """
170
+ 最优点检测算算法输入一张脖子图片(无论这张图片是否已经被二值化,我都认为没有被二值化),输出一个小数(脖子最上方与衔接点位置/脖子图像长度)
171
+ 与beta版不同的是它新增了一个阈值限定内容.
172
+ 对于脖子而言,我我们首先可以定位到上面的部分,然后根据上面的这个点向下进行遍历检测.
173
+ 与beta版类似,我们使用一个stepSize来用作斜率的检测
174
+ 但是对于遍历检测而言,与beta版不同的是,我们需要对遍历的地方进行一定的限制.
175
+ 限制的标准是,如果当前遍历的点的横坐标和起始点横坐标的插值超过了某个阈值,则认为是越界.
176
+ """
177
+ point_k = 1
178
+ _, _, _, a = cv2.split(image) # 这应该是一个四通道的图像
179
+ height, width = a.shape
180
+ ret, a_thresh = cv2.threshold(a, 127, 255, cv2.THRESH_BINARY) # 将透明图层二值化
181
+ # 直接返回脖子的位置信息, 修正系数为0, get_box_pro内部也封装了二值化,所以直接输入原图
182
+ y_high, y_low, _, _ = get_box_pro(image=image, model=1, correction_factor=0)
183
+ # 真正有用的只有上下y轴的两个值...
184
+ # 首先当然是确定起始点的位置,我们用同样的scan扫描函数进行行遍历.
185
+ def scan(y_:int, max_num:int=2):
186
+ num = 0
187
+ # 设定两个值,分别代表脖子的左边和右边
188
+ left = False
189
+ right = False
190
+ for x_ in range(width):
191
+ if a_thresh[y_][x_] != 0:
192
+ # 检测左边
193
+ if x_ < width // 2 and left is False:
194
+ num += 1
195
+ left = True
196
+ # 检测右边
197
+ elif x_ > width // 2 and right is False:
198
+ num += 1
199
+ right = True
200
+ return True if num >= max_num else False
201
+ def locate_neck_above():
202
+ """
203
+ 定位脖子的尖尖脚
204
+ """
205
+ # y_high就是脖子的最高点
206
+ for y_ in range(y_high, height):
207
+ if scan(y_):
208
+ return y_
209
+ y_start = locate_neck_above() # 得到遍历的初始高度
210
+ if y_low - y_start < stepSize: assert ValueError("脖子太小!")
211
+ # 然后获取一下初始的坐标点
212
+ x_left, x_right = 0, width
213
+ for x_left_ in range(0, width):
214
+ if a_thresh[y_start][x_left_] != 0:
215
+ x_left = x_left_
216
+ break
217
+ for x_right_ in range(width -1 , -1, -1):
218
+ if a_thresh[y_start][x_right_] != 0:
219
+ x_right = x_right_
220
+ break
221
+ # 接下来我定义两个生成器,首先是脖子轮廓(向下寻找的)生成器,每进行一次next,生成器会返回y+1的脖子轮廓点
222
+ def contoursGenerator(image_:np.ndarray, y_:int, mode):
223
+ """
224
+ 这会是一个生成器,用于生成脖子两边的轮廓
225
+ y_ 是启始点的y坐标,每一次寻找都会让y_+1
226
+ mode==1说明是找左边的边,即,image_[y_][x_] == 0 且image_[y_][x_ + 1] !=0 时跳出;
227
+ 否则 当image_[y_][x_] != 0 时, x_ - 1; 当image_[y_][x_] == 0 且 image_[y_][x_ + 1] ==0 时x_ + 1
228
+ mode==2说明是找右边的边,即,image_[y_][x_] == 0 且image_[y_][x_ - 1] !=0 时跳出
229
+ 否则 当image_[y_][x_] != 0 时, x_ + 1; 当image_[y_][x_] == 0 且 image_[y_][x_ - 1] ==0 时x_ - 1
230
+ """
231
+ y_ += 1
232
+ try:
233
+ if mode == 1:
234
+ x_ = 0
235
+ while 0 <= y_ < height and 0 <= x_ < width:
236
+ while image_[y_][x_] != 0 and x_ >= 0: x_ -= 1
237
+ # 这里其实会有bug,不过可以不管
238
+ while x_ < width and image_[y_][x_] == 0 and image_[y_][x_ + 1] == 0: x_ += 1
239
+ yield [y_, x_]
240
+ y_ += 1
241
+ elif mode == 2:
242
+ x_ = width-1
243
+ while 0 <= y_ < height and 0 <= x_ < width:
244
+ while x_ < width and image_[y_][x_] != 0: x_ += 1
245
+ while x_ >= 0 and image_[y_][x_] == 0 and image_[y_][x_ - 1] == 0: x_ -= 1
246
+ yield [y_, x_]
247
+ y_ += 1
248
+ # 当处理失败则返回False
249
+ except IndexError:
250
+ yield False
251
+ # 然后是斜率生成器,这个生成器依赖子轮廓生成器,每一次生成轮廓后会计算斜率,另一个点的选取和stepSize有关
252
+ def kGenerator(image_: np.ndarray, mode):
253
+ """
254
+ 导数生成器,用来生成每一个点对应的导数
255
+ """
256
+ y_ = y_start
257
+ # 对起始点建立一个生成器, mode=1时是左边轮廓,mode=2时是右边轮廓
258
+ c_generator = contoursGenerator(image_=image_, y_=y_, mode=mode)
259
+ for cell in c_generator:
260
+ # 寻找距离当前cell距离为stepSize的轮廓点
261
+ kc = contoursGenerator(image_=image_, y_=cell[0] + stepSize, mode=mode)
262
+ kCell = next(kc)
263
+ if kCell is False:
264
+ # 寻找失败
265
+ yield False, False
266
+ else:
267
+ # 寻找成功,返回当坐标点和斜率值
268
+ # 对于左边而言,斜率必然是前一个点的坐标减去后一个点的坐标
269
+ # 对于右边而言,斜率必然是后一个点的坐标减去前一个点的坐标
270
+ k = (cell[1] - kCell[1]) / stepSize if mode == 1 else (kCell[1] - cell[1]) / stepSize
271
+ yield k, cell
272
+ # 接着开始写寻找算法,需要注意的是我们是分两边选择的
273
+ def findPt(image_:np.ndarray, mode):
274
+ x_base = x_left if mode == 1 else x_right
275
+ k_generator = kGenerator(image_=image_, mode=mode)
276
+ k, cell = k_generator.__next__()
277
+ if k is False:
278
+ raise ValueError("无法找到拐点!")
279
+ k_next, cell_next = k_generator.__next__()
280
+ while k_next is not False:
281
+ cell = cell_next
282
+ # if cell[1] > x_base and mode == 2:
283
+ # x_base = cell[1]
284
+ # elif cell[1] < x_base and mode == 1:
285
+ # x_base = cell[1]
286
+ # 跳出循环的方式一:斜率超过了某个值
287
+ if k_next > point_k:
288
+ print("K out")
289
+ break
290
+ # 跳出循环的方式二:超出阈值
291
+ elif abs(cell[1] - x_base) > offset:
292
+ print("O out")
293
+ break
294
+ k_next, cell_next = k_generator.__next__()
295
+ if abs(cell[1] - x_base) > offset:
296
+ cell[0] = cell[0] - offset - 1
297
+ return cell[0]
298
+ # 先找左边的拐点:
299
+ pointY_left = findPt(image_=a_thresh, mode=1)
300
+ # 再找右边的拐点:
301
+ pointY_right = findPt(image_=a_thresh, mode=2)
302
+ point = min(pointY_right, pointY_left)
303
+ per = (point - y_high) / (y_low - y_high)
304
+ # pointX_left = next(contoursGenerator(image_=a_thresh, y_= point- 1, mode=1))[1]
305
+ # pointX_right = next(contoursGenerator(image_=a_thresh, y_=point - 1, mode=2))[1]
306
+ # return [pointX_left, point], [pointX_right, point]
307
+ return per
308
+
309
+
310
+
311
+
312
+
313
+ if __name__ == "__main__":
314
+ img = cv2.imread("./neck_temp/neck_image6.png", cv2.IMREAD_UNCHANGED)
315
+ new = transformationNeck(img)
316
+ cv2.imwrite("./1.png", new)
317
+
318
+
319
+
320
+
hivisionai/hycv/matting_tools.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from PIL import Image
3
+ import cv2
4
+ import onnxruntime
5
+ from .tensor2numpy import NNormalize, NTo_Tensor, NUnsqueeze
6
+ from .vision import image2bgr
7
+
8
+
9
+ def read_modnet_image(input_image, ref_size=512):
10
+ im = Image.fromarray(np.uint8(input_image))
11
+ width, length = im.size[0], im.size[1]
12
+ im = np.asarray(im)
13
+ im = image2bgr(im)
14
+ im = cv2.resize(im, (ref_size, ref_size), interpolation=cv2.INTER_AREA)
15
+ im = NNormalize(im, mean=np.array([0.5, 0.5, 0.5]), std=np.array([0.5, 0.5, 0.5]))
16
+ im = NUnsqueeze(NTo_Tensor(im))
17
+
18
+ return im, width, length
19
+
20
+
21
+ def get_modnet_matting(input_image, checkpoint_path="./test.onnx", ref_size=512):
22
+
23
+ print("checkpoint_path:", checkpoint_path)
24
+ sess = onnxruntime.InferenceSession(checkpoint_path)
25
+
26
+ input_name = sess.get_inputs()[0].name
27
+ output_name = sess.get_outputs()[0].name
28
+
29
+ im, width, length = read_modnet_image(input_image=input_image, ref_size=ref_size)
30
+
31
+ matte = sess.run([output_name], {input_name: im})
32
+ matte = (matte[0] * 255).astype('uint8')
33
+ matte = np.squeeze(matte)
34
+ mask = cv2.resize(matte, (width, length), interpolation=cv2.INTER_AREA)
35
+ b, g, r = cv2.split(np.uint8(input_image))
36
+
37
+ output_image = cv2.merge((b, g, r, mask))
38
+
39
+ return output_image
hivisionai/hycv/mtcnn_onnx/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ from .visualization_utils import show_bboxes
2
+ from .detector import detect_faces
hivisionai/hycv/mtcnn_onnx/box_utils.py ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from PIL import Image
3
+
4
+
5
+ def nms(boxes, overlap_threshold=0.5, mode='union'):
6
+ """Non-maximum suppression.
7
+
8
+ Arguments:
9
+ boxes: a float numpy array of shape [n, 5],
10
+ where each row is (xmin, ymin, xmax, ymax, score).
11
+ overlap_threshold: a float number.
12
+ mode: 'union' or 'min'.
13
+
14
+ Returns:
15
+ list with indices of the selected boxes
16
+ """
17
+
18
+ # if there are no boxes, return the empty list
19
+ if len(boxes) == 0:
20
+ return []
21
+
22
+ # list of picked indices
23
+ pick = []
24
+
25
+ # grab the coordinates of the bounding boxes
26
+ x1, y1, x2, y2, score = [boxes[:, i] for i in range(5)]
27
+
28
+ area = (x2 - x1 + 1.0)*(y2 - y1 + 1.0)
29
+ ids = np.argsort(score) # in increasing order
30
+
31
+ while len(ids) > 0:
32
+
33
+ # grab index of the largest value
34
+ last = len(ids) - 1
35
+ i = ids[last]
36
+ pick.append(i)
37
+
38
+ # compute intersections
39
+ # of the box with the largest score
40
+ # with the rest of boxes
41
+
42
+ # left top corner of intersection boxes
43
+ ix1 = np.maximum(x1[i], x1[ids[:last]])
44
+ iy1 = np.maximum(y1[i], y1[ids[:last]])
45
+
46
+ # right bottom corner of intersection boxes
47
+ ix2 = np.minimum(x2[i], x2[ids[:last]])
48
+ iy2 = np.minimum(y2[i], y2[ids[:last]])
49
+
50
+ # width and height of intersection boxes
51
+ w = np.maximum(0.0, ix2 - ix1 + 1.0)
52
+ h = np.maximum(0.0, iy2 - iy1 + 1.0)
53
+
54
+ # intersections' areas
55
+ inter = w * h
56
+ if mode == 'min':
57
+ overlap = inter/np.minimum(area[i], area[ids[:last]])
58
+ elif mode == 'union':
59
+ # intersection over union (IoU)
60
+ overlap = inter/(area[i] + area[ids[:last]] - inter)
61
+
62
+ # delete all boxes where overlap is too big
63
+ ids = np.delete(
64
+ ids,
65
+ np.concatenate([[last], np.where(overlap > overlap_threshold)[0]])
66
+ )
67
+
68
+ return pick
69
+
70
+
71
+ def convert_to_square(bboxes):
72
+ """Convert bounding boxes to a square form.
73
+
74
+ Arguments:
75
+ bboxes: a float numpy array of shape [n, 5].
76
+
77
+ Returns:
78
+ a float numpy array of shape [n, 5],
79
+ squared bounding boxes.
80
+ """
81
+
82
+ square_bboxes = np.zeros_like(bboxes)
83
+ x1, y1, x2, y2 = [bboxes[:, i] for i in range(4)]
84
+ h = y2 - y1 + 1.0
85
+ w = x2 - x1 + 1.0
86
+ max_side = np.maximum(h, w)
87
+ square_bboxes[:, 0] = x1 + w*0.5 - max_side*0.5
88
+ square_bboxes[:, 1] = y1 + h*0.5 - max_side*0.5
89
+ square_bboxes[:, 2] = square_bboxes[:, 0] + max_side - 1.0
90
+ square_bboxes[:, 3] = square_bboxes[:, 1] + max_side - 1.0
91
+ return square_bboxes
92
+
93
+
94
+ def calibrate_box(bboxes, offsets):
95
+ """Transform bounding boxes to be more like true bounding boxes.
96
+ 'offsets' is one of the outputs of the nets.
97
+
98
+ Arguments:
99
+ bboxes: a float numpy array of shape [n, 5].
100
+ offsets: a float numpy array of shape [n, 4].
101
+
102
+ Returns:
103
+ a float numpy array of shape [n, 5].
104
+ """
105
+ x1, y1, x2, y2 = [bboxes[:, i] for i in range(4)]
106
+ w = x2 - x1 + 1.0
107
+ h = y2 - y1 + 1.0
108
+ w = np.expand_dims(w, 1)
109
+ h = np.expand_dims(h, 1)
110
+
111
+ # this is what happening here:
112
+ # tx1, ty1, tx2, ty2 = [offsets[:, i] for i in range(4)]
113
+ # x1_true = x1 + tx1*w
114
+ # y1_true = y1 + ty1*h
115
+ # x2_true = x2 + tx2*w
116
+ # y2_true = y2 + ty2*h
117
+ # below is just more compact form of this
118
+
119
+ # are offsets always such that
120
+ # x1 < x2 and y1 < y2 ?
121
+
122
+ translation = np.hstack([w, h, w, h])*offsets
123
+ bboxes[:, 0:4] = bboxes[:, 0:4] + translation
124
+ return bboxes
125
+
126
+
127
+ def get_image_boxes(bounding_boxes, img, size=24):
128
+ """Cut out boxes from the image.
129
+
130
+ Arguments:
131
+ bounding_boxes: a float numpy array of shape [n, 5].
132
+ img: an instance of PIL.Image.
133
+ size: an integer, size of cutouts.
134
+
135
+ Returns:
136
+ a float numpy array of shape [n, 3, size, size].
137
+ """
138
+
139
+ num_boxes = len(bounding_boxes)
140
+ width, height = img.size
141
+
142
+ [dy, edy, dx, edx, y, ey, x, ex, w, h] = correct_bboxes(bounding_boxes, width, height)
143
+ img_boxes = np.zeros((num_boxes, 3, size, size), 'float32')
144
+
145
+ for i in range(num_boxes):
146
+ img_box = np.zeros((h[i], w[i], 3), 'uint8')
147
+
148
+ img_array = np.asarray(img, 'uint8')
149
+ img_box[dy[i]:(edy[i] + 1), dx[i]:(edx[i] + 1), :] =\
150
+ img_array[y[i]:(ey[i] + 1), x[i]:(ex[i] + 1), :]
151
+
152
+ # resize
153
+ img_box = Image.fromarray(img_box)
154
+ img_box = img_box.resize((size, size), Image.BILINEAR)
155
+ img_box = np.asarray(img_box, 'float32')
156
+
157
+ img_boxes[i, :, :, :] = _preprocess(img_box)
158
+
159
+ return img_boxes
160
+
161
+
162
+ def correct_bboxes(bboxes, width, height):
163
+ """Crop boxes that are too big and get coordinates
164
+ with respect to cutouts.
165
+
166
+ Arguments:
167
+ bboxes: a float numpy array of shape [n, 5],
168
+ where each row is (xmin, ymin, xmax, ymax, score).
169
+ width: a float number.
170
+ height: a float number.
171
+
172
+ Returns:
173
+ dy, dx, edy, edx: a int numpy arrays of shape [n],
174
+ coordinates of the boxes with respect to the cutouts.
175
+ y, x, ey, ex: a int numpy arrays of shape [n],
176
+ corrected ymin, xmin, ymax, xmax.
177
+ h, w: a int numpy arrays of shape [n],
178
+ just heights and widths of boxes.
179
+
180
+ in the following order:
181
+ [dy, edy, dx, edx, y, ey, x, ex, w, h].
182
+ """
183
+
184
+ x1, y1, x2, y2 = [bboxes[:, i] for i in range(4)]
185
+ w, h = x2 - x1 + 1.0, y2 - y1 + 1.0
186
+ num_boxes = bboxes.shape[0]
187
+
188
+ # 'e' stands for end
189
+ # (x, y) -> (ex, ey)
190
+ x, y, ex, ey = x1, y1, x2, y2
191
+
192
+ # we need to cut out a box from the image.
193
+ # (x, y, ex, ey) are corrected coordinates of the box
194
+ # in the image.
195
+ # (dx, dy, edx, edy) are coordinates of the box in the cutout
196
+ # from the image.
197
+ dx, dy = np.zeros((num_boxes,)), np.zeros((num_boxes,))
198
+ edx, edy = w.copy() - 1.0, h.copy() - 1.0
199
+
200
+ # if box's bottom right corner is too far right
201
+ ind = np.where(ex > width - 1.0)[0]
202
+ edx[ind] = w[ind] + width - 2.0 - ex[ind]
203
+ ex[ind] = width - 1.0
204
+
205
+ # if box's bottom right corner is too low
206
+ ind = np.where(ey > height - 1.0)[0]
207
+ edy[ind] = h[ind] + height - 2.0 - ey[ind]
208
+ ey[ind] = height - 1.0
209
+
210
+ # if box's top left corner is too far left
211
+ ind = np.where(x < 0.0)[0]
212
+ dx[ind] = 0.0 - x[ind]
213
+ x[ind] = 0.0
214
+
215
+ # if box's top left corner is too high
216
+ ind = np.where(y < 0.0)[0]
217
+ dy[ind] = 0.0 - y[ind]
218
+ y[ind] = 0.0
219
+
220
+ return_list = [dy, edy, dx, edx, y, ey, x, ex, w, h]
221
+ return_list = [i.astype('int32') for i in return_list]
222
+
223
+ return return_list
224
+
225
+
226
+ def _preprocess(img):
227
+ """Preprocessing step before feeding the network.
228
+
229
+ Arguments:
230
+ img: a float numpy array of shape [h, w, c].
231
+
232
+ Returns:
233
+ a float numpy array of shape [1, c, h, w].
234
+ """
235
+ img = img.transpose((2, 0, 1))
236
+ img = np.expand_dims(img, 0)
237
+ img = (img - 127.5)*0.0078125
238
+ return img
hivisionai/hycv/mtcnn_onnx/detector.py ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from .box_utils import nms, calibrate_box, get_image_boxes, convert_to_square
3
+ from .first_stage import run_first_stage
4
+ import onnxruntime
5
+ import os
6
+ from os.path import exists
7
+ import requests
8
+
9
+
10
+ def download_img(img_url, base_dir):
11
+ print("Downloading Onnx Model in:",img_url)
12
+ r = requests.get(img_url, stream=True)
13
+ filename = img_url.split("/")[-1]
14
+ # print(r.status_code) # 返回状态码
15
+ if r.status_code == 200:
16
+ open(f'{base_dir}/{filename}', 'wb').write(r.content) # 将内容写入图片
17
+ print(f"Download Finshed -- {filename}")
18
+ del r
19
+
20
+
21
+ def detect_faces(image, min_face_size=20.0, thresholds=None, nms_thresholds=None):
22
+ """
23
+ Arguments:
24
+ image: an instance of PIL.Image.
25
+ min_face_size: a float number.
26
+ thresholds: a list of length 3.
27
+ nms_thresholds: a list of length 3.
28
+
29
+ Returns:
30
+ two float numpy arrays of shapes [n_boxes, 4] and [n_boxes, 10],
31
+ bounding boxes and facial landmarks.
32
+ """
33
+ if nms_thresholds is None:
34
+ nms_thresholds = [0.7, 0.7, 0.7]
35
+ if thresholds is None:
36
+ thresholds = [0.6, 0.7, 0.8]
37
+ base_url = "https://linimages.oss-cn-beijing.aliyuncs.com/"
38
+ onnx_filedirs = ["pnet.onnx", "rnet.onnx", "onet.onnx"]
39
+
40
+ # LOAD MODELS
41
+ basedir = os.path.dirname(os.path.realpath(__file__)).split("detector.py")[0]
42
+
43
+ for onnx_filedir in onnx_filedirs:
44
+ if not exists(f"{basedir}/weights"):
45
+ os.mkdir(f"{basedir}/weights")
46
+ if not exists(f"{basedir}/weights/{onnx_filedir}"):
47
+ # download onnx model
48
+ download_img(img_url=base_url+onnx_filedir, base_dir=f"{basedir}/weights")
49
+
50
+ pnet = onnxruntime.InferenceSession(f"{basedir}/weights/pnet.onnx") # Load a ONNX model
51
+ input_name_pnet = pnet.get_inputs()[0].name
52
+ output_name_pnet1 = pnet.get_outputs()[0].name
53
+ output_name_pnet2 = pnet.get_outputs()[1].name
54
+ pnet = [pnet, input_name_pnet, [output_name_pnet1, output_name_pnet2]]
55
+
56
+ rnet = onnxruntime.InferenceSession(f"{basedir}/weights/rnet.onnx") # Load a ONNX model
57
+ input_name_rnet = rnet.get_inputs()[0].name
58
+ output_name_rnet1 = rnet.get_outputs()[0].name
59
+ output_name_rnet2 = rnet.get_outputs()[1].name
60
+ rnet = [rnet, input_name_rnet, [output_name_rnet1, output_name_rnet2]]
61
+
62
+ onet = onnxruntime.InferenceSession(f"{basedir}/weights/onet.onnx") # Load a ONNX model
63
+ input_name_onet = onet.get_inputs()[0].name
64
+ output_name_onet1 = onet.get_outputs()[0].name
65
+ output_name_onet2 = onet.get_outputs()[1].name
66
+ output_name_onet3 = onet.get_outputs()[2].name
67
+ onet = [onet, input_name_onet, [output_name_onet1, output_name_onet2, output_name_onet3]]
68
+
69
+ # BUILD AN IMAGE PYRAMID
70
+ width, height = image.size
71
+ min_length = min(height, width)
72
+
73
+ min_detection_size = 12
74
+ factor = 0.707 # sqrt(0.5)
75
+
76
+ # scales for scaling the image
77
+ scales = []
78
+
79
+ # scales the image so that
80
+ # minimum size that we can detect equals to
81
+ # minimum face size that we want to detect
82
+ m = min_detection_size/min_face_size
83
+ min_length *= m
84
+
85
+ factor_count = 0
86
+ while min_length > min_detection_size:
87
+ scales.append(m*factor**factor_count)
88
+ min_length *= factor
89
+ factor_count += 1
90
+
91
+ # STAGE 1
92
+
93
+ # it will be returned
94
+ bounding_boxes = []
95
+
96
+ # run P-Net on different scales
97
+ for s in scales:
98
+ boxes = run_first_stage(image, pnet, scale=s, threshold=thresholds[0])
99
+ bounding_boxes.append(boxes)
100
+
101
+ # collect boxes (and offsets, and scores) from different scales
102
+ bounding_boxes = [i for i in bounding_boxes if i is not None]
103
+ bounding_boxes = np.vstack(bounding_boxes)
104
+
105
+ keep = nms(bounding_boxes[:, 0:5], nms_thresholds[0])
106
+ bounding_boxes = bounding_boxes[keep]
107
+
108
+ # use offsets predicted by pnet to transform bounding boxes
109
+ bounding_boxes = calibrate_box(bounding_boxes[:, 0:5], bounding_boxes[:, 5:])
110
+ # shape [n_boxes, 5]
111
+
112
+ bounding_boxes = convert_to_square(bounding_boxes)
113
+ bounding_boxes[:, 0:4] = np.round(bounding_boxes[:, 0:4])
114
+
115
+ # STAGE 2
116
+
117
+ img_boxes = get_image_boxes(bounding_boxes, image, size=24)
118
+
119
+ output = rnet[0].run([rnet[2][0], rnet[2][1]], {rnet[1]: img_boxes})
120
+ offsets = output[0] # shape [n_boxes, 4]
121
+ probs = output[1] # shape [n_boxes, 2]
122
+
123
+ keep = np.where(probs[:, 1] > thresholds[1])[0]
124
+ bounding_boxes = bounding_boxes[keep]
125
+ bounding_boxes[:, 4] = probs[keep, 1].reshape((-1,))
126
+ offsets = offsets[keep]
127
+
128
+ keep = nms(bounding_boxes, nms_thresholds[1])
129
+ bounding_boxes = bounding_boxes[keep]
130
+ bounding_boxes = calibrate_box(bounding_boxes, offsets[keep])
131
+ bounding_boxes = convert_to_square(bounding_boxes)
132
+ bounding_boxes[:, 0:4] = np.round(bounding_boxes[:, 0:4])
133
+
134
+ # STAGE 3
135
+
136
+ img_boxes = get_image_boxes(bounding_boxes, image, size=48)
137
+ if len(img_boxes) == 0:
138
+ return [], []
139
+ #img_boxes = Variable(torch.FloatTensor(img_boxes), volatile=True)
140
+ # with torch.no_grad():
141
+ # img_boxes = torch.FloatTensor(img_boxes)
142
+ # output = onet(img_boxes)
143
+ output = onet[0].run([onet[2][0], onet[2][1], onet[2][2]], {rnet[1]: img_boxes})
144
+ landmarks = output[0] # shape [n_boxes, 10]
145
+ offsets = output[1] # shape [n_boxes, 4]
146
+ probs = output[2] # shape [n_boxes, 2]
147
+
148
+ keep = np.where(probs[:, 1] > thresholds[2])[0]
149
+ bounding_boxes = bounding_boxes[keep]
150
+ bounding_boxes[:, 4] = probs[keep, 1].reshape((-1,))
151
+ offsets = offsets[keep]
152
+ landmarks = landmarks[keep]
153
+
154
+ # compute landmark points
155
+ width = bounding_boxes[:, 2] - bounding_boxes[:, 0] + 1.0
156
+ height = bounding_boxes[:, 3] - bounding_boxes[:, 1] + 1.0
157
+ xmin, ymin = bounding_boxes[:, 0], bounding_boxes[:, 1]
158
+ landmarks[:, 0:5] = np.expand_dims(xmin, 1) + np.expand_dims(width, 1)*landmarks[:, 0:5]
159
+ landmarks[:, 5:10] = np.expand_dims(ymin, 1) + np.expand_dims(height, 1)*landmarks[:, 5:10]
160
+
161
+ bounding_boxes = calibrate_box(bounding_boxes, offsets)
162
+ keep = nms(bounding_boxes, nms_thresholds[2], mode='min')
163
+ bounding_boxes = bounding_boxes[keep]
164
+ landmarks = landmarks[keep]
165
+
166
+ return bounding_boxes, landmarks
hivisionai/hycv/mtcnn_onnx/first_stage.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ from PIL import Image
3
+ import numpy as np
4
+ from .box_utils import nms, _preprocess
5
+
6
+
7
+ def run_first_stage(image, net, scale, threshold):
8
+ """Run P-Net, generate bounding boxes, and do NMS.
9
+
10
+ Arguments:
11
+ image: an instance of PIL.Image.
12
+ net: an instance of pytorch's nn.Module, P-Net.
13
+ scale: a float number,
14
+ scale width and height of the image by this number.
15
+ threshold: a float number,
16
+ threshold on the probability of a face when generating
17
+ bounding boxes from predictions of the net.
18
+
19
+ Returns:
20
+ a float numpy array of shape [n_boxes, 9],
21
+ bounding boxes with scores and offsets (4 + 1 + 4).
22
+ """
23
+
24
+ # scale the image and convert it to a float array
25
+
26
+ width, height = image.size
27
+ sw, sh = math.ceil(width*scale), math.ceil(height*scale)
28
+ img = image.resize((sw, sh), Image.BILINEAR)
29
+ img = np.asarray(img, 'float32')
30
+ img = _preprocess(img)
31
+ # with torch.no_grad():
32
+ # img = torch.FloatTensor(_preprocess(img))
33
+ output = net[0].run([net[2][0],net[2][1]], {net[1]: img})
34
+ probs = output[1][0, 1, :, :]
35
+ offsets = output[0]
36
+ # probs: probability of a face at each sliding window
37
+ # offsets: transformations to true bounding boxes
38
+
39
+ boxes = _generate_bboxes(probs, offsets, scale, threshold)
40
+ if len(boxes) == 0:
41
+ return None
42
+
43
+ keep = nms(boxes[:, 0:5], overlap_threshold=0.5)
44
+ return boxes[keep]
45
+
46
+
47
+ def _generate_bboxes(probs, offsets, scale, threshold):
48
+ """Generate bounding boxes at places
49
+ where there is probably a face.
50
+
51
+ Arguments:
52
+ probs: a float numpy array of shape [n, m].
53
+ offsets: a float numpy array of shape [1, 4, n, m].
54
+ scale: a float number,
55
+ width and height of the image were scaled by this number.
56
+ threshold: a float number.
57
+
58
+ Returns:
59
+ a float numpy array of shape [n_boxes, 9]
60
+ """
61
+
62
+ # applying P-Net is equivalent, in some sense, to
63
+ # moving 12x12 window with stride 2
64
+ stride = 2
65
+ cell_size = 12
66
+
67
+ # indices of boxes where there is probably a face
68
+ inds = np.where(probs > threshold)
69
+
70
+ if inds[0].size == 0:
71
+ return np.array([])
72
+
73
+ # transformations of bounding boxes
74
+ tx1, ty1, tx2, ty2 = [offsets[0, i, inds[0], inds[1]] for i in range(4)]
75
+ # they are defined as:
76
+ # w = x2 - x1 + 1
77
+ # h = y2 - y1 + 1
78
+ # x1_true = x1 + tx1*w
79
+ # x2_true = x2 + tx2*w
80
+ # y1_true = y1 + ty1*h
81
+ # y2_true = y2 + ty2*h
82
+
83
+ offsets = np.array([tx1, ty1, tx2, ty2])
84
+ score = probs[inds[0], inds[1]]
85
+
86
+ # P-Net is applied to scaled images
87
+ # so we need to rescale bounding boxes back
88
+ bounding_boxes = np.vstack([
89
+ np.round((stride*inds[1] + 1.0)/scale),
90
+ np.round((stride*inds[0] + 1.0)/scale),
91
+ np.round((stride*inds[1] + 1.0 + cell_size)/scale),
92
+ np.round((stride*inds[0] + 1.0 + cell_size)/scale),
93
+ score, offsets
94
+ ])
95
+ # why one is added?
96
+
97
+ return bounding_boxes.T
hivisionai/hycv/mtcnn_onnx/visualization_utils.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from PIL import ImageDraw
2
+
3
+
4
+ def show_bboxes(img, bounding_boxes, facial_landmarks=[]):
5
+ """Draw bounding boxes and facial landmarks.
6
+
7
+ Arguments:
8
+ img: an instance of PIL.Image.
9
+ bounding_boxes: a float numpy array of shape [n, 5].
10
+ facial_landmarks: a float numpy array of shape [n, 10].
11
+
12
+ Returns:
13
+ an instance of PIL.Image.
14
+ """
15
+
16
+ img_copy = img.copy()
17
+ draw = ImageDraw.Draw(img_copy)
18
+
19
+ for b in bounding_boxes:
20
+ draw.rectangle([
21
+ (b[0], b[1]), (b[2], b[3])
22
+ ], outline='white')
23
+
24
+ for p in facial_landmarks:
25
+ for i in range(5):
26
+ draw.ellipse([
27
+ (p[i] - 1.0, p[i + 5] - 1.0),
28
+ (p[i] + 1.0, p[i + 5] + 1.0)
29
+ ], outline='blue')
30
+
31
+ return img_copy
hivisionai/hycv/tensor2numpy.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 作者:林泽毅
3
+ 建这个开源库的起源呢,是因为在做onnx推理的时候,需要将原来的tensor转换成numpy.array
4
+ 问题是Tensor和Numpy的矩阵排布逻辑不同
5
+ 包括Tensor推理经常会进行Transform,比如ToTensor,Normalize等
6
+ 就想做一些等价转换的函数。
7
+ """
8
+ import numpy as np
9
+
10
+
11
+ def NTo_Tensor(array):
12
+ """
13
+ :param array: opencv/PIL读取的numpy矩阵
14
+ :return:返回一个形如Tensor的numpy矩阵
15
+ Example:
16
+ Inputs:array.shape = (512,512,3)
17
+ Outputs:output.shape = (3,512,512)
18
+ """
19
+ output = array.transpose((2, 0, 1))
20
+ return output
21
+
22
+
23
+ def NNormalize(array, mean=np.array([0.5, 0.5, 0.5]), std=np.array([0.5, 0.5, 0.5]), dtype=np.float32):
24
+ """
25
+ :param array: opencv/PIL读取的numpy矩阵
26
+ mean: 归一化均值,np.array格式
27
+ std: 归一化标准差,np.array格式
28
+ dtype:输出的numpy数据格式,一般onnx需要float32
29
+ :return:numpy矩阵
30
+ Example:
31
+ Inputs:array为opencv/PIL读取的一张图片
32
+ mean=np.array([0.5,0.5,0.5])
33
+ std=np.array([0.5,0.5,0.5])
34
+ dtype=np.float32
35
+ Outputs:output为归一化后的numpy矩阵
36
+ """
37
+ im = array / 255.0
38
+ im = np.divide(np.subtract(im, mean), std)
39
+ output = np.asarray(im, dtype=dtype)
40
+
41
+ return output
42
+
43
+
44
+ def NUnsqueeze(array, axis=0):
45
+ """
46
+ :param array: opencv/PIL读取的numpy矩阵
47
+ axis:要增加的维度
48
+ :return:numpy矩阵
49
+ Example:
50
+ Inputs:array为opencv/PIL读取的一张图片,array.shape为[512,512,3]
51
+ axis=0
52
+ Outputs:output为array在第0维增加一个维度,shape转为[1,512,512,3]
53
+ """
54
+ if axis == 0:
55
+ output = array[None, :, :, :]
56
+ elif axis == 1:
57
+ output = array[:, None, :, :]
58
+ elif axis == 2:
59
+ output = array[:, :, None, :]
60
+ else:
61
+ output = array[:, :, :, None]
62
+
63
+ return output
hivisionai/hycv/utils.py ADDED
@@ -0,0 +1,452 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 本文件存放一些自制的简单的图像处理函数
3
+ """
4
+ from PIL import Image
5
+ import cv2
6
+ import numpy as np
7
+ import math
8
+ import warnings
9
+ import csv
10
+ import glob
11
+
12
+
13
+ def cover_mask(image_path, mask_path, alpha=0.85, rate=0.1, if_save=True):
14
+ """
15
+ 在图片右下角盖上水印
16
+ :param image_path:
17
+ :param mask_path: 水印路径,以PNG方式读取
18
+ :param alpha: 不透明度,默认为0.85
19
+ :param rate: 水印比例,越小水印也越小,默认为0.1
20
+ :param if_save: 是否将裁剪后的图片保存,如果为True,则保存并返回新图路径,否则不保存,返回截取后的图片对象
21
+ :return: 新的图片路径
22
+ """
23
+ # 生成新的图片路径,我们默认图片后缀存在且必然包含“.”
24
+ path_len = len(image_path)
25
+ index = 0
26
+ for index in range(path_len - 1, -1, -1):
27
+ if image_path[index] == ".":
28
+ break
29
+ if 3 >= path_len - index >= 6:
30
+ raise TypeError("输入的图片格式有误!")
31
+ new_path = image_path[0:index] + "_with_mask" + image_path[index:path_len]
32
+ # 以png方式读取水印图
33
+ mask = Image.open(mask_path).convert('RGBA')
34
+ mask_h, mask_w = mask.size
35
+ # 以png的方式读取原图
36
+ im = Image.open(image_path).convert('RGBA')
37
+ # 我采取的策略是,先拷贝一张原图im为base作为基底,然后在im上利用paste函数添加水印
38
+ # 此时的水印是完全不透明的,我需要利用blend函数内置参数alpha进行不透明度调整
39
+ base = im.copy()
40
+ # layer = Image.new('RGBA', im.size, (0, 0, 0, ))
41
+ # tmp = Image.new('RGBA', im.size, (0, 0, 0, 0))
42
+ h, w = im.size
43
+ # 根据原图大小缩放水印图
44
+ mask = mask.resize((int(rate*math.sqrt(w*h*mask_h/mask_w)), int(rate*math.sqrt(w*h*mask_w/mask_h))), Image.ANTIALIAS)
45
+ mh, mw = mask.size
46
+ r, g, b, a = mask.split()
47
+ im.paste(mask, (h-mh, w-mw), mask=a)
48
+ # im.show()
49
+ out = Image.blend(base, im, alpha=alpha).convert('RGB')
50
+ # out = Image.alpha_composite(im, layer).convert('RGB')
51
+ if if_save:
52
+ out.save(new_path)
53
+ return new_path
54
+ else:
55
+ return out
56
+
57
+ def check_image(image) ->np.ndarray:
58
+ """
59
+ 判断某一对象是否为图像/矩阵类型,最终返回图像/矩阵
60
+ """
61
+ if not isinstance(image, np.ndarray):
62
+ image = cv2.imread(image, cv2.IMREAD_UNCHANGED)
63
+ return image
64
+
65
+ def get_box(image) -> list:
66
+ """
67
+ 这是一个简单的扣图后图像定位函数,不考虑噪点影响
68
+ 我们使用遍历的方法,碰到非透明点以后立即返回位置坐标
69
+ :param image:图像信息,可以是图片路径,也可以是已经读取后的图像
70
+ 如果传入的是图片路径,我会首先通过读取图片、二值化,然后再进行图像处理
71
+ 如果传入的是图像,直接处理,不会二值化
72
+ :return: 回传一个列表,分别是图像的上下(y)左右(x)自个值
73
+ """
74
+ image = check_image(image)
75
+ height, width, _ = image.shape
76
+ try:
77
+ b, g, r, a = cv2.split(image)
78
+ # 二值化处理
79
+ a = (a > 127).astype(np.int_)
80
+ except ValueError:
81
+ # 说明传入的是无透明图层的图像,直接返回图像尺寸
82
+ warnings.warn("你传入了一张非四通道格式的图片!")
83
+ return [0, height, 0, width]
84
+ flag1, flag2 = 0, 0
85
+ box = [0, 0, 0, 0] # 上下左右
86
+ # 采用两面夹击战术,使用flag1和2确定两面的裁剪程度
87
+ # 先得到上下
88
+ for i in range(height):
89
+ for j in range(width):
90
+ if flag1 == 0 and a[i][j] != 0:
91
+ flag1 = 1
92
+ box[0] = i
93
+ if flag2 == 0 and a[height - i -1][j] != 0:
94
+ flag2 = 1
95
+ box[1] = height - i - 1
96
+ if flag2 * flag1 == 1:
97
+ break
98
+ # 再得到左右
99
+ flag1, flag2 = 0, 0
100
+ for j in range(width):
101
+ for i in range(height):
102
+ if flag1 == 0 and a[i][j] != 0:
103
+ flag1 = 1
104
+ box[2] = j
105
+ if flag2 == 0 and a[i][width - j - 1] != 0:
106
+ flag2 = 1
107
+ box[3] = width - j - 1
108
+ if flag2 * flag1 == 1:
109
+ break
110
+ return box
111
+
112
+ def filtering(img, f, x, y, x_max, y_max, x_min, y_min, area=0, noise_size=50) ->tuple:
113
+ """
114
+ filtering将使用递归的方法得到一个连续图像(这个连续矩阵必须得是单通道的)的范围(坐标)
115
+ :param img: 传入的矩阵
116
+ :param f: 和img相同尺寸的全零矩阵,用于标记递归递归过的点
117
+ :param x: 当前递归到的x轴坐标
118
+ :param y: 当前递归到的y轴坐标
119
+ :param x_max: 递归过程中x轴坐标的最大值
120
+ :param y_max: 递归过程中y轴坐标的最大值
121
+ :param x_min: 递归过程中x轴坐标的最小值
122
+ :param y_min: 递归过程中y��坐标的最小值
123
+ :param area: 当前递归区域面积大小
124
+ :param noise_size: 最大递归区域面积大小,当area大于noise_size时,函数返回(0, 1)
125
+ :return: 分两种情况,当area大于noise_size时,函数返回(0, 1),当area小于等于noise_size时,函数返回(box, 0)
126
+ 其中box是连续图像的坐标和像素点面积(上下左右,面积)
127
+ 理论上来讲,我们可以用这个函数递归出任一图像的形状和坐标,但是从计算机内存、计算速度上考虑,这并不是一个好的选择
128
+ 所以这个函数一般用于判断和过滤噪点
129
+ """
130
+ dire_dir = [(1, 0), (-1, 0), (0, 1), (0, -1), (1, 1), (1, -1), (-1, -1), (-1, 1)]
131
+ height, width = img.shape
132
+ f[x][y] = 1
133
+ for dire in dire_dir:
134
+ delta_x, delta_y = dire
135
+ tmp_x, tmp_y = (x + delta_x, y + delta_y)
136
+ if height > tmp_x >= 0 and width > tmp_y >= 0:
137
+ if img[tmp_x][tmp_y] != 0 and f[tmp_x][tmp_y] == 0:
138
+ f[tmp_x][tmp_y] = 1
139
+ # cv2.imshow("test", f)
140
+ # cv2.waitKey(3)
141
+ area += 1
142
+ if area > noise_size:
143
+ return 0, 1
144
+ else:
145
+ x_max = tmp_x if tmp_x > x_max else x_max
146
+ x_min = tmp_x if tmp_x < x_min else x_min
147
+ y_max = tmp_y if tmp_y > y_max else y_max
148
+ y_min = tmp_y if tmp_y < y_min else y_min
149
+ box, flag = filtering(img, f, tmp_x, tmp_y, x_max, y_max, x_min, y_min, area=area, noise_size=noise_size)
150
+ if flag == 1:
151
+ return 0, 1
152
+ else:
153
+ (x_max, x_min, y_max, y_min, area) = box
154
+ return [x_min, x_max, y_min, y_max, area], 0
155
+
156
+
157
+ def get_box_pro(image: np.ndarray, model: int = 1, correction_factor=None, thresh: int = 127):
158
+ """
159
+ 本函数能够实现输入一张四通道图像,返回图像中最大连续非透明面积的区域的矩形坐标
160
+ 本函数将采用opencv内置函数来解析整个图像的mask,并提供一些参数,用于读取图像的位置信息
161
+ Args:
162
+ image: 四通道矩阵图像
163
+ model: 返回值模式
164
+ correction_factor: 提供一些边缘扩张接口,输入格式为list或者int:[up, down, left, right]。
165
+ 举个例子,假设我们希望剪切出的矩形框左边能够偏左1个像素,则输入[0, 0, 1, 0];
166
+ 如果希望右边偏右1个像素,则输入[0, 0, 0, 1]
167
+ 如果输入为int,则默认只会对左右两边做拓展,比如输入2,则和[0, 0, 2, 2]是等效的
168
+ thresh: 二值化阈值,为了保持一些羽化效果,thresh必须要小
169
+ Returns:
170
+ model为1时,将会返回切割出的矩形框的四个坐标点信息
171
+ model为2时,将会返回矩形框四边相距于原图四边的距离
172
+ """
173
+ # ------------ 数据格式规范部分 -------------- #
174
+ # 输入必须为四通道
175
+ if correction_factor is None:
176
+ correction_factor = [0, 0, 0, 0]
177
+ if not isinstance(image, np.ndarray) or len(cv2.split(image)) != 4:
178
+ raise TypeError("输入的图像必须为四通道np.ndarray类型矩阵!")
179
+ # correction_factor规范化
180
+ if isinstance(correction_factor, int):
181
+ correction_factor = [0, 0, correction_factor, correction_factor]
182
+ elif not isinstance(correction_factor, list):
183
+ raise TypeError("correction_factor 必须为int或者list类型!")
184
+ # ------------ 数据格式规范完毕 -------------- #
185
+ # 分离mask
186
+ _, _, _, mask = cv2.split(image)
187
+ # mask二值化处理
188
+ _, mask = cv2.threshold(mask, thresh=thresh, maxval=255, type=0)
189
+ contours, hierarchy = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
190
+ temp = np.ones(image.shape, np.uint8)*255
191
+ cv2.drawContours(temp, contours, -1, (0, 0, 255), -1)
192
+ contours_area = []
193
+ for cnt in contours:
194
+ contours_area.append(cv2.contourArea(cnt))
195
+ idx = contours_area.index(max(contours_area))
196
+ x, y, w, h = cv2.boundingRect(contours[idx]) # 框出图像
197
+ # ------------ 开始输出数据 -------------- #
198
+ height, width, _ = image.shape
199
+ y_up = y - correction_factor[0] if y - correction_factor[0] >= 0 else 0
200
+ y_down = y + h + correction_factor[1] if y + h + correction_factor[1] < height else height - 1
201
+ x_left = x - correction_factor[2] if x - correction_factor[2] >= 0 else 0
202
+ x_right = x + w + correction_factor[3] if x + w + correction_factor[3] < width else width - 1
203
+ if model == 1:
204
+ # model=1,将会返回切割出的矩形框的四个坐标点信息
205
+ return [y_up, y_down, x_left, x_right]
206
+ elif model == 2:
207
+ # model=2, 将会返回矩形框四边相距于原图四边的距离
208
+ return [y_up, height - y_down, x_left, width - x_right]
209
+ else:
210
+ raise EOFError("请选择正确的模式!")
211
+
212
+
213
+ def cut(image_path:str, box:list, if_save=True):
214
+ """
215
+ 根据box,裁剪对应的图片区域后保存
216
+ :param image_path: 原图路径
217
+ :param box: 坐标列表,上下左右
218
+ :param if_save:是否将裁剪后的图片保存,如果为True,则保存并返回新图路径,否则不保存,返回截取后的图片对象
219
+ :return: 新图路径或者是新图对象
220
+ """
221
+ index = 0
222
+ path_len = len(image_path)
223
+ up, down, left, right = box
224
+ image = cv2.imread(image_path, cv2.IMREAD_UNCHANGED)
225
+ new_image = image[up: down, left: right]
226
+ if if_save:
227
+ for index in range(path_len - 1, -1, -1):
228
+ if image_path[index] == ".":
229
+ break
230
+ if 3 >= path_len - index >= 6:
231
+ raise TypeError("输入的图片格式有误!")
232
+ new_path = image_path[0:index] + "_cut" + image_path[index:path_len]
233
+ cv2.imwrite(new_path, new_image, [cv2.IMWRITE_PNG_COMPRESSION, 9])
234
+ return new_path
235
+ else:
236
+ return new_image
237
+
238
+
239
+ def zoom_image_without_change_size(image:np.ndarray, zoom_rate, interpolation=cv2.INTER_NEAREST) ->np.ndarray:
240
+ """
241
+ 在不改变原图大小的情况下,对图像进行放大,目前只支持从图像中心放大
242
+ :param image: 传入的图像对象
243
+ :param zoom_rate: 放大比例,单位为倍(初始为1倍)
244
+ :param interpolation: 插值方式,与opencv的resize内置参数相对应,默认为最近邻插值
245
+ :return: 裁剪后的图像实例
246
+ """
247
+ height, width, _ = image.shape
248
+ if zoom_rate < 1:
249
+ # zoom_rate不能小于1
250
+ raise ValueError("zoom_rate不能小于1!")
251
+ height_tmp = int(height * zoom_rate)
252
+ width_tmp = int(width * zoom_rate)
253
+ image_tmp = cv2.resize(image, (height_tmp, width_tmp), interpolation=interpolation)
254
+ # 定位一下被裁剪的位置,实际上是裁剪框的左上角的点的坐标
255
+ delta_x = (width_tmp - width) // 2 # 横向
256
+ delta_y = (height_tmp - height) // 2 # 纵向
257
+ return image_tmp[delta_y : delta_y + height, delta_x : delta_x + width]
258
+
259
+
260
+ def filedir2csv(scan_filedir, csv_filedir):
261
+ file_list = glob.glob(scan_filedir+"/*")
262
+
263
+ with open(csv_filedir, "w") as csv_file:
264
+ writter = csv.writer(csv_file)
265
+ for file_dir in file_list:
266
+ writter.writerow([file_dir])
267
+
268
+ print("filedir2csv success!")
269
+
270
+
271
+ def full_ties(image_pre:np.ndarray):
272
+ height, width = image_pre.shape
273
+ # 先膨胀
274
+ kernel = np.ones((5, 5), dtype=np.uint8)
275
+ dilate = cv2.dilate(image_pre, kernel, 1)
276
+ # cv2.imshow("dilate", dilate)
277
+ def FillHole(image):
278
+ # 复制 image 图像
279
+ im_floodFill = image.copy()
280
+ # Mask 用于 floodFill,官方要求长宽+2
281
+ mask = np.zeros((height + 2, width + 2), np.uint8)
282
+ seedPoint = (0, 0)
283
+ # floodFill函数中的seedPoint对应像素必须是背景
284
+ is_break = False
285
+ for i in range(im_floodFill.shape[0]):
286
+ for j in range(im_floodFill.shape[1]):
287
+ if (im_floodFill[i][j] == 0):
288
+ seedPoint = (i, j)
289
+ is_break = True
290
+ break
291
+ if (is_break):
292
+ break
293
+ # 得到im_floodFill 255填充非孔洞值
294
+ cv2.floodFill(im_floodFill, mask, seedPoint, 255)
295
+ # cv2.imshow("tmp1", im_floodFill)
296
+ # 得到im_floodFill的逆im_floodFill_inv
297
+ im_floodFill_inv = cv2.bitwise_not(im_floodFill)
298
+ # cv2.imshow("tmp2", im_floodFill_inv)
299
+ # 把image、im_floodFill_inv这两幅图像结合起来得到前景
300
+ im_out = image | im_floodFill_inv
301
+ return im_out
302
+ # 洪流算法填充
303
+ image_floodFill = FillHole(dilate)
304
+ # 填充图和原图合并
305
+ image_final = image_floodFill | image_pre
306
+ # 再腐蚀
307
+ kernel = np.ones((5, 5), np.uint8)
308
+ erosion= cv2.erode(image_final, kernel, iterations=6)
309
+ # cv2.imshow("erosion", erosion)
310
+ # 添加高斯模糊
311
+ blur = cv2.GaussianBlur(erosion, (5, 5), 2.5)
312
+ # cv2.imshow("blur", blur)
313
+ # image_final = merge_image(image_pre, erosion)
314
+ # 再与原图合并
315
+ image_final = image_pre | blur
316
+ # cv2.imshow("final", image_final)
317
+ return image_final
318
+
319
+
320
+ def cut_BiggestAreas(image):
321
+ # 裁剪出整张图轮廓最大的部分
322
+ def find_BiggestAreas(image_pre):
323
+ # 定义一个三乘三的卷积核
324
+ kernel = np.ones((3, 3), dtype=np.uint8)
325
+ # 将输入图片膨胀
326
+ # dilate = cv2.dilate(image_pre, kernel, 3)
327
+ # cv2.imshow("dilate", dilate)
328
+ # 将输入图片二值化
329
+ _, thresh = cv2.threshold(image_pre, 127, 255, cv2.THRESH_BINARY)
330
+ # cv2.imshow("thresh", thresh)
331
+ # 将二值化后的图片膨胀
332
+ dilate_afterThresh = cv2.dilate(thresh, kernel, 5)
333
+ # cv2.imshow("thresh_afterThresh", dilate_afterThresh)
334
+ # 找轮廓
335
+ contours_, hierarchy = cv2.findContours(dilate_afterThresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
336
+ # 识别出最大的轮廓
337
+ # 需要注意的是,在低版本的findContours当中返回的结果是tuple,不支持pop,所以需要将其转为pop
338
+ contours = [x for x in contours_]
339
+ area = map(cv2.contourArea, contours)
340
+ area_list = list(area)
341
+ area_max = max(area_list)
342
+ post = area_list.index(area_max)
343
+ # 将最大的区域保留,其余全部填黑
344
+ contours.pop(post)
345
+ for i in range(len(contours)):
346
+ cv2.drawContours(image_pre, contours, i, 0, cv2.FILLED)
347
+ # cv2.imshow("cut", image_pre)
348
+ return image_pre
349
+ b, g, r, a = cv2.split(image)
350
+ a_new = find_BiggestAreas(a)
351
+ new_image = cv2.merge((b, g, r, a_new))
352
+ return new_image
353
+
354
+
355
+ def locate_neck(image:np.ndarray, proportion):
356
+ """
357
+ 根据输入的图片(四通道)和proportion(自上而下)的比例,定位到相应的y点,然后向内收缩,直到两边的像素点不透明
358
+ """
359
+ if image.shape[-1] != 4:
360
+ raise TypeError("请输入一张png格式的四通道图片!")
361
+ if proportion > 1 or proportion <=0:
362
+ raise ValueError("proportion 必须在0~1之间!")
363
+ _, _, _, a = cv2.split(image)
364
+ height, width = a.shape
365
+ _, a = cv2.threshold(a, 127, 255, cv2.THRESH_BINARY)
366
+ y = int(height * proportion)
367
+ x = 0
368
+ for x in range(width):
369
+ if a[y][x] == 255:
370
+ break
371
+ left = (y, x)
372
+ for x in range(width - 1, -1 , -1):
373
+ if a[y][x] == 255:
374
+ break
375
+ right = (y, x)
376
+ return left, right, right[1] - left[1]
377
+
378
+
379
+ def get_cutbox_image(input_image):
380
+ height, width = input_image.shape[0], input_image.shape[1]
381
+ y_top, y_bottom, x_left, x_right = get_box_pro(input_image, model=2)
382
+ result_image = input_image[y_top:height - y_bottom, x_left:width - x_right]
383
+ return result_image
384
+
385
+
386
+ def brightnessAdjustment(image: np.ndarray, bright_factor: int=0):
387
+ """
388
+ 图像亮度调节
389
+ :param image: 输入的图像矩阵
390
+ :param bright_factor:亮度调节因子,可正可负,没有范围限制
391
+ 当bright_factor ---> +无穷 时,图像全白
392
+ 当bright_factor ---> -无穷 时,图像全黑
393
+ :return: 处理后的图片
394
+ """
395
+ res = np.uint8(np.clip(np.int16(image) + bright_factor, 0, 255))
396
+ return res
397
+
398
+
399
+ def contrastAdjustment(image: np.ndarray, contrast_factor: int = 0):
400
+ """
401
+ 图像对比度调节,实际上调节对比度的同时对亮度也有一定的影响
402
+ :param image: 输入的图像矩阵
403
+ :param contrast_factor:亮度调节因子,可正可负,范围在[-100, +100]之间
404
+ 当contrast_factor=-100时,图像变为灰色
405
+ :return: 处理后的图片
406
+ """
407
+ contrast_factor = 1 + min(contrast_factor, 100) / 100 if contrast_factor > 0 else 1 + max(contrast_factor,
408
+ -100) / 100
409
+ image_b = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
410
+ bright_ = image_b.mean()
411
+ res = np.uint8(np.clip(contrast_factor * (np.int16(image) - bright_) + bright_, 0, 255))
412
+ return res
413
+
414
+
415
+ class CV2Bytes(object):
416
+ @staticmethod
417
+ def byte_cv2(image_byte, flags=cv2.IMREAD_COLOR) ->np.ndarray:
418
+ """
419
+ 将传入的字节流解码为图像, 当flags为 -1 的时候为无损解码
420
+ """
421
+ np_arr = np.frombuffer(image_byte,np.uint8)
422
+ image = cv2.imdecode(np_arr, flags)
423
+ return image
424
+
425
+ @staticmethod
426
+ def cv2_byte(image:np.ndarray, imageType:str=".jpg"):
427
+ """
428
+ 将传入的图像解码为字节流
429
+ """
430
+ _, image_encode = cv2.imencode(imageType, image)
431
+ image_byte = image_encode.tobytes()
432
+ return image_byte
433
+
434
+
435
+ def comb2images(src_white:np.ndarray, src_black:np.ndarray, mask:np.ndarray) -> np.ndarray:
436
+ """输入两张图片,将这两张图片根据输入的mask进行叠加处理
437
+ 这里并非简单的cv2.add(),因为也考虑了羽化部分,所以需要进行一些其他的处理操作
438
+ 核心的算法为: dst = (mask * src_white + (1 - mask) * src_black).astype(np.uint8)
439
+
440
+ Args:
441
+ src_white (np.ndarray): 第一张图像,代表的是mask中的白色区域,三通道
442
+ src_black (np.ndarray): 第二张图像,代表的是mask中的黑色区域,三通道
443
+ mask (np.ndarray): mask.输入为单通道,后续会归一化并转为三通道
444
+ 需要注意的是这三者的尺寸应该是一样的
445
+
446
+ Returns:
447
+ np.ndarray: 返回的三通道图像
448
+ """
449
+ # 函数内部不检查相关参数是否一样,使用的时候需要注意一下
450
+ mask = cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR).astype(np.float32) / 255
451
+ return (mask * src_white + (1 - mask) * src_black).astype(np.uint8)
452
+
hivisionai/hycv/vision.py ADDED
@@ -0,0 +1,446 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ from PIL import Image
3
+ import numpy as np
4
+ import functools
5
+ import time
6
+
7
+ def calTime(mark):
8
+ """
9
+ 一个输出函数时间的装饰器.
10
+ :param mark: str, 可选填, 如果填了就会在print开头加上mark标签。
11
+ """
12
+ if isinstance(mark, str):
13
+ def decorater(func):
14
+ @functools.wraps(func)
15
+ def wrapper(*args, **kw):
16
+ start_time = time.time()
17
+ return_param = func(*args, **kw)
18
+ print("[Mark-{}] {} 函数花费的时间为 {:.2f}.".format(mark, func.__name__, time.time() - start_time))
19
+ return return_param
20
+
21
+ return wrapper
22
+
23
+ return decorater
24
+ else:
25
+ func = mark
26
+
27
+ @functools.wraps(func)
28
+ def wrapper(*args, **kw):
29
+ start_time = time.time()
30
+ return_param = func(*args, **kw)
31
+ print("{} 函数花费的时间为 {:.2f}.".format(func.__name__, time.time() - start_time))
32
+ return return_param
33
+
34
+ return wrapper
35
+
36
+
37
+ def ChangeImageDPI(input_path, output_path, dpi=300):
38
+ """
39
+ 改变输入图像的dpi.
40
+ input_path: 输入图像路径
41
+ output_path: 输出图像路径
42
+ dpi:打印分辨率
43
+ """
44
+ image = Image.open(input_path)
45
+ image.save(output_path, dpi=(dpi, dpi))
46
+ # print(1)
47
+ print("Your Image's DPI have been changed. The last DPI = ({},{}) ".format(dpi,dpi))
48
+
49
+
50
+ def IDphotos_cut(x1, y1, x2, y2, img):
51
+ """
52
+ 在图片上进行滑动裁剪,输入输出为
53
+ 输入:一张图片img,和裁剪框信息(x1,x2,y1,y2)
54
+ 输出: 裁剪好的图片,然后裁剪框超出了图像范围,那么将用0矩阵补位
55
+ ------------------------------------
56
+ x:裁剪框左上的横坐标
57
+ y:裁剪框左上的纵坐标
58
+ x2:裁剪框右下的横坐标
59
+ y2:裁剪框右下的纵坐标
60
+ crop_size:裁剪框大小
61
+ img:裁剪图像(numpy.array)
62
+ output_path:裁剪图片的输出路径
63
+ ------------------------------------
64
+ """
65
+
66
+ crop_size = (y2-y1, x2-x1)
67
+ """
68
+ ------------------------------------
69
+ temp_x_1:裁剪框左边超出图像部分
70
+ temp_y_1:裁剪框上边超出图像部分
71
+ temp_x_2:裁剪框右边超出图像部分
72
+ temp_y_2:裁剪框下边超出图像部分
73
+ ------------------------------------
74
+ """
75
+ temp_x_1 = 0
76
+ temp_y_1 = 0
77
+ temp_x_2 = 0
78
+ temp_y_2 = 0
79
+
80
+ if y1 < 0:
81
+ temp_y_1 = abs(y1)
82
+ y1 = 0
83
+ if y2 > img.shape[0]:
84
+ temp_y_2 = y2
85
+ y2 = img.shape[0]
86
+ temp_y_2 = temp_y_2 - y2
87
+
88
+ if x1 < 0:
89
+ temp_x_1 = abs(x1)
90
+ x1 = 0
91
+ if x2 > img.shape[1]:
92
+ temp_x_2 = x2
93
+ x2 = img.shape[1]
94
+ temp_x_2 = temp_x_2 - x2
95
+
96
+ # 生成一张全透明背景
97
+ print("crop_size:", crop_size)
98
+ background_bgr = np.full((crop_size[0], crop_size[1]), 255, dtype=np.uint8)
99
+ background_a = np.full((crop_size[0], crop_size[1]), 0, dtype=np.uint8)
100
+ background = cv2.merge((background_bgr, background_bgr, background_bgr, background_a))
101
+
102
+ background[temp_y_1: crop_size[0] - temp_y_2, temp_x_1: crop_size[1] - temp_x_2] = img[y1:y2, x1:x2]
103
+
104
+ return background
105
+
106
+
107
+ def resize_image_esp(input_image, esp=2000):
108
+ """
109
+ 输入:
110
+ input_path:numpy图片
111
+ esp:限制的最大边长
112
+ """
113
+ # resize函数=>可以让原图压缩到最大边为esp的尺寸(不改变比例)
114
+ width = input_image.shape[0]
115
+
116
+ length = input_image.shape[1]
117
+ max_num = max(width, length)
118
+
119
+ if max_num > esp:
120
+ print("Image resizing...")
121
+ if width == max_num:
122
+ length = int((esp / width) * length)
123
+ width = esp
124
+
125
+ else:
126
+ width = int((esp / length) * width)
127
+ length = esp
128
+ print(length, width)
129
+ im_resize = cv2.resize(input_image, (length, width), interpolation=cv2.INTER_AREA)
130
+ return im_resize
131
+ else:
132
+ return input_image
133
+
134
+
135
+ def resize_image_by_min(input_image, esp=600):
136
+ """
137
+ 将图像缩放为最短边至少为esp的图像。
138
+ :param input_image: 输入图像(OpenCV矩阵)
139
+ :param esp: 缩放后的最短边长
140
+ :return: 缩放后的图像,缩放倍率
141
+ """
142
+ height, width = input_image.shape[0], input_image.shape[1]
143
+ min_border = min(height, width)
144
+ if min_border < esp:
145
+ if height >= width:
146
+ new_width = esp
147
+ new_height = height * esp // width
148
+ else:
149
+ new_height = esp
150
+ new_width = width * esp // height
151
+
152
+ return cv2.resize(input_image, (new_width, new_height), interpolation=cv2.INTER_AREA), new_height / height
153
+
154
+ else:
155
+ return input_image, 1
156
+
157
+
158
+ def detect_distance(value, crop_heigh, max=0.06, min=0.04):
159
+ """
160
+ 检测人头顶与照片顶部的距离是否在适当范���内。
161
+ 输入:与顶部的差值
162
+ 输出:(status, move_value)
163
+ status=0 不动
164
+ status=1 人脸应向上移动(裁剪框向下移动)
165
+ status-2 人脸应向下移动(裁剪框向上移动)
166
+ ---------------------------------------
167
+ value:头顶与照片顶部的距离·
168
+ crop_heigh: 裁剪框的高度
169
+ max: 距离的最大值
170
+ min: 距离的最小值
171
+ ---------------------------------------
172
+ """
173
+ value = value / crop_heigh # 头顶往上的像素占图像的比例
174
+ if min <= value <= max:
175
+ return 0, 0
176
+ elif value > max:
177
+ # 头顶往上的像素比例高于max
178
+ move_value = value - max
179
+ move_value = int(move_value * crop_heigh)
180
+ # print("上移{}".format(move_value))
181
+ return 1, move_value
182
+ else:
183
+ # 头顶往上的像素比例低于min
184
+ move_value = min - value
185
+ move_value = int(move_value * crop_heigh)
186
+ # print("下移{}".format(move_value))
187
+ return -1, move_value
188
+
189
+
190
+ def draw_picture_dots(image, dots, pen_size=10, pen_color=(0, 0, 255)):
191
+ """
192
+ 给一张照片上绘制点。
193
+ image: Opencv图像矩阵
194
+ dots: 一堆点,形如[(100,100),(150,100)]
195
+ pen_size: 画笔的大小
196
+ pen_color: 画笔的颜色
197
+ """
198
+ if isinstance(dots, dict):
199
+ dots = [v for u, v in dots.items()]
200
+ image = image.copy()
201
+ for x, y in dots:
202
+ cv2.circle(image, (int(x), int(y)), pen_size, pen_color, -1)
203
+ return image
204
+
205
+
206
+ def draw_picture_rectangle(image, bbox, pen_size=2, pen_color=(0, 0, 255)):
207
+ image = image.copy()
208
+ x1 = int(bbox[0])
209
+ y1 = int(bbox[1])
210
+ x2 = int(bbox[2])
211
+ y2 = int(bbox[3])
212
+ cv2.rectangle(image, (x1,y1), (x2, y2), pen_color, pen_size)
213
+ return image
214
+
215
+
216
+ def generate_gradient(start_color, width, height, mode="updown"):
217
+ # 定义背景颜色
218
+ end_color = (255, 255, 255) # 白色
219
+
220
+ # 创建一个空白图像
221
+ r_out = np.zeros((height, width), dtype=int)
222
+ g_out = np.zeros((height, width), dtype=int)
223
+ b_out = np.zeros((height, width), dtype=int)
224
+
225
+ if mode == "updown":
226
+ # 生成上下渐变色
227
+ for y in range(height):
228
+ r = int((y / height) * end_color[0] + ((height - y) / height) * start_color[0])
229
+ g = int((y / height) * end_color[1] + ((height - y) / height) * start_color[1])
230
+ b = int((y / height) * end_color[2] + ((height - y) / height) * start_color[2])
231
+ r_out[y, :] = r
232
+ g_out[y, :] = g
233
+ b_out[y, :] = b
234
+
235
+ else:
236
+ # 生成中心渐变色
237
+ img = np.zeros((height, width, 3))
238
+ # 定义椭圆中心和半径
239
+ center = (width//2, height//2)
240
+ end_axies = max(height, width)
241
+ # 定义渐变色
242
+ end_color = (255, 255, 255)
243
+ # 绘制椭圆
244
+ for y in range(end_axies):
245
+ axes = (end_axies - y, end_axies - y)
246
+ r = int((y / end_axies) * end_color[0] + ((end_axies - y) / end_axies) * start_color[0])
247
+ g = int((y / end_axies) * end_color[1] + ((end_axies - y) / end_axies) * start_color[1])
248
+ b = int((y / end_axies) * end_color[2] + ((end_axies - y) / end_axies) * start_color[2])
249
+
250
+ cv2.ellipse(img, center, axes, 0, 0, 360, (b, g, r), -1)
251
+ b_out, g_out, r_out = cv2.split(np.uint64(img))
252
+
253
+ return r_out, g_out, b_out
254
+
255
+
256
+ def add_background(input_image, bgr=(0, 0, 0), mode="pure_color"):
257
+ """
258
+ 本函数的功能为为透明图像加上背景。
259
+ :param input_image: numpy.array(4 channels), 透明图像
260
+ :param bgr: tuple, 合成纯色底时的BGR值
261
+ :param new_background: numpy.array(3 channels),合成自定义图像底时的背景图
262
+ :return: output: 合成好的输出图像
263
+ """
264
+ height, width = input_image.shape[0], input_image.shape[1]
265
+ b, g, r, a = cv2.split(input_image)
266
+ a_cal = a / 255
267
+ if mode == "pure_color":
268
+ # 纯色填充
269
+ b2 = np.full([height, width], bgr[0], dtype=int)
270
+ g2 = np.full([height, width], bgr[1], dtype=int)
271
+ r2 = np.full([height, width], bgr[2], dtype=int)
272
+ elif mode == "updown_gradient":
273
+ b2, g2, r2 = generate_gradient(bgr, width, height, mode="updown")
274
+ else:
275
+ b2, g2, r2 = generate_gradient(bgr, width, height, mode="center")
276
+
277
+ output = cv2.merge(((b - b2) * a_cal + b2, (g - g2) * a_cal + g2, (r - r2) * a_cal + r2))
278
+
279
+ return output
280
+
281
+
282
+ def rotate_bound(image, angle):
283
+ """
284
+ 一个旋转函数,输入一张图片和一个旋转角,可以实现不损失图像信息的旋转。
285
+ - image: numpy.array(3 channels)
286
+ - angle: 旋转角(度)
287
+ """
288
+ (h, w) = image.shape[:2]
289
+ (cX, cY) = (w / 2, h / 2)
290
+
291
+ M = cv2.getRotationMatrix2D((cX, cY), -angle, 1.0)
292
+ cos = np.abs(M[0, 0])
293
+ sin = np.abs(M[0, 1])
294
+
295
+ nW = int((h * sin) + (w * cos))
296
+ nH = int((h * cos) + (w * sin))
297
+
298
+ M[0, 2] += (nW / 2) - cX
299
+ M[1, 2] += (nH / 2) - cY
300
+
301
+ return cv2.warpAffine(image, M, (nW, nH)), cos, sin
302
+
303
+
304
+ def rotate_bound_4channels(image, a, angle):
305
+ """
306
+ 【rotate_bound_4channels的4通道版本】
307
+ 一个旋转函数,输入一张图片和一个旋转角,可以实现不损失图像信息的旋转。
308
+ Inputs:
309
+ - image: numpy.array(3 channels), 输入图像
310
+ - a: numpy.array(1 channels), 输入图像的A矩阵
311
+ - angle: 旋转角(度)
312
+ Returns:
313
+ - input_image: numpy.array(3 channels), 对image进行旋转后的图像
314
+ - result_image: numpy.array(4 channels), 旋转且透明的图像
315
+ - cos: float, 旋转角的余弦值
316
+ - sin: float, 旋转角的正弦值
317
+ """
318
+ input_image, cos, sin = rotate_bound(image, angle)
319
+ new_a, _, _ = rotate_bound(a, angle) # 对做matte旋转,以便之后merge
320
+ b, g, r = cv2.split(input_image)
321
+ result_image = cv2.merge((b, g, r, new_a)) # 得到抠图结果图的无损旋转结果
322
+
323
+ return input_image, result_image, cos, sin
324
+
325
+
326
+ def cover_image(image, background, x, y, mode=1):
327
+ """
328
+ mode = 1: directly cover
329
+ mode = 2: cv2.add
330
+ mode = 3: bgra cover
331
+ """
332
+ image = image.copy()
333
+ background = background.copy()
334
+ height1, width1 = background.shape[0], background.shape[1]
335
+ height2, width2 = image.shape[0], image.shape[1]
336
+ wuqiong_bg_y = height1 + 1
337
+ wuqiong_bg_x = width1 + 1
338
+ wuqiong_img_y = height2 + 1
339
+ wuqiong_img_x = width2 + 1
340
+
341
+ def cover_mode(image, background, imgy1=0, imgy2=-1, imgx1=0, imgx2=-1, bgy1=0, bgy2=-1, bgx1=0, bgx2=-1, mode=1):
342
+ if mode == 1:
343
+ background[bgy1:bgy2, bgx1:bgx2] = image[imgy1:imgy2, imgx1:imgx2]
344
+ elif mode == 2:
345
+ background[bgy1:bgy2, bgx1:bgx2] = cv2.add(background[bgy1:bgy2, bgx1:bgx2], image[imgy1:imgy2, imgx1:imgx2])
346
+ elif mode == 3:
347
+ b, g, r, a = cv2.split(image[imgy1:imgy2, imgx1:imgx2])
348
+ b2, g2, r2, a2 = cv2.split(background[bgy1:bgy2, bgx1:bgx2])
349
+ background[bgy1:bgy2, bgx1:bgx2, 0] = b * (a / 255) + b2 * (1 - a / 255)
350
+ background[bgy1:bgy2, bgx1:bgx2, 1] = g * (a / 255) + g2 * (1 - a / 255)
351
+ background[bgy1:bgy2, bgx1:bgx2, 2] = r * (a / 255) + r2 * (1 - a / 255)
352
+ background[bgy1:bgy2, bgx1:bgx2, 3] = cv2.add(a, a2)
353
+
354
+ return background
355
+
356
+ if x >= 0 and y >= 0:
357
+ x2 = x + width2
358
+ y2 = y + height2
359
+
360
+ if x2 <= width1 and y2 <= height1:
361
+ background = cover_mode(image, background,0,wuqiong_img_y,0,wuqiong_img_x,y,y2,x,x2,mode)
362
+
363
+ elif x2 > width1 and y2 <= height1:
364
+ # background[y:y2, x:] = image[:, :width1 - x]
365
+ background = cover_mode(image, background, 0, wuqiong_img_y, 0, width1-x, y, y2, x, wuqiong_bg_x,mode)
366
+
367
+ elif x2 <= width1 and y2 > height1:
368
+ # background[y:, x:x2] = image[:height1 - y, :]
369
+ background = cover_mode(image, background, 0, height1-y, 0, wuqiong_img_x, y, wuqiong_bg_y, x, x2,mode)
370
+ else:
371
+ # background[y:, x:] = image[:height1 - y, :width1 - x]
372
+ background = cover_mode(image, background, 0, height1-y, 0, width1-x, y, wuqiong_bg_y, x, wuqiong_bg_x,mode)
373
+
374
+ elif x < 0 and y >= 0:
375
+ x2 = x + width2
376
+ y2 = y + height2
377
+
378
+ if x2 <= width1 and y2 <= height1:
379
+ # background[y:y2, :x + width2] = image[:, abs(x):]
380
+ background = cover_mode(image, background, 0, wuqiong_img_y, abs(x), wuqiong_img_x, y, y2, 0, x+width2,mode)
381
+ elif x2 > width1 and y2 <= height1:
382
+ background = cover_mode(image, background, 0, wuqiong_img_y, abs(x), width1+abs(x), y, y2, 0, wuqiong_bg_x,mode)
383
+ elif x2 <= 0:
384
+ pass
385
+ elif x2 <= width1 and y2 > height1:
386
+ background = cover_mode(image, background, 0, height1-y, abs(x), wuqiong_img_x, y, wuqiong_bg_y, 0, x2, mode)
387
+ else:
388
+ # background[y:, :] = image[:height1 - y, abs(x):width1 + abs(x)]
389
+ background = cover_mode(image, background, 0, height1-y, abs(x), width1+abs(x), y, wuqiong_bg_y, 0, wuqiong_bg_x,mode)
390
+
391
+ elif x >= 0 and y < 0:
392
+ x2 = x + width2
393
+ y2 = y + height2
394
+ if y2 <= 0:
395
+ pass
396
+ if x2 <= width1 and y2 <= height1:
397
+ # background[:y2, x:x2] = image[abs(y):, :]
398
+ background = cover_mode(image, background, abs(y), wuqiong_img_y, 0, wuqiong_img_x, 0, y2, x, x2,mode)
399
+ elif x2 > width1 and y2 <= height1:
400
+ # background[:y2, x:] = image[abs(y):, :width1 - x]
401
+ background = cover_mode(image, background, abs(y), wuqiong_img_y, 0, width1-x, 0, y2, x, wuqiong_bg_x,mode)
402
+ elif x2 <= width1 and y2 > height1:
403
+ # background[:, x:x2] = image[abs(y):height1 + abs(y), :]
404
+ background = cover_mode(image, background, abs(y), height1+abs(y), 0, wuqiong_img_x, 0, wuqiong_bg_y, x, x2,mode)
405
+ else:
406
+ # background[:, x:] = image[abs(y):height1 + abs(y), :width1 - abs(x)]
407
+ background = cover_mode(image, background, abs(y), height1+abs(y), 0, width1-abs(x), 0, wuqiong_bg_x, x, wuqiong_bg_x,mode)
408
+
409
+ else:
410
+ x2 = x + width2
411
+ y2 = y + height2
412
+ if y2 <= 0 or x2 <= 0:
413
+ pass
414
+ if x2 <= width1 and y2 <= height1:
415
+ # background[:y2, :x2] = image[abs(y):, abs(x):]
416
+ background = cover_mode(image, background, abs(y), wuqiong_img_y, abs(x), wuqiong_img_x, 0, y2, 0, x2,mode)
417
+ elif x2 > width1 and y2 <= height1:
418
+ # background[:y2, :] = image[abs(y):, abs(x):width1 + abs(x)]
419
+ background = cover_mode(image, background, abs(y), wuqiong_img_y, abs(x), width1+abs(x), 0, y2, 0, wuqiong_bg_x,mode)
420
+ elif x2 <= width1 and y2 > height1:
421
+ # background[:, :x2] = image[abs(y):height1 + abs(y), abs(x):]
422
+ background = cover_mode(image, background, abs(y), height1+abs(y), abs(x), wuqiong_img_x, 0, wuqiong_bg_y, 0, x2,mode)
423
+ else:
424
+ # background[:, :] = image[abs(y):height1 - abs(y), abs(x):width1 + abs(x)]
425
+ background = cover_mode(image, background, abs(y), height1-abs(y), abs(x), width1+abs(x), 0, wuqiong_bg_y, 0, wuqiong_bg_x,mode)
426
+
427
+ return background
428
+
429
+
430
+ def image2bgr(input_image):
431
+ if len(input_image.shape) == 2:
432
+ input_image = input_image[:, :, None]
433
+ if input_image.shape[2] == 1:
434
+ result_image = np.repeat(input_image, 3, axis=2)
435
+ elif input_image.shape[2] == 4:
436
+ result_image = input_image[:, :, 0:3]
437
+ else:
438
+ result_image = input_image
439
+
440
+ return result_image
441
+
442
+
443
+ if __name__ == "__main__":
444
+ image = cv2.imread("./03.png", -1)
445
+ result_image = add_background(image, bgr=(255, 255, 255))
446
+ cv2.imwrite("test.jpg", result_image)
images/test.jpg ADDED
images/test2.jpg ADDED
images/test3.jpg ADDED
images/test4.jpg ADDED
src/EulerZ.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ @author: cuny
3
+ @file: EulerX.py
4
+ @time: 2022/4/1 13:54
5
+ @description:
6
+ 寻找三维z轴旋转角roll,实现:
7
+ 1. 输入一张三通道图片(四通道、单通道将默认转为三通道)
8
+ 2. 输出人脸在x轴的转角roll,顺时针为正方向,角度制
9
+ """
10
+ import cv2
11
+ import numpy as np
12
+ from math import asin, pi # -pi/2 ~ pi/2
13
+
14
+
15
+ # 获得人脸的关键点信息
16
+ def get_facePoints(src: np.ndarray, fd68):
17
+ if len(src.shape) == 2:
18
+ src = cv2.cvtColor(src, cv2.COLOR_GRAY2BGR)
19
+ elif src.shape[2] == 4:
20
+ src = cv2.cvtColor(src, cv2.COLOR_BGRA2BGR)
21
+ status, dets, landmarks, _ = fd68.facePointsEuler(src)
22
+
23
+ if status == 0:
24
+ return 0, None, None
25
+ elif status == 2:
26
+ return 2, None, None
27
+ else:
28
+ return 1, dets, np.fliplr(landmarks)
29
+
30
+
31
+ def eulerZ(landmark: np.matrix):
32
+ # 我们规定顺时针为正方向
33
+ def get_pi_2(r):
34
+ pi_2 = pi / 2.
35
+ if r >= 0.0:
36
+ return pi_2
37
+ else:
38
+ return -pi_2
39
+ orbit_points = np.array([[landmark[21, 0], landmark[21, 1]], [landmark[71, 0], landmark[71, 1]],
40
+ [landmark[25, 0], landmark[25, 1]], [landmark[67, 0], landmark[67, 1]]])
41
+ # [[cos a],[sin a],[point_x],[point_y]]
42
+ # 前面两项是有关直线与Y正半轴夹角a的三角函数,所以对于眼睛部分来讲sin a应该接近1
43
+ # "我可以认为"cv2.fitLine的y轴正方向为竖直向下,且生成的拟合直线的方向为从起点指向终点
44
+ # 与y轴的夹角为y轴夹角与直线方向的夹角,方向从y指向直线,逆时针为正方向
45
+ # 所以最后对于鼻梁的计算结果需要取个负号
46
+ orbit_line = cv2.fitLine(orbit_points, cv2.DIST_L2, 0, 0.01, 0.01)
47
+ orbit_a = asin(orbit_line[1][0])
48
+ nose_points = np.array([[landmark[55, 0], landmark[55, 1]], [landmark[69, 0], landmark[69, 1]]])
49
+ nose_line = cv2.fitLine(nose_points, cv2.DIST_L2, 0, 0.01, 0.01)
50
+ nose_a = asin(nose_line[1][0])
51
+ return (orbit_a + nose_a) * (180.0 / (2 * pi))
src/cuny_tools.py ADDED
@@ -0,0 +1,621 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+ from hivisionai.hycv.utils import get_box_pro
4
+ from hivisionai.hycv.vision import cover_image, draw_picture_dots
5
+ from math import fabs, sin, radians, cos
6
+
7
+ def opencv_rotate(img, angle):
8
+ h, w = img.shape[:2]
9
+ center = (w / 2, h / 2)
10
+ scale = 1.0
11
+ # 2.1获取M矩阵
12
+ """
13
+ M矩阵
14
+ [
15
+ cosA -sinA (1-cosA)*centerX+sinA*centerY
16
+ sinA cosA -sinA*centerX+(1-cosA)*centerY
17
+ ]
18
+ """
19
+ M = cv2.getRotationMatrix2D(center, angle, scale)
20
+ # 2.2 新的宽高,radians(angle) 把角度转为弧度 sin(弧度)
21
+ new_H = int(w * fabs(sin(radians(angle))) + h * fabs(cos(radians(angle))))
22
+ new_W = int(h * fabs(sin(radians(angle))) + w * fabs(cos(radians(angle))))
23
+ # 2.3 平移
24
+ M[0, 2] += (new_W - w) / 2
25
+ M[1, 2] += (new_H - h) / 2
26
+ rotate = cv2.warpAffine(img, M, (new_W, new_H), borderValue=(0, 0, 0))
27
+ return rotate
28
+
29
+
30
+ def transformationNeck2(image:np.ndarray, per_to_side:float=0.8)->np.ndarray:
31
+ """
32
+ 透视变换脖子函数,输入图像和四个点(矩形框)
33
+ 矩形框内的图像可能是不完整的(边角有透明区域)
34
+ 我们将根据透视变换将矩形框内的图像拉伸成和矩形框一样的形状.
35
+ 算法分为几个步骤: 选择脖子的四个点 -> 选定这四个点拉伸后的坐标 -> 透视变换 -> 覆盖原图
36
+ """
37
+ _, _, _, a = cv2.split(image) # 这应该是一个四通道的图像
38
+ height, width = a.shape
39
+ def locate_side(image_:np.ndarray, x_:int, y_max:int) -> int:
40
+ # 寻找x=y, 且 y <= y_max 上从下往上第一个非0的点,如果没找到就返回0
41
+ y_ = 0
42
+ for y_ in range(y_max - 1, -1, -1):
43
+ if image_[y_][x_] != 0:
44
+ break
45
+ return y_
46
+ def locate_width(image_:np.ndarray, y_:int, mode, left_or_right:int=None):
47
+ # 从y=y这个水平线上寻找两边的非零点
48
+ # 增加left_or_right的原因在于为下面check_jaw服务
49
+ if mode==1: # 左往右
50
+ x_ = 0
51
+ if left_or_right is None:
52
+ left_or_right = 0
53
+ for x_ in range(left_or_right, width):
54
+ if image_[y_][x_] != 0:
55
+ break
56
+ else: # 右往左
57
+ x_ = width
58
+ if left_or_right is None:
59
+ left_or_right = width - 1
60
+ for x_ in range(left_or_right, -1, -1):
61
+ if image_[y_][x_] != 0:
62
+ break
63
+ return x_
64
+ def check_jaw(image_:np.ndarray, left_, right_):
65
+ """
66
+ 检查选择的点是否与截到下巴,如果截到了,就往下平移一个单位
67
+ """
68
+ f= True # True代表没截到下巴
69
+ # [x, y]
70
+ for x_cell in range(left_[0] + 1, right_[0]):
71
+ if image_[left_[1]][x_cell] == 0:
72
+ f = False
73
+ break
74
+ if f is True:
75
+ return left_, right_
76
+ else:
77
+ y_ = left_[1] + 2
78
+ x_left_ = locate_width(image_, y_, mode=1, left_or_right=left_[0])
79
+ x_right_ = locate_width(image_, y_, mode=2, left_or_right=right_[0])
80
+ left_, right_ = check_jaw(image_, [x_left_, y_], [x_right_, y_])
81
+ return left_, right_
82
+ # 选择脖子的四个点,核心在于选择上面的两个点,这两个点的确定的位置应该是"宽出来的"两个点
83
+ _, _ ,_, a = cv2.split(image) # 这应该是一个四通道的图像
84
+ ret,a_thresh = cv2.threshold(a,127,255,cv2.THRESH_BINARY)
85
+ y_high, y_low, x_left, x_right = get_box_pro(image=image, model=1) # 直接返回矩阵信息
86
+ y_left_side = locate_side(image_=a_thresh, x_=x_left, y_max=y_low) # 左边的点的y轴坐标
87
+ y_right_side = locate_side(image_=a_thresh, x_=x_right, y_max=y_low) # 右边的点的y轴坐标
88
+ y = min(y_left_side, y_right_side) # 将两点的坐标保持相同
89
+ cell_left_above, cell_right_above = check_jaw(a_thresh,[x_left, y], [x_right, y])
90
+ x_left, x_right = cell_left_above[0], cell_right_above[0]
91
+ # 此时我们寻找到了脖子的"宽出来的"两个点,这两个点作为上面的两个点, 接下来寻找下面的两个点
92
+ if per_to_side >1:
93
+ assert ValueError("per_to_side 必须小于1!")
94
+ # 在后面的透视变换中我会把它拉成矩形, 在这里我先获取四个点的高和宽
95
+ height_ = 150 # 这个值应该是个变化的值,与拉伸的长度有关,但是现在先规定为150
96
+ width_ = x_right - x_left # 其实也就是 cell_right_above[1] - cell_left_above[1]
97
+ y = int((y_low - y)*per_to_side + y) # 定位y轴坐标
98
+ cell_left_below, cell_right_bellow = ([locate_width(a_thresh, y_=y, mode=1), y], [locate_width(a_thresh, y_=y, mode=2), y])
99
+ # 四个点全齐,开始透视变换
100
+ # 寻找透视变换后的四个点,只需要变换below的两个点即可
101
+ # cell_left_below_final, cell_right_bellow_final = ([cell_left_above[1], y_low], [cell_right_above[1], y_low])
102
+ # 需要变换的四个点为 cell_left_above, cell_right_above, cell_left_below, cell_right_bellow
103
+ rect = np.array([cell_left_above, cell_right_above, cell_left_below, cell_right_bellow],
104
+ dtype='float32')
105
+ # 变化后的坐标点
106
+ dst = np.array([[0, 0], [width_, 0], [0 , height_], [width_, height_]],
107
+ dtype='float32')
108
+ # 计算变换矩阵
109
+ M = cv2.getPerspectiveTransform(rect, dst)
110
+ warped = cv2.warpPerspective(image, M, (width_, height_))
111
+ final = cover_image(image=warped, background=image, mode=3, x=cell_left_above[0], y=cell_left_above[1])
112
+ # tmp = np.zeros(image.shape)
113
+ # final = cover_image(image=warped, background=tmp, mode=3, x=cell_left_above[0], y=cell_left_above[1])
114
+ # final = cover_image(image=image, background=final, mode=3, x=0, y=0)
115
+ return final
116
+
117
+
118
+ def transformationNeck(image:np.ndarray, cutNeckHeight:int, neckBelow:int,
119
+ toHeight:int,per_to_side:float=0.75) -> np.ndarray:
120
+ """
121
+ 脖子扩充算法, 其实需要输入的只是脖子扣出来的部分以及需要被扩充的高度/需要被扩充成的高度.
122
+ """
123
+ height, width, channels = image.shape
124
+ _, _, _, a = cv2.split(image) # 这应该是一个四通道的图像
125
+ ret, a_thresh = cv2.threshold(a, 127, 255, cv2.THRESH_BINARY) # 将透明图层二值化
126
+ def locate_width(image_:np.ndarray, y_:int, mode, left_or_right:int=None):
127
+ # 从y=y这个水平线上寻找两边的非零点
128
+ # 增加left_or_right的原因在于为下面check_jaw服务
129
+ if mode==1: # 左往右
130
+ x_ = 0
131
+ if left_or_right is None:
132
+ left_or_right = 0
133
+ for x_ in range(left_or_right, width):
134
+ if image_[y_][x_] != 0:
135
+ break
136
+ else: # 右往左
137
+ x_ = width
138
+ if left_or_right is None:
139
+ left_or_right = width - 1
140
+ for x_ in range(left_or_right, -1, -1):
141
+ if image_[y_][x_] != 0:
142
+ break
143
+ return x_
144
+ def check_jaw(image_:np.ndarray, left_, right_):
145
+ """
146
+ 检查选择的点是否与截到下巴,如果截到了,就往下平移一个单位
147
+ """
148
+ f= True # True代表没截到下巴
149
+ # [x, y]
150
+ for x_cell in range(left_[0] + 1, right_[0]):
151
+ if image_[left_[1]][x_cell] == 0:
152
+ f = False
153
+ break
154
+ if f is True:
155
+ return left_, right_
156
+ else:
157
+ y_ = left_[1] + 2
158
+ x_left_ = locate_width(image_, y_, mode=1, left_or_right=left_[0])
159
+ x_right_ = locate_width(image_, y_, mode=2, left_or_right=right_[0])
160
+ left_, right_ = check_jaw(image_, [x_left_, y_], [x_right_, y_])
161
+ return left_, right_
162
+ x_left = locate_width(image_=a_thresh, mode=1, y_=cutNeckHeight)
163
+ x_right = locate_width(image_=a_thresh, mode=2, y_=cutNeckHeight)
164
+ # 在这里我们取消了对下巴的检查,原因在于输入的imageHeight并不能改变
165
+ # cell_left_above, cell_right_above = check_jaw(a_thresh, [x_left, imageHeight], [x_right, imageHeight])
166
+ cell_left_above, cell_right_above = [x_left, cutNeckHeight], [x_right, cutNeckHeight]
167
+ toWidth = x_right - x_left # 矩形宽
168
+ # 此时我们寻找到了脖子的"宽出来的"两个点,这两个点作为上面的两个点, 接下来寻找下面的两个点
169
+ if per_to_side >1:
170
+ assert ValueError("per_to_side 必须小于1!")
171
+ y_below = int((neckBelow - cutNeckHeight) * per_to_side + cutNeckHeight) # 定位y轴坐标
172
+ cell_left_below = [locate_width(a_thresh, y_=y_below, mode=1), y_below]
173
+ cell_right_bellow = [locate_width(a_thresh, y_=y_below, mode=2), y_below]
174
+ # 四个点全齐,开始透视变换
175
+ # 需要变换的四个点为 cell_left_above, cell_right_above, cell_left_below, cell_right_bellow
176
+ rect = np.array([cell_left_above, cell_right_above, cell_left_below, cell_right_bellow],
177
+ dtype='float32')
178
+ # 变化后的坐标点
179
+ dst = np.array([[0, 0], [toWidth, 0], [0 , toHeight], [toWidth, toHeight]],
180
+ dtype='float32')
181
+ M = cv2.getPerspectiveTransform(rect, dst)
182
+ warped = cv2.warpPerspective(image, M, (toWidth, toHeight))
183
+ # 将变换后的图像覆盖到原图上
184
+ final = cover_image(image=warped, background=image, mode=3, x=cell_left_above[0], y=cell_left_above[1])
185
+ return final
186
+
187
+
188
+ def bestJunctionCheck_beta(image:np.ndarray, stepSize:int=4, if_per:bool=False):
189
+ """
190
+ 最优衔接点检测算法, 去寻找脖子的"拐点"
191
+ """
192
+ point_k = 1
193
+ _, _, _, a = cv2.split(image) # 这应该是一个四通道的图像
194
+ height, width = a.shape
195
+ ret, a_thresh = cv2.threshold(a, 127, 255, cv2.THRESH_BINARY) # 将透明图层二值化
196
+ y_high, y_low, x_left, x_right = get_box_pro(image=image, model=1) # 直接返回矩阵信息
197
+ def scan(y_:int, max_num:int=2):
198
+ num = 0
199
+ left = False
200
+ right = False
201
+ for x_ in range(width):
202
+ if a_thresh[y_][x_] != 0:
203
+ if x_ < width // 2 and left is False:
204
+ num += 1
205
+ left = True
206
+ elif x_ > width // 2 and right is False:
207
+ num += 1
208
+ right = True
209
+ return True if num >= max_num else False
210
+ def locate_neck_above():
211
+ """
212
+ 定位脖子的尖尖脚
213
+ """
214
+ for y_ in range( y_high - 2, height):
215
+ if scan(y_):
216
+ return y_, y_
217
+ y_high_left, y_high_right = locate_neck_above()
218
+ def locate_width_pro(image_:np.ndarray, y_:int, mode):
219
+ """
220
+ 这会是一个生成器,用于生成脖子两边的轮廓
221
+ x_, y_ 是启始点的坐标,每一次寻找都会让y_+1
222
+ mode==1说明是找左边的边,即,image_[y_][x_] == 0 且image_[y_][x_ + 1] !=0 时跳出;
223
+ 否则 当image_[y_][x_] != 0 时, x_ - 1; 当image_[y_][x_] == 0 且 image_[y_][x_ + 1] ==0 时x_ + 1
224
+ mode==2说明是找右边的边,即,image_[y_][x_] == 0 且image_[y_][x_ - 1] !=0 时跳出
225
+ 否则 当image_[y_][x_] != 0 时, x_ + 1; 当image_[y_][x_] == 0 且 image_[y_][x_ - 1] ==0 时x_ - 1
226
+ """
227
+ y_ += 1
228
+ if mode == 1:
229
+ x_ = 0
230
+ while 0 <= y_ < height and 0 <= x_ < width:
231
+ while image_[y_][x_] != 0 and x_ >= 0:
232
+ x_ -= 1
233
+ while image_[y_][x_] == 0 and image_[y_][x_ + 1] == 0 and x_ < width - 2:
234
+ x_ += 1
235
+ yield [y_, x_]
236
+ y_ += 1
237
+ elif mode == 2:
238
+ x_ = width-1
239
+ while 0 <= y_ < height and 0 <= x_ < width:
240
+ while image_[y_][x_] != 0 and x_ < width - 2: x_ += 1
241
+ while image_[y_][x_] == 0 and image_[y_][x_ - 1] == 0 and x_ >= 0: x_ -= 1
242
+ yield [y_, x_]
243
+ y_ += 1
244
+ yield False
245
+ def kGenerator(image_:np.ndarray, mode):
246
+ """
247
+ 导数生成器,用来生成每一个点对应的导数
248
+ """
249
+ y_ = y_high_left if mode == 1 else y_high_right
250
+ c_generator = locate_width_pro(image_=image_, y_=y_, mode=mode)
251
+ for cell in c_generator:
252
+ nc = locate_width_pro(image_=image_, y_=cell[0] + stepSize, mode=mode)
253
+ nextCell = next(nc)
254
+ if nextCell is False:
255
+ yield False, False
256
+ else:
257
+ k = (cell[1] - nextCell[1]) / stepSize
258
+ yield k, cell
259
+ def findPt(image_:np.ndarray, mode):
260
+ k_generator = kGenerator(image_=image_, mode=mode)
261
+ k, cell = next(k_generator)
262
+ k_next, cell_next = next(k_generator)
263
+ if k is False:
264
+ raise ValueError("无法找到拐点!")
265
+ while k_next is not False:
266
+ k_next, cell_next = next(k_generator)
267
+ if (k_next < - 1 / stepSize) or k_next > point_k:
268
+ break
269
+ cell = cell_next
270
+ # return int(cell[0] + stepSize / 2)
271
+ return cell[0]
272
+ # 先找左边的拐点:
273
+ pointY_left = findPt(image_=a_thresh, mode=1)
274
+ # 再找右边的拐点:
275
+ pointY_right = findPt(image_=a_thresh, mode=2)
276
+ point = (pointY_left + pointY_right) // 2
277
+ if if_per is True:
278
+ point = (pointY_left + pointY_right) // 2
279
+ return point / (y_low - y_high)
280
+ pointX_left = next(locate_width_pro(image_=a_thresh, y_= point - 1, mode=1))[1]
281
+ pointX_right = next(locate_width_pro(image_=a_thresh, y_=point- 1, mode=2))[1]
282
+ return [pointX_left, point], [pointX_right, point]
283
+
284
+
285
+ def bestJunctionCheck(image:np.ndarray, offset:int, stepSize:int=4):
286
+ """
287
+ 最优点检测算算法输入一张脖子图片(无论这张图片是否已经被二值化,我都认为没有被二值化),输出一个小数(脖子最上方与衔接点位置/脖子图像长度)
288
+ 与beta版不同的是它新增了一个阈值限定内容.
289
+ 对于脖子而言,我我们首先可以定位到上面的部分,然后根据上面的这个点向下进行遍历检测.
290
+ 与beta版类似,我们使用一个stepSize来用作斜率的检测
291
+ 但是对于遍历检测而言,与beta版不同的是,我们需要对遍历的地方进行一定的限制.
292
+ 限制的标准是,如果当前遍历的点的横坐标和起始点横坐标的插值超过了某个阈值,则认为是越界.
293
+ """
294
+ point_k = 1
295
+ _, _, _, a = cv2.split(image) # 这应该是一个四通道的图像
296
+ height, width = a.shape
297
+ ret, a_thresh = cv2.threshold(a, 127, 255, cv2.THRESH_BINARY) # 将透明图层二值化
298
+ # 直接返回脖子的位置信息, 修正系数为0, get_box_pro内部也封装了二值化,所以直接输入原图
299
+ y_high, y_low, _, _ = get_box_pro(image=image, model=1, correction_factor=0)
300
+ # 真正有用的只有上下y轴的两个值...
301
+ # 首先当然是确定起始点的位置,我们用同样的scan扫描函数进行行遍历.
302
+ def scan(y_:int, max_num:int=2):
303
+ num = 0
304
+ # 设定两个值,分别代表脖子的左边和右边
305
+ left = False
306
+ right = False
307
+ for x_ in range(width):
308
+ if a_thresh[y_][x_] != 0:
309
+ # 检测左边
310
+ if x_ < width // 2 and left is False:
311
+ num += 1
312
+ left = True
313
+ # 检测右边
314
+ elif x_ > width // 2 and right is False:
315
+ num += 1
316
+ right = True
317
+ return True if num >= max_num else False
318
+ def locate_neck_above():
319
+ """
320
+ 定位脖子的尖尖脚
321
+ """
322
+ # y_high就是脖子的最高点
323
+ for y_ in range(y_high, height):
324
+ if scan(y_):
325
+ return y_
326
+ y_start = locate_neck_above() # 得到遍历的初始高度
327
+ if y_low - y_start < stepSize: assert ValueError("脖子太小!")
328
+ # 然后获取一下初始的坐标点
329
+ x_left, x_right = 0, width
330
+ for x_left_ in range(0, width):
331
+ if a_thresh[y_start][x_left_] != 0:
332
+ x_left = x_left_
333
+ break
334
+ for x_right_ in range(width -1 , -1, -1):
335
+ if a_thresh[y_start][x_right_] != 0:
336
+ x_right = x_right_
337
+ break
338
+ # 接下来我定义两个生成器,首先是脖子轮廓(向下寻找的)生成器,每进行一次next,生成器会返回y+1的脖子轮廓点
339
+ def contoursGenerator(image_:np.ndarray, y_:int, mode):
340
+ """
341
+ 这会是一个生成器,用于生成脖子两边的轮廓
342
+ y_ 是启始点的y坐标,每一次寻找都会让y_+1
343
+ mode==1说明是找左边的边,即,image_[y_][x_] == 0 且image_[y_][x_ + 1] !=0 时跳出;
344
+ 否则 当image_[y_][x_] != 0 时, x_ - 1; 当image_[y_][x_] == 0 且 image_[y_][x_ + 1] ==0 时x_ + 1
345
+ mode==2说明是找右边的边,即,image_[y_][x_] == 0 且image_[y_][x_ - 1] !=0 时跳出
346
+ 否则 当image_[y_][x_] != 0 时, x_ + 1; 当image_[y_][x_] == 0 且 image_[y_][x_ - 1] ==0 时x_ - 1
347
+ """
348
+ y_ += 1
349
+ try:
350
+ if mode == 1:
351
+ x_ = 0
352
+ while 0 <= y_ < height and 0 <= x_ < width:
353
+ while image_[y_][x_] != 0 and x_ >= 0: x_ -= 1
354
+ # 这里其实会有bug,不过可以不管
355
+ while x_ < width and image_[y_][x_] == 0 and image_[y_][x_ + 1] == 0: x_ += 1
356
+ yield [y_, x_]
357
+ y_ += 1
358
+ elif mode == 2:
359
+ x_ = width-1
360
+ while 0 <= y_ < height and 0 <= x_ < width:
361
+ while x_ < width and image_[y_][x_] != 0: x_ += 1
362
+ while x_ >= 0 and image_[y_][x_] == 0 and image_[y_][x_ - 1] == 0: x_ -= 1
363
+ yield [y_, x_]
364
+ y_ += 1
365
+ # 当处理失败则返回False
366
+ except IndexError:
367
+ yield False
368
+ # 然后是斜率生成器,这个生成器依赖子轮廓生成器,每一次生成轮廓后会计算斜率,另一个点的选取和stepSize有关
369
+ def kGenerator(image_: np.ndarray, mode):
370
+ """
371
+ 导数生成器,用来生成每一个点对应的导数
372
+ """
373
+ y_ = y_start
374
+ # 对起始点建立一个生成器, mode=1时是左边轮廓,mode=2时是右边轮廓
375
+ c_generator = contoursGenerator(image_=image_, y_=y_, mode=mode)
376
+ for cell in c_generator:
377
+ # 寻找距离当前cell距离为stepSize的轮廓点
378
+ kc = contoursGenerator(image_=image_, y_=cell[0] + stepSize, mode=mode)
379
+ kCell = next(kc)
380
+ if kCell is False:
381
+ # 寻找失败
382
+ yield False, False
383
+ else:
384
+ # 寻找成功,返回当坐标点和斜率值
385
+ # 对于左边而言,斜率必然是前一个点的坐标减去后一个点的坐标
386
+ # 对于右边而言,斜率必然是后一个点的坐标减去前一个点的坐标
387
+ k = (cell[1] - kCell[1]) / stepSize if mode == 1 else (kCell[1] - cell[1]) / stepSize
388
+ yield k, cell
389
+ # 接着开始写寻找算法,需要注意的是我们是分两边选择的
390
+ def findPt(image_:np.ndarray, mode):
391
+ x_base = x_left if mode == 1 else x_right
392
+ k_generator = kGenerator(image_=image_, mode=mode)
393
+ k, cell = k_generator.__next__()
394
+ if k is False:
395
+ raise ValueError("无法找到拐点!")
396
+ k_next, cell_next = k_generator.__next__()
397
+ while k_next is not False:
398
+ cell = cell_next
399
+ if cell[1] > x_base and mode == 2:
400
+ x_base = cell[1]
401
+ elif cell[1] < x_base and mode == 1:
402
+ x_base = cell[1]
403
+ # 跳出循环的方式一:斜率超过了某个值
404
+ if k_next > point_k:
405
+ print("K out")
406
+ break
407
+ # 跳出循环的方式二:超出阈值
408
+ elif abs(cell[1] - x_base) > offset:
409
+ print("O out")
410
+ break
411
+ k_next, cell_next = k_generator.__next__()
412
+ if abs(cell[1] - x_base) > offset:
413
+ cell[0] = cell[0] - offset - 1
414
+ return cell[0]
415
+ # 先找左边的拐点:
416
+ pointY_left = findPt(image_=a_thresh, mode=1)
417
+ # 再找右边的拐点:
418
+ pointY_right = findPt(image_=a_thresh, mode=2)
419
+ point = min(pointY_right, pointY_left)
420
+ per = (point - y_high) / (y_low - y_high)
421
+ # pointX_left = next(contoursGenerator(image_=a_thresh, y_= point- 1, mode=1))[1]
422
+ # pointX_right = next(contoursGenerator(image_=a_thresh, y_=point - 1, mode=2))[1]
423
+ # return [pointX_left, point], [pointX_right, point]
424
+ return per
425
+
426
+
427
+ def checkSharpCorner(image:np.ndarray):
428
+ _, _, _, a = cv2.split(image) # 这应该是一个四通道的图像
429
+ height, width = a.shape
430
+ ret, a_thresh = cv2.threshold(a, 127, 255, cv2.THRESH_BINARY) # 将透明图层二值化
431
+ # 直接返回脖子的位置信息, 修正系数为0, get_box_pro内部也封装了二值化,所以直接输入原图
432
+ y_high, y_low, _, _ = get_box_pro(image=image, model=1, correction_factor=0)
433
+ def scan(y_:int, max_num:int=2):
434
+ num = 0
435
+ # 设定两个值,分别代表脖子的左边和右边
436
+ left = False
437
+ right = False
438
+ for x_ in range(width):
439
+ if a_thresh[y_][x_] != 0:
440
+ # 检测左边
441
+ if x_ < width // 2 and left is False:
442
+ num += 1
443
+ left = True
444
+ # 检测右边
445
+ elif x_ > width // 2 and right is False:
446
+ num += 1
447
+ right = True
448
+ return True if num >= max_num else False
449
+ def locate_neck_above():
450
+ """
451
+ 定位脖子的尖尖脚
452
+ """
453
+ # y_high就是脖子的最高点
454
+ for y_ in range(y_high, height):
455
+ if scan(y_):
456
+ return y_
457
+ y_start = locate_neck_above()
458
+ return y_start
459
+
460
+
461
+ def checkJaw(image:np.ndarray, y_start:int):
462
+ # 寻找"马鞍点"
463
+ _, _, _, a = cv2.split(image) # 这应该是一个四通道的图像
464
+ height, width = a.shape
465
+ ret, a_thresh = cv2.threshold(a, 127, 255, cv2.THRESH_BINARY) # 将透明图层二值化
466
+ if width <=1: raise TypeError("图像太小!")
467
+ x_left, x_right = 0, width - 1
468
+ for x_left in range(width):
469
+ if a_thresh[y_start][x_left] != 0:
470
+ while a_thresh[y_start][x_left] != 0: x_left += 1
471
+ break
472
+ for x_right in range(width-1, -1, -1):
473
+ if a_thresh[y_start][x_right] != 0:
474
+ while a_thresh[y_start][x_right] != 0: x_right -= 1
475
+ break
476
+ point_list_y = []
477
+ point_list_x = []
478
+ for x in range(x_left, x_right):
479
+ y = y_start
480
+ while a_thresh[y][x] == 0: y += 1
481
+ point_list_y.append(y)
482
+ point_list_x.append(x)
483
+ y = max(point_list_y)
484
+ x = point_list_x[point_list_y.index(y)]
485
+ return x, y
486
+
487
+
488
+ def checkHairLOrR(cloth_image_input_cut,
489
+ input_a,
490
+ neck_a,
491
+ cloth_image_input_top_y,
492
+ cutbar_top=0.4,
493
+ cutbar_bottom=0.5,
494
+ threshold=0.3):
495
+ """
496
+ 本函数用于检测衣服是否被头发遮挡,当前只考虑左右是否被遮挡,即"一刀切"
497
+ 返回int
498
+ 0代表没有被遮挡
499
+ 1代表左边被遮挡
500
+ 2代表右边被遮挡
501
+ 3代表全被遮挡了
502
+ 约定,输入的图像是一张灰度图,且被二值化过.
503
+ """
504
+ def per_darkPoint(img:np.ndarray) -> int:
505
+ """
506
+ 用于遍历相加图像上的黑点.
507
+ 然后返回黑点数/图像面积
508
+ """
509
+ h, w = img.shape
510
+ sum_darkPoint = 0
511
+ for y in range(h):
512
+ for x in range(w):
513
+ if img[y][x] == 0:
514
+ sum_darkPoint += 1
515
+ return sum_darkPoint / (h * w)
516
+
517
+ if threshold < 0 or threshold > 1: raise TypeError("阈值设置必须在0和1之间!")
518
+
519
+ # 裁出cloth_image_input_cut按高度40%~50%的区域-cloth_image_input_cutbar,并转换为A矩阵,做二值化
520
+ cloth_image_input_height = cloth_image_input_cut.shape[0]
521
+ _, _, _, cloth_image_input_cutbar = cv2.split(cloth_image_input_cut[
522
+ int(cloth_image_input_height * cutbar_top):int(
523
+ cloth_image_input_height * cutbar_bottom), :])
524
+ _, cloth_image_input_cutbar = cv2.threshold(cloth_image_input_cutbar, 127, 255, cv2.THRESH_BINARY)
525
+
526
+ # 裁出input_image、neck_image的A矩阵的对应区域,���做二值化
527
+ input_a_cutbar = input_a[cloth_image_input_top_y + int(cloth_image_input_height * cutbar_top):
528
+ cloth_image_input_top_y + int(cloth_image_input_height * cutbar_bottom), :]
529
+ _, input_a_cutbar = cv2.threshold(input_a_cutbar, 127, 255, cv2.THRESH_BINARY)
530
+ neck_a_cutbar = neck_a[cloth_image_input_top_y + int(cloth_image_input_height * cutbar_top):
531
+ cloth_image_input_top_y + int(cloth_image_input_height * cutbar_bottom), :]
532
+ _, neck_a_cutbar = cv2.threshold(neck_a_cutbar, 50, 255, cv2.THRESH_BINARY)
533
+
534
+ # 将三个cutbar合到一起-result_a_cutbar
535
+ input_a_cutbar = np.uint8(255 - input_a_cutbar)
536
+ result_a_cutbar = cv2.add(input_a_cutbar, cloth_image_input_cutbar)
537
+ result_a_cutbar = cv2.add(result_a_cutbar, neck_a_cutbar)
538
+
539
+ if_mask = 0
540
+ # 我们将图像 一刀切,分为左边和右边
541
+ height, width = result_a_cutbar.shape # 一通道图像
542
+ left_image = result_a_cutbar[:, :width//2]
543
+ right_image = result_a_cutbar[:, width//2:]
544
+ if per_darkPoint(left_image) > threshold:
545
+ if_mask = 1
546
+ if per_darkPoint(right_image) > threshold:
547
+ if_mask = 3 if if_mask == 1 else 2
548
+ return if_mask
549
+
550
+
551
+ def find_black(image):
552
+ """
553
+ 找黑色点函数,遇到输入矩阵中的第一个黑点,返回它的y值
554
+ """
555
+ height, width = image.shape[0], image.shape[1]
556
+ for i in range(height):
557
+ for j in range(width):
558
+ if image[i, j] < 127:
559
+ return i
560
+ return None
561
+
562
+
563
+ def convert_black_array(image):
564
+ height, width = image.shape[0], image.shape[1]
565
+ mask = np.zeros([height, width])
566
+ for j in range(width):
567
+ for i in range(height):
568
+ if image[i, j] > 127:
569
+ mask[i:, j] = 1
570
+ break
571
+ return mask
572
+
573
+
574
+ def checkLongHair(neck_image, head_bottom_y, neck_top_y):
575
+ """
576
+ 长发检测函数,输入为head/neck图像,通过下巴是否为最低点,来判断是否为长发
577
+ :return 0 : 短发
578
+ :return 1 : 长发
579
+ """
580
+ jaw_y = neck_top_y + checkJaw(neck_image, y_start=checkSharpCorner(neck_image))[1]
581
+ if jaw_y >= head_bottom_y-3:
582
+ return 0
583
+ else:
584
+ return 1
585
+
586
+
587
+ def checkLongHair2(head_bottom_y, cloth_top_y):
588
+ if head_bottom_y > cloth_top_y+10:
589
+ return 1
590
+ else:
591
+ return 0
592
+
593
+
594
+ if __name__ == "__main__":
595
+ for i in range(1, 8):
596
+ img = cv2.imread(f"./neck_temp/neck_image{i}.png", cv2.IMREAD_UNCHANGED)
597
+ # new = transformationNeck(image=img, cutNeckHeight=419,neckBelow=472, toHeight=150)
598
+ # point_list = bestJunctionCheck(img, offset=5, stepSize=3)
599
+ # per = bestJunctionCheck(img, offset=5, stepSize=3)
600
+ # # 返回一个小数的形式, 接下来我将它处理为两个点
601
+ point_list = []
602
+ # y_high_, y_low_, _, _ = get_box_pro(image=img, model=1, conreection_factor=0)
603
+ # _y = y_high_ + int((y_low_ - y_high_) * per)
604
+ # _, _, _, a_ = cv2.split(img) # 这应该是一个四通道的图像
605
+ # h, w = a_.shape
606
+ # r, a_t = cv2.threshold(a_, 127, 255, cv2.THRESH_BINARY) # 将透明图层二值化
607
+ # _x = 0
608
+ # for _x in range(w):
609
+ # if a_t[_y][_x] != 0:
610
+ # break
611
+ # point_list.append([_x, _y])
612
+ # for _x in range(w - 1, -1, -1):
613
+ # if a_t[_y][_x] != 0:
614
+ # break
615
+ # point_list.append([_x, _y])
616
+ y = checkSharpCorner(img)
617
+ point = checkJaw(image=img, y_start=y)
618
+ point_list.append(point)
619
+ new = draw_picture_dots(img, point_list, pen_size=2)
620
+ cv2.imshow(f"{i}", new)
621
+ cv2.waitKey(0)
src/error.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ @author: cuny
3
+ @file: error.py
4
+ @time: 2022/4/7 15:50
5
+ @description:
6
+ 定义证件照制作的错误类
7
+ """
8
+ from hivisionai.hyService.error import ProcessError
9
+
10
+
11
+ class IDError(ProcessError):
12
+ def __init__(self, err, diary=None, face_num=-1, status_id: str = "1500"):
13
+ """
14
+ 用于报错
15
+ Args:
16
+ err: 错误描述
17
+ diary: 函数运行日志,默认为None
18
+ face_num: 告诉此时识别到的人像个数,如果为-1则说明为未知错误
19
+ """
20
+ super().__init__(err)
21
+ if diary is None:
22
+ diary = {}
23
+ self.err = err
24
+ self.diary = diary
25
+ self.face_num = face_num
26
+ self.status_id = status_id
27
+
src/face_judgement_align.py ADDED
@@ -0,0 +1,576 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ import cv2
3
+ import numpy as np
4
+ from hivisionai.hycv.face_tools import face_detect_mtcnn
5
+ from hivisionai.hycv.utils import get_box_pro
6
+ from hivisionai.hycv.vision import resize_image_esp, IDphotos_cut, add_background, calTime, resize_image_by_min, \
7
+ rotate_bound_4channels
8
+ import onnxruntime
9
+ from src.error import IDError
10
+ from src.imageTransform import standard_photo_resize, hollowOutFix, get_modnet_matting, draw_picture_dots, detect_distance
11
+ from src.layoutCreate import generate_layout_photo
12
+ from src.move_image import move
13
+
14
+ testImages = []
15
+
16
+
17
+ class LinearFunction_TwoDots(object):
18
+ """
19
+ 通过两个坐标点构建线性函数
20
+ """
21
+
22
+ def __init__(self, dot1, dot2):
23
+ self.d1 = dot1
24
+ self.d2 = dot2
25
+ self.mode = "normal"
26
+ if self.d2.x != self.d1.x:
27
+ self.k = (self.d2.y - self.d1.y) / max((self.d2.x - self.d1.x), 1)
28
+ self.b = self.d2.y - self.k * self.d2.x
29
+ else:
30
+ self.mode = "x=1"
31
+
32
+ def forward(self, input_, mode="x"):
33
+ if mode == "x":
34
+ if self.mode == "normal":
35
+ return self.k * input_ + self.b
36
+ else:
37
+ return 0
38
+ elif mode == "y":
39
+ if self.mode == "normal":
40
+ return (input_ - self.b) / self.k
41
+ else:
42
+ return self.d1.x
43
+
44
+ def forward_x(self, x):
45
+ if self.mode == "normal":
46
+ return self.k * x + self.b
47
+ else:
48
+ return 0
49
+
50
+ def forward_y(self, y):
51
+ if self.mode == "normal":
52
+ return (y - self.b) / self.k
53
+ else:
54
+ return self.d1.x
55
+
56
+
57
+ class Coordinate(object):
58
+ def __init__(self, x, y):
59
+ self.x = x
60
+ self.y = y
61
+
62
+ def __str__(self):
63
+ return "({}, {})".format(self.x, self.y)
64
+
65
+
66
+ @calTime
67
+ def face_number_and_angle_detection(input_image):
68
+ """
69
+ 本函数的功能是利用机器学习算法计算图像中人脸的数目与关键点,并通过关键点信息来计算人脸在平面上的旋转角度。
70
+ 当前人脸数目!=1时,将raise一个错误信息并终止全部程序。
71
+ Args:
72
+ input_image: numpy.array(3 channels),用户上传的原图(经过了一些简单的resize)
73
+
74
+ Returns:
75
+ - dets: list,人脸定位信息(x1, y1, x2, y2)
76
+ - rotation: int,旋转角度,正数代表逆时针偏离,负数代表顺时针偏离
77
+ - landmark: list,人脸关键点信息
78
+ """
79
+
80
+ # face++人脸检测
81
+ # input_image_bytes = CV2Bytes.cv2_byte(input_image, ".jpg")
82
+ # face_num, face_rectangle, landmarks, headpose = megvii_face_detector(input_image_bytes)
83
+ # print(face_rectangle)
84
+
85
+ faces, landmarks = face_detect_mtcnn(input_image)
86
+ face_num = len(faces)
87
+
88
+ # 排除不合人脸数目要求(必须是1)的照片
89
+ if face_num == 0 or face_num >= 2:
90
+ if face_num == 0:
91
+ status_id_ = "1101"
92
+ else:
93
+ status_id_ = "1102"
94
+ raise IDError(f"人脸检测出错!检测出了{face_num}张人脸", face_num=face_num, status_id=status_id_)
95
+
96
+ # 获得人脸定位坐标
97
+ face_rectangle = []
98
+ for iter, (x1, y1, x2, y2, _) in enumerate(faces):
99
+ x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2)
100
+ face_rectangle.append({'top': x1, 'left': y1, 'width': x2 - x1, 'height': y2 - y1})
101
+
102
+ # 获取人脸定位坐标与关键点信息
103
+ dets = face_rectangle[0]
104
+ # landmark = landmarks[0]
105
+ #
106
+ # # 人脸旋转角度计算
107
+ # rotation = eulerZ(landmark)
108
+ # return dets, rotation, landmark
109
+ return dets
110
+
111
+ @calTime
112
+ def image_matting(input_image, params):
113
+ """
114
+ 本函数的功能为全局人像抠图。
115
+ Args:
116
+ - input_image: numpy.array(3 channels),用户原图
117
+
118
+ Returns:
119
+ - origin_png_image: numpy.array(4 channels), 抠好的图
120
+ """
121
+
122
+ print("抠图采用本地模型")
123
+ origin_png_image = get_modnet_matting(input_image, sess=params["modnet"]["human_sess"])
124
+
125
+ origin_png_image = hollowOutFix(origin_png_image) # 抠图洞洞修补
126
+ return origin_png_image
127
+
128
+
129
+ @calTime
130
+ def rotation_ajust(input_image, rotation, a, IS_DEBUG=False):
131
+ """
132
+ 本函数的功能是根据旋转角对原图进行无损旋转,并返回结果图与附带信息。
133
+ Args:
134
+ - input_image: numpy.array(3 channels), 用户上传的原图(经过了一些简单的resize、美颜)
135
+ - rotation: float, 人的五官偏离"端正"形态的旋转角
136
+ - a: numpy.array(1 channel), matting图的matte
137
+ - IS_DEBUG: DEBUG模式开关
138
+
139
+ Returns:
140
+ - result_jpg_image: numpy.array(3 channels), 原图旋转的结果图
141
+ - result_png_image: numpy.array(4 channels), matting图旋转的结果图
142
+ - L1: CLassObject, 根据旋转点连线所构造函数
143
+ - L2: ClassObject, 根据旋转点连线所构造函数
144
+ - dotL3: ClassObject, 一个特殊裁切点的坐标
145
+ - clockwise: int, 表示照片是顺时针偏离还是逆时针偏离
146
+ - drawed_dots_image: numpy.array(3 channels), 在result_jpg_image上标定了4个旋转点的结果图,用于DEBUG模式
147
+ """
148
+
149
+ # Step1. 数据准备
150
+ rotation = -1 * rotation # rotation为正数->原图顺时针偏离,为负数->逆时针偏离
151
+ h, w = input_image.copy().shape[:2]
152
+
153
+ # Step2. 无损旋转
154
+ result_jpg_image, result_png_image, cos, sin = rotate_bound_4channels(input_image, a, rotation)
155
+
156
+ # Step3. 附带信息计算
157
+ nh, nw = result_jpg_image.shape[:2] # 旋转后的新的长宽
158
+ clockwise = -1 if rotation < 0 else 1 # clockwise代表时针,即1为顺时针,-1为逆时针
159
+ # 如果逆时针偏离:
160
+ if rotation < 0:
161
+ p1 = Coordinate(0, int(w * sin))
162
+ p2 = Coordinate(int(w * cos), 0)
163
+ p3 = Coordinate(nw, int(h * cos))
164
+ p4 = Coordinate(int(h * sin), nh)
165
+ L1 = LinearFunction_TwoDots(p1, p4)
166
+ L2 = LinearFunction_TwoDots(p4, p3)
167
+ dotL3 = Coordinate(int(0.25 * p2.x + 0.75 * p3.x), int(0.25 * p2.y + 0.75 * p3.y))
168
+
169
+ # 如果顺时针偏离:
170
+ else:
171
+ p1 = Coordinate(int(h * sin), 0)
172
+ p2 = Coordinate(nw, int(w * sin))
173
+ p3 = Coordinate(int(w * cos), nh)
174
+ p4 = Coordinate(0, int(h * cos))
175
+ L1 = LinearFunction_TwoDots(p4, p3)
176
+ L2 = LinearFunction_TwoDots(p3, p2)
177
+ dotL3 = Coordinate(int(0.75 * p4.x + 0.25 * p1.x), int(0.75 * p4.y + 0.25 * p1.y))
178
+
179
+ # Step4. 根据附带信息进行图像绘制(4个旋转点),便于DEBUG模式验证
180
+ drawed_dots_image = draw_picture_dots(result_jpg_image, [(p1.x, p1.y), (p2.x, p2.y), (p3.x, p3.y),
181
+ (p4.x, p4.y), (dotL3.x, dotL3.y)])
182
+ if IS_DEBUG:
183
+ testImages.append(["drawed_dots_image", drawed_dots_image])
184
+
185
+ return result_jpg_image, result_png_image, L1, L2, dotL3, clockwise, drawed_dots_image
186
+
187
+
188
+ @calTime
189
+ def face_number_detection_mtcnn(input_image):
190
+ """
191
+ 本函数的功能是对旋转矫正的结果图进行基于MTCNN模型的人脸检测。
192
+ Args:
193
+ - input_image: numpy.array(3 channels), 旋转矫正(rotation_adjust)的3通道结果图
194
+
195
+ Returns:
196
+ - faces: list, 人脸检测的结果,包含人脸位置信息
197
+ """
198
+ # 如果图像的长或宽>1500px,则对图像进行1/2的resize再做MTCNN人脸检测,以加快处理速度
199
+ if max(input_image.shape[0], input_image.shape[1]) >= 1500:
200
+ input_image_resize = cv2.resize(input_image,
201
+ (input_image.shape[1] // 2, input_image.shape[0] // 2),
202
+ interpolation=cv2.INTER_AREA)
203
+ faces, _ = face_detect_mtcnn(input_image_resize, filter=True) # MTCNN人脸检测
204
+ # 如果缩放后图像的MTCNN人脸数目检测结果等于1->两次人脸检测结果没有偏差,则对定位数据x2
205
+ if len(faces) == 1:
206
+ for item, param in enumerate(faces[0]):
207
+ faces[0][item] = param * 2
208
+ # 如果两次人脸检测结果有偏差,则默认缩放后图像的MTCNN检测存在误差,则将原图输入再做一次MTCNN(保险措施)
209
+ else:
210
+ faces, _ = face_detect_mtcnn(input_image, filter=True)
211
+ # 如果图像的长或宽<1500px,则直接进行MTCNN检测
212
+ else:
213
+ faces, _ = face_detect_mtcnn(input_image, filter=True)
214
+
215
+ return faces
216
+
217
+
218
+ @calTime
219
+ def cutting_rect_pan(x1, y1, x2, y2, width, height, L1, L2, L3, clockwise, standard_size):
220
+ """
221
+ 本函数的功能是对旋转矫正结果图的裁剪框进行修正 ———— 解决"旋转三角形"现象。
222
+ Args:
223
+ - x1: int, 裁剪框左上角的横坐标
224
+ - y1: int, 裁剪框左上角的纵坐标
225
+ - x2: int, 裁剪框右下角的横坐标
226
+ - y2: int, 裁剪框右下角的纵坐标
227
+ - width: int, 待裁剪图的宽度
228
+ - height:int, 待裁剪图的高度
229
+ - L1: CLassObject, 根据旋转点连线所构造函数
230
+ - L2: CLassObject, 根据旋转点连线所构造函数
231
+ - L3: ClassObject, 一个特殊裁切点的坐标
232
+ - clockwise: int, 旋转时针状态
233
+ - standard_size: tuple, 标准照的尺寸
234
+
235
+ Returns:
236
+ - x1: int, 新的裁剪框左上角的横坐标
237
+ - y1: int, 新的裁剪框左上角的纵坐标
238
+ - x2: int, 新的裁剪框右下角的横坐标
239
+ - y2: int, 新的裁剪框右下角的纵坐标
240
+ - x_bias: int, 裁剪框横坐标方向上的计算偏置量
241
+ - y_bias: int, 裁剪框纵坐标方向上的计算偏置量
242
+ """
243
+ # 用于计算的裁剪框坐标x1_cal,x2_cal,y1_cal,y2_cal(如果裁剪框超出了图像范围,则缩小直至在范围内)
244
+ x1_std = x1 if x1 > 0 else 0
245
+ x2_std = x2 if x2 < width else width
246
+ # y1_std = y1 if y1 > 0 else 0
247
+ y2_std = y2 if y2 < height else height
248
+
249
+ # 初始化x和y的计算偏置项x_bias和y_bias
250
+ x_bias = 0
251
+ y_bias = 0
252
+
253
+ # 如果顺时针偏转
254
+ if clockwise == 1:
255
+ if y2 > L1.forward_x(x1_std):
256
+ y_bias = int(-(y2_std - L1.forward_x(x1_std)))
257
+ if y2 > L2.forward_x(x2_std):
258
+ x_bias = int(-(x2_std - L2.forward_y(y2_std)))
259
+ x2 = x2_std + x_bias
260
+ if x1 < L3.x:
261
+ x1 = L3.x
262
+ # 如果逆时针偏转
263
+ else:
264
+ if y2 > L1.forward_x(x1_std):
265
+ x_bias = int(L1.forward_y(y2_std) - x1_std)
266
+ if y2 > L2.forward_x(x2_std):
267
+ y_bias = int(-(y2_std - L2.forward_x(x2_std)))
268
+ x1 = x1_std + x_bias
269
+ if x2 > L3.x:
270
+ x2 = L3.x
271
+
272
+ # 计算裁剪框的y的变化
273
+ y2 = int(y2_std + y_bias)
274
+ new_cut_width = x2 - x1
275
+ new_cut_height = int(new_cut_width / standard_size[1] * standard_size[0])
276
+ y1 = y2 - new_cut_height
277
+
278
+ return x1, y1, x2, y2, x_bias, y_bias
279
+
280
+
281
+ @calTime
282
+ def idphoto_cutting(faces, head_measure_ratio, standard_size, head_height_ratio, origin_png_image, origin_png_image_pre,
283
+ rotation_params, align=False, IS_DEBUG=False, top_distance_max=0.12, top_distance_min=0.10):
284
+ """
285
+ 本函数的功能为进行证件照的自适应裁剪,自适应依据Setting.json的控制参数,以及输入图像的自身情况。
286
+ Args:
287
+ - faces: list, 人脸位置信息
288
+ - head_measure_ratio: float, 人脸面积与全图面积的期望比值
289
+ - standard_size: tuple, 标准照尺寸, 如(413, 295)
290
+ - head_height_ratio: float, 人脸中心处在全图高度的比例期望值
291
+ - origin_png_image: numpy.array(4 channels), 经过一系列转换后的用户输入图
292
+ - origin_png_image_pre:numpy.array(4 channels),经过一系列转换(但没有做旋转矫正)的用户输入图
293
+ - rotation_params:旋转参数字典
294
+ - L1: classObject, 来自rotation_ajust的L1线性函数
295
+ - L2: classObject, 来自rotation_ajust的L2线性函数
296
+ - L3: classObject, 来自rotation_ajust的dotL3点
297
+ - clockwise: int, (顺/逆)时针偏差
298
+ - drawed_image: numpy.array, 红点标定4个旋转点的图像
299
+ - align: bool, 是否图像做过旋转矫正
300
+ - IS_DEBUG: DEBUG模式开关
301
+ - top_distance_max: float, 头距离顶部的最大比例
302
+ - top_distance_min: float, 头距离顶部的最小比例
303
+
304
+ Returns:
305
+ - result_image_hd: numpy.array(4 channels), 高清照
306
+ - result_image_standard: numpy.array(4 channels), 标准照
307
+ - clothing_params: json, 换装配置参数,便于后续换装功能的使用
308
+
309
+ """
310
+ # Step0. 旋转参数准备
311
+ L1 = rotation_params["L1"]
312
+ L2 = rotation_params["L2"]
313
+ L3 = rotation_params["L3"]
314
+ clockwise = rotation_params["clockwise"]
315
+ drawed_image = rotation_params["drawed_image"]
316
+
317
+ # Step1. 准备人脸参数
318
+ face_rect = faces[0]
319
+ x, y = face_rect[0], face_rect[1]
320
+ w, h = face_rect[2] - x + 1, face_rect[3] - y + 1
321
+ height, width = origin_png_image.shape[:2]
322
+ width_height_ratio = standard_size[0] / standard_size[1] # 高宽比
323
+
324
+ # Step2. 计算高级参数
325
+ face_center = (x + w / 2, y + h / 2) # 面部中心坐标
326
+ face_measure = w * h # 面部面积
327
+ crop_measure = face_measure / head_measure_ratio # 裁剪框面积:为面部面积的5倍
328
+ resize_ratio = crop_measure / (standard_size[0] * standard_size[1]) # 裁剪框缩放率
329
+ resize_ratio_single = math.sqrt(resize_ratio) # 长和宽的缩放率(resize_ratio的开方)
330
+ crop_size = (int(standard_size[0] * resize_ratio_single),
331
+ int(standard_size[1] * resize_ratio_single)) # 裁剪框大小
332
+
333
+ # 裁剪框的定位信息
334
+ x1 = int(face_center[0] - crop_size[1] / 2)
335
+ y1 = int(face_center[1] - crop_size[0] * head_height_ratio)
336
+ y2 = y1 + crop_size[0]
337
+ x2 = x1 + crop_size[1]
338
+
339
+ # Step3. 对于旋转矫正图片的裁切处理
340
+ # if align:
341
+ # y_top_pre, _, _, _ = get_box_pro(origin_png_image.astype(np.uint8), model=2,
342
+ # correction_factor=0) # 获取matting结果图的顶距
343
+ # # 裁剪参数重新计算,目标是以最小的图像损失来消除"旋转三角形"
344
+ # x1, y1, x2, y2, x_bias, y_bias = cutting_rect_pan(x1, y1, x2, y2, width, height, L1, L2, L3, clockwise,
345
+ # standard_size)
346
+ # # 这里设定一个拒绝判定条件,如果裁剪框切进了人脸检测框的话,就不进行旋转
347
+ # if y1 > y_top_pre:
348
+ # y2 = y2 - (y1 - y_top_pre)
349
+ # y1 = y_top_pre
350
+ # # 如何遇到裁剪到人脸的情况,则转为不旋转裁切
351
+ # if x1 > x or x2 < (x + w) or y1 > y or y2 < (y + h):
352
+ # return idphoto_cutting(faces, head_measure_ratio, standard_size, head_height_ratio, origin_png_image_pre,
353
+ # origin_png_image_pre, rotation_params, align=False, IS_DEBUG=False)
354
+ #
355
+ # if y_bias != 0:
356
+ # origin_png_image = origin_png_image[:y2, :]
357
+ # if x_bias > 0: # 逆时针
358
+ # origin_png_image = origin_png_image[:, x1:]
359
+ # if drawed_image is not None and IS_DEBUG:
360
+ # drawed_x = x1
361
+ # x = x - x1
362
+ # x2 = x2 - x1
363
+ # x1 = 0
364
+ # else: # 顺时针
365
+ # origin_png_image = origin_png_image[:, :x2]
366
+ #
367
+ # if drawed_image is not None and IS_DEBUG:
368
+ # drawed_x = drawed_x if x_bias > 0 else 0
369
+ # drawed_image = draw_picture_dots(drawed_image, [(x1 + drawed_x, y1), (x1 + drawed_x, y2),
370
+ # (x2 + drawed_x, y1), (x2 + drawed_x, y2)],
371
+ # pen_color=(255, 0, 0))
372
+ # testImages.append(["drawed_image", drawed_image])
373
+
374
+ # Step4. 对照片的第一轮裁剪
375
+ cut_image = IDphotos_cut(x1, y1, x2, y2, origin_png_image)
376
+ cut_image = cv2.resize(cut_image, (crop_size[1], crop_size[0]))
377
+ y_top, y_bottom, x_left, x_right = get_box_pro(cut_image.astype(np.uint8), model=2,
378
+ correction_factor=0) # 得到cut_image中人像的上下左右距离信息
379
+ if IS_DEBUG:
380
+ testImages.append(["firstCut", cut_image])
381
+
382
+ # Step5. 判定cut_image中的人像是否处于合理的位置,若不合理,则处理数据以便之后调整位置
383
+ # 检测人像与裁剪框左边或右边是否存在空隙
384
+ if x_left > 0 or x_right > 0:
385
+ status_left_right = 1
386
+ cut_value_top = int(((x_left + x_right) * width_height_ratio) / 2) # 减去左右,为了保持比例,上下也要相应减少cut_value_top
387
+ else:
388
+ status_left_right = 0
389
+ cut_value_top = 0
390
+
391
+ """
392
+ 检测人头顶与照片的顶部是否在合适的距离内:
393
+ - status==0: 距离合适, 无需移动
394
+ - status=1: 距离过大, 人像应向上移动
395
+ - status=2: 距离过小, 人像应向下移动
396
+ """
397
+ status_top, move_value = detect_distance(y_top - cut_value_top, crop_size[0], max=top_distance_max,
398
+ min=top_distance_min)
399
+
400
+ # Step6. 对照片的第二轮裁剪
401
+ if status_left_right == 0 and status_top == 0:
402
+ result_image = cut_image
403
+ else:
404
+ result_image = IDphotos_cut(x1 + x_left,
405
+ y1 + cut_value_top + status_top * move_value,
406
+ x2 - x_right,
407
+ y2 - cut_value_top + status_top * move_value,
408
+ origin_png_image)
409
+ if IS_DEBUG:
410
+ testImages.append(["result_image_pre", result_image])
411
+
412
+ # 换装参数准备
413
+ relative_x = x - (x1 + x_left)
414
+ relative_y = y - (y1 + cut_value_top + status_top * move_value)
415
+
416
+ # Step7. 当照片底部存在空隙时,下拉至底部
417
+ result_image, y_high = move(result_image.astype(np.uint8))
418
+ relative_y = relative_y + y_high # 更新换装参数
419
+
420
+ # cv2.imwrite("./temp_image.png", result_image)
421
+
422
+ # Step8. 标准照与高清照转换
423
+ result_image_standard = standard_photo_resize(result_image, standard_size)
424
+ result_image_hd, resize_ratio_max = resize_image_by_min(result_image, esp=max(600, standard_size[1]))
425
+
426
+ # Step9. 参数准备-为换装服务
427
+ clothing_params = {
428
+ "relative_x": relative_x * resize_ratio_max,
429
+ "relative_y": relative_y * resize_ratio_max,
430
+ "w": w * resize_ratio_max,
431
+ "h": h * resize_ratio_max
432
+ }
433
+
434
+ return result_image_hd, result_image_standard, clothing_params
435
+
436
+
437
+ @calTime
438
+ def debug_mode_process(testImages):
439
+ for item, (text, imageItem) in enumerate(testImages):
440
+ channel = imageItem.shape[2]
441
+ (height, width) = imageItem.shape[:2]
442
+ if channel == 4:
443
+ imageItem = add_background(imageItem, bgr=(255, 255, 255))
444
+ imageItem = np.uint8(imageItem)
445
+ if item == 0:
446
+ testHeight = height
447
+ result_image_test = imageItem
448
+ result_image_test = cv2.putText(result_image_test, text, (50, 50), cv2.FONT_HERSHEY_COMPLEX, 1.0,
449
+ (200, 100, 100), 3)
450
+ else:
451
+ imageItem = cv2.resize(imageItem, (int(width * testHeight / height), testHeight))
452
+ imageItem = cv2.putText(imageItem, text, (50, 50), cv2.FONT_HERSHEY_COMPLEX, 1.0, (200, 100, 100),
453
+ 3)
454
+ result_image_test = cv2.hconcat([result_image_test, imageItem])
455
+ if item == len(testImages) - 1:
456
+ return result_image_test
457
+
458
+
459
+ @calTime("主函数")
460
+ def IDphotos_create(input_image,
461
+ mode="ID",
462
+ size=(413, 295),
463
+ head_measure_ratio=0.2,
464
+ head_height_ratio=0.45,
465
+ align=False,
466
+ beauty=True,
467
+ fd68=None,
468
+ human_sess=None,
469
+ IS_DEBUG=False,
470
+ top_distance_max=0.12,
471
+ top_distance_min=0.10):
472
+ """
473
+ 证件照制作主函数
474
+ Args:
475
+ input_image: 输入图像矩阵
476
+ size: (h, w)
477
+ head_measure_ratio: 头部占比?
478
+ head_height_ratio: 头部高度占比?
479
+ align: 是否进行人脸矫正(roll),默认为True(是)
480
+ fd68: 人脸68关键点检测类,详情参见hycv.FaceDetection68.faceDetection68
481
+ human_sess: 人像抠图模型类,由onnx载入(不与下面两个参数连用)
482
+ oss_image_name: 阿里云api需要的参数,实际上是上传到oss的路径
483
+ user: 阿里云api的accessKey配置对象
484
+ top_distance_max: float, 头距离顶部的最大比例
485
+ top_distance_min: float, 头距离顶部的最小比例
486
+ Returns:
487
+ result_image(高清版), result_image(普清版), api请求日志,
488
+ 排版照参数(list),排版照是否旋转参数,照片尺寸(x, y)
489
+ 在函数不出错的情况下,函数会因为一些原因主动抛出异常:
490
+ 1. 无人脸(或者只有半张,dlib无法检测出来),抛出IDError异常,内部参数face_num为0
491
+ 2. 人脸数量超过1,抛出IDError异常,内部参数face_num为2
492
+ 3. 抠图api请求失败,抛出IDError异常,内部参数face_num为-1
493
+ """
494
+
495
+ # Step0. 数据准备/图像预处理
496
+ matting_params = {"modnet": {"human_sess": human_sess}}
497
+ rotation_params = {"L1": None, "L2": None, "L3": None, "clockwise": None, "drawed_image": None}
498
+ input_image = resize_image_esp(input_image, 2000) # 将输入图片resize到最大边长为2000
499
+
500
+ # Step1. 人脸检测
501
+ # dets, rotation, landmark = face_number_and_angle_detection(input_image)
502
+ # dets = face_number_and_angle_detection(input_image)
503
+
504
+ # Step2. 美颜
505
+ # if beauty:
506
+ # input_image = makeBeautiful(input_image, landmark, 2, 2, 5, 4)
507
+
508
+ # Step3. 抠图
509
+ origin_png_image = image_matting(input_image, matting_params)
510
+ if mode == "只换底":
511
+ return origin_png_image, origin_png_image, None, None, None, None, None, None, 1
512
+
513
+ origin_png_image_pre = origin_png_image.copy() # 备份一下现在抠图结果图,之后在iphoto_cutting函数有用
514
+
515
+ # Step4. 旋转矫正
516
+ # 如果旋转角不大于2, 则不做旋转
517
+ # if abs(rotation) <= 2:
518
+ # align = False
519
+ # # 否则,进行旋转矫正
520
+ # if align:
521
+ # input_image_candidate, origin_png_image_candidate, L1, L2, L3, clockwise, drawed_image \
522
+ # = rotation_ajust(input_image, rotation, cv2.split(origin_png_image)[-1], IS_DEBUG=IS_DEBUG) # 图像旋转
523
+ #
524
+ # origin_png_image_pre = origin_png_image.copy()
525
+ # input_image = input_image_candidate.copy()
526
+ # origin_png_image = origin_png_image_candidate.copy()
527
+ #
528
+ # rotation_params["L1"] = L1
529
+ # rotation_params["L2"] = L2
530
+ # rotation_params["L3"] = L3
531
+ # rotation_params["clockwise"] = clockwise
532
+ # rotation_params["drawed_image"] = drawed_image
533
+
534
+ # Step5. MTCNN人脸检测
535
+ faces = face_number_detection_mtcnn(input_image)
536
+
537
+ # Step6. 证件照自适应裁剪
538
+ face_num = len(faces)
539
+ # 报错MTCNN检测结果不等于1的图片
540
+ if face_num != 1:
541
+ return None, None, None, None, None, None, None, None, 0
542
+ # 符合条件的进入下一环
543
+ else:
544
+ result_image_hd, result_image_standard, clothing_params = \
545
+ idphoto_cutting(faces, head_measure_ratio, size, head_height_ratio, origin_png_image,
546
+ origin_png_image_pre, rotation_params, align=align, IS_DEBUG=IS_DEBUG,
547
+ top_distance_max=top_distance_max, top_distance_min=top_distance_min)
548
+
549
+ # Step7. 排版照参数获取
550
+ typography_arr, typography_rotate = generate_layout_photo(input_height=size[0], input_width=size[1])
551
+
552
+ return result_image_hd, result_image_standard, typography_arr, typography_rotate, \
553
+ clothing_params["relative_x"], clothing_params["relative_y"], clothing_params["w"], clothing_params["h"], 1
554
+
555
+
556
+ if __name__ == "__main__":
557
+ HY_HUMAN_MATTING_WEIGHTS_PATH = "./hivision_modnet.onnx"
558
+ sess = onnxruntime.InferenceSession(HY_HUMAN_MATTING_WEIGHTS_PATH)
559
+
560
+ input_image = cv2.imread("test.jpg")
561
+
562
+ result_image_hd, result_image_standard, typography_arr, typography_rotate, \
563
+ _, _, _, _, _ = IDphotos_create(input_image,
564
+ size=(413, 295),
565
+ head_measure_ratio=0.2,
566
+ head_height_ratio=0.45,
567
+ align=True,
568
+ beauty=True,
569
+ fd68=None,
570
+ human_sess=sess,
571
+ oss_image_name="test_tmping.jpg",
572
+ user=None,
573
+ IS_DEBUG=False,
574
+ top_distance_max=0.12,
575
+ top_distance_min=0.10)
576
+ cv2.imwrite("result_image_hd.png", result_image_hd)
src/imageTransform.py ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import cv2
3
+ import functools
4
+ import time
5
+ from hivisionai.hycv.matting_tools import read_modnet_image
6
+
7
+
8
+ def calTime(mark):
9
+ if isinstance(mark, str):
10
+ def decorater(func):
11
+ @functools.wraps(func)
12
+ def wrapper(*args, **kw):
13
+ start_time = time.time()
14
+ return_param = func(*args, **kw)
15
+ print("[Mark-{}] {} 函数花费的时间为 {:.2f}.".format(mark, func.__name__, time.time() - start_time))
16
+ return return_param
17
+
18
+ return wrapper
19
+
20
+ return decorater
21
+ else:
22
+ func = mark
23
+
24
+ @functools.wraps(func)
25
+ def wrapper(*args, **kw):
26
+ start_time = time.time()
27
+ return_param = func(*args, **kw)
28
+ print("{} 函数花费的时间为 {:.2f}.".format(func.__name__, time.time() - start_time))
29
+ return return_param
30
+
31
+ return wrapper
32
+
33
+
34
+ def standard_photo_resize(input_image: np.array, size):
35
+ """
36
+ input_image: 输入图像,即高清照
37
+ size: 标准照的尺寸
38
+ """
39
+ resize_ratio = input_image.shape[0] / size[0]
40
+ resize_item = int(round(input_image.shape[0] / size[0]))
41
+ if resize_ratio >= 2:
42
+ for i in range(resize_item - 1):
43
+ if i == 0:
44
+ result_image = cv2.resize(input_image,
45
+ (size[1] * (resize_item - i - 1), size[0] * (resize_item - i - 1)),
46
+ interpolation=cv2.INTER_AREA)
47
+ else:
48
+ result_image = cv2.resize(result_image,
49
+ (size[1] * (resize_item - i - 1), size[0] * (resize_item - i - 1)),
50
+ interpolation=cv2.INTER_AREA)
51
+ else:
52
+ result_image = cv2.resize(input_image, (size[1], size[0]), interpolation=cv2.INTER_AREA)
53
+
54
+ return result_image
55
+
56
+
57
+ def hollowOutFix(src: np.ndarray) -> np.ndarray:
58
+ b, g, r, a = cv2.split(src)
59
+ src_bgr = cv2.merge((b, g, r))
60
+ # -----------padding---------- #
61
+ add_area = np.zeros((10, a.shape[1]), np.uint8)
62
+ a = np.vstack((add_area, a, add_area))
63
+ add_area = np.zeros((a.shape[0], 10), np.uint8)
64
+ a = np.hstack((add_area, a, add_area))
65
+ # -------------end------------ #
66
+ _, a_threshold = cv2.threshold(a, 127, 255, 0)
67
+ a_erode = cv2.erode(a_threshold, kernel=cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)), iterations=3)
68
+ contours, hierarchy = cv2.findContours(a_erode, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
69
+ contours = [x for x in contours]
70
+ # contours = np.squeeze(contours)
71
+ contours.sort(key=lambda c: cv2.contourArea(c), reverse=True)
72
+ a_contour = cv2.drawContours(np.zeros(a.shape, np.uint8), contours[0], -1, 255, 2)
73
+ # a_base = a_contour[1:-1, 1:-1]
74
+ h, w = a.shape[:2]
75
+ mask = np.zeros([h + 2, w + 2], np.uint8) # mask必须行和列都加2,且必须为uint8单通道阵列
76
+ cv2.floodFill(a_contour, mask=mask, seedPoint=(0, 0), newVal=255)
77
+ a = cv2.add(a, 255 - a_contour)
78
+ return cv2.merge((src_bgr, a[10:-10, 10:-10]))
79
+
80
+
81
+ def resize_image_by_min(input_image, esp=600):
82
+ """
83
+ 将图像缩放为最短边至少为600的图像。
84
+ :param input_image: 输入图像(OpenCV矩阵)
85
+ :param esp: 缩放后的最短边长
86
+ :return: 缩放后的图像,缩放倍率
87
+ """
88
+ height, width = input_image.shape[0], input_image.shape[1]
89
+ min_border = min(height, width)
90
+ if min_border < esp:
91
+ if height >= width:
92
+ new_width = esp
93
+ new_height = height * esp // width
94
+ else:
95
+ new_height = esp
96
+ new_width = width * esp // height
97
+
98
+ return cv2.resize(input_image, (new_width, new_height), interpolation=cv2.INTER_AREA), new_height / height
99
+
100
+ else:
101
+ return input_image, 1
102
+
103
+
104
+ def rotate_bound(image, angle):
105
+ """
106
+ 一个旋转函数,输入一张图片和一个旋转角,可以实现不损失图像信息的旋转。
107
+ """
108
+ # grab the dimensions of the image and then determine the
109
+ # center
110
+ (h, w) = image.shape[:2]
111
+ (cX, cY) = (w / 2, h / 2)
112
+
113
+ # grab the rotation matrix (applying the negative of the
114
+ # angle to rotate clockwise), then grab the sine and cosine
115
+ # (i.e., the rotation components of the matrix)
116
+ M = cv2.getRotationMatrix2D((cX, cY), -angle, 1.0)
117
+ cos = np.abs(M[0, 0])
118
+ sin = np.abs(M[0, 1])
119
+
120
+ # compute the new bounding dimensions of the image
121
+ nW = int((h * sin) + (w * cos))
122
+ nH = int((h * cos) + (w * sin))
123
+
124
+ # adjust the rotation matrix to take into account translation
125
+ M[0, 2] += (nW / 2) - cX
126
+ M[1, 2] += (nH / 2) - cY
127
+
128
+ # perform the actual rotation and return the image
129
+ return cv2.warpAffine(image, M, (nW, nH)), cos, sin
130
+
131
+
132
+ def rotate_bound_4channels(image, a, angle):
133
+ """
134
+ 一个旋转函数,输入一张图片和一个旋转角,可以实现不损失图像信息的旋转。
135
+ """
136
+ input_image, cos, sin = rotate_bound(image, angle)
137
+ new_a, _, _ = rotate_bound(a, angle) # 对做matte旋转,以便之后merge
138
+ b, g, r = cv2.split(input_image)
139
+ result_image = cv2.merge((b, g, r, new_a)) # 得到抠图结果图的无损旋转结果
140
+
141
+ # perform the actual rotation and return the image
142
+ return input_image, result_image, cos, sin
143
+
144
+
145
+ def draw_picture_dots(image, dots, pen_size=10, pen_color=(0, 0, 255)):
146
+ """
147
+ 给一张照片上绘制点。
148
+ image: Opencv图像矩阵
149
+ dots: 一堆点,形如[(100,100),(150,100)]
150
+ pen_size: 画笔的大小
151
+ pen_color: 画笔的颜色
152
+ """
153
+ if isinstance(dots, dict):
154
+ dots = [v for u, v in dots.items()]
155
+ image = image.copy()
156
+ dots = list(dots)
157
+ for dot in dots:
158
+ # print("dot: ", dot)
159
+ x = dot[0]
160
+ y = dot[1]
161
+ cv2.circle(image, (int(x), int(y)), pen_size, pen_color, -1)
162
+ return image
163
+
164
+
165
+ def get_modnet_matting(input_image, sess, ref_size=512):
166
+ """
167
+ 使用modnet模型对图像进行抠图处理。
168
+ :param input_image: 输入图像(opencv矩阵)
169
+ :param sess: onnxruntime推理主体
170
+ :param ref_size: 缩放参数
171
+ :return: 抠图后的图像
172
+ """
173
+ input_name = sess.get_inputs()[0].name
174
+ output_name = sess.get_outputs()[0].name
175
+
176
+ im, width, length = read_modnet_image(input_image=input_image, ref_size=ref_size)
177
+
178
+ matte = sess.run([output_name], {input_name: im})
179
+ matte = (matte[0] * 255).astype('uint8')
180
+ matte = np.squeeze(matte)
181
+ mask = cv2.resize(matte, (width, length), interpolation=cv2.INTER_AREA)
182
+ b, g, r = cv2.split(np.uint8(input_image))
183
+
184
+ output_image = cv2.merge((b, g, r, mask))
185
+
186
+ return output_image
187
+
188
+
189
+ def detect_distance(value, crop_heigh, max=0.06, min=0.04):
190
+ """
191
+ 检测人头顶与照片顶部的距离是否在适当范围内。
192
+ 输入:与顶部的差值
193
+ 输出:(status, move_value)
194
+ status=0 不动
195
+ status=1 人脸应向上移动(裁剪框向下移动)
196
+ status-2 人脸应向下移动(裁剪框向上移动)
197
+ ---------------------------------------
198
+ value:头顶与照片顶部的距离
199
+ crop_heigh: 裁剪框的高度
200
+ max: 距离的最大值
201
+ min: 距离的最小值
202
+ ---------------------------------------
203
+ """
204
+ value = value / crop_heigh # 头顶往上的像素占图像的比例
205
+ if min <= value <= max:
206
+ return 0, 0
207
+ elif value > max:
208
+ # 头顶往上的像素比例高于max
209
+ move_value = value - max
210
+ move_value = int(move_value * crop_heigh)
211
+ # print("上移{}".format(move_value))
212
+ return 1, move_value
213
+ else:
214
+ # 头顶往上的像素比例低于min
215
+ move_value = min - value
216
+ move_value = int(move_value * crop_heigh)
217
+ # print("下移{}".format(move_value))
218
+ return -1, move_value
src/layoutCreate.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2.detail
2
+ import numpy as np
3
+
4
+ def judge_layout(input_width, input_height, PHOTO_INTERVAL_W, PHOTO_INTERVAL_H, LIMIT_BLOCK_W, LIMIT_BLOCK_H):
5
+ centerBlockHeight_1, centerBlockWidth_1 = input_height, input_width # 由证件照们组成的一个中心区块(1代表不转置排列)
6
+ centerBlockHeight_2, centerBlockWidth_2 = input_width, input_height # 由证件照们组成的一个中心区块(2代表转置排列)
7
+
8
+ # 1.不转置排列的情况下:
9
+ layout_col_no_transpose = 0 # 行
10
+ layout_row_no_transpose = 0 # 列
11
+ for i in range(1, 4):
12
+ centerBlockHeight_temp = input_height * i + PHOTO_INTERVAL_H * (i-1)
13
+ if centerBlockHeight_temp < LIMIT_BLOCK_H:
14
+ centerBlockHeight_1 = centerBlockHeight_temp
15
+ layout_row_no_transpose = i
16
+ else:
17
+ break
18
+ for j in range(1, 9):
19
+ centerBlockWidth_temp = input_width * j + PHOTO_INTERVAL_W * (j-1)
20
+ if centerBlockWidth_temp < LIMIT_BLOCK_W:
21
+ centerBlockWidth_1 = centerBlockWidth_temp
22
+ layout_col_no_transpose = j
23
+ else:
24
+ break
25
+ layout_number_no_transpose = layout_row_no_transpose*layout_col_no_transpose
26
+
27
+ # 2.转置排列的情况下:
28
+ layout_col_transpose = 0 # 行
29
+ layout_row_transpose = 0 # 列
30
+ for i in range(1, 4):
31
+ centerBlockHeight_temp = input_width * i + PHOTO_INTERVAL_H * (i-1)
32
+ if centerBlockHeight_temp < LIMIT_BLOCK_H:
33
+ centerBlockHeight_2 = centerBlockHeight_temp
34
+ layout_row_transpose = i
35
+ else:
36
+ break
37
+ for j in range(1, 9):
38
+ centerBlockWidth_temp = input_height * j + PHOTO_INTERVAL_W * (j-1)
39
+ if centerBlockWidth_temp < LIMIT_BLOCK_W:
40
+ centerBlockWidth_2 = centerBlockWidth_temp
41
+ layout_col_transpose = j
42
+ else:
43
+ break
44
+ layout_number_transpose = layout_row_transpose*layout_col_transpose
45
+
46
+ if layout_number_transpose > layout_number_no_transpose:
47
+ layout_mode = (layout_col_transpose, layout_row_transpose, 2)
48
+ return layout_mode, centerBlockWidth_2, centerBlockHeight_2
49
+ else:
50
+ layout_mode = (layout_col_no_transpose, layout_row_no_transpose, 1)
51
+ return layout_mode, centerBlockWidth_1, centerBlockHeight_1
52
+
53
+
54
+ def generate_layout_photo(input_height, input_width):
55
+ # 1.基础参数表
56
+ LAYOUT_WIDTH = 1746
57
+ LAYOUT_HEIGHT = 1180
58
+ PHOTO_INTERVAL_H = 30 # 证件照与证件照之间的垂直距离
59
+ PHOTO_INTERVAL_W = 30 # 证件照与证件照之间的水平距离
60
+ SIDES_INTERVAL_H = 50 # 证件照与画布边缘的垂直距离
61
+ SIDES_INTERVAL_W = 70 # 证件照与画布边缘的水平距离
62
+ LIMIT_BLOCK_W = LAYOUT_WIDTH - 2*SIDES_INTERVAL_W
63
+ LIMIT_BLOCK_H = LAYOUT_HEIGHT - 2*SIDES_INTERVAL_H
64
+
65
+ # 2.创建一个1180x1746的空白画布
66
+ white_background = np.zeros([LAYOUT_HEIGHT, LAYOUT_WIDTH, 3], np.uint8)
67
+ white_background.fill(255)
68
+
69
+ # 3.计算照片的layout(列、行、横竖朝向),证件照组成的中心区块的分辨率
70
+ layout_mode, centerBlockWidth, centerBlockHeight = judge_layout(input_width, input_height, PHOTO_INTERVAL_W,
71
+ PHOTO_INTERVAL_H, LIMIT_BLOCK_W, LIMIT_BLOCK_H)
72
+ # 4.开始排列组合
73
+ x11 = (LAYOUT_WIDTH - centerBlockWidth)//2
74
+ y11 = (LAYOUT_HEIGHT - centerBlockHeight)//2
75
+ typography_arr = []
76
+ typography_rotate = False
77
+ if layout_mode[2] == 2:
78
+ input_height, input_width = input_width, input_height
79
+ typography_rotate = True
80
+
81
+ for j in range(layout_mode[1]):
82
+ for i in range(layout_mode[0]):
83
+ xi = x11 + i*input_width + i*PHOTO_INTERVAL_W
84
+ yi = y11 + j*input_height + j*PHOTO_INTERVAL_H
85
+ typography_arr.append([xi, yi])
86
+
87
+ return typography_arr, typography_rotate
88
+
89
+ def generate_layout_image(input_image, typography_arr, typography_rotate, width=295, height=413):
90
+ LAYOUT_WIDTH = 1746
91
+ LAYOUT_HEIGHT = 1180
92
+ white_background = np.zeros([LAYOUT_HEIGHT, LAYOUT_WIDTH, 3], np.uint8)
93
+ white_background.fill(255)
94
+ if input_image.shape[0] != height:
95
+ input_image = cv2.resize(input_image, (width, height))
96
+ if typography_rotate:
97
+ input_image = cv2.transpose(input_image)
98
+ height, width = width, height
99
+ for arr in typography_arr:
100
+ locate_x, locate_y = arr[0], arr[1]
101
+ white_background[locate_y:locate_y+height, locate_x:locate_x+width] = input_image
102
+
103
+ return white_background
104
+
105
+
106
+ if __name__ == "__main__":
107
+ typography_arr, typography_rotate = generate_layout_photo(input_height=413, input_width=295)
108
+ print("typography_arr:", typography_arr)
109
+ print("typography_rotate:", typography_rotate)
110
+ result_image = generate_layout_image(cv2.imread("./32.jpg"), typography_arr, typography_rotate, width=295, height=413)
111
+ cv2.imwrite("./result_image.jpg", result_image)
112
+
113
+