Spaces:
Paused
Paused
gordonchan
commited on
Upload 51 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- beautyPlugin/GrindSkin.py +43 -0
- beautyPlugin/MakeBeautiful.py +45 -0
- beautyPlugin/MakeWhiter.py +108 -0
- beautyPlugin/ThinFace.py +267 -0
- beautyPlugin/__init__.py +4 -0
- beautyPlugin/lut_image/1.png +0 -0
- beautyPlugin/lut_image/3.png +0 -0
- beautyPlugin/lut_image/lutOrigin.png +0 -0
- hivisionai/__init__.py +0 -0
- hivisionai/app.py +452 -0
- hivisionai/hyService/__init__.py +0 -0
- hivisionai/hyService/cloudService.py +406 -0
- hivisionai/hyService/dbTools.py +337 -0
- hivisionai/hyService/error.py +20 -0
- hivisionai/hyService/serviceTest.py +34 -0
- hivisionai/hyService/utils.py +92 -0
- hivisionai/hyTrain/APIs.py +197 -0
- hivisionai/hyTrain/DataProcessing.py +37 -0
- hivisionai/hyTrain/__init__.py +0 -0
- hivisionai/hycv/FaceDetection68/__init__.py +8 -0
- hivisionai/hycv/FaceDetection68/faceDetection68.py +443 -0
- hivisionai/hycv/__init__.py +1 -0
- hivisionai/hycv/error.py +16 -0
- hivisionai/hycv/face_tools.py +427 -0
- hivisionai/hycv/idphoto.py +2 -0
- hivisionai/hycv/idphotoTool/__init__.py +0 -0
- hivisionai/hycv/idphotoTool/cuny_tools.py +593 -0
- hivisionai/hycv/idphotoTool/idphoto_change_cloth.py +271 -0
- hivisionai/hycv/idphotoTool/idphoto_cut.py +420 -0
- hivisionai/hycv/idphotoTool/move_image.py +121 -0
- hivisionai/hycv/idphotoTool/neck_processing.py +320 -0
- hivisionai/hycv/matting_tools.py +39 -0
- hivisionai/hycv/mtcnn_onnx/__init__.py +2 -0
- hivisionai/hycv/mtcnn_onnx/box_utils.py +238 -0
- hivisionai/hycv/mtcnn_onnx/detector.py +166 -0
- hivisionai/hycv/mtcnn_onnx/first_stage.py +97 -0
- hivisionai/hycv/mtcnn_onnx/visualization_utils.py +31 -0
- hivisionai/hycv/tensor2numpy.py +63 -0
- hivisionai/hycv/utils.py +452 -0
- hivisionai/hycv/vision.py +446 -0
- images/test.jpg +0 -0
- images/test2.jpg +0 -0
- images/test3.jpg +0 -0
- images/test4.jpg +0 -0
- src/EulerZ.py +51 -0
- src/cuny_tools.py +621 -0
- src/error.py +27 -0
- src/face_judgement_align.py +576 -0
- src/imageTransform.py +218 -0
- 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 |
+
|