update:code

This commit is contained in:
KAAANG
2024-09-05 21:52:27 +08:00
parent 14fc91919c
commit c864c8ecf6
71 changed files with 264 additions and 7572 deletions
+2
View File
@@ -12,3 +12,5 @@ build
*.pth
*.pt
*.onnx
test/temp/*
!test/temp/.gitkeep
+1 -1
View File
@@ -2,7 +2,7 @@ import os
import gradio as gr
import onnxruntime
from hivision.creator.face_judgement_align import IDphotos_create
from hivisionai.hycv.vision import add_background
from hivision.creator.vision import add_background
from hivision.creator.layoutCreate import generate_layout_photo, generate_layout_image
import pathlib
import numpy as np
-51
View File
@@ -1,51 +0,0 @@
"""
@author: cuny
@file: EulerX.py
@time: 2022/4/1 13:54
@description:
寻找三维 z 轴旋转角 roll,实现:
1. 输入一张三通道图片(四通道、单通道将默认转为三通道)
2. 输出人脸在 x 轴的转角 roll,顺时针为正方向,角度制
"""
import cv2
import numpy as np
from math import asin, pi # -pi/2 ~ pi/2
# 获得人脸的关键点信息
def get_facePoints(src: np.ndarray, fd68):
if len(src.shape) == 2:
src = cv2.cvtColor(src, cv2.COLOR_GRAY2BGR)
elif src.shape[2] == 4:
src = cv2.cvtColor(src, cv2.COLOR_BGRA2BGR)
status, dets, landmarks, _ = fd68.facePointsEuler(src)
if status == 0:
return 0, None, None
elif status == 2:
return 2, None, None
else:
return 1, dets, np.fliplr(landmarks)
def eulerZ(landmark: np.matrix):
# 我们规定顺时针为正方向
def get_pi_2(r):
pi_2 = pi / 2.
if r >= 0.0:
return pi_2
else:
return -pi_2
orbit_points = np.array([[landmark[21, 0], landmark[21, 1]], [landmark[71, 0], landmark[71, 1]],
[landmark[25, 0], landmark[25, 1]], [landmark[67, 0], landmark[67, 1]]])
# [[cos a],[sin a],[point_x],[point_y]]
# 前面两项是有关直线与 Y 正半轴夹角 a 的三角函数,所以对于眼睛部分来讲 sin a 应该接近 1
# "我可以认为"cv2.fitLine 的 y 轴正方向为竖直向下,且生成的拟合直线的方向为从起点指向终点
# 与 y 轴的夹角为 y 轴夹角与直线方向的夹角,方向从 y 指向直线,逆时针为正方向
# 所以最后对于鼻梁的计算结果需要取个负号
orbit_line = cv2.fitLine(orbit_points, cv2.DIST_L2, 0, 0.01, 0.01)
orbit_a = asin(orbit_line[1][0])
nose_points = np.array([[landmark[55, 0], landmark[55, 1]], [landmark[69, 0], landmark[69, 1]]])
nose_line = cv2.fitLine(nose_points, cv2.DIST_L2, 0, 0.01, 0.01)
nose_a = asin(nose_line[1][0])
return (orbit_a + nose_a) * (180.0 / (2 * pi))
+5 -4
View File
@@ -11,6 +11,7 @@ import numpy as np
from typing import Tuple
import hivision.creator.utils as U
from .context import Context, ContextHandler, Params, Result
from .human_matting import extract_human
from .face_detector import detect_face
from .photo_adjuster import adjust_photo
@@ -44,8 +45,8 @@ class IDCreator:
"""
# 处理者
self.matting_handler: ContextHandler = None
self.detection_handler: ContextHandler = None
self.matting_handler: ContextHandler = extract_human
self.detection_handler: ContextHandler = detect_face
# 上下文
self.ctx = None
@@ -83,13 +84,13 @@ class IDCreator:
ctx.processing_image = U.resize_image_esp(
ctx.processing_image, 2000
) # 将输入图片 resize 到最大边长为 2000
ctx.origin_image = ctx.processing_image.copy()
self.before_all and self.before_all(ctx)
# 1. 人像抠图
self.matting_handler(ctx)
ctx.matting_image = ctx.processing_image.copy()
self.after_matting and self.after_matting(ctx)
# 2. 人脸检测
ctx.face = detect_face(ctx)
self.detection_handler(ctx)
self.after_detect and self.after_detect(ctx)
# 3. 图像调整
result_image_hd, result_image_standard, clothing_params, typography_params = (
-621
View File
@@ -1,621 +0,0 @@
import cv2
import numpy as np
from hivisionai.hycv.utils import get_box_pro
from hivisionai.hycv.vision import cover_image, draw_picture_dots
from math import fabs, sin, radians, cos
def opencv_rotate(img, angle):
h, w = img.shape[:2]
center = (w / 2, h / 2)
scale = 1.0
# 2.1 获取 M 矩阵
"""
M 矩阵
[
cosA -sinA (1-cosA)*centerX+sinA*centerY
sinA cosA -sinA*centerX+(1-cosA)*centerY
]
"""
M = cv2.getRotationMatrix2D(center, angle, scale)
# 2.2 新的宽高,radians(angle) 把角度转为弧度 sin(弧度)
new_H = int(w * fabs(sin(radians(angle))) + h * fabs(cos(radians(angle))))
new_W = int(h * fabs(sin(radians(angle))) + w * fabs(cos(radians(angle))))
# 2.3 平移
M[0, 2] += (new_W - w) / 2
M[1, 2] += (new_H - h) / 2
rotate = cv2.warpAffine(img, M, (new_W, new_H), borderValue=(0, 0, 0))
return rotate
def transformationNeck2(image:np.ndarray, per_to_side:float=0.8)->np.ndarray:
"""
透视变换脖子函数,输入图像和四个点 (矩形框)
矩形框内的图像可能是不完整的 (边角有透明区域)
我们将根据透视变换将矩形框内的图像拉伸成和矩形框一样的形状。
算法分为几个步骤:选择脖子的四个点 -> 选定这四个点拉伸后的坐标 -> 透视变换 -> 覆盖原图
"""
_, _, _, a = cv2.split(image) # 这应该是一个四通道的图像
height, width = a.shape
def locate_side(image_:np.ndarray, x_:int, y_max:int) -> int:
# 寻找 x=y, 且 y <= y_max 上从下往上第一个非 0 的点,如果没找到就返回 0
y_ = 0
for y_ in range(y_max - 1, -1, -1):
if image_[y_][x_] != 0:
break
return y_
def locate_width(image_:np.ndarray, y_:int, mode, left_or_right:int=None):
# 从 y=y 这个水平线上寻找两边的非零点
# 增加 left_or_right 的原因在于为下面 check_jaw 服务
if mode==1: # 左往右
x_ = 0
if left_or_right is None:
left_or_right = 0
for x_ in range(left_or_right, width):
if image_[y_][x_] != 0:
break
else: # 右往左
x_ = width
if left_or_right is None:
left_or_right = width - 1
for x_ in range(left_or_right, -1, -1):
if image_[y_][x_] != 0:
break
return x_
def check_jaw(image_:np.ndarray, left_, right_):
"""
检查选择的点是否与截到下巴,如果截到了,就往下平移一个单位
"""
f= True # True 代表没截到下巴
# [x, y]
for x_cell in range(left_[0] + 1, right_[0]):
if image_[left_[1]][x_cell] == 0:
f = False
break
if f is True:
return left_, right_
else:
y_ = left_[1] + 2
x_left_ = locate_width(image_, y_, mode=1, left_or_right=left_[0])
x_right_ = locate_width(image_, y_, mode=2, left_or_right=right_[0])
left_, right_ = check_jaw(image_, [x_left_, y_], [x_right_, y_])
return left_, right_
# 选择脖子的四个点,核心在于选择上面的两个点,这两个点的确定的位置应该是"宽出来的"两个点
_, _ ,_, a = cv2.split(image) # 这应该是一个四通道的图像
ret,a_thresh = cv2.threshold(a,127,255,cv2.THRESH_BINARY)
y_high, y_low, x_left, x_right = get_box_pro(image=image, model=1) # 直接返回矩阵信息
y_left_side = locate_side(image_=a_thresh, x_=x_left, y_max=y_low) # 左边的点的 y 轴坐标
y_right_side = locate_side(image_=a_thresh, x_=x_right, y_max=y_low) # 右边的点的 y 轴坐标
y = min(y_left_side, y_right_side) # 将两点的坐标保持相同
cell_left_above, cell_right_above = check_jaw(a_thresh,[x_left, y], [x_right, y])
x_left, x_right = cell_left_above[0], cell_right_above[0]
# 此时我们寻找到了脖子的"宽出来的"两个点,这两个点作为上面的两个点,接下来寻找下面的两个点
if per_to_side >1:
assert ValueError("per_to_side 必须小于 1!")
# 在后面的透视变换中我会把它拉成矩形,在这里我先获取四个点的高和宽
height_ = 150 # 这个值应该是个变化的值,与拉伸的长度有关,但是现在先规定为 150
width_ = x_right - x_left # 其实也就是 cell_right_above[1] - cell_left_above[1]
y = int((y_low - y)*per_to_side + y) # 定位 y 轴坐标
cell_left_below, cell_right_bellow = ([locate_width(a_thresh, y_=y, mode=1), y], [locate_width(a_thresh, y_=y, mode=2), y])
# 四个点全齐,开始透视变换
# 寻找透视变换后的四个点,只需要变换 below 的两个点即可
# cell_left_below_final, cell_right_bellow_final = ([cell_left_above[1], y_low], [cell_right_above[1], y_low])
# 需要变换的四个点为 cell_left_above, cell_right_above, cell_left_below, cell_right_bellow
rect = np.array([cell_left_above, cell_right_above, cell_left_below, cell_right_bellow],
dtype='float32')
# 变化后的坐标点
dst = np.array([[0, 0], [width_, 0], [0 , height_], [width_, height_]],
dtype='float32')
# 计算变换矩阵
M = cv2.getPerspectiveTransform(rect, dst)
warped = cv2.warpPerspective(image, M, (width_, height_))
final = cover_image(image=warped, background=image, mode=3, x=cell_left_above[0], y=cell_left_above[1])
# tmp = np.zeros(image.shape)
# final = cover_image(image=warped, background=tmp, mode=3, x=cell_left_above[0], y=cell_left_above[1])
# final = cover_image(image=image, background=final, mode=3, x=0, y=0)
return final
def transformationNeck(image:np.ndarray, cutNeckHeight:int, neckBelow:int,
toHeight:int,per_to_side:float=0.75) -> np.ndarray:
"""
脖子扩充算法,其实需要输入的只是脖子扣出来的部分以及需要被扩充的高度/需要被扩充成的高度。
"""
height, width, channels = image.shape
_, _, _, a = cv2.split(image) # 这应该是一个四通道的图像
ret, a_thresh = cv2.threshold(a, 127, 255, cv2.THRESH_BINARY) # 将透明图层二值化
def locate_width(image_:np.ndarray, y_:int, mode, left_or_right:int=None):
# 从 y=y 这个水平线上寻找两边的非零点
# 增加 left_or_right 的原因在于为下面 check_jaw 服务
if mode==1: # 左往右
x_ = 0
if left_or_right is None:
left_or_right = 0
for x_ in range(left_or_right, width):
if image_[y_][x_] != 0:
break
else: # 右往左
x_ = width
if left_or_right is None:
left_or_right = width - 1
for x_ in range(left_or_right, -1, -1):
if image_[y_][x_] != 0:
break
return x_
def check_jaw(image_:np.ndarray, left_, right_):
"""
检查选择的点是否与截到下巴,如果截到了,就往下平移一个单位
"""
f= True # True 代表没截到下巴
# [x, y]
for x_cell in range(left_[0] + 1, right_[0]):
if image_[left_[1]][x_cell] == 0:
f = False
break
if f is True:
return left_, right_
else:
y_ = left_[1] + 2
x_left_ = locate_width(image_, y_, mode=1, left_or_right=left_[0])
x_right_ = locate_width(image_, y_, mode=2, left_or_right=right_[0])
left_, right_ = check_jaw(image_, [x_left_, y_], [x_right_, y_])
return left_, right_
x_left = locate_width(image_=a_thresh, mode=1, y_=cutNeckHeight)
x_right = locate_width(image_=a_thresh, mode=2, y_=cutNeckHeight)
# 在这里我们取消了对下巴的检查,原因在于输入的 imageHeight 并不能改变
# cell_left_above, cell_right_above = check_jaw(a_thresh, [x_left, imageHeight], [x_right, imageHeight])
cell_left_above, cell_right_above = [x_left, cutNeckHeight], [x_right, cutNeckHeight]
toWidth = x_right - x_left # 矩形宽
# 此时我们寻找到了脖子的"宽出来的"两个点,这两个点作为上面的两个点,接下来寻找下面的两个点
if per_to_side >1:
assert ValueError("per_to_side 必须小于 1!")
y_below = int((neckBelow - cutNeckHeight) * per_to_side + cutNeckHeight) # 定位 y 轴坐标
cell_left_below = [locate_width(a_thresh, y_=y_below, mode=1), y_below]
cell_right_bellow = [locate_width(a_thresh, y_=y_below, mode=2), y_below]
# 四个点全齐,开始透视变换
# 需要变换的四个点为 cell_left_above, cell_right_above, cell_left_below, cell_right_bellow
rect = np.array([cell_left_above, cell_right_above, cell_left_below, cell_right_bellow],
dtype='float32')
# 变化后的坐标点
dst = np.array([[0, 0], [toWidth, 0], [0 , toHeight], [toWidth, toHeight]],
dtype='float32')
M = cv2.getPerspectiveTransform(rect, dst)
warped = cv2.warpPerspective(image, M, (toWidth, toHeight))
# 将变换后的图像覆盖到原图上
final = cover_image(image=warped, background=image, mode=3, x=cell_left_above[0], y=cell_left_above[1])
return final
def bestJunctionCheck_beta(image:np.ndarray, stepSize:int=4, if_per:bool=False):
"""
最优衔接点检测算法,去寻找脖子的"拐点"
"""
point_k = 1
_, _, _, a = cv2.split(image) # 这应该是一个四通道的图像
height, width = a.shape
ret, a_thresh = cv2.threshold(a, 127, 255, cv2.THRESH_BINARY) # 将透明图层二值化
y_high, y_low, x_left, x_right = get_box_pro(image=image, model=1) # 直接返回矩阵信息
def scan(y_:int, max_num:int=2):
num = 0
left = False
right = False
for x_ in range(width):
if a_thresh[y_][x_] != 0:
if x_ < width // 2 and left is False:
num += 1
left = True
elif x_ > width // 2 and right is False:
num += 1
right = True
return True if num >= max_num else False
def locate_neck_above():
"""
定位脖子的尖尖脚
"""
for y_ in range( y_high - 2, height):
if scan(y_):
return y_, y_
y_high_left, y_high_right = locate_neck_above()
def locate_width_pro(image_:np.ndarray, y_:int, mode):
"""
这会是一个生成器,用于生成脖子两边的轮廓
x_, y_ 是启始点的坐标,每一次寻找都会让 y_+1
mode==1 说明是找左边的边,即,image_[y_][x_] == 0 且 image_[y_][x_ + 1] !=0 时跳出;
否则 当 image_[y_][x_] != 0 时,x_ - 1; 当 image_[y_][x_] == 0 且 image_[y_][x_ + 1] ==0 时 x_ + 1
mode==2 说明是找右边的边,即,image_[y_][x_] == 0 且 image_[y_][x_ - 1] !=0 时跳出
否则 当 image_[y_][x_] != 0 时,x_ + 1; 当 image_[y_][x_] == 0 且 image_[y_][x_ - 1] ==0 时 x_ - 1
"""
y_ += 1
if mode == 1:
x_ = 0
while 0 <= y_ < height and 0 <= x_ < width:
while image_[y_][x_] != 0 and x_ >= 0:
x_ -= 1
while image_[y_][x_] == 0 and image_[y_][x_ + 1] == 0 and x_ < width - 2:
x_ += 1
yield [y_, x_]
y_ += 1
elif mode == 2:
x_ = width-1
while 0 <= y_ < height and 0 <= x_ < width:
while image_[y_][x_] != 0 and x_ < width - 2: x_ += 1
while image_[y_][x_] == 0 and image_[y_][x_ - 1] == 0 and x_ >= 0: x_ -= 1
yield [y_, x_]
y_ += 1
yield False
def kGenerator(image_:np.ndarray, mode):
"""
导数生成器,用来生成每一个点对应的导数
"""
y_ = y_high_left if mode == 1 else y_high_right
c_generator = locate_width_pro(image_=image_, y_=y_, mode=mode)
for cell in c_generator:
nc = locate_width_pro(image_=image_, y_=cell[0] + stepSize, mode=mode)
nextCell = next(nc)
if nextCell is False:
yield False, False
else:
k = (cell[1] - nextCell[1]) / stepSize
yield k, cell
def findPt(image_:np.ndarray, mode):
k_generator = kGenerator(image_=image_, mode=mode)
k, cell = next(k_generator)
k_next, cell_next = next(k_generator)
if k is False:
raise ValueError("无法找到拐点!")
while k_next is not False:
k_next, cell_next = next(k_generator)
if (k_next < - 1 / stepSize) or k_next > point_k:
break
cell = cell_next
# return int(cell[0] + stepSize / 2)
return cell[0]
# 先找左边的拐点:
pointY_left = findPt(image_=a_thresh, mode=1)
# 再找右边的拐点:
pointY_right = findPt(image_=a_thresh, mode=2)
point = (pointY_left + pointY_right) // 2
if if_per is True:
point = (pointY_left + pointY_right) // 2
return point / (y_low - y_high)
pointX_left = next(locate_width_pro(image_=a_thresh, y_= point - 1, mode=1))[1]
pointX_right = next(locate_width_pro(image_=a_thresh, y_=point- 1, mode=2))[1]
return [pointX_left, point], [pointX_right, point]
def bestJunctionCheck(image:np.ndarray, offset:int, stepSize:int=4):
"""
最优点检测算算法输入一张脖子图片 (无论这张图片是否已经被二值化,我都认为没有被二值化),输出一个小数 (脖子最上方与衔接点位置/脖子图像长度)
与 beta 版不同的是它新增了一个阈值限定内容。
对于脖子而言,我我们首先可以定位到上面的部分,然后根据上面的这个点向下进行遍历检测。
与 beta 版类似,我们使用一个 stepSize 来用作斜率的检测
但是对于遍历检测而言,与 beta 版不同的是,我们需要对遍历的地方进行一定的限制。
限制的标准是,如果当前遍历的点的横坐标和起始点横坐标的插值超过了某个阈值,则认为是越界。
"""
point_k = 1
_, _, _, a = cv2.split(image) # 这应该是一个四通道的图像
height, width = a.shape
ret, a_thresh = cv2.threshold(a, 127, 255, cv2.THRESH_BINARY) # 将透明图层二值化
# 直接返回脖子的位置信息,修正系数为 0, get_box_pro 内部也封装了二值化,所以直接输入原图
y_high, y_low, _, _ = get_box_pro(image=image, model=1, correction_factor=0)
# 真正有用的只有上下 y 轴的两个值...
# 首先当然是确定起始点的位置,我们用同样的 scan 扫描函数进行行遍历。
def scan(y_:int, max_num:int=2):
num = 0
# 设定两个值,分别代表脖子的左边和右边
left = False
right = False
for x_ in range(width):
if a_thresh[y_][x_] != 0:
# 检测左边
if x_ < width // 2 and left is False:
num += 1
left = True
# 检测右边
elif x_ > width // 2 and right is False:
num += 1
right = True
return True if num >= max_num else False
def locate_neck_above():
"""
定位脖子的尖尖脚
"""
# y_high 就是脖子的最高点
for y_ in range(y_high, height):
if scan(y_):
return y_
y_start = locate_neck_above() # 得到遍历的初始高度
if y_low - y_start < stepSize: assert ValueError("脖子太小!")
# 然后获取一下初始的坐标点
x_left, x_right = 0, width
for x_left_ in range(0, width):
if a_thresh[y_start][x_left_] != 0:
x_left = x_left_
break
for x_right_ in range(width -1 , -1, -1):
if a_thresh[y_start][x_right_] != 0:
x_right = x_right_
break
# 接下来我定义两个生成器,首先是脖子轮廓 (向下寻找的) 生成器,每进行一次 next,生成器会返回 y+1 的脖子轮廓点
def contoursGenerator(image_:np.ndarray, y_:int, mode):
"""
这会是一个生成器,用于生成脖子两边的轮廓
y_ 是启始点的 y 坐标,每一次寻找都会让 y_+1
mode==1 说明是找左边的边,即,image_[y_][x_] == 0 且 image_[y_][x_ + 1] !=0 时跳出;
否则 当 image_[y_][x_] != 0 时,x_ - 1; 当 image_[y_][x_] == 0 且 image_[y_][x_ + 1] ==0 时 x_ + 1
mode==2 说明是找右边的边,即,image_[y_][x_] == 0 且 image_[y_][x_ - 1] !=0 时跳出
否则 当 image_[y_][x_] != 0 时,x_ + 1; 当 image_[y_][x_] == 0 且 image_[y_][x_ - 1] ==0 时 x_ - 1
"""
y_ += 1
try:
if mode == 1:
x_ = 0
while 0 <= y_ < height and 0 <= x_ < width:
while image_[y_][x_] != 0 and x_ >= 0: x_ -= 1
# 这里其实会有 bug,不过可以不管
while x_ < width and image_[y_][x_] == 0 and image_[y_][x_ + 1] == 0: x_ += 1
yield [y_, x_]
y_ += 1
elif mode == 2:
x_ = width-1
while 0 <= y_ < height and 0 <= x_ < width:
while x_ < width and image_[y_][x_] != 0: x_ += 1
while x_ >= 0 and image_[y_][x_] == 0 and image_[y_][x_ - 1] == 0: x_ -= 1
yield [y_, x_]
y_ += 1
# 当处理失败则返回 False
except IndexError:
yield False
# 然后是斜率生成器,这个生成器依赖子轮廓生成器,每一次生成轮廓后会计算斜率,另一个点的选取和 stepSize 有关
def kGenerator(image_: np.ndarray, mode):
"""
导数生成器,用来生成每一个点对应的导数
"""
y_ = y_start
# 对起始点建立一个生成器,mode=1 时是左边轮廓,mode=2 时是右边轮廓
c_generator = contoursGenerator(image_=image_, y_=y_, mode=mode)
for cell in c_generator:
# 寻找距离当前 cell 距离为 stepSize 的轮廓点
kc = contoursGenerator(image_=image_, y_=cell[0] + stepSize, mode=mode)
kCell = next(kc)
if kCell is False:
# 寻找失败
yield False, False
else:
# 寻找成功,返回当坐标点和斜率值
# 对于左边而言,斜率必然是前一个点的坐标减去后一个点的坐标
# 对于右边而言,斜率必然是后一个点的坐标减去前一个点的坐标
k = (cell[1] - kCell[1]) / stepSize if mode == 1 else (kCell[1] - cell[1]) / stepSize
yield k, cell
# 接着开始写寻找算法,需要注意的是我们是分两边选择的
def findPt(image_:np.ndarray, mode):
x_base = x_left if mode == 1 else x_right
k_generator = kGenerator(image_=image_, mode=mode)
k, cell = k_generator.__next__()
if k is False:
raise ValueError("无法找到拐点!")
k_next, cell_next = k_generator.__next__()
while k_next is not False:
cell = cell_next
if cell[1] > x_base and mode == 2:
x_base = cell[1]
elif cell[1] < x_base and mode == 1:
x_base = cell[1]
# 跳出循环的方式一:斜率超过了某个值
if k_next > point_k:
print("K out")
break
# 跳出循环的方式二:超出阈值
elif abs(cell[1] - x_base) > offset:
print("O out")
break
k_next, cell_next = k_generator.__next__()
if abs(cell[1] - x_base) > offset:
cell[0] = cell[0] - offset - 1
return cell[0]
# 先找左边的拐点:
pointY_left = findPt(image_=a_thresh, mode=1)
# 再找右边的拐点:
pointY_right = findPt(image_=a_thresh, mode=2)
point = min(pointY_right, pointY_left)
per = (point - y_high) / (y_low - y_high)
# pointX_left = next(contoursGenerator(image_=a_thresh, y_= point- 1, mode=1))[1]
# pointX_right = next(contoursGenerator(image_=a_thresh, y_=point - 1, mode=2))[1]
# return [pointX_left, point], [pointX_right, point]
return per
def checkSharpCorner(image:np.ndarray):
_, _, _, a = cv2.split(image) # 这应该是一个四通道的图像
height, width = a.shape
ret, a_thresh = cv2.threshold(a, 127, 255, cv2.THRESH_BINARY) # 将透明图层二值化
# 直接返回脖子的位置信息,修正系数为 0, get_box_pro 内部也封装了二值化,所以直接输入原图
y_high, y_low, _, _ = get_box_pro(image=image, model=1, correction_factor=0)
def scan(y_:int, max_num:int=2):
num = 0
# 设定两个值,分别代表脖子的左边和右边
left = False
right = False
for x_ in range(width):
if a_thresh[y_][x_] != 0:
# 检测左边
if x_ < width // 2 and left is False:
num += 1
left = True
# 检测右边
elif x_ > width // 2 and right is False:
num += 1
right = True
return True if num >= max_num else False
def locate_neck_above():
"""
定位脖子的尖尖脚
"""
# y_high 就是脖子的最高点
for y_ in range(y_high, height):
if scan(y_):
return y_
y_start = locate_neck_above()
return y_start
def checkJaw(image:np.ndarray, y_start:int):
# 寻找"马鞍点"
_, _, _, a = cv2.split(image) # 这应该是一个四通道的图像
height, width = a.shape
ret, a_thresh = cv2.threshold(a, 127, 255, cv2.THRESH_BINARY) # 将透明图层二值化
if width <=1: raise TypeError("图像太小!")
x_left, x_right = 0, width - 1
for x_left in range(width):
if a_thresh[y_start][x_left] != 0:
while a_thresh[y_start][x_left] != 0: x_left += 1
break
for x_right in range(width-1, -1, -1):
if a_thresh[y_start][x_right] != 0:
while a_thresh[y_start][x_right] != 0: x_right -= 1
break
point_list_y = []
point_list_x = []
for x in range(x_left, x_right):
y = y_start
while a_thresh[y][x] == 0: y += 1
point_list_y.append(y)
point_list_x.append(x)
y = max(point_list_y)
x = point_list_x[point_list_y.index(y)]
return x, y
def checkHairLOrR(cloth_image_input_cut,
input_a,
neck_a,
cloth_image_input_top_y,
cutbar_top=0.4,
cutbar_bottom=0.5,
threshold=0.3):
"""
本函数用于检测衣服是否被头发遮挡,当前只考虑左右是否被遮挡,即"一刀切"
返回 int
0 代表没有被遮挡
1 代表左边被遮挡
2 代表右边被遮挡
3 代表全被遮挡了
约定,输入的图像是一张灰度图,且被二值化过。
"""
def per_darkPoint(img:np.ndarray) -> int:
"""
用于遍历相加图像上的黑点。
然后返回黑点数/图像面积
"""
h, w = img.shape
sum_darkPoint = 0
for y in range(h):
for x in range(w):
if img[y][x] == 0:
sum_darkPoint += 1
return sum_darkPoint / (h * w)
if threshold < 0 or threshold > 1: raise TypeError("阈值设置必须在 0 和 1 之间!")
# 裁出 cloth_image_input_cut 按高度 40%50% 的区域-cloth_image_input_cutbar,并转换为 A 矩阵,做二值化
cloth_image_input_height = cloth_image_input_cut.shape[0]
_, _, _, cloth_image_input_cutbar = cv2.split(cloth_image_input_cut[
int(cloth_image_input_height * cutbar_top):int(
cloth_image_input_height * cutbar_bottom), :])
_, cloth_image_input_cutbar = cv2.threshold(cloth_image_input_cutbar, 127, 255, cv2.THRESH_BINARY)
# 裁出 input_image、neck_image 的 A 矩阵的对应区域,并做二值化
input_a_cutbar = input_a[cloth_image_input_top_y + int(cloth_image_input_height * cutbar_top):
cloth_image_input_top_y + int(cloth_image_input_height * cutbar_bottom), :]
_, input_a_cutbar = cv2.threshold(input_a_cutbar, 127, 255, cv2.THRESH_BINARY)
neck_a_cutbar = neck_a[cloth_image_input_top_y + int(cloth_image_input_height * cutbar_top):
cloth_image_input_top_y + int(cloth_image_input_height * cutbar_bottom), :]
_, neck_a_cutbar = cv2.threshold(neck_a_cutbar, 50, 255, cv2.THRESH_BINARY)
# 将三个 cutbar 合到一起-result_a_cutbar
input_a_cutbar = np.uint8(255 - input_a_cutbar)
result_a_cutbar = cv2.add(input_a_cutbar, cloth_image_input_cutbar)
result_a_cutbar = cv2.add(result_a_cutbar, neck_a_cutbar)
if_mask = 0
# 我们将图像 一刀切,分为左边和右边
height, width = result_a_cutbar.shape # 一通道图像
left_image = result_a_cutbar[:, :width//2]
right_image = result_a_cutbar[:, width//2:]
if per_darkPoint(left_image) > threshold:
if_mask = 1
if per_darkPoint(right_image) > threshold:
if_mask = 3 if if_mask == 1 else 2
return if_mask
def find_black(image):
"""
找黑色点函数,遇到输入矩阵中的第一个黑点,返回它的 y 值
"""
height, width = image.shape[0], image.shape[1]
for i in range(height):
for j in range(width):
if image[i, j] < 127:
return i
return None
def convert_black_array(image):
height, width = image.shape[0], image.shape[1]
mask = np.zeros([height, width])
for j in range(width):
for i in range(height):
if image[i, j] > 127:
mask[i:, j] = 1
break
return mask
def checkLongHair(neck_image, head_bottom_y, neck_top_y):
"""
长发检测函数,输入为 head/neck 图像,通过下巴是否为最低点,来判断是否为长发
:return 0 : 短发
:return 1 : 长发
"""
jaw_y = neck_top_y + checkJaw(neck_image, y_start=checkSharpCorner(neck_image))[1]
if jaw_y >= head_bottom_y-3:
return 0
else:
return 1
def checkLongHair2(head_bottom_y, cloth_top_y):
if head_bottom_y > cloth_top_y+10:
return 1
else:
return 0
if __name__ == "__main__":
for i in range(1, 8):
img = cv2.imread(f"./neck_temp/neck_image{i}.png", cv2.IMREAD_UNCHANGED)
# new = transformationNeck(image=img, cutNeckHeight=419,neckBelow=472, toHeight=150)
# point_list = bestJunctionCheck(img, offset=5, stepSize=3)
# per = bestJunctionCheck(img, offset=5, stepSize=3)
# # 返回一个小数的形式,接下来我将它处理为两个点
point_list = []
# y_high_, y_low_, _, _ = get_box_pro(image=img, model=1, conreection_factor=0)
# _y = y_high_ + int((y_low_ - y_high_) * per)
# _, _, _, a_ = cv2.split(img) # 这应该是一个四通道的图像
# h, w = a_.shape
# r, a_t = cv2.threshold(a_, 127, 255, cv2.THRESH_BINARY) # 将透明图层二值化
# _x = 0
# for _x in range(w):
# if a_t[_y][_x] != 0:
# break
# point_list.append([_x, _y])
# for _x in range(w - 1, -1, -1):
# if a_t[_y][_x] != 0:
# break
# point_list.append([_x, _y])
y = checkSharpCorner(img)
point = checkJaw(image=img, y_start=y)
point_list.append(point)
new = draw_picture_dots(img, point_list, pen_size=2)
cv2.imshow(f"{i}", new)
cv2.waitKey(0)
-647
View File
@@ -1,647 +0,0 @@
import math
import cv2
import numpy as np
from hivisionai.hycv.face_tools import face_detect_mtcnn
from hivisionai.hycv.utils import get_box_pro
from hivisionai.hycv.vision import (
resize_image_esp,
IDphotos_cut,
add_background,
calTime,
resize_image_by_min,
rotate_bound_4channels,
)
import onnxruntime
from hivision.error import FaceError
from hivision.creator.imageTransform import (
standard_photo_resize,
hollowOutFix,
get_modnet_matting,
draw_picture_dots,
detect_distance,
)
from hivision.creator.layoutCreate import generate_layout_photo
from hivision.creator.move_image import move
testImages = []
class LinearFunction_TwoDots(object):
"""
通过两个坐标点构建线性函数
"""
def __init__(self, dot1, dot2):
self.d1 = dot1
self.d2 = dot2
self.mode = "normal"
if self.d2.x != self.d1.x:
self.k = (self.d2.y - self.d1.y) / max((self.d2.x - self.d1.x), 1)
self.b = self.d2.y - self.k * self.d2.x
else:
self.mode = "x=1"
def forward(self, input_, mode="x"):
if mode == "x":
if self.mode == "normal":
return self.k * input_ + self.b
else:
return 0
elif mode == "y":
if self.mode == "normal":
return (input_ - self.b) / self.k
else:
return self.d1.x
def forward_x(self, x):
if self.mode == "normal":
return self.k * x + self.b
else:
return 0
def forward_y(self, y):
if self.mode == "normal":
return (y - self.b) / self.k
else:
return self.d1.x
class Coordinate(object):
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return "({}, {})".format(self.x, self.y)
@calTime
def face_number_and_angle_detection(input_image):
"""
本函数的功能是利用机器学习算法计算图像中人脸的数目与关键点,并通过关键点信息来计算人脸在平面上的旋转角度。
当前人脸数目!=1 时,将 raise 一个错误信息并终止全部程序。
Args:
input_image: numpy.array(3 channels),用户上传的原图(经过了一些简单的 resize)
Returns:
- dets: list,人脸定位信息 (x1, y1, x2, y2)
- rotation: int,旋转角度,正数代表逆时针偏离,负数代表顺时针偏离
- landmark: list,人脸关键点信息
"""
faces, landmarks = face_detect_mtcnn(input_image)
face_num = len(faces)
# 排除不合人脸数目要求(必须是 1)的照片
if face_num == 0 or face_num >= 2:
raise FaceError(f"人脸检测出错!检测出了{face_num}张人脸", face_num=face_num)
# 获得人脸定位坐标
face_rectangle = []
for iter, (x1, y1, x2, y2, _) in enumerate(faces):
x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2)
face_rectangle.append(
{"top": x1, "left": y1, "width": x2 - x1, "height": y2 - y1}
)
# 获取人脸定位坐标与关键点信息
dets = face_rectangle[0]
# landmark = landmarks[0]
#
# # 人脸旋转角度计算
# rotation = eulerZ(landmark)
# return dets, rotation, landmark
return dets
@calTime
def image_matting(input_image, params):
"""
本函数的功能为全局人像抠图。
Args:
- input_image: numpy.array(3 channels),用户原图
Returns:
- origin_png_image: numpy.array(4 channels),抠好的图
"""
print("抠图采用本地模型")
origin_png_image = get_modnet_matting(
input_image, sess=params["modnet"]["human_sess"]
)
origin_png_image = hollowOutFix(origin_png_image) # 抠图洞洞修补
return origin_png_image
@calTime
def rotation_ajust(input_image, rotation, a, IS_DEBUG=False):
"""
本函数的功能是根据旋转角对原图进行无损旋转,并返回结果图与附带信息。
Args:
- input_image: numpy.array(3 channels), 用户上传的原图(经过了一些简单的 resize、美颜)
- rotation: float, 人的五官偏离"端正"形态的旋转角
- a: numpy.array(1 channel), matting 图的 matte
- IS_DEBUG: DEBUG 模式开关
Returns:
- result_jpg_image: numpy.array(3 channels), 原图旋转的结果图
- result_png_image: numpy.array(4 channels), matting 图旋转的结果图
- L1: CLassObject, 根据旋转点连线所构造函数
- L2: ClassObject, 根据旋转点连线所构造函数
- dotL3: ClassObject, 一个特殊裁切点的坐标
- clockwise: int, 表示照片是顺时针偏离还是逆时针偏离
- drawed_dots_image: numpy.array(3 channels), 在 result_jpg_image 上标定了 4 个旋转点的结果图,用于 DEBUG 模式
"""
# Step1. 数据准备
rotation = -1 * rotation # rotation 为正数->原图顺时针偏离,为负数->逆时针偏离
h, w = input_image.copy().shape[:2]
# Step2. 无损旋转
result_jpg_image, result_png_image, cos, sin = rotate_bound_4channels(
input_image, a, rotation
)
# Step3. 附带信息计算
nh, nw = result_jpg_image.shape[:2] # 旋转后的新的长宽
clockwise = (
-1 if rotation < 0 else 1
) # clockwise 代表时针,即 1 为顺时针,-1 为逆时针
# 如果逆时针偏离:
if rotation < 0:
p1 = Coordinate(0, int(w * sin))
p2 = Coordinate(int(w * cos), 0)
p3 = Coordinate(nw, int(h * cos))
p4 = Coordinate(int(h * sin), nh)
L1 = LinearFunction_TwoDots(p1, p4)
L2 = LinearFunction_TwoDots(p4, p3)
dotL3 = Coordinate(
int(0.25 * p2.x + 0.75 * p3.x), int(0.25 * p2.y + 0.75 * p3.y)
)
# 如果顺时针偏离:
else:
p1 = Coordinate(int(h * sin), 0)
p2 = Coordinate(nw, int(w * sin))
p3 = Coordinate(int(w * cos), nh)
p4 = Coordinate(0, int(h * cos))
L1 = LinearFunction_TwoDots(p4, p3)
L2 = LinearFunction_TwoDots(p3, p2)
dotL3 = Coordinate(
int(0.75 * p4.x + 0.25 * p1.x), int(0.75 * p4.y + 0.25 * p1.y)
)
# Step4. 根据附带信息进行图像绘制(4 个旋转点),便于 DEBUG 模式验证
drawed_dots_image = draw_picture_dots(
result_jpg_image,
[(p1.x, p1.y), (p2.x, p2.y), (p3.x, p3.y), (p4.x, p4.y), (dotL3.x, dotL3.y)],
)
if IS_DEBUG:
testImages.append(["drawed_dots_image", drawed_dots_image])
return (
result_jpg_image,
result_png_image,
L1,
L2,
dotL3,
clockwise,
drawed_dots_image,
)
@calTime
def face_number_detection_mtcnn(input_image):
"""
本函数的功能是对旋转矫正的结果图进行基于 MTCNN 模型的人脸检测。
Args:
- input_image: numpy.array(3 channels), 旋转矫正 (rotation_adjust) 的 3 通道结果图
Returns:
- faces: list, 人脸检测的结果,包含人脸位置信息
"""
# 如果图像的长或宽>1500px,则对图像进行 1/2 的 resize 再做 MTCNN 人脸检测,以加快处理速度
if max(input_image.shape[0], input_image.shape[1]) >= 1500:
input_image_resize = cv2.resize(
input_image,
(input_image.shape[1] // 2, input_image.shape[0] // 2),
interpolation=cv2.INTER_AREA,
)
faces, _ = face_detect_mtcnn(input_image_resize, filter=True) # MTCNN 人脸检测
# 如果缩放后图像的 MTCNN 人脸数目检测结果等于 1->两次人脸检测结果没有偏差,则对定位数据 x2
if len(faces) == 1:
for item, param in enumerate(faces[0]):
faces[0][item] = param * 2
# 如果两次人脸检测结果有偏差,则默认缩放后图像的 MTCNN 检测存在误差,则将原图输入再做一次 MTCNN(保险措施)
else:
faces, _ = face_detect_mtcnn(input_image, filter=True)
# 如果图像的长或宽<1500px,则直接进行 MTCNN 检测
else:
faces, _ = face_detect_mtcnn(input_image, filter=True)
return faces
@calTime
def cutting_rect_pan(
x1, y1, x2, y2, width, height, L1, L2, L3, clockwise, standard_size
):
"""
本函数的功能是对旋转矫正结果图的裁剪框进行修正 ———— 解决"旋转三角形"现象。
Args:
- x1: int, 裁剪框左上角的横坐标
- y1: int, 裁剪框左上角的纵坐标
- x2: int, 裁剪框右下角的横坐标
- y2: int, 裁剪框右下角的纵坐标
- width: int, 待裁剪图的宽度
- height:int, 待裁剪图的高度
- L1: CLassObject, 根据旋转点连线所构造函数
- L2: CLassObject, 根据旋转点连线所构造函数
- L3: ClassObject, 一个特殊裁切点的坐标
- clockwise: int, 旋转时针状态
- standard_size: tuple, 标准照的尺寸
Returns:
- x1: int, 新的裁剪框左上角的横坐标
- y1: int, 新的裁剪框左上角的纵坐标
- x2: int, 新的裁剪框右下角的横坐标
- y2: int, 新的裁剪框右下角的纵坐标
- x_bias: int, 裁剪框横坐标方向上的计算偏置量
- y_bias: int, 裁剪框纵坐标方向上的计算偏置量
"""
# 用于计算的裁剪框坐标 x1_cal,x2_cal,y1_cal,y2_cal(如果裁剪框超出了图像范围,则缩小直至在范围内)
x1_std = x1 if x1 > 0 else 0
x2_std = x2 if x2 < width else width
# y1_std = y1 if y1 > 0 else 0
y2_std = y2 if y2 < height else height
# 初始化 x 和 y 的计算偏置项 x_bias 和 y_bias
x_bias = 0
y_bias = 0
# 如果顺时针偏转
if clockwise == 1:
if y2 > L1.forward_x(x1_std):
y_bias = int(-(y2_std - L1.forward_x(x1_std)))
if y2 > L2.forward_x(x2_std):
x_bias = int(-(x2_std - L2.forward_y(y2_std)))
x2 = x2_std + x_bias
if x1 < L3.x:
x1 = L3.x
# 如果逆时针偏转
else:
if y2 > L1.forward_x(x1_std):
x_bias = int(L1.forward_y(y2_std) - x1_std)
if y2 > L2.forward_x(x2_std):
y_bias = int(-(y2_std - L2.forward_x(x2_std)))
x1 = x1_std + x_bias
if x2 > L3.x:
x2 = L3.x
# 计算裁剪框的 y 的变化
y2 = int(y2_std + y_bias)
new_cut_width = x2 - x1
new_cut_height = int(new_cut_width / standard_size[1] * standard_size[0])
y1 = y2 - new_cut_height
return x1, y1, x2, y2, x_bias, y_bias
@calTime
def idphoto_cutting(
faces,
head_measure_ratio,
standard_size,
head_height_ratio,
origin_png_image,
rotation_params,
top_distance_max=0.12,
top_distance_min=0.10,
):
"""
本函数的功能为进行证件照的自适应裁剪,自适应依据 Setting.json 的控制参数,以及输入图像的自身情况。
Args:
- faces: list, 人脸位置信息
- head_measure_ratio: float, 人脸面积与全图面积的期望比值
- standard_size: tuple, 标准照尺寸,如 (413, 295)
- head_height_ratio: float, 人脸中心处在全图高度的比例期望值
- origin_png_image: numpy.array(4 channels), 经过一系列转换后的用户输入图
- origin_png_image_prenumpy.array(4 channels),经过一系列转换(但没有做旋转矫正)的用户输入图
- rotation_params:旋转参数字典
- L1: classObject, 来自 rotation_ajust 的 L1 线性函数
- L2: classObject, 来自 rotation_ajust 的 L2 线性函数
- L3: classObject, 来自 rotation_ajust 的 dotL3 点
- clockwise: int, (顺/逆) 时针偏差
- drawed_image: numpy.array, 红点标定 4 个旋转点的图像
- align: bool, 是否图像做过旋转矫正
- IS_DEBUG: DEBUG 模式开关
- top_distance_max: float, 头距离顶部的最大比例
- top_distance_min: float, 头距离顶部的最小比例
Returns:
- result_image_hd: numpy.array(4 channels), 高清照
- result_image_standard: numpy.array(4 channels), 标准照
- clothing_params: json, 换装配置参数,便于后续换装功能的使用
"""
# Step0. 旋转参数准备
L1 = rotation_params["L1"]
L2 = rotation_params["L2"]
L3 = rotation_params["L3"]
clockwise = rotation_params["clockwise"]
drawed_image = rotation_params["drawed_image"]
# Step1. 准备人脸参数
face_rect = faces[0]
x, y = face_rect[0], face_rect[1]
w, h = face_rect[2] - x + 1, face_rect[3] - y + 1
height, width = origin_png_image.shape[:2]
width_height_ratio = standard_size[0] / standard_size[1] # 高宽比
# Step2. 计算高级参数
face_center = (x + w / 2, y + h / 2) # 面部中心坐标
face_measure = w * h # 面部面积
crop_measure = face_measure / head_measure_ratio # 裁剪框面积:为面部面积的 5 倍
resize_ratio = crop_measure / (standard_size[0] * standard_size[1]) # 裁剪框缩放率
resize_ratio_single = math.sqrt(
resize_ratio
) # 长和宽的缩放率(resize_ratio 的开方)
crop_size = (
int(standard_size[0] * resize_ratio_single),
int(standard_size[1] * resize_ratio_single),
) # 裁剪框大小
# 裁剪框的定位信息
x1 = int(face_center[0] - crop_size[1] / 2)
y1 = int(face_center[1] - crop_size[0] * head_height_ratio)
y2 = y1 + crop_size[0]
x2 = x1 + crop_size[1]
# Step3. 对于旋转矫正图片的裁切处理
# if align:
# y_top_pre, _, _, _ = get_box_pro(origin_png_image.astype(np.uint8), model=2,
# correction_factor=0) # 获取 matting 结果图的顶距
# # 裁剪参数重新计算,目标是以最小的图像损失来消除"旋转三角形"
# x1, y1, x2, y2, x_bias, y_bias = cutting_rect_pan(x1, y1, x2, y2, width, height, L1, L2, L3, clockwise,
# standard_size)
# # 这里设定一个拒绝判定条件,如果裁剪框切进了人脸检测框的话,就不进行旋转
# if y1 > y_top_pre:
# y2 = y2 - (y1 - y_top_pre)
# y1 = y_top_pre
# # 如何遇到裁剪到人脸的情况,则转为不旋转裁切
# if x1 > x or x2 < (x + w) or y1 > y or y2 < (y + h):
# return idphoto_cutting(faces, head_measure_ratio, standard_size, head_height_ratio, origin_png_image_pre,
# origin_png_image_pre, rotation_params, align=False, IS_DEBUG=False)
#
# if y_bias != 0:
# origin_png_image = origin_png_image[:y2, :]
# if x_bias > 0: # 逆时针
# origin_png_image = origin_png_image[:, x1:]
# if drawed_image is not None and IS_DEBUG:
# drawed_x = x1
# x = x - x1
# x2 = x2 - x1
# x1 = 0
# else: # 顺时针
# origin_png_image = origin_png_image[:, :x2]
#
# if drawed_image is not None and IS_DEBUG:
# drawed_x = drawed_x if x_bias > 0 else 0
# drawed_image = draw_picture_dots(drawed_image, [(x1 + drawed_x, y1), (x1 + drawed_x, y2),
# (x2 + drawed_x, y1), (x2 + drawed_x, y2)],
# pen_color=(255, 0, 0))
# testImages.append(["drawed_image", drawed_image])
# Step4. 对照片的第一轮裁剪
cut_image = IDphotos_cut(x1, y1, x2, y2, origin_png_image)
cut_image = cv2.resize(cut_image, (crop_size[1], crop_size[0]))
y_top, y_bottom, x_left, x_right = get_box_pro(
cut_image.astype(np.uint8), model=2, correction_factor=0
) # 得到 cut_image 中人像的上下左右距离信息
# Step5. 判定 cut_image 中的人像是否处于合理的位置,若不合理,则处理数据以便之后调整位置
# 检测人像与裁剪框左边或右边是否存在空隙
if x_left > 0 or x_right > 0:
status_left_right = 1
cut_value_top = int(
((x_left + x_right) * width_height_ratio) / 2
) # 减去左右,为了保持比例,上下也要相应减少 cut_value_top
else:
status_left_right = 0
cut_value_top = 0
"""
检测人头顶与照片的顶部是否在合适的距离内:
- status==0: 距离合适,无需移动
- status=1: 距离过大,人像应向上移动
- status=2: 距离过小,人像应向下移动
"""
status_top, move_value = detect_distance(
y_top - cut_value_top, crop_size[0], max=top_distance_max, min=top_distance_min
)
# Step6. 对照片的第二轮裁剪
if status_left_right == 0 and status_top == 0:
result_image = cut_image
else:
result_image = IDphotos_cut(
x1 + x_left,
y1 + cut_value_top + status_top * move_value,
x2 - x_right,
y2 - cut_value_top + status_top * move_value,
origin_png_image,
)
# 换装参数准备
relative_x = x - (x1 + x_left)
relative_y = y - (y1 + cut_value_top + status_top * move_value)
# Step7. 当照片底部存在空隙时,下拉至底部
result_image, y_high = move(result_image.astype(np.uint8))
relative_y = relative_y + y_high # 更新换装参数
# cv2.imwrite("./temp_image.png", result_image)
# Step8. 标准照与高清照转换
result_image_standard = standard_photo_resize(result_image, standard_size)
result_image_hd, resize_ratio_max = resize_image_by_min(
result_image, esp=max(600, standard_size[1])
)
# Step9. 参数准备 - 为换装服务
clothing_params = {
"relative_x": relative_x * resize_ratio_max,
"relative_y": relative_y * resize_ratio_max,
"w": w * resize_ratio_max,
"h": h * resize_ratio_max,
}
return result_image_hd, result_image_standard, clothing_params
@calTime
def debug_mode_process(testImages):
for item, (text, imageItem) in enumerate(testImages):
channel = imageItem.shape[2]
(height, width) = imageItem.shape[:2]
if channel == 4:
imageItem = add_background(imageItem, bgr=(255, 255, 255))
imageItem = np.uint8(imageItem)
if item == 0:
testHeight = height
result_image_test = imageItem
result_image_test = cv2.putText(
result_image_test,
text,
(50, 50),
cv2.FONT_HERSHEY_COMPLEX,
1.0,
(200, 100, 100),
3,
)
else:
imageItem = cv2.resize(
imageItem, (int(width * testHeight / height), testHeight)
)
imageItem = cv2.putText(
imageItem,
text,
(50, 50),
cv2.FONT_HERSHEY_COMPLEX,
1.0,
(200, 100, 100),
3,
)
result_image_test = cv2.hconcat([result_image_test, imageItem])
if item == len(testImages) - 1:
return result_image_test
@calTime("主函数")
def IDphotos_create(
input_image,
mode="ID",
size=(413, 295),
head_measure_ratio=0.2,
head_height_ratio=0.45,
align=False,
beauty=True,
fd68=None,
human_sess=None,
IS_DEBUG=False,
top_distance_max=0.12,
top_distance_min=0.10,
):
"""
证件照制作主函数
Args:
input_image: 输入图像矩阵
size: (h, w)
head_measure_ratio: 头部占比?
head_height_ratio: 头部高度占比?
align: 是否进行人脸矫正(roll),默认为 True(是)
fd68: 人脸 68 关键点检测类,详情参见 hycv.FaceDetection68.faceDetection688
human_sess: 人像抠图模型类,由 onnx 载入(不与下面两个参数连用)连用)
oss_image_name: 阿里云 api 需要的参数,实际上是上传到 oss 的路径
user: 阿里云 api 的 accessKey 配置对象
top_distance_max: float, 头距离顶部的最大比例
top_distance_min: float, 头距离顶部的最小比例
Returns:
result_image(高清版), result_image(普清版), api 请求日志,
排版照参数 (list),排版照是否旋转参数,照片尺寸(x,y)
在函数不出错的情况下,函数会因为一些原因主动抛出异常:
1. 无人脸(或者只有半张,dlib 无法检测出来),抛出 IDError 异常,内部参数 face_num 为 0
2. 人脸数量超过 1,抛出 IDError 异常,内部参数 face_num 为 2
3. 抠图 api 请求失败,抛出 IDError 异常,内部参数 face_num 为 -1num 为 -1
"""
# Step0. 数据准备/图像预处理
matting_params = {"modnet": {"human_sess": human_sess}}
rotation_params = {
"L1": None,
"L2": None,
"L3": None,
"clockwise": None,
"drawed_image": None,
}
input_image = resize_image_esp(
input_image, 2000
) # 将输入图片 resize 到最大边长为 2000
# Step1. 人脸检测
# dets, rotation, landmark = face_number_and_angle_detection(input_image)
# dets = face_number_and_angle_detection(input_image)
# Step2. 美颜
# if beauty:
# input_image = makeBeautiful(input_image, landmark, 2, 2, 5, 4)
# Step3. 抠图
origin_png_image = image_matting(input_image, matting_params)
if mode == "只换底" or mode == "Only Change Background":
return origin_png_image, origin_png_image, None, None, None, None, None, None, 1
origin_png_image_pre = (
origin_png_image.copy()
) # 备份一下现在抠图结果图,之后在 iphoto_cutting 函数有用
# Step4. 旋转矫正
# 如果旋转角不大于 2, 则不做旋转
# if abs(rotation) <= 2:
# align = False
# # 否则,进行旋转矫正
# if align:
# input_image_candidate, origin_png_image_candidate, L1, L2, L3, clockwise, drawed_image \
# = rotation_ajust(input_image, rotation, cv2.split(origin_png_image)[-1], IS_DEBUG=IS_DEBUG) # 图像旋转
#
# origin_png_image_pre = origin_png_image.copy()
# input_image = input_image_candidate.copy()
# origin_png_image = origin_png_image_candidate.copy()
#
# rotation_params["L1"] = L1
# rotation_params["L2"] = L2
# rotation_params["L3"] = L3
# rotation_params["clockwise"] = clockwise
# rotation_params["drawed_image"] = drawed_image
# Step5. MTCNN 人脸检测
faces = face_number_detection_mtcnn(input_image)
# Step6. 证件照自适应裁剪
face_num = len(faces)
# 报错 MTCNN 检测结果不等于 1 的图片
if face_num != 1:
return None, None, None, None, None, None, None, None, 0
# 符合条件的进入下一环
else:
result_image_hd, result_image_standard, clothing_params = idphoto_cutting(
faces,
head_measure_ratio,
size,
head_height_ratio,
origin_png_image,
origin_png_image_pre,
rotation_params,
align=align,
IS_DEBUG=IS_DEBUG,
top_distance_max=top_distance_max,
top_distance_min=top_distance_min,
)
# Step7. 排版照参数获取
typography_arr, typography_rotate = generate_layout_photo(
input_height=size[0], input_width=size[1]
)
return (
result_image_hd,
result_image_standard,
typography_arr,
typography_rotate,
clothing_params["relative_x"],
clothing_params["relative_y"],
clothing_params["w"],
clothing_params["h"],
1,
)
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
r"""
@DATE: 2024/9/5 21:21
@File: human_matting.py
@IDE: pycharm
@Description:
人像抠图
"""
import numpy as np
from PIL import Image
import onnxruntime
from .tensor2numpy import NNormalize, NTo_Tensor, NUnsqueeze
from .context import Context
import cv2
import os
weight_path = os.path.join(os.path.dirname(__file__), "weights", "hivision_modnet.onnx")
def extract_human(ctx: Context):
"""
人像抠图
:param ctx: 上下文
"""
# 抠图
matting_image = get_modnet_matting(ctx.processing_image, weight_path)
# 修复抠图
ctx.processing_image = hollow_out_fix(matting_image)
ctx.matting_image = ctx.processing_image.copy()
def hollow_out_fix(src: np.ndarray) -> np.ndarray:
"""
修补抠图区域,作为抠图模型精度不够的补充
:param src:
:return:
"""
b, g, r, a = cv2.split(src)
src_bgr = cv2.merge((b, g, r))
# -----------padding---------- #
add_area = np.zeros((10, a.shape[1]), np.uint8)
a = np.vstack((add_area, a, add_area))
add_area = np.zeros((a.shape[0], 10), np.uint8)
a = np.hstack((add_area, a, add_area))
# -------------end------------ #
_, a_threshold = cv2.threshold(a, 127, 255, 0)
a_erode = cv2.erode(
a_threshold,
kernel=cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)),
iterations=3,
)
contours, hierarchy = cv2.findContours(
a_erode, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE
)
contours = [x for x in contours]
# contours = np.squeeze(contours)
contours.sort(key=lambda c: cv2.contourArea(c), reverse=True)
a_contour = cv2.drawContours(np.zeros(a.shape, np.uint8), contours[0], -1, 255, 2)
# a_base = a_contour[1:-1, 1:-1]
h, w = a.shape[:2]
mask = np.zeros(
[h + 2, w + 2], np.uint8
) # mask 必须行和列都加 2,且必须为 uint8 单通道阵列
cv2.floodFill(a_contour, mask=mask, seedPoint=(0, 0), newVal=255)
a = cv2.add(a, 255 - a_contour)
return cv2.merge((src_bgr, a[10:-10, 10:-10]))
def image2bgr(input_image):
if len(input_image.shape) == 2:
input_image = input_image[:, :, None]
if input_image.shape[2] == 1:
result_image = np.repeat(input_image, 3, axis=2)
elif input_image.shape[2] == 4:
result_image = input_image[:, :, 0:3]
else:
result_image = input_image
return result_image
def read_modnet_image(input_image, ref_size=512):
im = Image.fromarray(np.uint8(input_image))
width, length = im.size[0], im.size[1]
im = np.asarray(im)
im = image2bgr(im)
im = cv2.resize(im, (ref_size, ref_size), interpolation=cv2.INTER_AREA)
im = NNormalize(im, mean=np.array([0.5, 0.5, 0.5]), std=np.array([0.5, 0.5, 0.5]))
im = NUnsqueeze(NTo_Tensor(im))
return im, width, length
def get_modnet_matting(input_image, checkpoint_path, ref_size=512):
sess = onnxruntime.InferenceSession(checkpoint_path)
input_name = sess.get_inputs()[0].name
output_name = sess.get_outputs()[0].name
im, width, length = read_modnet_image(input_image=input_image, ref_size=ref_size)
matte = sess.run([output_name], {input_name: im})
matte = (matte[0] * 255).astype("uint8")
matte = np.squeeze(matte)
mask = cv2.resize(matte, (width, length), interpolation=cv2.INTER_AREA)
b, g, r = cv2.split(np.uint8(input_image))
output_image = cv2.merge((b, g, r, mask))
return output_image
-218
View File
@@ -1,218 +0,0 @@
import numpy as np
import cv2
import functools
import time
from hivisionai.hycv.matting_tools import read_modnet_image
def calTime(mark):
if isinstance(mark, str):
def decorater(func):
@functools.wraps(func)
def wrapper(*args, **kw):
start_time = time.time()
return_param = func(*args, **kw)
print("[Mark-{}] {} 函数花费的时间为 {:.2f}.".format(mark, func.__name__, time.time() - start_time))
return return_param
return wrapper
return decorater
else:
func = mark
@functools.wraps(func)
def wrapper(*args, **kw):
start_time = time.time()
return_param = func(*args, **kw)
print("{} 函数花费的时间为 {:.2f}.".format(func.__name__, time.time() - start_time))
return return_param
return wrapper
def standard_photo_resize(input_image: np.array, size):
"""
input_image: 输入图像,即高清照
size: 标准照的尺寸
"""
resize_ratio = input_image.shape[0] / size[0]
resize_item = int(round(input_image.shape[0] / size[0]))
if resize_ratio >= 2:
for i in range(resize_item - 1):
if i == 0:
result_image = cv2.resize(input_image,
(size[1] * (resize_item - i - 1), size[0] * (resize_item - i - 1)),
interpolation=cv2.INTER_AREA)
else:
result_image = cv2.resize(result_image,
(size[1] * (resize_item - i - 1), size[0] * (resize_item - i - 1)),
interpolation=cv2.INTER_AREA)
else:
result_image = cv2.resize(input_image, (size[1], size[0]), interpolation=cv2.INTER_AREA)
return result_image
def hollowOutFix(src: np.ndarray) -> np.ndarray:
b, g, r, a = cv2.split(src)
src_bgr = cv2.merge((b, g, r))
# -----------padding---------- #
add_area = np.zeros((10, a.shape[1]), np.uint8)
a = np.vstack((add_area, a, add_area))
add_area = np.zeros((a.shape[0], 10), np.uint8)
a = np.hstack((add_area, a, add_area))
# -------------end------------ #
_, a_threshold = cv2.threshold(a, 127, 255, 0)
a_erode = cv2.erode(a_threshold, kernel=cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)), iterations=3)
contours, hierarchy = cv2.findContours(a_erode, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
contours = [x for x in contours]
# contours = np.squeeze(contours)
contours.sort(key=lambda c: cv2.contourArea(c), reverse=True)
a_contour = cv2.drawContours(np.zeros(a.shape, np.uint8), contours[0], -1, 255, 2)
# a_base = a_contour[1:-1, 1:-1]
h, w = a.shape[:2]
mask = np.zeros([h + 2, w + 2], np.uint8) # mask 必须行和列都加 2,且必须为 uint8 单通道阵列
cv2.floodFill(a_contour, mask=mask, seedPoint=(0, 0), newVal=255)
a = cv2.add(a, 255 - a_contour)
return cv2.merge((src_bgr, a[10:-10, 10:-10]))
def resize_image_by_min(input_image, esp=600):
"""
将图像缩放为最短边至少为 600 的图像。
:param input_image: 输入图像(OpenCV 矩阵)
:param esp: 缩放后的最短边长
:return: 缩放后的图像,缩放倍率
"""
height, width = input_image.shape[0], input_image.shape[1]
min_border = min(height, width)
if min_border < esp:
if height >= width:
new_width = esp
new_height = height * esp // width
else:
new_height = esp
new_width = width * esp // height
return cv2.resize(input_image, (new_width, new_height), interpolation=cv2.INTER_AREA), new_height / height
else:
return input_image, 1
def rotate_bound(image, angle):
"""
一个旋转函数,输入一张图片和一个旋转角,可以实现不损失图像信息的旋转。
"""
# grab the dimensions of the image and then determine the
# center
(h, w) = image.shape[:2]
(cX, cY) = (w / 2, h / 2)
# grab the rotation matrix (applying the negative of the
# angle to rotate clockwise), then grab the sine and cosine
# (i.e., the rotation components of the matrix)
M = cv2.getRotationMatrix2D((cX, cY), -angle, 1.0)
cos = np.abs(M[0, 0])
sin = np.abs(M[0, 1])
# compute the new bounding dimensions of the image
nW = int((h * sin) + (w * cos))
nH = int((h * cos) + (w * sin))
# adjust the rotation matrix to take into account translation
M[0, 2] += (nW / 2) - cX
M[1, 2] += (nH / 2) - cY
# perform the actual rotation and return the image
return cv2.warpAffine(image, M, (nW, nH)), cos, sin
def rotate_bound_4channels(image, a, angle):
"""
一个旋转函数,输入一张图片和一个旋转角,可以实现不损失图像信息的旋转。
"""
input_image, cos, sin = rotate_bound(image, angle)
new_a, _, _ = rotate_bound(a, angle) # 对做 matte 旋转,以便之后 merge
b, g, r = cv2.split(input_image)
result_image = cv2.merge((b, g, r, new_a)) # 得到抠图结果图的无损旋转结果
# perform the actual rotation and return the image
return input_image, result_image, cos, sin
def draw_picture_dots(image, dots, pen_size=10, pen_color=(0, 0, 255)):
"""
给一张照片上绘制点。
image: Opencv 图像矩阵
dots: 一堆点,形如 [(100,100),(150,100)]
pen_size: 画笔的大小
pen_color: 画笔的颜色
"""
if isinstance(dots, dict):
dots = [v for u, v in dots.items()]
image = image.copy()
dots = list(dots)
for dot in dots:
# print("dot: ", dot)
x = dot[0]
y = dot[1]
cv2.circle(image, (int(x), int(y)), pen_size, pen_color, -1)
return image
def get_modnet_matting(input_image, sess, ref_size=512):
"""
使用 modnet 模型对图像进行抠图处理。
:param input_image: 输入图像(opencv 矩阵)
:param sess: onnxruntime 推理主体
:param ref_size: 缩放参数
:return: 抠图后的图像
"""
input_name = sess.get_inputs()[0].name
output_name = sess.get_outputs()[0].name
im, width, length = read_modnet_image(input_image=input_image, ref_size=ref_size)
matte = sess.run([output_name], {input_name: im})
matte = (matte[0] * 255).astype('uint8')
matte = np.squeeze(matte)
mask = cv2.resize(matte, (width, length), interpolation=cv2.INTER_AREA)
b, g, r = cv2.split(np.uint8(input_image))
output_image = cv2.merge((b, g, r, mask))
return output_image
def detect_distance(value, crop_heigh, max=0.06, min=0.04):
"""
检测人头顶与照片顶部的距离是否在适当范围内。
输入:与顶部的差值
输出:(status, move_value)
status=0 不动
status=1 人脸应向上移动(裁剪框向下移动)
status-2 人脸应向下移动(裁剪框向上移动)
---------------------------------------
value:头顶与照片顶部的距离
crop_heigh: 裁剪框的高度
max: 距离的最大值
min: 距离的最小值
---------------------------------------
"""
value = value / crop_heigh # 头顶往上的像素占图像的比例
if min <= value <= max:
return 0, 0
elif value > max:
# 头顶往上的像素比例高于 max
move_value = value - max
move_value = int(move_value * crop_heigh)
# print("上移{}".format(move_value))
return 1, move_value
else:
# 头顶往上的像素比例低于 min
move_value = min - value
move_value = int(move_value * crop_heigh)
# print("下移{}".format(move_value))
return -1, move_value
@@ -1,47 +1,71 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
r"""
@DATE: 2024/9/5 21:35
@File: layout_calculator.py
@IDE: pycharm
@Description:
布局计算器
"""
import cv2.detail
import numpy as np
def judge_layout(input_width, input_height, PHOTO_INTERVAL_W, PHOTO_INTERVAL_H, LIMIT_BLOCK_W, LIMIT_BLOCK_H):
centerBlockHeight_1, centerBlockWidth_1 = input_height, input_width # 由证件照们组成的一个中心区块(1 代表不转置排列)
centerBlockHeight_2, centerBlockWidth_2 = input_width, input_height # 由证件照们组成的一个中心区块(2 代表转置排列)
def judge_layout(
input_width,
input_height,
PHOTO_INTERVAL_W,
PHOTO_INTERVAL_H,
LIMIT_BLOCK_W,
LIMIT_BLOCK_H,
):
centerBlockHeight_1, centerBlockWidth_1 = (
input_height,
input_width,
) # 由证件照们组成的一个中心区块(1 代表不转置排列)
centerBlockHeight_2, centerBlockWidth_2 = (
input_width,
input_height,
) # 由证件照们组成的一个中心区块(2 代表转置排列)
# 1.不转置排列的情况下:
layout_col_no_transpose = 0 # 行
layout_row_no_transpose = 0 # 列
for i in range(1, 4):
centerBlockHeight_temp = input_height * i + PHOTO_INTERVAL_H * (i-1)
centerBlockHeight_temp = input_height * i + PHOTO_INTERVAL_H * (i - 1)
if centerBlockHeight_temp < LIMIT_BLOCK_H:
centerBlockHeight_1 = centerBlockHeight_temp
layout_row_no_transpose = i
else:
break
for j in range(1, 9):
centerBlockWidth_temp = input_width * j + PHOTO_INTERVAL_W * (j-1)
centerBlockWidth_temp = input_width * j + PHOTO_INTERVAL_W * (j - 1)
if centerBlockWidth_temp < LIMIT_BLOCK_W:
centerBlockWidth_1 = centerBlockWidth_temp
layout_col_no_transpose = j
else:
break
layout_number_no_transpose = layout_row_no_transpose*layout_col_no_transpose
layout_number_no_transpose = layout_row_no_transpose * layout_col_no_transpose
# 2.转置排列的情况下:
layout_col_transpose = 0 # 行
layout_row_transpose = 0 # 列
for i in range(1, 4):
centerBlockHeight_temp = input_width * i + PHOTO_INTERVAL_H * (i-1)
centerBlockHeight_temp = input_width * i + PHOTO_INTERVAL_H * (i - 1)
if centerBlockHeight_temp < LIMIT_BLOCK_H:
centerBlockHeight_2 = centerBlockHeight_temp
layout_row_transpose = i
else:
break
for j in range(1, 9):
centerBlockWidth_temp = input_height * j + PHOTO_INTERVAL_W * (j-1)
centerBlockWidth_temp = input_height * j + PHOTO_INTERVAL_W * (j - 1)
if centerBlockWidth_temp < LIMIT_BLOCK_W:
centerBlockWidth_2 = centerBlockWidth_temp
layout_col_transpose = j
else:
break
layout_number_transpose = layout_row_transpose*layout_col_transpose
layout_number_transpose = layout_row_transpose * layout_col_transpose
if layout_number_transpose > layout_number_no_transpose:
layout_mode = (layout_col_transpose, layout_row_transpose, 2)
@@ -59,19 +83,25 @@ def generate_layout_photo(input_height, input_width):
PHOTO_INTERVAL_W = 30 # 证件照与证件照之间的水平距离
SIDES_INTERVAL_H = 50 # 证件照与画布边缘的垂直距离
SIDES_INTERVAL_W = 70 # 证件照与画布边缘的水平距离
LIMIT_BLOCK_W = LAYOUT_WIDTH - 2*SIDES_INTERVAL_W
LIMIT_BLOCK_H = LAYOUT_HEIGHT - 2*SIDES_INTERVAL_H
LIMIT_BLOCK_W = LAYOUT_WIDTH - 2 * SIDES_INTERVAL_W
LIMIT_BLOCK_H = LAYOUT_HEIGHT - 2 * SIDES_INTERVAL_H
# 2.创建一个 1180x1746 的空白画布
white_background = np.zeros([LAYOUT_HEIGHT, LAYOUT_WIDTH, 3], np.uint8)
white_background.fill(255)
# 3.计算照片的 layout(列、行、横竖朝向),证件照组成的中心区块的分辨率
layout_mode, centerBlockWidth, centerBlockHeight = judge_layout(input_width, input_height, PHOTO_INTERVAL_W,
PHOTO_INTERVAL_H, LIMIT_BLOCK_W, LIMIT_BLOCK_H)
layout_mode, centerBlockWidth, centerBlockHeight = judge_layout(
input_width,
input_height,
PHOTO_INTERVAL_W,
PHOTO_INTERVAL_H,
LIMIT_BLOCK_W,
LIMIT_BLOCK_H,
)
# 4.开始排列组合
x11 = (LAYOUT_WIDTH - centerBlockWidth)//2
y11 = (LAYOUT_HEIGHT - centerBlockHeight)//2
x11 = (LAYOUT_WIDTH - centerBlockWidth) // 2
y11 = (LAYOUT_HEIGHT - centerBlockHeight) // 2
typography_arr = []
typography_rotate = False
if layout_mode[2] == 2:
@@ -80,13 +110,16 @@ def generate_layout_photo(input_height, input_width):
for j in range(layout_mode[1]):
for i in range(layout_mode[0]):
xi = x11 + i*input_width + i*PHOTO_INTERVAL_W
yi = y11 + j*input_height + j*PHOTO_INTERVAL_H
xi = x11 + i * input_width + i * PHOTO_INTERVAL_W
yi = y11 + j * input_height + j * PHOTO_INTERVAL_H
typography_arr.append([xi, yi])
return typography_arr, typography_rotate
def generate_layout_image(input_image, typography_arr, typography_rotate, width=295, height=413):
def generate_layout_image(
input_image, typography_arr, typography_rotate, width=295, height=413
):
LAYOUT_WIDTH = 1746
LAYOUT_HEIGHT = 1180
white_background = np.zeros([LAYOUT_HEIGHT, LAYOUT_WIDTH, 3], np.uint8)
@@ -98,16 +131,8 @@ def generate_layout_image(input_image, typography_arr, typography_rotate, width=
height, width = width, height
for arr in typography_arr:
locate_x, locate_y = arr[0], arr[1]
white_background[locate_y:locate_y+height, locate_x:locate_x+width] = input_image
white_background[locate_y : locate_y + height, locate_x : locate_x + width] = (
input_image
)
return white_background
if __name__ == "__main__":
typography_arr, typography_rotate = generate_layout_photo(input_height=413, input_width=295)
print("typography_arr:", typography_arr)
print("typography_rotate:", typography_rotate)
result_image = generate_layout_image(cv2.imread("./32.jpg"), typography_arr, typography_rotate, width=295, height=413)
cv2.imwrite("./result_image.jpg", result_image)
+58 -7
View File
@@ -8,17 +8,11 @@ r"""
证件照调整
"""
from .context import Context
from .layout_calculator import generate_layout_photo
import hivision.creator.utils as U
from hivision.creator.imageTransform import (
standard_photo_resize,
)
from hivisionai.hycv.vision import (
resize_image_by_min,
)
import numpy as np
import math
import cv2
from hivision.creator.layoutCreate import generate_layout_photo
def adjust_photo(ctx: Context):
@@ -205,3 +199,60 @@ def move(input_image):
png_img = png_img[0 : height - y_high, :, :] # for 循环
png_img = np.concatenate((base, png_img), axis=0)
return png_img, y_high
def standard_photo_resize(input_image: np.array, size):
"""
input_image: 输入图像,即高清照
size: 标准照的尺寸
"""
resize_ratio = input_image.shape[0] / size[0]
resize_item = int(round(input_image.shape[0] / size[0]))
if resize_ratio >= 2:
for i in range(resize_item - 1):
if i == 0:
result_image = cv2.resize(
input_image,
(size[1] * (resize_item - i - 1), size[0] * (resize_item - i - 1)),
interpolation=cv2.INTER_AREA,
)
else:
result_image = cv2.resize(
result_image,
(size[1] * (resize_item - i - 1), size[0] * (resize_item - i - 1)),
interpolation=cv2.INTER_AREA,
)
else:
result_image = cv2.resize(
input_image, (size[1], size[0]), interpolation=cv2.INTER_AREA
)
return result_image
def resize_image_by_min(input_image, esp=600):
"""
将图像缩放为最短边至少为 esp 的图像。
:param input_image: 输入图像(OpenCV 矩阵)
:param esp: 缩放后的最短边长
:return: 缩放后的图像,缩放倍率
"""
height, width = input_image.shape[0], input_image.shape[1]
min_border = min(height, width)
if min_border < esp:
if height >= width:
new_width = esp
new_height = height * esp // width
else:
new_height = esp
new_width = width * esp // height
return (
cv2.resize(
input_image, (new_width, new_height), interpolation=cv2.INTER_AREA
),
new_height / height,
)
else:
return input_image, 1
-527
View File
@@ -1,527 +0,0 @@
# -*- coding: utf-8 -*-
"""
@Time : 2022/8/27 14:17
@Author : cuny
@File : web.py
@Software : PyCharm
@Introduce:
查看包版本等一系列操作
"""
import os
import sys
import json
import shutil
import zipfile
import requests
from argparse import ArgumentParser
from importlib.metadata import version
try: # 加上这个 try 的原因在于本地环境和云函数端的 import 形式有所不同
from qcloud_cos import CosConfig
from qcloud_cos import CosS3Client
except ImportError:
try:
from qcloud_cos_v5 import CosConfig
from qcloud_cos_v5 import CosS3Client
from qcloud_cos.cos_exception import CosServiceError
except ImportError:
raise ImportError("请下载腾讯云 COS 相关代码包:pip install cos-python-sdk-v5")
class HivisionaiParams(object):
"""
定义一些基本常量
"""
# 文件所在路径
# 包名称
package_name = "HY-sdk"
# 腾讯云相关变量
region = "ap-beijing"
zip_key = "HY-sdk/" # zip 存储的云端文件夹路径,这里改了 publish.yml 也需要更改
# 云端用户配置,如果在 cloud_config_save 不存在,就需要下载此文件
user_url = "https://hy-sdk-config-1305323352.cos.ap-beijing.myqcloud.com/sdk-user/user_config.json"
bucket = "cloud-public-static-1306602019"
# 压缩包类型
file_format = ".zip"
# 下载路径(.hivisionai 文件夹路径)
download_path = os.path.expandvars("$HOME")
# zip 文件、zip 解压缩文件的存放路径
save_folder = f"{os.path.expandvars('$HOME')}/.hivisionai/sdk"
# 腾讯云配置文件存放路径
cloud_config_save = f"{os.path.expandvars('$HOME')}/.hivisionai/user_config.json"
# 项目路径
hivisionai_path = os.path.dirname(os.path.dirname(__file__))
# 使用 hivisionai 的路径
getcwd = os.getcwd()
# HY-func 的依赖配置
# 每个依赖会包含三个参数,保存路径(save_path,相对于 HY_func 的路径)、下载 urlurl
functionDependence = {
"configs": [
# --------- 配置文件部分
# _lib
{
"url": "https://hy-sdk-config-1305323352.cos.ap-beijing.myqcloud.com/hy-func/_lib/config/aliyun-human-matting-api.json",
"save_path": "_lib/config/aliyun-human-matting-api.json",
},
{
"url": "https://hy-sdk-config-1305323352.cos.ap-beijing.myqcloud.com/hy-func/_lib/config/megvii-face-plus-api.json",
"save_path": "_lib/config/megvii-face-plus-api.json",
},
{
"url": "https://hy-sdk-config-1305323352.cos.ap-beijing.myqcloud.com/hy-func/_lib/config/volcano-face-change-api.json",
"save_path": "_lib/config/volcano-face-change-api.json",
},
# _service
{
"url": "https://hy-sdk-config-1305323352.cos.ap-beijing.myqcloud.com/hy-func/_service/config/func_error_conf.json",
"save_path": "_service/utils/config/func_error_conf.json",
},
{
"url": "https://hy-sdk-config-1305323352.cos.ap-beijing.myqcloud.com/hy-func/_service/config/service_config.json",
"save_path": "_service/utils/config/service_config.json",
},
# --------- 模型部分
# 模型部分存储在 Notion 文档当中
# https://www.notion.so/HY-func-cc6cc41ba6e94b36b8fa5f5d67d1683f
],
"weights": "https://www.notion.so/HY-func-cc6cc41ba6e94b36b8fa5f5d67d1683f",
}
class HivisionaiUtils(object):
"""
本类为一些基本工具类,包含代码复用相关内容
"""
@staticmethod
def get_client():
"""获取 cos 客户端对象"""
def get_secret():
# 首先判断 cloud_config_save 下是否存在
if not os.path.exists(HivisionaiParams.cloud_config_save):
print("Downloading user_config...")
resp = requests.get(HivisionaiParams.user_url)
open(HivisionaiParams.cloud_config_save, "wb").write(resp.content)
config = json.load(open(HivisionaiParams.cloud_config_save, "r"))
return config["secret_id"], config["secret_key"]
# todo 接入 HY-Auth-Sync
secret_id, secret_key = get_secret()
return CosS3Client(
CosConfig(
Region=HivisionaiParams.region,
Secret_id=secret_id,
Secret_key=secret_key,
)
)
def get_all_versions(self):
"""获取云端的所有版本号"""
def getAllVersion_base():
"""
返回 cos 存储桶内部的某个文件夹的内部名称
ps:如果需要修改默认的存储桶配置,请在代码运行的时候加入代码 s.bucket = 存储桶名称(s 是对象实例)
返回的内容存储在 response["Content"],不过返回的数据大小是有限制的,具体内容还是请看官方文档。
Returns:
[版本列表]
"""
resp = client.list_objects(
Bucket=HivisionaiParams.bucket,
Prefix=HivisionaiParams.zip_key,
Marker=marker,
)
versions_list.extend(
[
x["Key"].split("/")[-1].split(HivisionaiParams.file_format)[0]
for x in resp["Contents"]
if int(x["Size"]) > 0
]
)
if resp["IsTruncated"] == "false": # 接下来没有数据了,就退出
return ""
else:
return resp["NextMarker"]
client = self.get_client()
marker = ""
versions_list = []
while True: # 轮询
try:
marker = getAllVersion_base()
except KeyError as e:
print(e)
raise
if len(marker) == 0: # 没有数据了
break
return versions_list
def get_newest_version(self):
"""获取最新的版本号"""
versions_list = self.get_all_versions()
# reverse=True,降序
versions_list.sort(key=lambda x: int(x.split(".")[-1]), reverse=True)
versions_list.sort(key=lambda x: int(x.split(".")[-2]), reverse=True)
versions_list.sort(key=lambda x: int(x.split(".")[-3]), reverse=True)
return versions_list[0]
def download_version(self, v):
"""
在存储桶中下载文件,将下载好的文件解压至本地
Args:
v: 版本号,x.x.x
Returns:
None
"""
file_name = v + HivisionaiParams.file_format
client = self.get_client()
print(f"Download to {HivisionaiParams.save_folder}...")
try:
resp = client.get_object(
HivisionaiParams.bucket, HivisionaiParams.zip_key + "/" + file_name
)
contents = resp["Body"].get_raw_stream().read()
except CosServiceError:
print(f"[{file_name}.zip] does not exist, please check your version!")
sys.exit()
if not os.path.exists(HivisionaiParams.save_folder):
os.makedirs(HivisionaiParams.save_folder)
open(os.path.join(HivisionaiParams.save_folder, file_name), "wb").write(
contents
)
print("Download success!")
@staticmethod
def download_dependence(path=None):
"""
一键下载 HY-sdk 所需要的所有依赖,需要注意的是,本方法必须在运行 pip install 之后使用(运行完 pip install 之后才会出现 hivisionai 文件夹)
Args:
path: 文件路径,精确到 hivisionai 文件夹的上一个目录,如果为 None,则默认下载到 python 环境下 hivisionai 安装的目录
Returns:
下载相应内容到指定位置
"""
# print("指定的下载路径:", path) # 此时在 path 路径下必然存在一个 hivisionai 文件夹
# print("系统安装的 hivisionai 库的路径:", HivisionaiParams.hivisionai_path)
print("Dependence downloading...")
if path is None:
path = HivisionaiParams.hivisionai_path
# ----------------下载 mtcnn 模型文件
mtcnn_path = os.path.join(path, "hivisionai/hycv/mtcnn_onnx/weights")
base_url = "https://linimages.oss-cn-beijing.aliyuncs.com/"
onnx_files = ["pnet.onnx", "rnet.onnx", "onet.onnx"]
print(f"Downloading mtcnn model in {mtcnn_path}")
if not os.path.exists(mtcnn_path):
os.mkdir(mtcnn_path)
for onnx_file in onnx_files:
if not os.path.exists(os.path.join(mtcnn_path, onnx_file)):
# download onnx model
onnx_url = base_url + onnx_file
print("Downloading Onnx Model in:", onnx_url)
r = requests.get(onnx_url, stream=True)
if r.status_code == 200:
open(os.path.join(mtcnn_path, onnx_file), "wb").write(
r.content
) # 将内容写入文件
print(f"Download finished -- {onnx_file}")
del r
# ----------------
print("Dependence download finished...")
class HivisionaiApps(object):
"""
本类为 app 对外暴露的接口,为了代码规整性,这里使用类来对暴露接口进行调整
"""
@staticmethod
def show_cloud_version():
"""查看在 cos 中的所有 HY-sdk 版本"""
print("Connect to COS...")
versions_list = hivisionai_utils.get_all_versions()
# reverse=True,降序
versions_list.sort(key=lambda x: int(x.split(".")[-1]), reverse=True)
versions_list.sort(key=lambda x: int(x.split(".")[-2]), reverse=True)
versions_list.sort(key=lambda x: int(x.split(".")[-3]), reverse=True)
if len(versions_list) == 0:
print("There is no version currently, please release it first!")
sys.exit()
versions = "The currently existing versions (Keep 10): \n"
for i, v in enumerate(versions_list):
versions += str(v) + " "
if i == 9:
break
print(versions)
@staticmethod
def upgrade(v: str, enforce: bool = False, save_cached: bool = False):
"""
自动升级 HY-sdk 到指定版本
Args:
v: 指定的版本号,格式为 x.x.x
enforce: 是否需要强制执行更新命令
save_cached: 是否保存下载的 wheel 文件,默认为否
Returns:
None
"""
def check_format():
# noinspection PyBroadException
try:
major, minor, patch = v.split(".")
int(major)
int(minor)
int(patch)
except Exception as e:
print(f"Illegal version number!\n{e}")
pass
print("Upgrading, please wait a moment...")
if v == "-1":
v = hivisionai_utils.get_newest_version()
# 检查 format 的格式
check_format()
if v == version(HivisionaiParams.package_name) and not enforce:
print(f"Current version: {v} already exists, skip installation.")
sys.exit()
hivisionai_utils.download_version(v)
# 下载完毕(下载至 save_folder),解压文件
target_zip = os.path.join(HivisionaiParams.save_folder, f"{v}.zip")
assert zipfile.is_zipfile(
target_zip
), "Decompression failed, and the target was not a zip file."
new_dir = target_zip.replace(".zip", "") # 解压的文件名
if os.path.exists(new_dir): # 判断文件夹是否存在
shutil.rmtree(new_dir)
os.mkdir(new_dir) # 新建文件夹
f = zipfile.ZipFile(target_zip)
f.extractall(new_dir) # 提取 zip 文件
print("Decompressed, begin to install...")
os.system(f'pip3 install {os.path.join(new_dir, "**.whl")}')
# 开始自动下载必要的模型依赖
hivisionai_utils.download_dependence()
# 安装完毕,如果 save_cached 为真,删除"$HOME/.hivisionai/sdk"内部的所有文件元素
if save_cached is True:
os.system(f"rm -rf {HivisionaiParams.save_folder}/**")
@staticmethod
def export(path):
"""
输出最新版本的文件到命令运行的 path 目录
Args:
path: 用户输入的路径
Returns:
输出最新的 hivisionai 到 path 目录
"""
# print(f"当前路径:{os.path.join(HivisionaiParams.getcwd, path)}")
# print(f"文件路径:{os.path.dirname(__file__)}")
export_path = os.path.join(HivisionaiParams.getcwd, path)
# 判断输出路径存不存在,如果不存在,就报错
assert os.path.exists(export_path), f"{export_path} dose not Exists!"
v = hivisionai_utils.get_newest_version()
# 下载文件到.hivisionai/sdk 当中
hivisionai_utils.download_version(v)
# 下载完毕(下载至 save_folder),解压文件
target_zip = os.path.join(HivisionaiParams.save_folder, f"{v}.zip")
assert zipfile.is_zipfile(
target_zip
), "Decompression failed, and the target was not a zip file."
new_dir = os.path.basename(target_zip.replace(".zip", "")) # 解压的文件名
new_dir = os.path.join(export_path, new_dir) # 解压的文件路径
if os.path.exists(new_dir): # 判断文件夹是否存在
shutil.rmtree(new_dir)
os.mkdir(new_dir) # 新建文件夹
f = zipfile.ZipFile(target_zip)
f.extractall(new_dir) # 提取 zip 文件
print("Decompressed, begin to export...")
# 强制删除 bin/hivisionai 和 hivisionai/以及 HY_sdk-**
bin_path = os.path.join(export_path, "bin")
hivisionai_path = os.path.join(export_path, "hivisionai")
sdk_path = os.path.join(export_path, "HY_sdk-**")
os.system(f"rm -rf {bin_path} {hivisionai_path} {sdk_path}")
# 删除完毕,开始 export
os.system(f'pip3 install {os.path.join(new_dir, "**.whl")} -t {export_path}')
hivisionai_utils.download_dependence(export_path)
# 将下载下来的文件夹删除
os.system(f"rm -rf {target_zip} && rm -rf {new_dir}")
print("Done.")
@staticmethod
def hy_func_init(force):
"""
在 HY-func 目录下使用 hivisionai --init,可以自动将需要的依赖下载到指定位置
不过对于比较大的模型——修复模型而言,需要手动下载
Args:
force: 如果 force 为 True,则会强制重新下载所有的内容,包括修复模型这种比较大的模型
Returns:
程序执行完毕,会将一些必要的依赖也下载完毕
"""
cwd = HivisionaiParams.getcwd
# 判断当前文件夹是否是 HY-func
dirName = os.path.basename(cwd)
assert dirName == "HY-func", "请在正确的文件目录下初始化 HY-func!"
# 需要下载的内容会存放在 HivisionaiParams 的 functionDependence 变量下
functionDependence = HivisionaiParams.functionDependence
# 下载配置文件
configs = functionDependence["configs"]
print("正在下载配置文件...")
for config in configs:
if not force and os.path.exists(config["save_path"]):
print(f"[pass]: {os.path.basename(config['url'])}")
continue
print(f"[Download]: {config['url']}")
resp = requests.get(config["url"])
# json 文件存储在 text 区域,但是其他的不一定
open(os.path.join(cwd, config["save_path"]), "w").write(resp.text)
# 其他文件,提示访问 notion 文档
print(
f"[NOTICE]: 一切准备就绪,请访问下面的文档下载剩下的模型文件:\n{functionDependence['weights']}"
)
@staticmethod
def hy_func_deploy(functionName: str = None, functionPath: str = None):
"""
在 HY-func 目录下使用此命令,并且随附功能函数的名称,就可以将 HY-func 的部署版放到桌面上
但是需要注意的是,本方式不适合修复功能使用,修复功能依旧需要手动制作镜像
Args:
functionName: 功能函数名称
functionPath: 需要注册的 HY-func 路径
Returns:
程序执行完毕,桌面会出现一个同名文件夹
"""
# 为了代码撰写的方便,这里仅仅把模型文件删除,其余配置文件保留
# 为了实现在任意位置输入 hivisionai --deploy funcName 都能成功,在使用前需要在.hivisionai/user_config.json 中注册
# print(functionName, functionPath)
if functionPath is not None:
# 更新/添加路径
# functionPath 为相对于使用路径的路径
assert os.path.basename(functionPath) == "HY-func", "所指向路径非 HY-func!"
func_path = os.path.join(HivisionaiParams.getcwd, functionPath)
assert os.path.join(func_path), f"路径不存在:{func_path}"
# functionPath 的路径写到 user_config 当中
user_config = json.load(open(HivisionaiParams.cloud_config_save, "rb"))
user_config["func_path"] = func_path
open(HivisionaiParams.cloud_config_save, "w").write(json.dumps(user_config))
print("HY-func 全局路径保存成功!")
try:
user_config = json.load(open(HivisionaiParams.cloud_config_save, "rb"))
func_path = user_config["func_path"]
except KeyError:
return print("请先使用-p 命令注册全局 HY-func 路径!")
# 此时 func_path 必然存在
# print(os.listdir(func_path))
assert functionName in os.listdir(func_path), functionName + "功能不存在!"
func_path_deploy = os.path.join(func_path, functionName)
# 开始复制文件到指定目录
# 我们默认移动到 Desktop 目录下,如果没有此目录,需要先创建一个
target_dir = os.path.join(HivisionaiParams.download_path, "Desktop")
assert os.path.exists(target_dir), (
target_dir + "文件路径不存在,你需要先创建一下!"
)
# 开始移动
target_dir = os.path.join(target_dir, functionName)
print("正在复制需要部署的文件...")
os.system(f"rm -rf {target_dir}")
os.system(f"cp -rf {func_path_deploy} {target_dir}")
os.system(f"cp -rf {os.path.join(func_path, '_lib')} {target_dir}")
os.system(f"cp -rf {os.path.join(func_path, '_service')} {target_dir}")
# 生成最新的 hivisionai
print("正在生成 hivisionai 代码包...")
os.system(f"hivisionai -t {target_dir}")
# 移动完毕,删除模型文件
print("移动完毕,正在删除不需要的文件...")
# 模型文件
os.system(f"rm -rf {os.path.join(target_dir, '_lib', 'weights', '**')}")
# hivisionai 生成时的多余文件
os.system(
f"rm -rf {os.path.join(target_dir, 'bin')} {os.path.join(target_dir, 'HY_sdk**')}"
)
print("部署文件生成成功,你可以开始部署了!")
hivisionai_utils = HivisionaiUtils()
def entry_point():
parser = ArgumentParser()
# 查看版本号
parser.add_argument(
"-v",
"--version",
action="store_true",
help="View the current HY-sdk version, which does not represent the final cloud version.",
)
# 自动更新
parser.add_argument(
"-u",
"--upgrade",
nargs="?",
const="-1",
type=str,
help="Automatically update HY-sdk to the latest version",
)
# 查找云端的 HY-sdk 版本
parser.add_argument(
"-l",
"--list",
action="store_true",
help="Find HY-sdk versions of the cloud, and keep up to ten",
)
# 下载云端的版本到本地路径
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",
)
# 强制更新附带参数,当一个功能需要强制执行一遍的时候,需要附带此参数
parser.add_argument(
"-f",
"--force",
action="store_true",
help="Enforcement of other functions, execution of a single parameter is meaningless",
)
# 初始化 HY-func
parser.add_argument("--init", action="store_true", help="Initialization HY-func")
# 部署 HY-func
parser.add_argument(
"-d", "--deploy", nargs="?", const="-1", type=str, help="Deploy HY-func"
)
# 涉及注册一些自定义内容的时候,需要附带此参数,并写上自定义内容
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.",
)
args = parser.parse_args()
if args.version:
print(version(HivisionaiParams.package_name))
sys.exit()
if args.upgrade:
HivisionaiApps.upgrade(args.upgrade, args.force)
sys.exit()
if args.list:
HivisionaiApps.show_cloud_version()
sys.exit()
if args.export:
HivisionaiApps.export(args.export)
sys.exit()
if args.init:
HivisionaiApps.hy_func_init(args.force)
sys.exit()
if args.deploy:
HivisionaiApps.hy_func_deploy(args.deploy, args.param)
if __name__ == "__main__":
entry_point()
View File
-406
View File
@@ -1,406 +0,0 @@
"""
焕影小程序功能服务端的基本工具函数,以类的形式封装
"""
try: # 加上这个 try 的原因在于本地环境和云函数端的 import 形式有所不同
from qcloud_cos import CosConfig
from qcloud_cos import CosS3Client
except ImportError:
try:
from qcloud_cos_v5 import CosConfig
from qcloud_cos_v5 import CosS3Client
except ImportError:
raise ImportError("请下载腾讯云 COS 相关代码包:pip install cos-python-sdk-v5")
import requests
import datetime
import json
from .error import ProcessError
import os
local_path_ = os.path.dirname(__file__)
class GetConfig(object):
@staticmethod
def hy_sdk_client(Id:str, Key:str):
# 从 cos 中寻找文件
REGION: str = 'ap-beijing'
TOKEN = None
SCHEME: str = 'https'
BUCKET: str = 'hy-sdk-config-1305323352'
client_config = CosConfig(Region=REGION,
SecretId=Id,
SecretKey=Key,
Token=TOKEN,
Scheme=SCHEME)
return CosS3Client(client_config), BUCKET
def load_json(self, path:str, default_download=False):
try:
if os.path.isdir(path):
raise ProcessError("请输入具体的配置文件路径,而非文件夹!")
if default_download is True:
print(f"\033[34m 默认强制重新下载配置文件...\033[0m")
raise FileNotFoundError
with open(path) as f:
config = json.load(f)
return config
except FileNotFoundError:
dir_name = os.path.dirname(path)
try:
os.makedirs(dir_name)
except FileExistsError:
pass
base_name = os.path.basename(path)
print(f"\033[34m 正在从 COS 中下载配置文件...\033[0m")
print(f"\033[31m 请注意,接下来会在{dir_name}路径下生成文件{base_name}...\033[0m")
Id = input("请输入 SecretId:")
Key = input("请输入 SecretKey:")
client, bucket = self.hy_sdk_client(Id, Key)
data_bytes = client.get_object(Bucket=bucket,Key=base_name)["Body"].get_raw_stream().read()
data = json.loads(data_bytes.decode("utf-8"))
# data["SecretId"] = Id # 未来可以把这个加上
# data["SecretKey"] = Key
with open(path, "w") as f:
data_str = json.dumps(data, ensure_ascii=False)
# 如果 ensure_ascii 是 true (即默认值),输出保证将所有输入的非 ASCII 字符转义。
# 如果 ensure_ascii 是 false,这些字符会原样输出。
f.write(data_str)
f.close()
print(f"\033[32m 配置文件保存成功\033[0m")
return data
except json.decoder.JSONDecodeError:
print(f"\033[31m WARNING: 配置文件为空!\033[0m")
return {}
def load_file(self, cloud_path:str, local_path:str):
"""
从 COS 中下载文件到本地,本函数将会被默认执行的,在使用的时候建议加一些限制。
:param cloud_path: 云端的文件路径
:param local_path: 将云端文件保存在本地的路径
"""
if os.path.isdir(cloud_path):
raise ProcessError("请输入具体的云端文件路径,而非文件夹!")
if os.path.isdir(local_path):
raise ProcessError("请输入具体的本地文件路径,而非文件夹!")
dir_name = os.path.dirname(local_path)
base_name = os.path.basename(local_path)
try:
os.makedirs(dir_name)
except FileExistsError:
pass
cloud_name = os.path.basename(cloud_path)
print(f"\033[31m 请注意,接下来会在{dir_name}路径下生成文件{base_name}\033[0m")
Id = input("请输入 SecretId:")
Key = input("请输入 SecretKey:")
client, bucket = self.hy_sdk_client(Id, Key)
print(f"\033[34m 正在从 COS 中下载文件:{cloud_name}, 此过程可能耗费一些时间...\033[0m")
data_bytes = client.get_object(Bucket=bucket,Key=cloud_path)["Body"].get_raw_stream().read()
# data["SecretId"] = Id # 未来可以把这个加上
# data["SecretKey"] = Key
with open(local_path, "wb") as f:
# 如果 ensure_ascii 是 true (即默认值),输出保证将所有输入的非 ASCII 字符转义。
# 如果 ensure_ascii 是 false,这些字符会原样输出。
f.write(data_bytes)
f.close()
print(f"\033[32m 文件保存成功\033[0m")
class CosConf(GetConfig):
"""
从安全的角度出发,将一些默认配置文件上传至 COS 中,接下来使用 COS 和它的子类的时候,在第一次使用时需要输入 Cuny 给的 id 和 key
用于连接 cos 存储桶,下载配置文件。
当然,在 service_default_download = False 的时候,如果在运行路径下已经有 conf/service_config.json 文件了,
那么就不用再次下载了,也不用输入 id 和 key
事实上这只需要运行一次,因为配置文件将会被下载至源码文件夹中
如果要自定义路径,请在继承的子类中编写__init__函数,将 service_path 定向到指定路径
"""
def __init__(self) -> None:
# 下面这些参数是类的共享参数
self.__SECRET_ID: str = None # 服务的 id
self.__SECRET_KEY: str = None # 服务的 key
self.__REGION: str = None # 服务的存储桶地区
self.__TOKEN: str = None # 服务的 token,目前一直是 None
self.__SCHEME: str = None # 服务的访问协议,默认实际上是 https
self.__BUCKET: str = None # 服务的存储桶
self.__SERVICE_CONFIG: dict = None # 服务的配置文件
self.service_path: str = f"{local_path_}/conf/service_config.json"
# 配置文件路径,默认是函数运行的路径下的 conf 文件夹
self.service_default_download = False # 是否在每次访问配置的时候都重新下载文件
@property
def service_config(self):
if self.__SERVICE_CONFIG is None or self.service_default_download is True:
self.__SERVICE_CONFIG = self.load_json(self.service_path, self.service_default_download)
return self.__SERVICE_CONFIG
@property
def client(self):
client_config = CosConfig(Region=self.region,
SecretId=self.secret_id,
SecretKey=self.secret_key,
Token=self.token,
Scheme=self.scheme)
return CosS3Client(client_config)
def get_key(self, key:str):
try:
data = self.service_config[key]
if data == "None":
return None
else:
return data
except KeyError:
print(f"\033[31m 没有对应键值{key},默认返回 None\033[0m")
return None
@property
def secret_id(self):
if self.__SECRET_ID is None:
self.__SECRET_ID = self.get_key("SECRET_ID")
return self.__SECRET_ID
@secret_id.setter
def secret_id(self, value:str):
self.__SECRET_ID = value
@property
def secret_key(self):
if self.__SECRET_KEY is None:
self.__SECRET_KEY = self.get_key("SECRET_KEY")
return self.__SECRET_KEY
@secret_key.setter
def secret_key(self, value:str):
self.__SECRET_KEY = value
@property
def region(self):
if self.__REGION is None:
self.__REGION = self.get_key("REGION")
return self.__REGION
@region.setter
def region(self, value:str):
self.__REGION = value
@property
def token(self):
# if self.__TOKEN is None:
# self.__TOKEN = self.get_key("TOKEN")
# 这里可以注释掉
return self.__TOKEN
@token.setter
def token(self, value:str):
self.__TOKEN= value
@property
def scheme(self):
if self.__SCHEME is None:
self.__SCHEME = self.get_key("SCHEME")
return self.__SCHEME
@scheme.setter
def scheme(self, value:str):
self.__SCHEME = value
@property
def bucket(self):
if self.__BUCKET is None:
self.__BUCKET = self.get_key("BUCKET")
return self.__BUCKET
@bucket.setter
def bucket(self, value):
self.__BUCKET = value
def downloadFile_COS(self, key, bucket:str=None, if_read:bool=False):
"""
从 COS 下载对象 (二进制数据), 如果下载失败就返回 None
"""
CosBucket = self.bucket if bucket is None else bucket
try:
# 将本类的 Debug 继承给抛弃了
# self.debug_print(f"Download from {CosBucket}", font_color="blue")
obj = self.client.get_object(
Bucket=CosBucket,
Key=key
)
if if_read is True:
data = obj["Body"].get_raw_stream().read() # byte
return data
else:
return obj
except Exception as e:
print(f"\033[31m 下载失败!错误描述:{e}\033[0m")
return None
def showFileList_COS_base(self, key, bucket, marker:str=""):
"""
返回 cos 存储桶内部的某个文件夹的内部名称
:param key: cos 云端的存储路径
:param bucket: cos 存储桶名称,如果没指定名称(None)就会寻找默认的存储桶
:param marker: 标记,用于记录上次查询到哪里了
ps:如果需要修改默认的存储桶配置,请在代码运行的时候加入代码 s.bucket = 存储桶名称(s 是对象实例)
返回的内容存储在 response["Content"],不过返回的数据大小是有限制的,具体内容还是请看官方文档。
"""
response = self.client.list_objects(
Bucket=bucket,
Prefix=key,
Marker=marker
)
return response
def showFileList_COS(self, key, bucket:str=None)->list:
"""
实现查询存储桶中所有对象的操作,因为 cos 的 sdk 有返回数据包大小的限制,所以我们需要进行一定的改动
"""
marker = ""
file_list = []
CosBucket = self.bucket if bucket is None else bucket
while True: # 轮询
response = self.showFileList_COS_base(key, CosBucket, marker)
try:
file_list.extend(response["Contents"])
except KeyError as e:
print(e)
raise
if response['IsTruncated'] == 'false': # 接下来没有数据了,就退出
break
marker = response['NextMarker']
return file_list
def uploadFile_COS(self, buffer, key, bucket:str=None):
"""
从 COS 上传数据,需要注意的是必须得是二进制文件
"""
CosBucket = self.bucket if bucket is None else bucket
try:
self.client.put_object(
Bucket=CosBucket,
Body=buffer,
Key=key
)
return True
except Exception as e:
print(e)
return False
class FuncDiary(CosConf):
filter_dict = {"60a5e13da00e6e0001fd53c8": "Cuny",
"612c290f3a9af4000170faad": "守望平凡",
"614de96e1259260001506d6c": "林泽毅 - 焕影一新"}
def __init__(self, func_name: str, uid: str, error_conf_path: str = f"{local_path_}/conf/func_error_conf.json"):
"""
日志类的实例化
Args:
func_name: 功能名称,影响了日志投递的路径
"""
super().__init__()
# 配置文件路径,默认是函数运行的路径下的 conf 文件夹
self.service_path: str = os.path.join(os.path.dirname(error_conf_path), "service_config.json")
self.error_dict = self.load_json(path=error_conf_path)
self.__up: str = f"wx/invokeFunction_c/{datetime.datetime.now().strftime('%Y/%m/%d/%H')}/{func_name}/"
self.func_name: str = func_name
# 下面这个属性是的日志名称的前缀
self.__start_time = datetime.datetime.now().timestamp()
h_point = datetime.datetime.strptime(datetime.datetime.now().strftime('%Y/%m/%d/%H'), '%Y/%m/%d/%H')
h_point_timestamp = h_point.timestamp()
self.__prefix = int(self.__start_time - h_point_timestamp).__str__() + "_"
self.__uid = uid
self.__diary = None
def __str__(self):
return f"<{self.func_name}> DIARY for {self.__uid}"
@property
def content(self):
return self.__diary
@content.setter
def content(self, value: str):
if not isinstance(value, dict):
raise TypeError("content 只能是字典!")
if "status" in value:
raise KeyError("status 字段已被默认占用,请在日志信息中更换字段名称!")
if self.__diary is None:
self.__diary = value
else:
raise PermissionError("为了减小日志对整体代码的影响,<content>只能被覆写一次!")
def uploadDiary_COS(self, status_id: str, suffix: str = "", bucket: str = "hy-hcy-data-logs-1306602019"):
if self.__diary is None:
self.__diary = {"status": self.error_dict[status_id]}
if status_id == "0000":
self.__up += f"True/{self.__uid}/"
else:
self.__up += f"False/{self.__uid}/"
interval = int(10 * (datetime.datetime.now().timestamp() - self.__start_time))
prefix = self.__prefix + status_id + "_" + interval.__str__()
self.__diary["status"] = self.error_dict[status_id]
name = prefix + "_" + suffix if len(suffix) != 0 else prefix
self.uploadFile_COS(buffer=json.dumps(self.__diary), key=self.__up + name, bucket=bucket)
print(f"{self}上传成功。")
class ResponseWebSocket(CosConf):
# 网关推送地址
__HOST:str = None
@property
def sendBackHost(self):
if self.__HOST is None:
self.__HOST = self.get_key("HOST")
return self.__HOST
@sendBackHost.setter
def sendBackHost(self, value):
self.__HOST = value
def sendMsg_toWebSocket(self, message,connectionID:str = None):
if connectionID is not None:
retmsg = {'websocket': {}}
retmsg['websocket']['action'] = "data send"
retmsg['websocket']['secConnectionID'] = connectionID
retmsg['websocket']['dataType'] = 'text'
retmsg['websocket']['data'] = json.dumps(message)
requests.post(self.sendBackHost, json=retmsg)
print("send success!")
else:
pass
@staticmethod
def create_Msg(status, msg):
"""
本方法用于创建一个用于发送到 WebSocket 客户端的数据
输入的信息部分,需要有如下几个参数:
1. id,固定为"return-result"
2. status,如果输入为 1 则 status=true, 如果输入为 -1 则 status=false
3. obj_key, 图片的云端路径,这是输入的 msg 本身自带的
"""
msg['status'] = "false" if status == -1 else 'true' # 其实最好还是用 bool
msg['id'] = "async-back-msg"
msg['type'] = "funcType"
msg["format"] = "imageType"
return msg
# 功能服务类
class Service(ResponseWebSocket):
"""
服务的主函数,封装了 cos 上传/下载功能以及与 api 网关的一键通讯
将类的实例变成一个可被调用的对象,在服务运行的时候,只需要运行该对象即可
当然,因为是类,所以支持继承和修改
"""
@classmethod
def process(cls, *args, **kwargs):
"""
处理函数,在使用的时候请将之重构
"""
pass
@classmethod
def __call__(cls, *args, **kwargs):
pass
-337
View File
@@ -1,337 +0,0 @@
import os
import pymongo
import datetime
import time
from .cloudService import GetConfig
local_path = os.path.dirname(__file__)
class DBUtils(GetConfig):
"""
从安全的角度出发,将一些默认配置文件上传至 COS 中,接下来使用 COS 和它的子类的时候,在第一次使用时需要输入 Cuny 给的 id 和 key
用于连接数据库等对象
当然,在 db_default_download = False 的时候,如果在运行路径下已经有配置文件了,
那么就不用再次下载了,也不用输入 id 和 key
事实上这只需要运行一次,因为配置文件将会被下载至源码文件夹中
如果要自定义路径,请在继承的子类中编写__init__函数,将 service_path 定向到指定路径
"""
__BASE_DIR: dict = None
__PARAMS_DIR: dict = None
db_base_path: str = f"{local_path}/conf/base_config.json"
db_params_path: str = f"{local_path}/conf/params.json"
db_default_download: bool = False
@property
def base_config(self):
if self.__BASE_DIR is None:
self.__BASE_DIR = self.load_json(self.db_base_path, self.db_default_download)
return self.__BASE_DIR
@property
def db_config(self):
return self.base_config["database_config"]
@property
def params_config(self):
if self.__PARAMS_DIR is None:
self.__PARAMS_DIR = self.load_json(self.db_params_path, self.db_default_download)
return self.__PARAMS_DIR
@property
def size_dir(self):
return self.params_config["size_config"]
@property
def func_dir(self):
return self.params_config["func_config"]
@property
def wx_config(self):
return self.base_config["wx_config"]
def get_dbClient(self):
return pymongo.MongoClient(self.db_config["connect_url"])
@staticmethod
def get_time(yyyymmdd=None, delta_date=0):
"""
给出当前的时间
:param yyyymmdd: 以 yyyymmdd 给出的日期时间
:param delta_date: 获取减去 delta_day 后的时间,默认为 0 就是当天
时间格式:yyyy_mm_dd
"""
if yyyymmdd is None:
now_time = (datetime.datetime.now() - datetime.timedelta(delta_date)).strftime("%Y-%m-%d")
return now_time
# 输入了 yyyymmdd 的数据和 delta_date,通过这两个数据返回距离 yyyymmdd delta_date 天的时间
pre_time = datetime.datetime(int(yyyymmdd[0:4]), int(yyyymmdd[4:6]), int(yyyymmdd[6:8]))
return (pre_time - datetime.timedelta(delta_date)).strftime("%Y-%m-%d")
# 获得时间戳
def get_timestamp(self, date_time:str=None) -> int:
"""
输入的日期形式为:"2021-11-29 16:39:45.999"
真正必须输入的是前十个字符,及精确到日期,后面的时间可以不输入,不输入则默认置零
"""
def standardDateTime(dt:str) -> str:
"""
规范化时间字符串
"""
if len(dt) < 10:
raise ValueError("你必须至少输入准确到天的日期!比如:2021-11-29")
elif len(dt) == 10:
return dt + " 00:00:00.0"
else:
try:
date, time = dt.split(" ")
except ValueError:
raise ValueError("你只能也必须在日期与具体时间之间增加一个空格,其他地方不能出现空格!")
while len(time) < 10:
if len(time) in (2, 5):
time += ":"
elif len(time) == 8:
time += "."
else:
time += "0"
return date + " " + time
if date_time is None:
# 默认返回当前时间 (str), date_time 精确到毫秒
date_time = datetime.datetime.now()
# 转换成时间戳
else:
date_time = standardDateTime(dt=date_time)
date_time = datetime.datetime.strptime(date_time, "%Y-%m-%d %H:%M:%S.%f")
timestamp_ms = int(time.mktime(date_time.timetuple()) * 1000.0 + date_time.microsecond / 1000.0)
return timestamp_ms
@staticmethod
def get_standardTime(yyyy_mm_dd: str):
return yyyy_mm_dd[0:4] + yyyy_mm_dd[5:7] + yyyy_mm_dd[8:10]
def find_oneDay_data(self, db_name: str, collection_name: str, date: str = None) -> dict:
"""
获取指定天数的数据,如果 date is None,就自动寻找距今最近的有数据的那一天的数据
"""
df = None # 应该被返回的数据
collection = self.get_dbClient()[db_name][collection_name]
if date is None: # 自动寻找前几天的数据,最多三十天
for delta_date in range(1, 31):
date_yyyymmdd = self.get_standardTime(self.get_time(delta_date=delta_date))
filter_ = {"date": date_yyyymmdd}
df = collection.find_one(filter=filter_)
if df is not None:
del df["_id"]
break
else:
filter_ = {"date": date}
df = collection.find_one(filter=filter_)
if df is not None:
del df["_id"]
return df
def find_daysData_byPeriod(self, date_period: tuple, db_name: str, col_name: str):
# 给出一个指定的范围日期,返回相应的数据 (日期的两头都会被寻找)
# 这个函数我们默认数据库中的数据是连续的,即不会出现在 20211221 到 20211229 之间有一天没有数据的情况
if len(date_period) != 2:
raise ValueError("date_period 数据结构:(开始日期,截止日期)")
start, end = date_period # yyyymmdd
delta_date = int(end) - int(start)
if delta_date < 0:
raise ValueError("传入的日期有误!")
collection = self.get_dbClient()[db_name][col_name]
date = start
while int(date) <= int(end):
yield collection.find_one(filter={"date": date})
date = self.get_standardTime(self.get_time(date, -1))
@staticmethod
def find_biggest_valueDict(dict_: dict):
# 寻找字典中数值最大的字段,要求输入的字典的字段值全为数字
while len(dict_) > 0:
max_value = 0
p = None
for key in dict_:
if dict_[key] > max_value:
p = key
max_value = dict_[key]
yield p, max_value
del dict_[p]
def copy_andAdd_dict(self, dict_base, dict_):
# 深度拷贝字典,将后者赋值给前者
# 如果后者的键名在前者已经存在,则直接相加。这就要求两者的数据是数值型
for key in dict_:
if key not in dict_base:
dict_base[key] = dict_[key]
else:
if isinstance(dict_[key], int) or isinstance(dict_[key], float):
dict_base[key] = round(dict_[key] + dict_base[key], 2)
else:
dict_base[key] = self.copy_andAdd_dict(dict_base[key], dict_[key])
return dict_base
@staticmethod
def compare_data(dict1: dict, dict2: dict, suffix: str, save: int, **kwargs):
"""
有两个字典,并且通过 kwargs 会传输一个新的字典,根据字典中的键值我们进行比对,处理成相应的数据格式
并且在 dict1 中,生成一个新的键值,为 kwargs 中的元素+suffix
save:保留几位小数
"""
new_dict = dict1.copy()
for key in kwargs:
try:
if kwargs[key] not in dict2 or int(dict2[kwargs[key]]) == -1 or float(dict1[kwargs[key]]) <= 0.0:
# 数据不存在
data_new = 5002
else:
try:
data_new = round(
((float(dict1[kwargs[key]]) - float(dict2[kwargs[key]])) / float(dict2[kwargs[key]])) * 100
, save)
except ZeroDivisionError:
data_new = 5002
if data_new == 0.0:
data_new = 0
except TypeError as e:
print(e)
data_new = 5002 # 如果没有之前的数据,默认返回 0
new_dict[kwargs[key] + suffix] = data_new
return new_dict
@staticmethod
def sum_dictList_byKey(dictList: list, **kwargs) -> dict:
"""
有一个列表,列表中的元素为字典,并且所有字典都有一个键值为 key 的字段,字段值为数字
我们将每一个字典的 key 字段提取后相加,得到该字段值之和。
"""
sum_num = {}
if kwargs is None:
raise ImportError("Please input at least ONE key")
for key in kwargs:
sum_num[kwargs[key]] = 0
for dict_ in dictList:
if not isinstance(dict_, dict):
raise TypeError("object is not DICT!")
for key in kwargs:
sum_num[kwargs[key]] += dict_[kwargs[key]]
return sum_num
@staticmethod
def sum_2ListDict(list_dict1: list, list_dict2: list, key_name, data_name):
"""
有两个列表,列表内的元素为字典,我们根据 key 所对应的键值寻找列表中键值相同的两个元素,将他们的 data 对应的键值相加
生成新的列表字典 (其余键值被删除)
key 仅在一个列表中存在,则直接加入新的列表字典
"""
sum_list = []
def find_sameKey(kn, key_, ld: list) -> int:
for dic_ in ld:
if dic_[kn] == key_:
post_ = ld.index(dic_)
return post_
return -1
for dic in list_dict1:
key = dic[key_name] # 键名
post = find_sameKey(key_name, key, list_dict2) # 在 list2 中寻找相同的位置
data = dic[data_name] + list_dict2[post][data_name] if post != -1 else dic[data_name]
sum_list.append({key_name: key, data_name: data})
return sum_list
@staticmethod
def find_biggest_dictList(dictList: list, key: str = "key", data: str = "value"):
"""
有一个列表,里面每一个元素都是一个字典
这些字典有一些共通性质,那就是里面都有一个 key 键名和一个 data 键名,后者的键值必须是数字
我们根据 data 键值的大小进行生成,每一次返回列表中 data 键值最大的数和它的 key 键值
"""
while len(dictList) > 0:
point = 0
biggest_num = int(dictList[0][data])
biggest_key = dictList[0][key]
for i in range(len(dictList)):
num = int(dictList[i][data])
if num > biggest_num:
point = i
biggest_num = int(dictList[i][data])
biggest_key = dictList[i][key]
yield str(biggest_key), biggest_num
del dictList[point]
def get_share_data(self, date_yyyymmdd: str):
# 获得用户界面情况
visitPage = self.find_oneDay_data(date=date_yyyymmdd,
db_name="cuny-user-analysis",
collection_name="daily-userVisitPage")
if visitPage is not None:
# 这一部分没有得到数据是可以容忍的。不用抛出模态框错误
# 获得昨日用户分享情况
sum_num = self.sum_dictList_byKey(dictList=visitPage["data_list"],
key1="page_share_pv",
key2="page_share_uv")
else:
# 此时将分享次数等置为 -1
sum_num = {"page_share_pv": -1, "page_share_uv": -1}
return sum_num
@staticmethod
def compare_date(date1_yyyymmdd: str, date2_yyyymmdd: str):
# 如果 date1 是 date2 的昨天,那么就返回 True
date1 = int(date1_yyyymmdd)
date2 = int(date2_yyyymmdd)
return True if date2 - date1 == 1 else False
def change_time(self, date_yyyymmdd: str, mode: int):
# 将 yyyymmdd 的数据分开为相应的数据形式
if mode == 1:
if self.compare_date(date_yyyymmdd, self.get_standardTime(self.get_time(delta_date=0))) is False:
return date_yyyymmdd[0:4] + "" + date_yyyymmdd[4:6] + "" + date_yyyymmdd[6:8] + ""
else:
return "昨日"
elif mode == 2:
date = date_yyyymmdd[0:4] + "." + date_yyyymmdd[4:6] + "." + date_yyyymmdd[6:8]
if self.compare_date(date_yyyymmdd, self.get_standardTime(self.get_time(delta_date=0))) is True:
return date + "~" + date + " | 昨日"
else:
return date + "~" + date
@staticmethod
def changeList_dict2List_list(dl: list, order: list):
"""
列表内是一个个字典,本函数将字典拆解,以 order 的形式排列键值为列表
考虑到一些格式的问题,这里我采用生成器的形式封装
"""
for dic in dl:
# dic 是列表内的字典元素
tmp = []
for key_name in order:
key = dic[key_name]
tmp.append(key)
yield tmp
def dict_mapping(self, dict_name: str, id_: str):
"""
进行字典映射,输入字典名称和键名,返回具体的键值
如果不存在,则原路返回键名
"""
try:
return getattr(self, dict_name)[id_]
except KeyError:
return id_
except AttributeError:
print(f"[WARNING]: 本对象内部不存在{dict_name}!")
return id_
@staticmethod
def dictAddKey(dic: dict, dic_tmp: dict, **kwargs):
"""
往字典中加入参数,可迭代
"""
for key in kwargs:
dic[key] = dic_tmp[key]
return dic
if __name__ == "__main__":
dbu = DBUtils()
-20
View File
@@ -1,20 +0,0 @@
"""
@author: cuny
@fileName: error.py
@create_time: 2022/03/10 下午 3:14
@introduce:
保存一些定义的错误类型
"""
class ProcessError(Exception):
def __init__(self, err):
super().__init__(err)
self.err = err
def __str__(self):
return self.err
class WrongImageType(TypeError):
def __init__(self, err):
super().__init__(err)
self.err = err
def __str__(self):
return self.err
-34
View File
@@ -1,34 +0,0 @@
"""
用于测试云端或者本地服务的运行是否成功
"""
import requests
import functools
import cv2
import time
def httpPostTest(url, msg:dict):
"""
以 post 请求访问 api,携带 msg(dict) 信息
"""
re = requests.post(url=url, json=msg)
print(re.text)
return re
def localTestImageFunc(path):
"""
在本地端测试算法,需要注意的是本装饰器只支持测试和图像相关算法
path 代表测试图像的路径,其余参数请写入被装饰的函数中,并且只支持标签形式输入
被测试的函数的第一个输入参数必须为图像矩阵 (以 cv2 读入)
"""
def decorator(func):
@functools.wraps(func)
def wrapper(**kwargs):
start = time.time()
image = cv2.imread(path)
image_out = func(image) if len(kwargs) == 0 else func(image, kwargs)
print("END.\n处理时间 (不计算加载模型时间){}秒:".format(round(time.time()-start, 2)))
cv2.imshow("test", image_out)
cv2.waitKey(0)
return wrapper
return decorator
-92
View File
@@ -1,92 +0,0 @@
"""
@author: cuny
@fileName: utils.py
@create_time: 2021/12/29 下午 1:29
@introduce:
焕影服务的一些工具函数,涉及两类:
1. 开发 debug 时候的工具函数
2. 初始化 COS 配置时的工具函数
"""
import cv2
from .error import WrongImageType
import numpy as np
class Debug(object):
color_dir:dict = {
"red":"31m",
"green":"32m",
"yellow":"33m",
"blue":"34m",
"common":"38m"
} # 颜色值
__DEBUG:bool = True
@property
def debug(self):
return self.__DEBUG
@debug.setter
def debug(self, value):
if not isinstance(value, bool):
raise TypeError("你必须设定 debug 的值为 bool 的 True 或者 False")
print(f"设置 debug 为:{value}")
self.__DEBUG = value
def debug_print(self, text, **kwargs):
if self.debug is True:
key = self.color_dir["common"] if "font_color" not in kwargs else self.color_dir[kwargs["font_color"]]
print(f"\033[{key}{text}\033[0m")
@staticmethod
def resize_image_esp(input_image, esp=2000):
"""
输入:
input_pathnumpy 图片
esp:限制的最大边长
"""
# resize 函数=>可以让原图压缩到最大边为 esp 的尺寸 (不改变比例)
width = input_image.shape[0]
length = input_image.shape[1]
max_num = max(width, length)
if max_num > esp:
print("Image resizing...")
if width == max_num:
length = int((esp / width) * length)
width = esp
else:
width = int((esp / length) * width)
length = esp
print(length, width)
im_resize = cv2.resize(input_image, (length, width), interpolation=cv2.INTER_AREA)
return im_resize
else:
return input_image
def cv_show(self, *args, **kwargs):
def check_images(img):
# 判断是否是矩阵类型
if not isinstance(img, np.ndarray):
raise WrongImageType("输入的图像必须是 np.ndarray 类型!")
if self.debug is True:
size = 500 if "size" not in kwargs else kwargs["size"] # 默认缩放尺寸为最大边 500 像素点
if len(args) == 0:
raise ProcessError("你必须传入若干图像信息!")
flag = False
base = None
for image in args:
check_images(image)
if flag is False:
image = self.resize_image_esp(image, size)
h, w = image.shape[0], image.shape[1]
flag = (w, h)
base = image
else:
image = cv2.resize(image, flag)
base = np.hstack((base, image))
title = "cv_show" if "winname" not in kwargs else kwargs["winname"]
cv2.imshow(title, base)
cv2.waitKey(0)
else:
pass
-197
View File
@@ -1,197 +0,0 @@
import requests, os
import json
import hashlib, base64, hmac
import sys
import oss2
from aliyunsdkimageseg.request.v20191230.SegmentBodyRequest import SegmentBodyRequest
from aliyunsdkimageseg.request.v20191230.SegmentSkinRequest import SegmentSkinRequest
from aliyunsdkfacebody.request.v20191230.DetectFaceRequest import DetectFaceRequest
from aliyunsdkcore.client import AcsClient
# 头像抠图参数配置
def params_of_head(photo_base64, photo_type):
print ('测试头像抠图接口 ...')
host = 'https://person.market.alicloudapi.com'
uri = '/segment/person/headrgba' # 头像抠图返回透明 PNG 图
# uri = '/segment/person/head' # 头像抠图返回 alpha 图
# uri = '/segment/person/headborder' # 头像抠图返回带白边的透明 PNG 图
return host, uri, {
'photo': photo_base64,
'type': photo_type,
'face_required': 0, # 可选,检测是否必须带有人脸才进行抠图处理,0 为检测,1 为不检测,默认为 0
'border_ratio': 0.3, # 可选,仅带白边接口可用,
# 在头像边缘增加白边(或者其他颜色)宽度,取值为 0-0.5,
# 这个宽度是相对于图片宽度和高度最大值的比例,
# 比如原图尺寸为 640x480border_ratio 为 0.2
# 则添加的白边的宽度为:max(640,480) * 0.2 = 96 个像素
'margin_color': '#ff0000' # 可选,仅带白边接口可用,
# 在头像边缘增加边框的颜色,默认为白色
}
# 头像抠图 API
def wanxing_get_head_api(file_name='/home/parallels/Desktop/change_cloth/input_image/03.jpg',
output_path="./head.png",
app_key='204014294',
secret="pI2uo7AhCFjnaZWYrCCAEjmsZJbK6vzy",
stage='RELEASE'):
info = sys.version_info
if info[0] < 3:
is_python3 = False
else:
is_python3 = True
with open(file_name, 'rb') as fp:
photo_base64 = base64.b64encode(fp.read())
if is_python3:
photo_base64 = photo_base64.decode('utf8')
_, photo_type = os.path.splitext(file_name)
photo_type = photo_type.lstrip('.')
# print(photo_type)
# print(photo_base64)
# host, uri, body_json = params_of_portrait_matting(photo_base64, photo_type)
# host, uri, body_json = params_of_object_matting(photo_base64)
# host, uri, body_json = params_of_idphoto(photo_base64, photo_type)
host, uri, body_json = params_of_head(photo_base64, photo_type)
# host, uri, body_json = params_of_crop(photo_base64)
api = host + uri
body = json.dumps(body_json)
md5lib = hashlib.md5()
if is_python3:
md5lib.update(body.encode('utf8'))
else:
md5lib.update(body)
body_md5 = md5lib.digest()
body_md5 = base64.b64encode(body_md5)
if is_python3:
body_md5 = body_md5.decode('utf8')
method = 'POST'
accept = 'application/json'
content_type = 'application/octet-stream; charset=utf-8'
date_str = ''
headers = ''
string_to_sign = method + '\n' \
+ accept + '\n' \
+ body_md5 + '\n' \
+ content_type + '\n' \
+ date_str + '\n' \
+ headers \
+ uri
if is_python3:
signed = hmac.new(secret.encode('utf8'),
string_to_sign.encode('utf8'),
digestmod=hashlib.sha256).digest()
else:
signed = hmac.new(secret, string_to_sign, digestmod=hashlib.sha256).digest()
signed = base64.b64encode(signed)
if is_python3:
signed = signed.decode('utf8')
headers = {
'Accept': accept,
'Content-MD5': body_md5,
'Content-Type': content_type,
'X-Ca-Key': app_key,
'X-Ca-Stage': stage,
'X-Ca-Signature': signed
}
#print signed
resp = requests.post(api, data=body, headers=headers)
# for u,v in resp.headers.items():
# print(u+": " + v)
try:
res = resp.content
res = json.loads(res)
# print ('res:', res)
if str(res['status']) == '0':
# print ('成功!')
file_object = requests.get(res["data"]["result"])
# print(file_object)
with open(output_path, 'wb') as local_file:
local_file.write(file_object.content)
# image = cv2.imread("./test_head.png", -1)
# return image
else:
pass
# print ('失败!')
except:
print('failed parse:', resp)
# 阿里云抠图 API
def aliyun_human_matting_api(input_path, output_path, type="human"):
auth = oss2.Auth('LTAI5tP2NxdzSFfpKYxZFCuJ', 'VzbGdUbRawuMAitekP3ORfrw0i3NEX')
bucket = oss2.Bucket(auth, 'https://oss-cn-shanghai.aliyuncs.com', 'huanying-api')
key = os.path.basename(input_path)
origin_image = input_path
try:
bucket.put_object_from_file(key, origin_image, headers={"Connection":"close"})
except Exception as e:
print(e)
url = bucket.sign_url('GET', key, 10 * 60)
client = AcsClient('LTAI5tP2NxdzSFfpKYxZFCuJ', 'VzbGdUbRawuMAitekP3ORfrw0i3NEX', 'cn-shanghai')
if type == "human":
request = SegmentBodyRequest()
elif type == "skin":
request = SegmentSkinRequest()
request.set_accept_format('json')
request.set_ImageURL(url)
try:
response = client.do_action_with_exception(request)
response_dict = eval(str(response, encoding='utf-8'))
if type == "human":
output_url = response_dict['Data']['ImageURL']
elif type == "skin":
output_url = response_dict['Data']['Elements'][0]['URL']
file_object = requests.get(output_url)
with open(output_path, 'wb') as local_file:
local_file.write(file_object.content)
bucket.delete_object(key)
except Exception as e:
print(e)
response = client.do_action_with_exception(request)
response_dict = eval(str(response, encoding='utf-8'))
print(response_dict)
output_url = response_dict['Data']['ImageURL']
file_object = requests.get(output_url)
with open(output_path, 'wb') as local_file:
local_file.write(file_object.content)
bucket.delete_object(key)
# 阿里云人脸检测 API
def aliyun_face_detect_api(input_path, type="human"):
auth = oss2.Auth('LTAI5tP2NxdzSFfpKYxZFCuJ', 'VzbGdUbRawuMAitekP3ORfrw0i3NEX')
bucket = oss2.Bucket(auth, 'https://oss-cn-shanghai.aliyuncs.com', 'huanying-api')
key = os.path.basename(input_path)
origin_image = input_path
try:
bucket.put_object_from_file(key, origin_image, headers={"Connection":"close"})
except Exception as e:
print(e)
url = bucket.sign_url('GET', key, 10 * 60)
client = AcsClient('LTAI5tP2NxdzSFfpKYxZFCuJ', 'VzbGdUbRawuMAitekP3ORfrw0i3NEX', 'cn-shanghai')
if type == "human":
request = DetectFaceRequest()
request.set_accept_format('json')
request.set_ImageURL(url)
try:
response = client.do_action_with_exception(request)
response_json = json.loads(str(response, encoding='utf-8'))
print(response_json["Data"]["PoseList"][-1])
bucket.delete_object(key)
return response_json["Data"]["PoseList"][-1]
except Exception as e:
print(e)
if __name__ == "__main__":
wanxing_get_head_api()
-37
View File
@@ -1,37 +0,0 @@
import cv2
import random
from scipy.ndimage import grey_erosion, grey_dilation
import numpy as np
from glob import glob
import random
def make_a_and_trimaps(input_image, resize=(512, 512)):
image = cv2.resize(input_image, resize)
b, g, r, a = cv2.split(image)
a_scale_resize = a / 255
trimap = (a_scale_resize >= 0.95).astype("float32")
not_bg = (a_scale_resize > 0).astype("float32")
d_size = a.shape[0] // 256 * random.randint(10, 20)
e_size = a.shape[0] // 256 * random.randint(10, 20)
trimap[np.where((grey_dilation(not_bg, size=(d_size, d_size))
- grey_erosion(trimap, size=(e_size, e_size))) != 0)] = 0.5
return a, trimap*255
def get_filedir_filelist(input_path):
return glob(input_path+"/*")
def extChange(filedir, ext="png"):
ext_origin = str(filedir).split(".")[-1]
return filedir.replace(ext_origin, ext)
def random_image_crop(input_image:np.array, crop_size=(512,512)):
height, width = input_image.shape[0], input_image.shape[1]
crop_height, crop_width = crop_size[0], crop_size[1]
x = random.randint(0, width-crop_width)
y = random.randint(0, height-crop_height)
return input_image[y:y+crop_height, x:x+crop_width]
View File
@@ -1,8 +0,0 @@
"""
@author: cuny
@fileName: __init__.py
@create_time: 2022/01/03 下午 9:39
@introduce:
人脸 68 关键点检测 sdk 的__init__包,实际上是对 dlib 的封装
"""
from .faceDetection68 import FaceDetection68, PoseEstimator68
@@ -1,443 +0,0 @@
"""
@author: cuny
@fileName: faceDetection68.py
@create_time: 2022/01/03 下午 10:20
@introduce:
人脸 68 关键点检测主文件,以类的形式封装
"""
from hivisionai.hyService.cloudService import GetConfig
import os
import cv2
import dlib
import numpy as np
local_file = os.path.dirname(__file__)
PREDICTOR_PATH = f"{local_file}/weights/shape_predictor_68_face_landmarks.dat" # 关键点检测模型路径
MODULE3D_PATH = f"{local_file}/weights/68_points_3D_model.txt" # 3d 的 68 点配置文件路径
# 定义一个人脸检测错误的错误类
class FaceError(Exception):
def __init__(self, err):
super().__init__(err)
self.err = err
def __str__(self):
return self.err
class FaceConfig68(object):
face_area:list = None # 一些其他的参数,在本类中实际没啥用
FACE_POINTS = list(range(17, 68)) # 人脸轮廓点索引
MOUTH_POINTS = list(range(48, 61)) # 嘴巴点索引
RIGHT_BROW_POINTS = list(range(17, 22)) # 右眉毛索引
LEFT_BROW_POINTS = list(range(22, 27)) # 左眉毛索引
RIGHT_EYE_POINTS = list(range(36, 42)) # 右眼索引
LEFT_EYE_POINTS = list(range(42, 48)) # 左眼索引
NOSE_POINTS = list(range(27, 35)) # 鼻子索引
JAW_POINTS = list(range(0, 17)) # 下巴索引
LEFT_FACE = list(range(42, 48)) + list(range(22, 27)) # 左半边脸索引
RIGHT_FACE = list(range(36, 42)) + list(range(17, 22)) # 右半边脸索引
JAW_END = 17 # 下巴结束点
FACE_START = 0 # 人脸识别开始
FACE_END = 68 # 人脸识别结束
# 下面这个是整张脸的 mark 点,可以用:
# for group in self.OVERLAY_POINTS:
# cv2.fillConvexPoly(face_mask, cv2.convexHull(dst_matrix[group]), (255, 255, 255))
# 来形成人脸蒙版
OVERLAY_POINTS = [
JAW_POINTS,
LEFT_FACE,
RIGHT_FACE
]
class FaceDetection68(FaceConfig68):
"""
人脸 68 关键点检测主类,当然使用的是 dlib 开源包
"""
def __init__(self, model_path:str=None, default_download:bool=False, *args, **kwargs):
# 初始化,检查并下载模型
self.model_path = PREDICTOR_PATH if model_path is None else model_path
if not os.path.exists(self.model_path) or default_download: # 下载配置
gc = GetConfig()
gc.load_file(cloud_path="weights/shape_predictor_68_face_landmarks.dat",
local_path=self.model_path)
self.__detector = None
self.__predictor = None
@property
def detector(self):
if self.__detector is None:
self.__detector = dlib.get_frontal_face_detector() # 获取人脸分类器
return self.__detector
@property
def predictor(self):
if self.__predictor is None:
self.__predictor = dlib.shape_predictor(self.model_path) # 输入模型,构建特征提取器
return self.__predictor
@staticmethod
def draw_face(img:np.ndarray, dets:dlib.rectangles, *args, **kwargs):
# 画人脸检测框,为了一些兼容操作我没有设置默认显示,可以在运行完本函数后将返回值进行 self.cv_show()
tmp = img.copy()
for face in dets:
# 左上角 (x1,y1),右下角 (x2,y2)
x1, y1, x2, y2 = face.left(), face.top(), face.right(), face.bottom()
# print(x1, y1, x2, y2)
cv2.rectangle(tmp, (x1, y1), (x2, y2), (0, 255, 0), 2)
return tmp
@staticmethod
def draw_points(img:np.ndarray, landmarks:np.matrix, if_num:int=False, *args, **kwargs):
"""
画人脸关键点,为了一些兼容操作我没有设置默认显示,可以在运行完本函数后将返回值进行 self.cv_show()
:param img: 输入的是人脸检测的图,必须是 3 通道或者灰度图
:param if_num: 是否在画关键点的同时画上编号
:param landmarks: 输入的关键点矩阵信息
"""
tmp = img.copy()
h, w, c = tmp.shape
r = int(h / 100) - 2 if h > w else int(w / 100) - 2
for idx, point in enumerate(landmarks):
# 68 点的坐标
pos = (point[0, 0], point[0, 1])
# 利用 cv2.circle 给每个特征点画一个圈,共 68 个
cv2.circle(tmp, pos, r, color=(0, 0, 255), thickness=-1) # bgr
if if_num is True:
# 利用 cv2.putText 输出 1-68
font = cv2.FONT_HERSHEY_SIMPLEX
cv2.putText(tmp, str(idx + 1), pos, font, 0.8, (0, 0, 255), 1, cv2.LINE_AA)
return tmp
@staticmethod
def resize_image_esp(input_image_, esp=2000):
"""
输入:
input_pathnumpy 图片
esp:限制的最大边长
"""
# resize 函数=>可以让原图压缩到最大边为 esp 的尺寸 (不改变比例)
width = input_image_.shape[0]
length = input_image_.shape[1]
max_num = max(width, length)
if max_num > esp:
print("Image resizing...")
if width == max_num:
length = int((esp / width) * length)
width = esp
else:
width = int((esp / length) * width)
length = esp
print(length, width)
im_resize = cv2.resize(input_image_, (length, width), interpolation=cv2.INTER_AREA)
return im_resize
else:
return input_image_
def facesPoints(self, img:np.ndarray, esp:int=None, det_num:int=1,*args, **kwargs):
"""
:param img: 输入的是人脸检测的图,必须是 3 通道或者灰度图
:param esp: 如果输入了具体数值,会将图片的最大边长缩放至 esp,另一边等比例缩放
:param det_num: 人脸检测的迭代次数,采样次数越多,越有利于检测到更多的人脸
:return
返回人脸检测框对象 dets, 人脸关键点矩阵列表 (列表中每个元素为一个人脸的关键点矩阵), 人脸关键点元组列表 (列表中每个元素为一个人脸的关键点列表)
"""
# win = dlib.image_window()
# win.clear_overlay()
# win.set_image(img)
# dlib 的人脸检测装置
if esp is not None:
img = self.resize_image_esp(input_image_=img, esp=esp)
dets = self.detector(img, det_num)
# self.draw_face(img, dets)
# font_color = "green" if len(dets) == 1 else "red"
# dg.debug_print("Number of faces detected: {}".format(len(dets)), font_color=font_color)
landmarkList = []
pointsList = []
for d in dets:
shape = self.predictor(img, d)
landmark = np.matrix([[p.x, p.y] for p in shape.parts()])
landmarkList.append(landmark)
point_list = []
for p in landmark.tolist():
point_list.append((p[0], p[1]))
pointsList.append(point_list)
# dg.debug_print("Key point detection SUCCESS.", font_color="green")
return dets, landmarkList, pointsList
def facePoints(self, img:np.ndarray, esp:int=None, det_num:int=1, *args, **kwargs):
"""
本函数与 facesPoints 大致类似,主要区别在于本函数默认只能返回一个人脸关键点参数
"""
# win = dlib.image_window()
# win.clear_overlay()
# win.set_image(img)
# dlib 的人脸检测装置,参数 1 表示对图片进行上采样一次,采样次数越多,越有利于检测到更多的人脸
if esp is not None:
img = self.resize_image_esp(input_image_=img, esp=esp)
dets = self.detector(img, det_num)
# self.draw_face(img, dets)
font_color = "green" if len(dets) == 1 else "red"
# dg.debug_print("Number of faces detected: {}".format(len(dets)), font_color=font_color)
if font_color=="red":
# 本检测函数必然只能检测出一张人脸
raise FaceError("Face detection error!!!")
d = dets[0] # 唯一人脸
shape = self.predictor(img, d)
landmark = np.matrix([[p.x, p.y] for p in shape.parts()])
# print("face_landmark:", landmark) # 打印关键点矩阵
# shape = predictor(img, )
# dlib.hit_enter_to_continue()
# 返回关键点矩阵,关键点,
point_list = []
for p in landmark.tolist():
point_list.append((p[0], p[1]))
# dg.debug_print("Key point detection SUCCESS.", font_color="green")
# 最后的一个返回参数只会被计算一次,用于标明脸部框的位置
# [人脸框左上角纵坐标(top),左上角横坐标(left),人脸框宽度(width),人脸框高度(height]
return dets, landmark, point_list
class PoseEstimator68(object):
"""
Estimate head pose according to the facial landmarks
本类将实现但输入图的人脸姿态检测
"""
def __init__(self, img:np.ndarray, params_path:str=None, default_download:bool=False):
self.params_path = MODULE3D_PATH if params_path is None else params_path
if not os.path.exists(self.params_path) or default_download:
gc = GetConfig()
gc.load_file(cloud_path="weights/68_points_3D_model.txt",
local_path=self.params_path)
h, w, c = img.shape
self.size = (h, w)
# 3D model points.
self.model_points = np.array([
(0.0, 0.0, 0.0), # Nose tip
(0.0, -330.0, -65.0), # Chin
(-225.0, 170.0, -135.0), # Left eye left corner
(225.0, 170.0, -135.0), # Right eye right corner
(-150.0, -150.0, -125.0), # Mouth left corner
(150.0, -150.0, -125.0) # Mouth right corner
]) / 4.5
self.model_points_68 = self._get_full_model_points()
# Camera internals
self.focal_length = self.size[1]
self.camera_center = (self.size[1] / 2, self.size[0] / 2)
self.camera_matrix = np.array(
[[self.focal_length, 0, self.camera_center[0]],
[0, self.focal_length, self.camera_center[1]],
[0, 0, 1]], dtype="double")
# Assuming no lens distortion
self.dist_coeefs = np.zeros((4, 1))
# Rotation vector and translation vector
self.r_vec = np.array([[0.01891013], [0.08560084], [-3.14392813]])
self.t_vec = np.array(
[[-14.97821226], [-10.62040383], [-2053.03596872]])
# self.r_vec = None
# self.t_vec = None
def _get_full_model_points(self):
"""Get all 68 3D model points from file"""
raw_value = []
with open(self.params_path) as file:
for line in file:
raw_value.append(line)
model_points = np.array(raw_value, dtype=np.float32)
model_points = np.reshape(model_points, (3, -1)).T
# Transform the model into a front view.
# model_points[:, 0] *= -1
model_points[:, 1] *= -1
model_points[:, 2] *= -1
return model_points
def show_3d_model(self):
from matplotlib import pyplot
from mpl_toolkits.mplot3d import Axes3D
fig = pyplot.figure()
ax = Axes3D(fig)
x = self.model_points_68[:, 0]
y = self.model_points_68[:, 1]
z = self.model_points_68[:, 2]
ax.scatter(x, y, z)
ax.axis('auto')
pyplot.xlabel('x')
pyplot.ylabel('y')
pyplot.show()
def solve_pose(self, image_points):
"""
Solve pose from image points
Return (rotation_vector, translation_vector) as pose.
"""
assert image_points.shape[0] == self.model_points_68.shape[0], "3D points and 2D points should be of same number."
(_, rotation_vector, translation_vector) = cv2.solvePnP(
self.model_points, image_points, self.camera_matrix, self.dist_coeefs)
# (success, rotation_vector, translation_vector) = cv2.solvePnP(
# self.model_points,
# image_points,
# self.camera_matrix,
# self.dist_coeefs,
# rvec=self.r_vec,
# tvec=self.t_vec,
# useExtrinsicGuess=True)
return rotation_vector, translation_vector
def solve_pose_by_68_points(self, image_points):
"""
Solve pose from all the 68 image points
Return (rotation_vector, translation_vector) as pose.
"""
if self.r_vec is None:
(_, rotation_vector, translation_vector) = cv2.solvePnP(
self.model_points_68, image_points, self.camera_matrix, self.dist_coeefs)
self.r_vec = rotation_vector
self.t_vec = translation_vector
(_, rotation_vector, translation_vector) = cv2.solvePnP(
self.model_points_68,
image_points,
self.camera_matrix,
self.dist_coeefs,
rvec=self.r_vec,
tvec=self.t_vec,
useExtrinsicGuess=True)
return rotation_vector, translation_vector
# def draw_annotation_box(self, image, rotation_vector, translation_vector, color=(255, 255, 255), line_width=2):
# """Draw a 3D box as annotation of pose"""
# point_3d = []
# rear_size = 75
# rear_depth = 0
# point_3d.append((-rear_size, -rear_size, rear_depth))
# point_3d.append((-rear_size, rear_size, rear_depth))
# point_3d.append((rear_size, rear_size, rear_depth))
# point_3d.append((rear_size, -rear_size, rear_depth))
# point_3d.append((-rear_size, -rear_size, rear_depth))
#
# front_size = 100
# front_depth = 100
# point_3d.append((-front_size, -front_size, front_depth))
# point_3d.append((-front_size, front_size, front_depth))
# point_3d.append((front_size, front_size, front_depth))
# point_3d.append((front_size, -front_size, front_depth))
# point_3d.append((-front_size, -front_size, front_depth))
# point_3d = np.array(point_3d, dtype=np.float64).reshape(-1, 3)
#
# # Map to 2d image points
# (point_2d, _) = cv2.projectPoints(point_3d,
# rotation_vector,
# translation_vector,
# self.camera_matrix,
# self.dist_coeefs)
# point_2d = np.int32(point_2d.reshape(-1, 2))
#
# # Draw all the lines
# cv2.polylines(image, [point_2d], True, color, line_width, cv2.LINE_AA)
# cv2.line(image, tuple(point_2d[1]), tuple(
# point_2d[6]), color, line_width, cv2.LINE_AA)
# cv2.line(image, tuple(point_2d[2]), tuple(
# point_2d[7]), color, line_width, cv2.LINE_AA)
# cv2.line(image, tuple(point_2d[3]), tuple(
# point_2d[8]), color, line_width, cv2.LINE_AA)
#
# def draw_axis(self, img, R, t):
# points = np.float32(
# [[30, 0, 0], [0, 30, 0], [0, 0, 30], [0, 0, 0]]).reshape(-1, 3)
#
# axisPoints, _ = cv2.projectPoints(
# points, R, t, self.camera_matrix, self.dist_coeefs)
#
# img = cv2.line(img, tuple(axisPoints[3].ravel()), tuple(
# axisPoints[0].ravel()), (255, 0, 0), 3)
# img = cv2.line(img, tuple(axisPoints[3].ravel()), tuple(
# axisPoints[1].ravel()), (0, 255, 0), 3)
# img = cv2.line(img, tuple(axisPoints[3].ravel()), tuple(
# axisPoints[2].ravel()), (0, 0, 255), 3)
def draw_axes(self, img, R, t):
"""
OX is drawn in red, OY in green and OZ in blue.
"""
return cv2.drawFrameAxes(img, self.camera_matrix, self.dist_coeefs, R, t, 30)
@staticmethod
def get_pose_marks(marks):
"""Get marks ready for pose estimation from 68 marks"""
pose_marks = [marks[30], marks[8], marks[36], marks[45], marks[48], marks[54]]
return pose_marks
@staticmethod
def rot_params_rm(R):
from math import pi,atan2,asin, fabs
# x 轴
pitch = (180 * atan2(-R[2][1], R[2][2]) / pi)
f = (0 > pitch) - (0 < pitch)
pitch = f * (180 - fabs(pitch))
# y 轴
yaw = -(180 * asin(R[2][0]) / pi)
# z 轴
roll = (180 * atan2(-R[1][0], R[0][0]) / pi)
f = (0 > roll) - (0 < roll)
roll = f * (180 - fabs(roll))
if not fabs(roll) < 90.0:
roll = f * (180 - fabs(roll))
rot_params = [pitch, yaw, roll]
return rot_params
@staticmethod
def rot_params_rv(rvec_):
from math import pi, atan2, asin, fabs
R = cv2.Rodrigues(rvec_)[0]
# x 轴
pitch = (180 * atan2(-R[2][1], R[2][2]) / pi)
f = (0 > pitch) - (0 < pitch)
pitch = f * (180 - fabs(pitch))
# y 轴
yaw = -(180 * asin(R[2][0]) / pi)
# z 轴
roll = (180 * atan2(-R[1][0], R[0][0]) / pi)
f = (0 > roll) - (0 < roll)
roll = f * (180 - fabs(roll))
rot_params = [pitch, yaw, roll]
return rot_params
def imageEulerAngle(self, img_points):
# 这里的 img_points 对应的是 facePoints 的第三个返回值,注意是 facePoints 而非 facesPoints
# 对于 facesPoints 而言,需要把第三个返回值逐一取出再输入
# 把列表转为矩阵,且编码形式为 float64
img_points = np.array(img_points, dtype=np.float64)
rvec, tvec = self.solve_pose_by_68_points(img_points)
# 旋转向量转旋转矩阵
R = cv2.Rodrigues(rvec)[0]
# theta = np.linalg.norm(rvec)
# r = rvec / theta
# R_ = np.array([[0, -r[2][0], r[1][0]],
# [r[2][0], 0, -r[0][0]],
# [-r[1][0], r[0][0], 0]])
# R = np.cos(theta) * np.eye(3) + (1 - np.cos(theta)) * r * r.T + np.sin(theta) * R_
# 旋转矩阵转欧拉角
eulerAngle = self.rot_params_rm(R)
# 返回一个元组和欧拉角列表
return (rvec, tvec, R), eulerAngle
# if __name__ == "__main__":
# # 示例
# from hyService.utils import Debug
# dg = Debug()
# image_input = cv2.imread("./test.jpg") # 读取一张图片,必须是三通道或者灰度图
# fd68 = FaceDetection68() # 初始化人脸关键点检测类
# dets_, landmark_, point_list_ = fd68.facePoints(image_input) # 输入图片。检测单张人脸
# # dets_, landmark_, point_list_ = fd68.facesPoints(input_image) # 输入图片。检测多张人脸
# img = fd68.draw_points(image_input, landmark_)
# dg.cv_show(img)
# pe = PoseEstimator68(image_input)
# _, ea = pe.imageEulerAngle(point_list_) # 输入关键点列表,如果要使用 facesPoints,则输入的是 point_list_[i]
# print(ea) # 结果
-1
View File
@@ -1 +0,0 @@
from .utils import cover_mask, get_box, get_box_pro, filtering, cut, zoom_image_without_change_size
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-16
View File
@@ -1,16 +0,0 @@
"""
定义 hycv 的一些错误类型,其实和 hyService 大致相同
"""
class ProcessError(Exception):
def __init__(self, err):
super().__init__(err)
self.err = err
def __str__(self):
return self.err
class WrongImageType(TypeError):
def __init__(self, err):
super().__init__(err)
self.err = err
def __str__(self):
return self.err
-427
View File
@@ -1,427 +0,0 @@
import cv2
import os
import onnxruntime
from .mtcnn_onnx.detector import detect_faces
from .tensor2numpy import *
from PIL import Image
import requests
from os.path import exists
def download_img(img_url, base_dir):
print("Downloading Onnx Model in:", img_url)
r = requests.get(img_url, stream=True)
filename = img_url.split("/")[-1]
# print(r.status_code) # 返回状态码
if r.status_code == 200:
open(f'{base_dir}/{filename}', 'wb').write(r.content) # 将内容写入图片
print(f"Download Finshed -- {filename}")
del r
class BBox(object):
# bbox is a list of [left, right, top, bottom]
def __init__(self, bbox):
self.left = bbox[0]
self.right = bbox[1]
self.top = bbox[2]
self.bottom = bbox[3]
self.x = bbox[0]
self.y = bbox[2]
self.w = bbox[1] - bbox[0]
self.h = bbox[3] - bbox[2]
# scale to [0,1]
def projectLandmark(self, landmark):
landmark_= np.asarray(np.zeros(landmark.shape))
for i, point in enumerate(landmark):
landmark_[i] = ((point[0]-self.x)/self.w, (point[1]-self.y)/self.h)
return landmark_
# landmark of (5L, 2L) from [0,1] to real range
def reprojectLandmark(self, landmark):
landmark_= np.asarray(np.zeros(landmark.shape))
for i, point in enumerate(landmark):
x = point[0] * self.w + self.x
y = point[1] * self.h + self.y
landmark_[i] = (x, y)
return landmark_
def face_detect_mtcnn(input_image, color_key=None, filter=None):
"""
Inputs:
- input_image: OpenCV Numpy.array
- color_key: 当 color_key 等于"RGB"时,将不进行转换操作
- filter:当 filter 等于 True 时,将抛弃掉置信度小于 0.98 或人脸框面积小于 3600 的人脸
return:
- faces: 带有人脸信息的变量
- landmarks: face alignment
"""
if color_key != "RGB":
input_image = cv2.cvtColor(input_image, cv2.COLOR_BGR2RGB)
input_image = Image.fromarray(input_image)
faces, landmarks = detect_faces(input_image)
if filter:
face_clean = []
for face in faces:
confidence = face[-1]
x1 = face[0]
y1 = face[1]
x2 = face[2]
y2 = face[3]
w = x2 - x1 + 1
h = y2 - y1 + 1
measure = w * h
if confidence >= 0.98 and measure > 3600:
# 如果检测到的人脸置信度小于 0.98 或人脸框面积小于 3600,则抛弃该人脸
face_clean.append(face)
faces = face_clean
return faces, landmarks
def mtcnn_bbox(face, width, height):
x1 = face[0]
y1 = face[1]
x2 = face[2]
y2 = face[3]
w = x2 - x1 + 1
h = y2 - y1 + 1
size = int(max([w, h]) * 1.1)
cx = x1 + w // 2
cy = y1 + h // 2
x1 = cx - size // 2
x2 = x1 + size
y1 = cy - size // 2
y2 = y1 + size
dx = max(0, -x1)
dy = max(0, -y1)
x1 = max(0, x1)
y1 = max(0, y1)
edx = max(0, x2 - width)
edy = max(0, y2 - height)
x2 = min(width, x2)
y2 = min(height, y2)
return x1, x2, y1, y2, dx, dy, edx, edy
def mtcnn_cropped_face(face_box, image, width, height):
x1, x2, y1, y2, dx, dy, edx, edy = mtcnn_bbox(face_box, width, height)
new_bbox = list(map(int, [x1, x2, y1, y2]))
new_bbox = BBox(new_bbox)
cropped = image[new_bbox.top:new_bbox.bottom, new_bbox.left:new_bbox.right]
if (dx > 0 or dy > 0 or edx > 0 or edy > 0):
cropped = cv2.copyMakeBorder(cropped, int(dy), int(edy), int(dx), int(edx), cv2.BORDER_CONSTANT, 0)
return cropped, new_bbox
def face_landmark_56(input_image, faces_box=None):
basedir = os.path.dirname(os.path.realpath(__file__)).split("mtcnn.py")[0]
mean = np.asarray([0.485, 0.456, 0.406])
std = np.asarray([0.229, 0.224, 0.225])
base_url = "https://linimages.oss-cn-beijing.aliyuncs.com/"
if not exists(f"{basedir}/mtcnn_onnx/weights/landmark_detection_56_se_external.onnx"):
# download onnx model
download_img(img_url=base_url + "landmark_detection_56_se_external.onnx",
base_dir=f"{basedir}/mtcnn_onnx/weights")
ort_session = onnxruntime.InferenceSession(f"{basedir}/mtcnn_onnx/weights/landmark_detection_56_se_external.onnx")
out_size = 56
height, width, _ = input_image.shape
if faces_box is None:
faces_box, _ = face_detect_mtcnn(input_image)
if len(faces_box) == 0:
print('NO face is detected!')
return None
else:
landmarks = []
for face_box in faces_box:
cropped, new_bbox = mtcnn_cropped_face(face_box, input_image, width, height)
cropped_face = cv2.resize(cropped, (out_size, out_size))
test_face = NNormalize(cropped_face, mean=mean, std=std)
test_face = NTo_Tensor(test_face)
test_face = NUnsqueeze(test_face)
ort_inputs = {ort_session.get_inputs()[0].name: test_face}
ort_outs = ort_session.run(None, ort_inputs)
landmark = ort_outs[0]
landmark = landmark.reshape(-1, 2)
landmark = new_bbox.reprojectLandmark(landmark)
landmarks.append(landmark)
return landmarks
REFERENCE_FACIAL_POINTS = [
[30.29459953, 51.69630051],
[65.53179932, 51.50139999],
[48.02519989, 71.73660278],
[33.54930115, 92.3655014],
[62.72990036, 92.20410156]
]
DEFAULT_CROP_SIZE = (96, 112)
def _umeyama(src, dst, estimate_scale=True, scale=1.0):
"""Estimate N-D similarity transformation with or without scaling.
Parameters
----------
src : (M, N) array
Source coordinates.
dst : (M, N) array
Destination coordinates.
estimate_scale : bool
Whether to estimate scaling factor.
Returns
-------
T : (N + 1, N + 1)
The homogeneous similarity transformation matrix. The matrix contains
NaN values only if the problem is not well-conditioned.
References
----------
.. [1] "Least-squares estimation of transformation parameters between two
point patterns", Shinji Umeyama, PAMI 1991, :DOI:`10.1109/34.88573`
"""
num = src.shape[0]
dim = src.shape[1]
# Compute mean of src and dst.
src_mean = src.mean(axis=0)
dst_mean = dst.mean(axis=0)
# Subtract mean from src and dst.
src_demean = src - src_mean
dst_demean = dst - dst_mean
# Eq. (38).
A = dst_demean.T @ src_demean / num
# Eq. (39).
d = np.ones((dim,), dtype=np.double)
if np.linalg.det(A) < 0:
d[dim - 1] = -1
T = np.eye(dim + 1, dtype=np.double)
U, S, V = np.linalg.svd(A)
# Eq. (40) and (43).
rank = np.linalg.matrix_rank(A)
if rank == 0:
return np.nan * T
elif rank == dim - 1:
if np.linalg.det(U) * np.linalg.det(V) > 0:
T[:dim, :dim] = U @ V
else:
s = d[dim - 1]
d[dim - 1] = -1
T[:dim, :dim] = U @ np.diag(d) @ V
d[dim - 1] = s
else:
T[:dim, :dim] = U @ np.diag(d) @ V
if estimate_scale:
# Eq. (41) and (42).
scale = 1.0 / src_demean.var(axis=0).sum() * (S @ d)
else:
scale = scale
T[:dim, dim] = dst_mean - scale * (T[:dim, :dim] @ src_mean.T)
T[:dim, :dim] *= scale
return T, scale
class FaceWarpException(Exception):
def __str__(self):
return 'In File {}:{}'.format(
__file__, super.__str__(self))
def get_reference_facial_points_5(output_size=None,
inner_padding_factor=0.0,
outer_padding=(0, 0),
default_square=False):
tmp_5pts = np.array(REFERENCE_FACIAL_POINTS)
tmp_crop_size = np.array(DEFAULT_CROP_SIZE)
# 0) make the inner region a square
if default_square:
size_diff = max(tmp_crop_size) - tmp_crop_size
tmp_5pts += size_diff / 2
tmp_crop_size += size_diff
if (output_size and
output_size[0] == tmp_crop_size[0] and
output_size[1] == tmp_crop_size[1]):
print('output_size == DEFAULT_CROP_SIZE {}: return default reference points'.format(tmp_crop_size))
return tmp_5pts
if (inner_padding_factor == 0 and
outer_padding == (0, 0)):
if output_size is None:
print('No paddings to do: return default reference points')
return tmp_5pts
else:
raise FaceWarpException(
'No paddings to do, output_size must be None or {}'.format(tmp_crop_size))
# check output size
if not (0 <= inner_padding_factor <= 1.0):
raise FaceWarpException('Not (0 <= inner_padding_factor <= 1.0)')
if ((inner_padding_factor > 0 or outer_padding[0] > 0 or outer_padding[1] > 0)
and output_size is None):
output_size = tmp_crop_size * \
(1 + inner_padding_factor * 2).astype(np.int32)
output_size += np.array(outer_padding)
print(' deduced from paddings, output_size = ', output_size)
if not (outer_padding[0] < output_size[0]
and outer_padding[1] < output_size[1]):
raise FaceWarpException('Not (outer_padding[0] < output_size[0]'
'and outer_padding[1] < output_size[1])')
# 1) pad the inner region according inner_padding_factor
# print('---> STEP1: pad the inner region according inner_padding_factor')
if inner_padding_factor > 0:
size_diff = tmp_crop_size * inner_padding_factor * 2
tmp_5pts += size_diff / 2
tmp_crop_size += np.round(size_diff).astype(np.int32)
# print(' crop_size = ', tmp_crop_size)
# print(' reference_5pts = ', tmp_5pts)
# 2) resize the padded inner region
# print('---> STEP2: resize the padded inner region')
size_bf_outer_pad = np.array(output_size) - np.array(outer_padding) * 2
# print(' crop_size = ', tmp_crop_size)
# print(' size_bf_outer_pad = ', size_bf_outer_pad)
if size_bf_outer_pad[0] * tmp_crop_size[1] != size_bf_outer_pad[1] * tmp_crop_size[0]:
raise FaceWarpException('Must have (output_size - outer_padding)'
'= some_scale * (crop_size * (1.0 + inner_padding_factor)')
scale_factor = size_bf_outer_pad[0].astype(np.float32) / tmp_crop_size[0]
# print(' resize scale_factor = ', scale_factor)
tmp_5pts = tmp_5pts * scale_factor
# size_diff = tmp_crop_size * (scale_factor - min(scale_factor))
# tmp_5pts = tmp_5pts + size_diff / 2
tmp_crop_size = size_bf_outer_pad
# print(' crop_size = ', tmp_crop_size)
# print(' reference_5pts = ', tmp_5pts)
# 3) add outer_padding to make output_size
reference_5point = tmp_5pts + np.array(outer_padding)
tmp_crop_size = output_size
# print('---> STEP3: add outer_padding to make output_size')
# print(' crop_size = ', tmp_crop_size)
# print(' reference_5pts = ', tmp_5pts)
#
# print('===> end get_reference_facial_points\n')
return reference_5point
def get_affine_transform_matrix(src_pts, dst_pts):
tfm = np.float32([[1, 0, 0], [0, 1, 0]])
n_pts = src_pts.shape[0]
ones = np.ones((n_pts, 1), src_pts.dtype)
src_pts_ = np.hstack([src_pts, ones])
dst_pts_ = np.hstack([dst_pts, ones])
A, res, rank, s = np.linalg.lstsq(src_pts_, dst_pts_)
if rank == 3:
tfm = np.float32([
[A[0, 0], A[1, 0], A[2, 0]],
[A[0, 1], A[1, 1], A[2, 1]]
])
elif rank == 2:
tfm = np.float32([
[A[0, 0], A[1, 0], 0],
[A[0, 1], A[1, 1], 0]
])
return tfm
def warp_and_crop_face(src_img,
facial_pts,
reference_pts=None,
crop_size=(96, 112),
align_type='smilarity'): #smilarity cv2_affine affine
if reference_pts is None:
if crop_size[0] == 96 and crop_size[1] == 112:
reference_pts = REFERENCE_FACIAL_POINTS
else:
default_square = False
inner_padding_factor = 0
outer_padding = (0, 0)
output_size = crop_size
reference_pts = get_reference_facial_points_5(output_size,
inner_padding_factor,
outer_padding,
default_square)
ref_pts = np.float32(reference_pts)
ref_pts_shp = ref_pts.shape
if max(ref_pts_shp) < 3 or min(ref_pts_shp) != 2:
raise FaceWarpException(
'reference_pts.shape must be (K,2) or (2,K) and K>2')
if ref_pts_shp[0] == 2:
ref_pts = ref_pts.T
src_pts = np.float32(facial_pts)
src_pts_shp = src_pts.shape
if max(src_pts_shp) < 3 or min(src_pts_shp) != 2:
raise FaceWarpException(
'facial_pts.shape must be (K,2) or (2,K) and K>2')
if src_pts_shp[0] == 2:
src_pts = src_pts.T
if src_pts.shape != ref_pts.shape:
raise FaceWarpException(
'facial_pts and reference_pts must have the same shape')
if align_type == 'cv2_affine':
tfm = cv2.getAffineTransform(src_pts[0:3], ref_pts[0:3])
tfm_inv = cv2.getAffineTransform(ref_pts[0:3], src_pts[0:3])
elif align_type == 'affine':
tfm = get_affine_transform_matrix(src_pts, ref_pts)
tfm_inv = get_affine_transform_matrix(ref_pts, src_pts)
else:
params, scale = _umeyama(src_pts, ref_pts)
tfm = params[:2, :]
params, _ = _umeyama(ref_pts, src_pts, False, scale=1.0/scale)
tfm_inv = params[:2, :]
face_img = cv2.warpAffine(src_img, tfm, (crop_size[0], crop_size[1]), flags=3)
return face_img, tfm_inv
if __name__ == "__main__":
image = cv2.imread("/home/parallels/Desktop/IDPhotos/input_image/03.jpg")
face_detect_mtcnn(image)
-2
View File
@@ -1,2 +0,0 @@
from .idphotoTool.idphoto_cut import IDphotos_create
from .idphotoTool.idphoto_change_cloth import change_cloth
-593
View File
@@ -1,593 +0,0 @@
import cv2
import numpy as np
from ..utils import get_box_pro
from ..vision import cover_image, draw_picture_dots
def transformationNeck2(image:np.ndarray, per_to_side:float=0.8)->np.ndarray:
"""
透视变换脖子函数,输入图像和四个点 (矩形框)
矩形框内的图像可能是不完整的 (边角有透明区域)
我们将根据透视变换将矩形框内的图像拉伸成和矩形框一样的形状。
算法分为几个步骤:选择脖子的四个点 -> 选定这四个点拉伸后的坐标 -> 透视变换 -> 覆盖原图
"""
_, _, _, a = cv2.split(image) # 这应该是一个四通道的图像
height, width = a.shape
def locate_side(image_:np.ndarray, x_:int, y_max:int) -> int:
# 寻找 x=y, 且 y <= y_max 上从下往上第一个非 0 的点,如果没找到就返回 0
y_ = 0
for y_ in range(y_max - 1, -1, -1):
if image_[y_][x_] != 0:
break
return y_
def locate_width(image_:np.ndarray, y_:int, mode, left_or_right:int=None):
# 从 y=y 这个水平线上寻找两边的非零点
# 增加 left_or_right 的原因在于为下面 check_jaw 服务
if mode==1: # 左往右
x_ = 0
if left_or_right is None:
left_or_right = 0
for x_ in range(left_or_right, width):
if image_[y_][x_] != 0:
break
else: # 右往左
x_ = width
if left_or_right is None:
left_or_right = width - 1
for x_ in range(left_or_right, -1, -1):
if image_[y_][x_] != 0:
break
return x_
def check_jaw(image_:np.ndarray, left_, right_):
"""
检查选择的点是否与截到下巴,如果截到了,就往下平移一个单位
"""
f= True # True 代表没截到下巴
# [x, y]
for x_cell in range(left_[0] + 1, right_[0]):
if image_[left_[1]][x_cell] == 0:
f = False
break
if f is True:
return left_, right_
else:
y_ = left_[1] + 2
x_left_ = locate_width(image_, y_, mode=1, left_or_right=left_[0])
x_right_ = locate_width(image_, y_, mode=2, left_or_right=right_[0])
left_, right_ = check_jaw(image_, [x_left_, y_], [x_right_, y_])
return left_, right_
# 选择脖子的四个点,核心在于选择上面的两个点,这两个点的确定的位置应该是"宽出来的"两个点
_, _ ,_, a = cv2.split(image) # 这应该是一个四通道的图像
ret,a_thresh = cv2.threshold(a,127,255,cv2.THRESH_BINARY)
y_high, y_low, x_left, x_right = get_box_pro(image=image, model=1) # 直接返回矩阵信息
y_left_side = locate_side(image_=a_thresh, x_=x_left, y_max=y_low) # 左边的点的 y 轴坐标
y_right_side = locate_side(image_=a_thresh, x_=x_right, y_max=y_low) # 右边的点的 y 轴坐标
y = min(y_left_side, y_right_side) # 将两点的坐标保持相同
cell_left_above, cell_right_above = check_jaw(a_thresh,[x_left, y], [x_right, y])
x_left, x_right = cell_left_above[0], cell_right_above[0]
# 此时我们寻找到了脖子的"宽出来的"两个点,这两个点作为上面的两个点,接下来寻找下面的两个点
if per_to_side >1:
assert ValueError("per_to_side 必须小于 1!")
# 在后面的透视变换中我会把它拉成矩形,在这里我先获取四个点的高和宽
height_ = 150 # 这个值应该是个变化的值,与拉伸的长度有关,但是现在先规定为 150
width_ = x_right - x_left # 其实也就是 cell_right_above[1] - cell_left_above[1]
y = int((y_low - y)*per_to_side + y) # 定位 y 轴坐标
cell_left_below, cell_right_bellow = ([locate_width(a_thresh, y_=y, mode=1), y], [locate_width(a_thresh, y_=y, mode=2), y])
# 四个点全齐,开始透视变换
# 寻找透视变换后的四个点,只需要变换 below 的两个点即可
# cell_left_below_final, cell_right_bellow_final = ([cell_left_above[1], y_low], [cell_right_above[1], y_low])
# 需要变换的四个点为 cell_left_above, cell_right_above, cell_left_below, cell_right_bellow
rect = np.array([cell_left_above, cell_right_above, cell_left_below, cell_right_bellow],
dtype='float32')
# 变化后的坐标点
dst = np.array([[0, 0], [width_, 0], [0 , height_], [width_, height_]],
dtype='float32')
# 计算变换矩阵
M = cv2.getPerspectiveTransform(rect, dst)
warped = cv2.warpPerspective(image, M, (width_, height_))
final = cover_image(image=warped, background=image, mode=3, x=cell_left_above[0], y=cell_left_above[1])
# tmp = np.zeros(image.shape)
# final = cover_image(image=warped, background=tmp, mode=3, x=cell_left_above[0], y=cell_left_above[1])
# final = cover_image(image=image, background=final, mode=3, x=0, y=0)
return final
def transformationNeck(image:np.ndarray, cutNeckHeight:int, neckBelow:int,
toHeight:int,per_to_side:float=0.75) -> np.ndarray:
"""
脖子扩充算法,其实需要输入的只是脖子扣出来的部分以及需要被扩充的高度/需要被扩充成的高度。
"""
height, width, channels = image.shape
_, _, _, a = cv2.split(image) # 这应该是一个四通道的图像
ret, a_thresh = cv2.threshold(a, 127, 255, cv2.THRESH_BINARY) # 将透明图层二值化
def locate_width(image_:np.ndarray, y_:int, mode, left_or_right:int=None):
# 从 y=y 这个水平线上寻找两边的非零点
# 增加 left_or_right 的原因在于为下面 check_jaw 服务
if mode==1: # 左往右
x_ = 0
if left_or_right is None:
left_or_right = 0
for x_ in range(left_or_right, width):
if image_[y_][x_] != 0:
break
else: # 右往左
x_ = width
if left_or_right is None:
left_or_right = width - 1
for x_ in range(left_or_right, -1, -1):
if image_[y_][x_] != 0:
break
return x_
def check_jaw(image_:np.ndarray, left_, right_):
"""
检查选择的点是否与截到下巴,如果截到了,就往下平移一个单位
"""
f= True # True 代表没截到下巴
# [x, y]
for x_cell in range(left_[0] + 1, right_[0]):
if image_[left_[1]][x_cell] == 0:
f = False
break
if f is True:
return left_, right_
else:
y_ = left_[1] + 2
x_left_ = locate_width(image_, y_, mode=1, left_or_right=left_[0])
x_right_ = locate_width(image_, y_, mode=2, left_or_right=right_[0])
left_, right_ = check_jaw(image_, [x_left_, y_], [x_right_, y_])
return left_, right_
x_left = locate_width(image_=a_thresh, mode=1, y_=cutNeckHeight)
x_right = locate_width(image_=a_thresh, mode=2, y_=cutNeckHeight)
# 在这里我们取消了对下巴的检查,原因在于输入的 imageHeight 并不能改变
# cell_left_above, cell_right_above = check_jaw(a_thresh, [x_left, imageHeight], [x_right, imageHeight])
cell_left_above, cell_right_above = [x_left, cutNeckHeight], [x_right, cutNeckHeight]
toWidth = x_right - x_left # 矩形宽
# 此时我们寻找到了脖子的"宽出来的"两个点,这两个点作为上面的两个点,接下来寻找下面的两个点
if per_to_side >1:
assert ValueError("per_to_side 必须小于 1!")
y_below = int((neckBelow - cutNeckHeight) * per_to_side + cutNeckHeight) # 定位 y 轴坐标
cell_left_below = [locate_width(a_thresh, y_=y_below, mode=1), y_below]
cell_right_bellow = [locate_width(a_thresh, y_=y_below, mode=2), y_below]
# 四个点全齐,开始透视变换
# 需要变换的四个点为 cell_left_above, cell_right_above, cell_left_below, cell_right_bellow
rect = np.array([cell_left_above, cell_right_above, cell_left_below, cell_right_bellow],
dtype='float32')
# 变化后的坐标点
dst = np.array([[0, 0], [toWidth, 0], [0 , toHeight], [toWidth, toHeight]],
dtype='float32')
M = cv2.getPerspectiveTransform(rect, dst)
warped = cv2.warpPerspective(image, M, (toWidth, toHeight))
# 将变换后的图像覆盖到原图上
final = cover_image(image=warped, background=image, mode=3, x=cell_left_above[0], y=cell_left_above[1])
return final
def bestJunctionCheck_beta(image:np.ndarray, stepSize:int=4, if_per:bool=False):
"""
最优衔接点检测算法,去寻找脖子的"拐点"
"""
point_k = 1
_, _, _, a = cv2.split(image) # 这应该是一个四通道的图像
height, width = a.shape
ret, a_thresh = cv2.threshold(a, 127, 255, cv2.THRESH_BINARY) # 将透明图层二值化
y_high, y_low, x_left, x_right = get_box_pro(image=image, model=1) # 直接返回矩阵信息
def scan(y_:int, max_num:int=2):
num = 0
left = False
right = False
for x_ in range(width):
if a_thresh[y_][x_] != 0:
if x_ < width // 2 and left is False:
num += 1
left = True
elif x_ > width // 2 and right is False:
num += 1
right = True
return True if num >= max_num else False
def locate_neck_above():
"""
定位脖子的尖尖脚
"""
for y_ in range( y_high - 2, height):
if scan(y_):
return y_, y_
y_high_left, y_high_right = locate_neck_above()
def locate_width_pro(image_:np.ndarray, y_:int, mode):
"""
这会是一个生成器,用于生成脖子两边的轮廓
x_, y_ 是启始点的坐标,每一次寻找都会让 y_+1
mode==1 说明是找左边的边,即,image_[y_][x_] == 0 且 image_[y_][x_ + 1] !=0 时跳出;
否则 当 image_[y_][x_] != 0 时,x_ - 1; 当 image_[y_][x_] == 0 且 image_[y_][x_ + 1] ==0 时 x_ + 1
mode==2 说明是找右边的边,即,image_[y_][x_] == 0 且 image_[y_][x_ - 1] !=0 时跳出
否则 当 image_[y_][x_] != 0 时,x_ + 1; 当 image_[y_][x_] == 0 且 image_[y_][x_ - 1] ==0 时 x_ - 1
"""
y_ += 1
if mode == 1:
x_ = 0
while 0 <= y_ < height and 0 <= x_ < width:
while image_[y_][x_] != 0 and x_ >= 0:
x_ -= 1
while image_[y_][x_] == 0 and image_[y_][x_ + 1] == 0 and x_ < width - 2:
x_ += 1
yield [y_, x_]
y_ += 1
elif mode == 2:
x_ = width-1
while 0 <= y_ < height and 0 <= x_ < width:
while image_[y_][x_] != 0 and x_ < width - 2: x_ += 1
while image_[y_][x_] == 0 and image_[y_][x_ - 1] == 0 and x_ >= 0: x_ -= 1
yield [y_, x_]
y_ += 1
yield False
def kGenerator(image_:np.ndarray, mode):
"""
导数生成器,用来生成每一个点对应的导数
"""
y_ = y_high_left if mode == 1 else y_high_right
c_generator = locate_width_pro(image_=image_, y_=y_, mode=mode)
for cell in c_generator:
nc = locate_width_pro(image_=image_, y_=cell[0] + stepSize, mode=mode)
nextCell = next(nc)
if nextCell is False:
yield False, False
else:
k = (cell[1] - nextCell[1]) / stepSize
yield k, cell
def findPt(image_:np.ndarray, mode):
k_generator = kGenerator(image_=image_, mode=mode)
k, cell = next(k_generator)
k_next, cell_next = next(k_generator)
if k is False:
raise ValueError("无法找到拐点!")
while k_next is not False:
k_next, cell_next = next(k_generator)
if (k_next < - 1 / stepSize) or k_next > point_k:
break
cell = cell_next
# return int(cell[0] + stepSize / 2)
return cell[0]
# 先找左边的拐点:
pointY_left = findPt(image_=a_thresh, mode=1)
# 再找右边的拐点:
pointY_right = findPt(image_=a_thresh, mode=2)
point = (pointY_left + pointY_right) // 2
if if_per is True:
point = (pointY_left + pointY_right) // 2
return point / (y_low - y_high)
pointX_left = next(locate_width_pro(image_=a_thresh, y_= point - 1, mode=1))[1]
pointX_right = next(locate_width_pro(image_=a_thresh, y_=point- 1, mode=2))[1]
return [pointX_left, point], [pointX_right, point]
def bestJunctionCheck(image:np.ndarray, offset:int, stepSize:int=4):
"""
最优点检测算算法输入一张脖子图片 (无论这张图片是否已经被二值化,我都认为没有被二值化),输出一个小数 (脖子最上方与衔接点位置/脖子图像长度)
与 beta 版不同的是它新增了一个阈值限定内容。
对于脖子而言,我我们首先可以定位到上面的部分,然后根据上面的这个点向下进行遍历检测。
与 beta 版类似,我们使用一个 stepSize 来用作斜率的检测
但是对于遍历检测而言,与 beta 版不同的是,我们需要对遍历的地方进行一定的限制。
限制的标准是,如果当前遍历的点的横坐标和起始点横坐标的插值超过了某个阈值,则认为是越界。
"""
point_k = 1
_, _, _, a = cv2.split(image) # 这应该是一个四通道的图像
height, width = a.shape
ret, a_thresh = cv2.threshold(a, 127, 255, cv2.THRESH_BINARY) # 将透明图层二值化
# 直接返回脖子的位置信息,修正系数为 0, get_box_pro 内部也封装了二值化,所以直接输入原图
y_high, y_low, _, _ = get_box_pro(image=image, model=1, correction_factor=0)
# 真正有用的只有上下 y 轴的两个值...
# 首先当然是确定起始点的位置,我们用同样的 scan 扫描函数进行行遍历。
def scan(y_:int, max_num:int=2):
num = 0
# 设定两个值,分别代表脖子的左边和右边
left = False
right = False
for x_ in range(width):
if a_thresh[y_][x_] != 0:
# 检测左边
if x_ < width // 2 and left is False:
num += 1
left = True
# 检测右边
elif x_ > width // 2 and right is False:
num += 1
right = True
return True if num >= max_num else False
def locate_neck_above():
"""
定位脖子的尖尖脚
"""
# y_high 就是脖子的最高点
for y_ in range(y_high, height):
if scan(y_):
return y_
y_start = locate_neck_above() # 得到遍历的初始高度
if y_low - y_start < stepSize: assert ValueError("脖子太小!")
# 然后获取一下初始的坐标点
x_left, x_right = 0, width
for x_left_ in range(0, width):
if a_thresh[y_start][x_left_] != 0:
x_left = x_left_
break
for x_right_ in range(width -1 , -1, -1):
if a_thresh[y_start][x_right_] != 0:
x_right = x_right_
break
# 接下来我定义两个生成器,首先是脖子轮廓 (向下寻找的) 生成器,每进行一次 next,生成器会返回 y+1 的脖子轮廓点
def contoursGenerator(image_:np.ndarray, y_:int, mode):
"""
这会是一个生成器,用于生成脖子两边的轮廓
y_ 是启始点的 y 坐标,每一次寻找都会让 y_+1
mode==1 说明是找左边的边,即,image_[y_][x_] == 0 且 image_[y_][x_ + 1] !=0 时跳出;
否则 当 image_[y_][x_] != 0 时,x_ - 1; 当 image_[y_][x_] == 0 且 image_[y_][x_ + 1] ==0 时 x_ + 1
mode==2 说明是找右边的边,即,image_[y_][x_] == 0 且 image_[y_][x_ - 1] !=0 时跳出
否则 当 image_[y_][x_] != 0 时,x_ + 1; 当 image_[y_][x_] == 0 且 image_[y_][x_ - 1] ==0 时 x_ - 1
"""
y_ += 1
try:
if mode == 1:
x_ = 0
while 0 <= y_ < height and 0 <= x_ < width:
while image_[y_][x_] != 0 and x_ >= 0: x_ -= 1
# 这里其实会有 bug,不过可以不管
while x_ < width and image_[y_][x_] == 0 and image_[y_][x_ + 1] == 0: x_ += 1
yield [y_, x_]
y_ += 1
elif mode == 2:
x_ = width-1
while 0 <= y_ < height and 0 <= x_ < width:
while x_ < width and image_[y_][x_] != 0: x_ += 1
while x_ >= 0 and image_[y_][x_] == 0 and image_[y_][x_ - 1] == 0: x_ -= 1
yield [y_, x_]
y_ += 1
# 当处理失败则返回 False
except IndexError:
yield False
# 然后是斜率生成器,这个生成器依赖子轮廓生成器,每一次生成轮廓后会计算斜率,另一个点的选取和 stepSize 有关
def kGenerator(image_: np.ndarray, mode):
"""
导数生成器,用来生成每一个点对应的导数
"""
y_ = y_start
# 对起始点建立一个生成器,mode=1 时是左边轮廓,mode=2 时是右边轮廓
c_generator = contoursGenerator(image_=image_, y_=y_, mode=mode)
for cell in c_generator:
# 寻找距离当前 cell 距离为 stepSize 的轮廓点
kc = contoursGenerator(image_=image_, y_=cell[0] + stepSize, mode=mode)
kCell = next(kc)
if kCell is False:
# 寻找失败
yield False, False
else:
# 寻找成功,返回当坐标点和斜率值
# 对于左边而言,斜率必然是前一个点的坐标减去后一个点的坐标
# 对于右边而言,斜率必然是后一个点的坐标减去前一个点的坐标
k = (cell[1] - kCell[1]) / stepSize if mode == 1 else (kCell[1] - cell[1]) / stepSize
yield k, cell
# 接着开始写寻找算法,需要注意的是我们是分两边选择的
def findPt(image_:np.ndarray, mode):
x_base = x_left if mode == 1 else x_right
k_generator = kGenerator(image_=image_, mode=mode)
k, cell = k_generator.__next__()
if k is False:
raise ValueError("无法找到拐点!")
k_next, cell_next = k_generator.__next__()
while k_next is not False:
cell = cell_next
if cell[1] > x_base and mode == 2:
x_base = cell[1]
elif cell[1] < x_base and mode == 1:
x_base = cell[1]
# 跳出循环的方式一:斜率超过了某个值
if k_next > point_k:
print("K out")
break
# 跳出循环的方式二:超出阈值
elif abs(cell[1] - x_base) > offset:
print("O out")
break
k_next, cell_next = k_generator.__next__()
if abs(cell[1] - x_base) > offset:
cell[0] = cell[0] - offset - 1
return cell[0]
# 先找左边的拐点:
pointY_left = findPt(image_=a_thresh, mode=1)
# 再找右边的拐点:
pointY_right = findPt(image_=a_thresh, mode=2)
point = min(pointY_right, pointY_left)
per = (point - y_high) / (y_low - y_high)
# pointX_left = next(contoursGenerator(image_=a_thresh, y_= point- 1, mode=1))[1]
# pointX_right = next(contoursGenerator(image_=a_thresh, y_=point - 1, mode=2))[1]
# return [pointX_left, point], [pointX_right, point]
return per
def checkSharpCorner(image:np.ndarray):
_, _, _, a = cv2.split(image) # 这应该是一个四通道的图像
height, width = a.shape
ret, a_thresh = cv2.threshold(a, 127, 255, cv2.THRESH_BINARY) # 将透明图层二值化
# 直接返回脖子的位置信息,修正系数为 0, get_box_pro 内部也封装了二值化,所以直接输入原图
y_high, y_low, _, _ = get_box_pro(image=image, model=1, correction_factor=0)
def scan(y_:int, max_num:int=2):
num = 0
# 设定两个值,分别代表脖子的左边和右边
left = False
right = False
for x_ in range(width):
if a_thresh[y_][x_] != 0:
# 检测左边
if x_ < width // 2 and left is False:
num += 1
left = True
# 检测右边
elif x_ > width // 2 and right is False:
num += 1
right = True
return True if num >= max_num else False
def locate_neck_above():
"""
定位脖子的尖尖脚
"""
# y_high 就是脖子的最高点
for y_ in range(y_high, height):
if scan(y_):
return y_
y_start = locate_neck_above()
return y_start
def checkJaw(image:np.ndarray, y_start:int):
# 寻找"马鞍点"
_, _, _, a = cv2.split(image) # 这应该是一个四通道的图像
height, width = a.shape
ret, a_thresh = cv2.threshold(a, 127, 255, cv2.THRESH_BINARY) # 将透明图层二值化
if width <=1: raise TypeError("图像太小!")
x_left, x_right = 0, width - 1
for x_left in range(width):
if a_thresh[y_start][x_left] != 0:
while a_thresh[y_start][x_left] != 0: x_left += 1
break
for x_right in range(width-1, -1, -1):
if a_thresh[y_start][x_right] != 0:
while a_thresh[y_start][x_right] != 0: x_right -= 1
break
point_list_y = []
point_list_x = []
for x in range(x_left, x_right):
y = y_start
while a_thresh[y][x] == 0: y += 1
point_list_y.append(y)
point_list_x.append(x)
y = max(point_list_y)
x = point_list_x[point_list_y.index(y)]
return x, y
def checkHairLOrR(cloth_image_input_cut,
input_a,
neck_a,
cloth_image_input_top_y,
cutbar_top=0.4,
cutbar_bottom=0.5,
threshold=0.3):
"""
本函数用于检测衣服是否被头发遮挡,当前只考虑左右是否被遮挡,即"一刀切"
返回 int
0 代表没有被遮挡
1 代表左边被遮挡
2 代表右边被遮挡
3 代表全被遮挡了
约定,输入的图像是一张灰度图,且被二值化过。
"""
def per_darkPoint(img:np.ndarray) -> int:
"""
用于遍历相加图像上的黑点。
然后返回黑点数/图像面积
"""
h, w = img.shape
sum_darkPoint = 0
for y in range(h):
for x in range(w):
if img[y][x] == 0:
sum_darkPoint += 1
return sum_darkPoint / (h * w)
if threshold < 0 or threshold > 1: raise TypeError("阈值设置必须在 0 和 1 之间!")
# 裁出 cloth_image_input_cut 按高度 40%50% 的区域-cloth_image_input_cutbar,并转换为 A 矩阵,做二值化
cloth_image_input_height = cloth_image_input_cut.shape[0]
_, _, _, cloth_image_input_cutbar = cv2.split(cloth_image_input_cut[
int(cloth_image_input_height * cutbar_top):int(
cloth_image_input_height * cutbar_bottom), :])
_, cloth_image_input_cutbar = cv2.threshold(cloth_image_input_cutbar, 127, 255, cv2.THRESH_BINARY)
# 裁出 input_image、neck_image 的 A 矩阵的对应区域,并做二值化
input_a_cutbar = input_a[cloth_image_input_top_y + int(cloth_image_input_height * cutbar_top):
cloth_image_input_top_y + int(cloth_image_input_height * cutbar_bottom), :]
_, input_a_cutbar = cv2.threshold(input_a_cutbar, 127, 255, cv2.THRESH_BINARY)
neck_a_cutbar = neck_a[cloth_image_input_top_y + int(cloth_image_input_height * cutbar_top):
cloth_image_input_top_y + int(cloth_image_input_height * cutbar_bottom), :]
_, neck_a_cutbar = cv2.threshold(neck_a_cutbar, 50, 255, cv2.THRESH_BINARY)
# 将三个 cutbar 合到一起-result_a_cutbar
input_a_cutbar = np.uint8(255 - input_a_cutbar)
result_a_cutbar = cv2.add(input_a_cutbar, cloth_image_input_cutbar)
result_a_cutbar = cv2.add(result_a_cutbar, neck_a_cutbar)
if_mask = 0
# 我们将图像 一刀切,分为左边和右边
height, width = result_a_cutbar.shape # 一通道图像
left_image = result_a_cutbar[:, :width//2]
right_image = result_a_cutbar[:, width//2:]
if per_darkPoint(left_image) > threshold:
if_mask = 1
if per_darkPoint(right_image) > threshold:
if_mask = 3 if if_mask == 1 else 2
return if_mask
if __name__ == "__main__":
for i in range(1, 8):
img = cv2.imread(f"./neck_temp/neck_image{i}.png", cv2.IMREAD_UNCHANGED)
# new = transformationNeck(image=img, cutNeckHeight=419,neckBelow=472, toHeight=150)
# point_list = bestJunctionCheck(img, offset=5, stepSize=3)
# per = bestJunctionCheck(img, offset=5, stepSize=3)
# # 返回一个小数的形式,接下来我将它处理为两个点
point_list = []
# y_high_, y_low_, _, _ = get_box_pro(image=img, model=1, conreection_factor=0)
# _y = y_high_ + int((y_low_ - y_high_) * per)
# _, _, _, a_ = cv2.split(img) # 这应该是一个四通道的图像
# h, w = a_.shape
# r, a_t = cv2.threshold(a_, 127, 255, cv2.THRESH_BINARY) # 将透明图层二值化
# _x = 0
# for _x in range(w):
# if a_t[_y][_x] != 0:
# break
# point_list.append([_x, _y])
# for _x in range(w - 1, -1, -1):
# if a_t[_y][_x] != 0:
# break
# point_list.append([_x, _y])
y = checkSharpCorner(img)
point = checkJaw(image=img, y_start=y)
point_list.append(point)
new = draw_picture_dots(img, point_list, pen_size=2)
cv2.imshow(f"{i}", new)
cv2.waitKey(0)
def find_black(image):
"""
找黑色点函数,遇到输入矩阵中的第一个黑点,返回它的 y 值
"""
height, width = image.shape[0], image.shape[1]
for i in range(height):
for j in range(width):
if image[i, j] < 127:
return i
return None
def convert_black_array(image):
height, width = image.shape[0], image.shape[1]
mask = np.zeros([height, width])
for j in range(width):
for i in range(height):
if image[i, j] > 127:
mask[i:, j] = 1
break
return mask
def checkLongHair(neck_image, head_bottom_y, neck_top_y):
"""
长发检测函数,输入为 head/neck 图像,通过下巴是否为最低点,来判断是否为长发
:return 0 : 短发
:return 1 : 长发
"""
jaw_y = neck_top_y + checkJaw(neck_image, y_start=checkSharpCorner(neck_image))[1]
if jaw_y >= head_bottom_y-3:
return 0
else:
return 1
def checkLongHair2(head_bottom_y, cloth_top_y):
if head_bottom_y > cloth_top_y+10:
return 1
else:
return 0
@@ -1,271 +0,0 @@
import cv2
import numpy as np
from ..utils import get_box_pro, cut_BiggestAreas, locate_neck, get_cutbox_image
from .move_image import move
from ..vision import add_background, cover_image
from ..matting_tools import get_modnet_matting
from .neck_processing import transformationNeck
from .cuny_tools import checkSharpCorner, checkJaw, checkHairLOrR,\
checkLongHair, checkLongHair2, convert_black_array, find_black
test_image_path = "./supple_image/"
def change_cloth(input_image:np.array,
cloth_model,
CLOTH_WIDTH,
CLOTH_X,
CLOTH_WIDTH_CHANGE,
CLOTH_X_CHANGE,
CLOTH_Y,
neck_ratio=0.2,
space_adjust=None,
hair_front=True
):
# ============= 1. 得到头脖子图、纯头图、纯脖子图的相关信息 =============== #
# 1.1 获取原图 input_image 属性
input_height, input_width = input_image.shape[0], input_image.shape[1]
# print("input_height:", input_height)
# print("input_width", input_width)
b, g, r, input_a = cv2.split(input_image)
# 1.2 得到头脖子图 headneck_image、纯头图 head_image
input_image = add_background(input_image, bgr=(255, 255, 255))
headneck_image = get_modnet_matting(input_image, checkpoint_path="./checkpoint/huanying_headneck3.onnx")
head_image = get_modnet_matting(input_image, checkpoint_path="./checkpoint/huanying_head3.onnx")
# 1.3 得到优化后的脖子图 neck_threshold_image
_, _, _, headneck_a = cv2.split(headneck_image)
_, _, _, head_a = cv2.split(head_image)
neck_a = cv2.subtract(headneck_a, head_a)
_, neck_threshold_a = cv2.threshold(neck_a, 180, 255, cv2.THRESH_BINARY)
neck_threshold_image = cut_BiggestAreas(cv2.merge(
(np.uint8(b), np.uint8(g), np.uint8(r), np.uint8(neck_threshold_a))))
# 1.4 得到优化后的头脖子图 headneck_threshold_image
_, headneck_threshold_a = cv2.threshold(headneck_a, 180, 255, cv2.THRESH_BINARY)
headneck_threshold_image = cut_BiggestAreas(
cv2.merge((np.uint8(b), np.uint8(g), np.uint8(r), np.uint8(headneck_threshold_a))))
# 1.5 获取脖子图、头脖子图的 A 矩阵
_, _, _, neck_threshold_a2 = cv2.split(neck_threshold_image)
_, _, _, headneck_threshold_a2 = cv2.split(headneck_threshold_image)
# 1.6 获取头发的底部坐标信息,以及头的左右坐标信息
_, headneck_y_bottom, headneck_x_left, headneck_x_right = get_box_pro(headneck_threshold_image,
model=2, correction_factor=0)
headneck_y_bottom = input_height-headneck_y_bottom
headneck_x_right = input_width-headneck_x_right
# ============= 2. 得到原来的衣服的相关信息 =============== #
# 2.1 抠出原来衣服 cloth_image_input
cloth_origin_image_a = cv2.subtract(np.uint8(input_a), np.uint8(headneck_a))
_, cloth_origin_image_a = cv2.threshold(cloth_origin_image_a, 180, 255, cv2.THRESH_BINARY)
cloth_image_input = cut_BiggestAreas(cv2.merge((np.uint8(b), np.uint8(g), np.uint8(r), np.uint8(cloth_origin_image_a))))
# 2.2 对 cloth_image_input 做裁剪(减去上面的大片透明区域)
cloth_image_input_top_y, _, _, _ = get_box_pro(cloth_image_input, model=2)
cloth_image_input_cut = cloth_image_input[cloth_image_input_top_y:, :]
# ============= 3.计算脖子的衔接点信息,为新服装拼接作准备 ===============#
# 3.1 得到裁剪透明区域后的脖子图 neck_cut_image,以及它的坐标信息
neck_y_top, neck_y_bottom, neck_x_left, neck_x_right = get_box_pro(neck_threshold_image, model=2)
neck_cut_image = get_cutbox_image(neck_threshold_image)
neck_height = input_height - (neck_y_top + neck_y_bottom)
neck_width = input_width - (neck_x_right + neck_x_left)
# 3.2 对 neck_cut_image 做“尖尖”检测,得到较低的“尖尖”对于脖子高度的比率 y_neck_corner_ratio
y_neck_corner = checkSharpCorner(neck_cut_image)
y_neck_corner_ratio = y_neck_corner / neck_height
# 3.3 取 y_neck_corner_ratio 与新衣服预先设定好的 neck_ratio 的最大值,作为最终的 neck_ratio
neck_ratio = max(neck_ratio, y_neck_corner_ratio)
# 3.4 计算在 neck_ratio 下的脖子左衔接点坐标 neck_left_x_byRationeck_left_y_byRatio、宽度 neck_width_byRatio
neck_coordinate1, neck_coordinate2, neck_width_byRatio = locate_neck(neck_cut_image, float(neck_ratio))
neck_width_byRatio = neck_width_byRatio + CLOTH_WIDTH_CHANGE
neck_left_x_byRatio = neck_x_left + neck_coordinate1[1] + CLOTH_X_CHANGE
neck_left_y_byRatio = neck_y_top + neck_coordinate1[0]
# ============= 4.读取新衣服图,调整大小 =============== #
# 4.1 得到新衣服图片的拼贴坐标 x, y 以及脖子最底部的坐标 y_neckline
CLOTH_HEIGHT = CLOTH_Y
RESIZE_RATIO = neck_width_byRatio / CLOTH_WIDTH
x, y = int(neck_left_x_byRatio - CLOTH_X * RESIZE_RATIO), neck_left_y_byRatio
y_neckline = y + int(CLOTH_HEIGHT * RESIZE_RATIO)
# 4.2 读取新衣服,并进行缩放
cloth = cv2.imread(cloth_model, -1)
cloth_height, cloth_width = cloth.shape[0], cloth.shape[1]
cloth = cv2.resize(cloth, (int(cloth_width * RESIZE_RATIO),
int(cloth_height * RESIZE_RATIO)), interpolation=cv2.INTER_AREA)
# 4.3 获得新衣服的 A 矩阵
_, _, _, cloth_a = cv2.split(cloth)
# ============= 5. 判断头发的前后关系,以及对于长发的背景填充、判定是否为长发等 =============== #
# 5.1 根据 hair_number, 判断是 0:头发披在后面、1:左前右后、2:左后右前还是 3:都在前面
hair_number = checkHairLOrR(cloth_image_input_cut, input_a, neck_a, cloth_image_input_top_y)
# 5.2 对于长发的背景填充 - 将原衣服区域的部分变成黑色,并填充到最终图片作为背景
cloth_image_input_save = cloth_origin_image_a[:int(y+cloth_height*RESIZE_RATIO),
max(0, headneck_x_left-1):min(headneck_x_right+1, input_width)]
headneck_threshold_a_save = headneck_a[:int(y+cloth_height*RESIZE_RATIO),
max(0, headneck_x_left-1):min(headneck_x_right+1, input_width)]
headneck_mask = convert_black_array(headneck_threshold_a_save)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 15))
cloth_image_input_save = cv2.dilate(cloth_image_input_save, kernel)
cloth_image_input_save = np.uint8(cloth_image_input_save*headneck_mask)
# 5.3 检测是否为长发
head_bottom_y = input_height - get_box_pro(head_image, model=2, correction_factor=0)[1]
isLongHair01 = checkLongHair(neck_cut_image, head_bottom_y, neck_top_y=neck_y_top)
isLongHair02 = checkLongHair2(head_bottom_y, cloth_image_input_top_y)
isLongHair = isLongHair01 and isLongHair02
# ============= 6.第一轮服装拼贴 =============== #
# 6.1 创建一个空白背景 background
background = np.uint8((np.zeros([input_height, input_width, 4])))
# 6.2 盖上 headneck_image
result_headneck_image = cover_image(headneck_image, background, 0, 0, mode=3)
# 6.3 如果 space_adjust 开启的话,background 的底部将增加一些行数
if space_adjust is not None:
insert_array = np.uint8(np.zeros((space_adjust, input_width, 4)))
result_headneck_image = np.r_[result_headneck_image, insert_array]
# 6.4 盖上新衣服
result_cloth_image = cover_image(cloth, result_headneck_image, x, y, mode=3)
# 6.5 截出脖子与衣服交接的区域 neck_cloth_image,以及它的 A 矩阵 neck_cloth_a
neck_cloth_image = result_cloth_image[y:y_neckline,
neck_left_x_byRatio:neck_left_x_byRatio+neck_width_byRatio]
_, _, _, neck_cloth_a = cv2.split(neck_cloth_image)
_, neck_cloth_a = cv2.threshold(neck_cloth_a, 127, 255, cv2.THRESH_BINARY)
# ============= 7.第二轮服装拼贴 =============== #
# 7.1 检测 neck_cloth_a 中是否有黑点(即镂空区域)
# 如果 black_dots_y 不为 None,说明存在镂空区域——需要进行脖子拉伸;反而则不存在,不需要
black_dots_y = find_black(neck_cloth_a)
# cv2.imwrite(test_image_path+"neck_cloth_a.jpg", neck_cloth_a)
# flag: 用于指示是否进行拉伸
flag = 0
# 7.2 如果存在镂空区域,则进行拉伸
if black_dots_y != None:
flag = 1
# cutNeckHeight:要拉伸的区域的顶部 y 值
# neckBelow:脖子底部的 y 值
# toHeight:拉伸区域的高度
cutNeckHeight = black_dots_y + y - 6
# if cutNeckHeight < neck_y_top+checkJaw(neck_cut_image, y_start=checkSharpCorner(neck_cut_image))[1]:
# print("拒绝!!!!!!")
# return 0, 0, 0, 0, 0
neckBelow = input_height-neck_y_bottom
toHeight = y_neckline-cutNeckHeight
print("cutNeckHeight:", cutNeckHeight)
print("toHeight:", toHeight)
print("neckBelow:", neckBelow)
# cv2.imwrite(test_image_path+"neck_image.png", neck_threshold_image)
# 对原有的脖子做拉伸,得到 new_neck_image
new_neck_image = transformationNeck(neck_threshold_image,
cutNeckHeight=cutNeckHeight,
neckBelow=neckBelow,
toHeight=toHeight)
# cv2.imwrite(test_image_path+"new_neck_image.png", new_neck_image)
# 重新进行拼贴
result_headneck_image = cover_image(new_neck_image, result_headneck_image, 0, 0, mode=3)
result_headneck_image = cover_image(head_image, result_headneck_image, 0, 0, mode=3)
result_cloth_image = cover_image(cloth, result_headneck_image, x, y, mode=3)
_, _, _, neck_a = cv2.split(new_neck_image)
# 7.3 下面是对最终图的 A 矩阵进行处理
# 首先将 neck_a 与新衣服衔接点的左边两边区域删去,得到 neck_a_leftright
neck_a_copy = neck_a.copy()
neck_a_copy[neck_left_y_byRatio:, :max(0, neck_left_x_byRatio-4)] = 0
neck_a_copy[neck_left_y_byRatio:,
min(input_width, neck_left_x_byRatio + neck_width_byRatio - CLOTH_X_CHANGE+4):] = 0
n_a_leftright = cv2.subtract(neck_a, neck_a_copy)
# 7.4 如果存在镂空区域,则对 headneck_a 做进一步处理
if black_dots_y != None:
neck_a = cv2.subtract(neck_a, n_a_leftright)
# 得到去掉脖子两翼的新的 headneck_a
headneck_a = cv2.subtract(headneck_a, n_a_leftright)
# 将 headneck_a 覆盖上拉伸后的脖子 A 矩阵
headneck_a = np.uint8(cover_image(neck_a, headneck_a, 0, 0, mode=2))
else:
headneck_a = cv2.subtract(headneck_a, n_a_leftright)
# 7.5 如果是长发
if isLongHair:
# 在背景加入黑色矩形,填充抠头模型可能会出现的,部分长发没有抠全的部分
black_background_x1 = int(neck_left_x_byRatio - neck_width_byRatio * 0.1)
black_background_x2 = int(neck_left_x_byRatio + neck_width_byRatio * 1.1)
black_background_y1 = int(neck_y_top - neck_height * 0.1)
black_background_y2 = min(input_height - neck_y_bottom - 3, head_bottom_y)
headneck_a[black_background_y1:black_background_y2, black_background_x1:black_background_x2] = 255
# 在背景中,将原本衣服区域置为黑色
headneck_a = cover_image(cloth_image_input_save, headneck_a, max(0, headneck_x_left-1), 0, mode=2)
# 7.6 如果 space_adjust 开启的话,headneck_a 的底部将增加一些行数
if space_adjust is not None:
insert_array = np.uint8(np.zeros((space_adjust, input_width)))
headneck_a = np.r_[headneck_a, insert_array]
# 7.7 盖上新衣服
new_a = np.uint8(cover_image(cloth_a, headneck_a, x, y, mode=2))
# neck_cloth_a = new_a[y:y_neckline, neck_left_x_byRatio:neck_left_x_byRatio + neck_width_byRatio]
# _, neck_cloth_a = cv2.threshold(neck_cloth_a, 127, 255, cv2.THRESH_BINARY)
# cv2.imwrite(test_image_path + "neck_cloth_a2.jpg", neck_cloth_a)
#
# if find_black(neck_cloth_a) != None:
# print("拒绝!!!!")
# return "拒绝"
# 7.8 如果有头发披在前面
if hair_front:
# 如果头发披在左边
if hair_number == 1:
result_cloth_image = cover_image(head_image[:, :head_image.shape[1] // 2], result_cloth_image, 0, 0, mode=3)
# 如果头发披在右边
elif hair_number == 2:
result_cloth_image = cover_image(head_image[:, head_image.shape[1] // 2:], result_cloth_image, head_image.shape[1] // 2, 0, mode=3)
# 如果头发披在两边
elif hair_number == 3:
result_cloth_image = cover_image(head_image, result_cloth_image, 0, 0, mode=3)
# 7.9 合成最终图片,并做底部空隙的移动
r, g, b, _ = cv2.split(result_cloth_image)
result_image = move(cv2.merge((r, g, b, new_a)))
# 7.10 返回:结果图、是否拉伸、头发前披状态、是否长发
return 1, result_image, flag, hair_number, isLongHair
if __name__ == "__main__":
pass
-420
View File
@@ -1,420 +0,0 @@
import cv2
import math
from ..utils import get_box_pro
from ..face_tools import face_detect_mtcnn
from ..vision import IDphotos_cut, detect_distance, resize_image_esp, draw_picture_dots
from ..matting_tools import get_modnet_matting
from .move_image import move
from src.hivisionai.hyTrain.APIs import aliyun_face_detect_api
import numpy as np
import json
def get_max(height, width, d1, d2, d3, d4, rotation_flag):
if rotation_flag:
height1 = height
height2 = height - int(d1.y) # d2
height3 = int(d4.y) # d3
height4 = int(d4.y) - int(d1.x)
width1 = width
width2 = width - int(d3.x)
width3 = int(d2.x)
width4 = int(d2.x) - int(d3.x)
else:
height1 = height
height2 = height - int(d2.y)
height3 = int(d3.y)
height4 = int(d3.y) - int(d2.y)
width1 = width
width2 = width - int(d1.x)
width3 = int(d4.x)
width4 = int(d4.x) - int(d1.x)
height_list = [height1, height2, height3, height4]
width_list = [width1, width2, width3, width4]
background_height = max(height_list)
status_height = height_list.index(background_height)
background_width = max(width_list)
status_width = width_list.index(background_width)
height_change = 0
width_change = 0
height_change2 = 0
width_change2 = 0
if status_height == 1 or status_height == 3:
if rotation_flag:
height_change = abs(d1.y)
height_change2 = d1.y
else:
height_change = abs(d2.y)
height_change2 = d2.y
if status_width == 1 or status_width == 3:
if rotation_flag:
width_change = abs(d3.x)
width_change2 = d3.x
else:
width_change = abs(d1.x)
width_change2 = d1.x
return background_height, status_height, background_width, status_width, height_change, width_change,\
height_change2, width_change2
class LinearFunction_TwoDots(object):
"""
通过两个坐标点构建线性函数
"""
def __init__(self, dot1, dot2):
self.d1 = dot1
self.d2 = dot2
self.k = (self.d2.y - self.d1.y) / (self.d2.x - self.d1.x)
self.b = self.d2.y - self.k * self.d2.x
def forward(self, input, mode="x"):
if mode == "x":
return self.k * input + self.b
elif mode == "y":
return (input - self.b) / self.k
def forward_x(self, x):
return self.k * x + self.b
def forward_y(self, y):
return (y - self.b) / self.k
class Coordinate(object):
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return "({}, {})".format(self.x, self.y)
def IDphotos_create(input_image, size=(413, 295), head_measure_ratio=0.2, head_height_ratio=0.45,
checkpoint_path="checkpoint/ModNet1.0.onnx", align=True):
"""
input_path: 输入图像路径
output_path: 输出图像路径
size: 裁剪尺寸,格式应该如 (413,295),竖直距离在前,水平距离在后
head_measure_ratio: 人头面积占照片面积的 head_ratio
head_height_ratio: 人头中心处于照片从上到下的 head_height
align: 是否进行人脸矫正
"""
input_image = resize_image_esp(input_image, 2000) # 将输入图片压缩到最大边长为 2000
# cv2.imwrite("./temp_input_image.jpg", input_image)
origin_png_image = get_modnet_matting(input_image, checkpoint_path)
# cv2.imwrite("./test_image/origin_png_image.png", origin_png_image)
_, _, _, a = cv2.split(origin_png_image)
width_length_ratio = size[0]/size[1] # 长宽比
rotation = aliyun_face_detect_api("./temp_input_image.jpg")
# 如果旋转角过小,则不进行矫正
if abs(rotation) < 0.025:
align=False
if align:
print("开始 align")
if rotation > 0:
rotation_flag = 0 # 逆时针旋转
else:
rotation_flag = 1 # 顺时针旋转
width, height, channels = input_image.shape
p_list = [(0, 0), (0, height), (width, 0), (width, height)]
rotate_list = []
rotate = cv2.getRotationMatrix2D((height * 0.5, width * 0.5), rotation, 0.75)
for p in p_list:
p_m = np.array([[p[1]], [p[0]], [1]])
rotate_list.append(np.dot(rotate[:2], p_m))
# print("旋转角的四个顶点", rotate_list)
input_image = cv2.warpAffine(input_image, rotate, (height, width), flags=cv2.INTER_AREA)
new_a = cv2.warpAffine(a, rotate, (height, width), flags=cv2.INTER_AREA)
# cv2.imwrite("./test_image/rotation.jpg", input_image)
# ===================== 开始人脸检测 ===================== #
faces, _ = face_detect_mtcnn(input_image, filter=True)
face_num = len(faces)
print("检测到的人脸数目为:", len(faces))
# ===================== 人脸检测结束 ===================== #
if face_num == 1:
face_rect = faces[0]
x, y = face_rect[0], face_rect[1]
w, h = face_rect[2] - x + 1, face_rect[3] - y + 1
elif face_num == 0:
print("无人脸,返回 0!!!")
return 0
else:
print("太多人脸,返回 2!!!")
return 2
d1, d2, d3, d4 = rotate_list[0], rotate_list[1], rotate_list[2], rotate_list[3]
d1 = Coordinate(int(d1[0]), int(d1[1]))
d2 = Coordinate(int(d2[0]), int(d2[1]))
d3 = Coordinate(int(d3[0]), int(d3[1]))
d4 = Coordinate(int(d4[0]), int(d4[1]))
print("d1:", d1)
print("d2:", d2)
print("d3:", d3)
print("d4:", d4)
background_height, status_height, background_width, status_width,\
height_change, width_change, height_change2, width_change2 = get_max(width, height, d1, d2, d3, d4, rotation_flag)
print("background_height:", background_height)
print("background_width:", background_width)
print("status_height:", status_height)
print("status_width:", status_width)
print("height_change:", height_change)
print("width_change:", width_change)
background = np.zeros([background_height, background_width, 3])
background_a = np.zeros([background_height, background_width])
background[height_change:height_change+width, width_change:width_change+height] = input_image
background_a[height_change:height_change+width, width_change:width_change+height] = new_a
d1 = Coordinate(int(d1.x)-width_change2, int(d1.y)-height_change2)
d2 = Coordinate(int(d2.x)-width_change2, int(d2.y)-height_change2)
d3 = Coordinate(int(d3.x)-width_change2, int(d3.y)-height_change2)
d4 = Coordinate(int(d4.x)-width_change2, int(d4.y)-height_change2)
print("d1:", d1)
print("d2:", d2)
print("d3:", d3)
print("d4:", d4)
if rotation_flag:
f13 = LinearFunction_TwoDots(d1, d3)
d5 = Coordinate(max(0, d3.x), f13.forward_x(max(0, d3.x)))
print("d5:", d5)
f42 = LinearFunction_TwoDots(d4, d2)
d7 = Coordinate(f42.forward_y(d5.y), d5.y)
print("d7", d7)
background_draw = draw_picture_dots(background, dots=[(d1.x, d1.y),
(d2.x, d2.y),
(d3.x, d3.y),
(d4.x, d4.y),
(d5.x, d5.y),
(d7.x, d7.y)])
# cv2.imwrite("./test_image/rotation_background.jpg", background_draw)
if x<d5.x or x+w>d7.x:
print("return 6")
return 6
background_output = background[:int(d5.y), int(d5.x):int(d7.x)]
background_a_output = background_a[:int(d5.y), int(d5.x):int(d7.x)]
# cv2.imwrite("./test_image/rotation_background_cut.jpg", background_output)
else:
f34 = LinearFunction_TwoDots(d3, d4)
d5 = Coordinate(min(width_change+height, d4.x), f34.forward_x(min(width_change+height, d4.x)))
print("d5:", d5)
f13 = LinearFunction_TwoDots(d1, d3)
d7 = Coordinate(f13.forward_y(d5.y), d5.y)
print("d7", d7)
if x<d7.x or x+w>d5.x:
print("return 6")
return 6
background_draw = draw_picture_dots(background, dots=[(d1.x, d1.y),
(d2.x, d2.y),
(d3.x, d3.y),
(d4.x, d4.y),
(d5.x, d5.y),
(d7.x, d7.y)])
# cv2.imwrite("./test_image/rotation_background.jpg", background_draw)
background_output = background[:int(d5.y), int(d7.x):int(d5.x)]
background_a_output = background_a[:int(d5.y), int(d7.x):int(d5.x)]
# cv2.imwrite("./test_image/rotation_background_cut.jpg", background_output)
input_image = np.uint8(background_output)
b, g, r = cv2.split(input_image)
origin_png_image = cv2.merge((b, g, r, np.uint8(background_a_output)))
# ===================== 开始人脸检测 ===================== #
width, length = input_image.shape[0], input_image.shape[1]
faces, _ = face_detect_mtcnn(input_image, filter=True)
face_num = len(faces)
print("检测到的人脸数目为:", len(faces))
# ===================== 人脸检测结束 ===================== #
if face_num == 1:
face_rect = faces[0]
x, y = face_rect[0], face_rect[1]
w, h = face_rect[2] - x + 1, face_rect[3] - y + 1
# x,y,w,h 代表人脸框的左上角坐标和宽高
# 检测头顶下方空隙,如果头顶下方空隙过小,则拒绝
if y+h >= 0.85*width:
# print("face bottom too short! y+h={} width={}".format(y+h, width))
print("在人脸下方的空间太少,返回值 3!!!")
return 3
# 第一次裁剪
# 确定裁剪的基本参数
face_center = (x+w/2, y+h/2) # 面部中心坐标
face_measure = w*h # 面部面积
crop_measure = face_measure/head_measure_ratio # 裁剪框面积:为面部面积的 5 倍
resize_ratio = crop_measure/(size[0]*size[1]) # 裁剪框缩放率 (以输入尺寸为标准)
resize_ratio_single = math.sqrt(resize_ratio)
crop_size = (int(size[0]*resize_ratio_single), int(size[1]*resize_ratio_single)) # 裁剪框大小
print("crop_size:", crop_size)
# 裁剪规则:x1 和 y1 为裁剪的起始坐标,x2 和 y2 为裁剪的最终坐标
# y 的确定由人脸中心在照片的 45% 位置决定
x1 = int(face_center[0]-crop_size[1]/2)
y1 = int(face_center[1]-crop_size[0]*head_height_ratio)
y2 = y1+crop_size[0]
x2 = x1+crop_size[1]
# 对原图进行抠图,得到透明图 img
print("开始进行抠图")
# origin_png_image => 对原图的抠图结果
# cut_image => 第一次裁剪后的图片
# result_image => 第二次裁剪后的图片/输出图片
# origin_png_image = get_human_matting(input_image, get_file_dir(checkpoint_path))
cut_image = IDphotos_cut(x1, y1, x2, y2, origin_png_image)
# cv2.imwrite("./temp.png", cut_image)
# 对裁剪得到的图片 temp_path,我们将 image=temp_path resize 为裁剪框大小,这样方便进行后续计算
cut_image = cv2.resize(cut_image, (crop_size[1], crop_size[0]))
y_top, y_bottom, x_left, x_right = get_box_pro(cut_image, model=2) # 得到透明图中人像的上下左右距离信息
print("y_top:{}, y_bottom:{}, x_left:{}, x_right:{}".format(y_top, y_bottom, x_left, x_right))
# 判断左右是否有间隙
if x_left > 0 or x_right > 0:
# 左右有空隙,我们需要减掉它
print("左右有空隙!")
status_left_right = 1
cut_value_top = int(((x_left + x_right) * width_length_ratio) / 2) # 减去左右,为了保持比例,上下也要相应减少 cut_value_top
print("cut_value_top:", cut_value_top)
else:
# 左右没有空隙,则不管
status_left_right = 0
cut_value_top = 0
print("cut_value_top:", cut_value_top)
# 检测人头顶与照片的顶部是否在合适的距离内
print("y_top:", y_top)
status_top, move_value = detect_distance(y_top-int((x_left+x_right)*width_length_ratio/2), crop_size[0])
# status=0 => 距离合适,无需移动
# status=1 => 距离过大,人像应向上移动
# status=2 => 距离过小,人像应向下移动
# move_value => 上下移动的距离
print("status_top:", status_top)
print("move_value:", move_value)
# 开始第二次裁剪
if status_top == 0:
# 如果上下距离合适,则无需移动
if status_left_right:
# 如果左右有空隙,则需要用到 cut_value_top
result_image = IDphotos_cut(x1 + x_left,
y1 + cut_value_top,
x2 - x_right,
y2 - cut_value_top,
origin_png_image)
else:
# 如果左右没有空隙,那么则无需改动
result_image = cut_image
elif status_top == 1:
# 如果头顶离照片顶部距离过大,需要人像向上移动,则需要用到 move_value
if status_left_right:
# 左右存在距离,则需要 cut_value_top
result_image = IDphotos_cut(x1 + x_left,
y1 + cut_value_top + move_value,
x2 - x_right,
y2 - cut_value_top + move_value,
origin_png_image)
else:
# 左右不存在距离
result_image = IDphotos_cut(x1 + x_left,
y1 + move_value,
x2 - x_right,
y2 + move_value,
origin_png_image)
else:
# 如果头顶离照片顶部距离过小,则需要人像向下移动,则需要用到 move_value
if status_left_right:
# 左右存在距离,则需要 cut_value_top
result_image = IDphotos_cut(x1 + x_left,
y1 + cut_value_top - move_value,
x2 - x_right,
y2 - cut_value_top - move_value,
origin_png_image)
else:
# 左右不存在距离
result_image = IDphotos_cut(x1 + x_left,
y1 - move_value,
x2 - x_right,
y2 - move_value,
origin_png_image)
# 调节头顶位置————防止底部空一块儿
result_image = move(result_image)
# 高清保存
# cv2.imwrite(output_path.replace(".png", "_HD.png"), result_image)
# 普清保存
result_image2 = cv2.resize(result_image, (size[1], size[0]), interpolation=cv2.INTER_AREA)
# cv2.imwrite("./output_image.png", result_image)
print("完成。返回 1")
return 1, result_image, result_image2
elif face_num == 0:
print("无人脸,返回 0!!!")
return 0
else:
print("太多人脸,返回 2!!!")
return 2
if __name__ == "__main__":
with open("./Setting.json") as json_file:
# file_list = get_filedir_filelist("./input_image")
setting = json.load(json_file)
filedir = "../IDPhotos/input_image/linzeyi.jpg"
file_list = [filedir]
for filedir in file_list:
print(filedir)
# try:
status_id, result_image, result_image2 = IDphotos_create(cv2.imread(filedir),
size=(setting["size_height"], setting["size_width"]),
head_height_ratio=setting["head_height_ratio"],
head_measure_ratio=setting["head_measure_ratio"],
checkpoint_path=setting["checkpoint_path"],
align=True)
# cv2.imwrite("./result_image.png", result_image)
if status_id == 1:
print("处理完毕!")
elif status_id == 0:
print("没有人脸!请重新上传有人脸的照片。")
elif status_id == 2:
print("人脸不只一张!请重新上传单独人脸的照片。")
elif status_id == 3:
print("人头下方空隙不足!")
elif status_id == 4:
print("此照片不能制作该规格!")
# except Exception as e:
# print(e)
-121
View File
@@ -1,121 +0,0 @@
"""
有一些 png 图像下部也会有一些透明的区域,使得图像无法对其底部边框
本程序实现移动图像,使其下部与 png 图像实际大小相对齐
"""
import os
import cv2
import numpy as np
from ..utils import get_box_pro
path_pre = os.path.join(os.getcwd(), 'pre')
path_final = os.path.join(os.getcwd(), 'final')
def merge(boxes):
"""
生成的边框可能不止只有一个,需要将边框合并
"""
x, y, h, w = boxes[0]
# x 和 y 应该是整个 boxes 里面最小的值
if len(boxes) > 1:
for tmp in boxes:
x_tmp, y_tmp, h_tmp, w_tmp = tmp
if x > x_tmp:
x_max = x_tmp + w_tmp if x_tmp + w_tmp > x + w else x + w
x = x_tmp
w = x_max - x
if y > y_tmp:
y_max = y_tmp + h_tmp if y_tmp + h_tmp > y + h else y + h
y = y_tmp
h = y_max - y
return tuple((x, y, h, w))
def get_box(png_img):
"""
获取矩形边框最终返回一个元组 (x,y,h,w),分别对应矩形左上角的坐标和矩形的高和宽
"""
r, g, b , a = cv2.split(png_img)
gray_img = a
th, binary = cv2.threshold(gray_img, 127 , 255, cv2.THRESH_BINARY) # 二值化
# cv2.imshow("name", binary)
# cv2.waitKey(0)
contours, hierarchy = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 得到轮廓列表 contours
bounding_boxes = merge([cv2.boundingRect(cnt) for cnt in contours]) # 轮廓合并
# print(bounding_boxes)
return bounding_boxes
def get_box_2(png_img):
"""
不用 opencv 内置算法生成矩形了,改用自己的算法(for 循环)
"""
_, _, _, a = cv2.split(png_img)
_, a = cv2.threshold(a, 127, 255, cv2.THRESH_BINARY)
# 将 r,g,b 通道丢弃,只留下透明度通道
# cv2.imshow("name", a)
# cv2.waitKey(0)
# 在透明度矩阵中,0 代表完全透明
height,width=a.shape # 高和宽
f=0
tmp1 = 0
"""
获取上下
"""
for tmp1 in range(0,height):
tmp_a_high= a[tmp1:tmp1+1,:][0]
for tmp2 in range(width):
# a = tmp_a_low[tmp2]
if tmp_a_high[tmp2]!=0:
f=1
if f == 1:
break
delta_y_high = tmp1 + 1
f = 0
for tmp1 in range(height,-1, -1):
tmp_a_low= a[tmp1-1:tmp1+1,:][0]
for tmp2 in range(width):
# a = tmp_a_low[tmp2]
if tmp_a_low[tmp2]!=0:
f=1
if f == 1:
break
delta_y_bottom = height - tmp1 + 3
"""
获取左右
"""
f = 0
for tmp1 in range(width):
tmp_a_left = a[:, tmp1:tmp1+1]
for tmp2 in range(height):
if tmp_a_left[tmp2] != 0:
f = 1
if f==1:
break
delta_x_left = tmp1 + 1
f = 0
for tmp1 in range(width, -1, -1):
tmp_a_left = a[:, tmp1-1:tmp1]
for tmp2 in range(height):
if tmp_a_left[tmp2] != 0:
f = 1
if f==1:
break
delta_x_right = width - tmp1 + 1
return delta_y_high, delta_y_bottom, delta_x_left, delta_x_right
def move(input_image):
"""
裁剪主函数,输入一张 png 图像,该图像周围是透明的
"""
png_img = input_image # 获取图像
height, width, channels = png_img.shape # 高 y、宽 x
y_low,y_high, _, _ = get_box_pro(png_img, model=2) # for 循环
base = np.zeros((y_high, width, channels),dtype=np.uint8) # for 循环
png_img = png_img[0:height - y_high, :, :] # for 循环
png_img = np.concatenate((base, png_img), axis=0)
return png_img
if __name__ == "__main__":
pass
@@ -1,320 +0,0 @@
import cv2
import numpy as np
from ..utils import get_box_pro
from ..vision import cover_image
def transformationNeck(image:np.ndarray, cutNeckHeight:int, neckBelow:int,
toHeight:int,per_to_side:float=0.75) -> np.ndarray:
"""
脖子扩充算法,其实需要输入的只是脖子扣出来的部分以及需要被扩充的高度/需要被扩充成的高度。
"""
height, width, channels = image.shape
_, _, _, a = cv2.split(image) # 这应该是一个四通道的图像
ret, a_thresh = cv2.threshold(a, 20, 255, cv2.THRESH_BINARY) # 将透明图层二值化
def locate_width(image_:np.ndarray, y_:int, mode, left_or_right:int=None):
# 从 y=y 这个水平线上寻找两边的非零点
# 增加 left_or_right 的原因在于为下面 check_jaw 服务
if mode==1: # 左往右
x_ = 0
if left_or_right is None:
left_or_right = 0
for x_ in range(left_or_right, width):
if image_[y_][x_] != 0:
break
else: # 右往左
x_ = width
if left_or_right is None:
left_or_right = width - 1
for x_ in range(left_or_right, -1, -1):
if image_[y_][x_] != 0:
break
return x_
def check_jaw(image_:np.ndarray, left_, right_):
"""
检查选择的点是否与截到下巴,如果截到了,就往下平移一个单位
"""
f= True # True 代表没截到下巴
# [x, y]
for x_cell in range(left_[0] + 1, right_[0]):
if image_[left_[1]][x_cell] == 0:
f = False
break
if f is True:
return left_, right_
else:
y_ = left_[1] + 2
x_left_ = locate_width(image_, y_, mode=1, left_or_right=left_[0])
x_right_ = locate_width(image_, y_, mode=2, left_or_right=right_[0])
left_, right_ = check_jaw(image_, [x_left_, y_], [x_right_, y_])
return left_, right_
x_left = locate_width(image_=a_thresh, mode=1, y_=cutNeckHeight)
x_right = locate_width(image_=a_thresh, mode=2, y_=cutNeckHeight)
# 在这里我们取消了对下巴的检查,原因在于输入的 imageHeight 并不能改变
# cell_left_above, cell_right_above = check_jaw(a_thresh, [x_left, imageHeight], [x_right, imageHeight])
cell_left_above, cell_right_above = [x_left, cutNeckHeight], [x_right, cutNeckHeight]
toWidth = x_right - x_left # 矩形宽
# 此时我们寻找到了脖子的"宽出来的"两个点,这两个点作为上面的两个点,接下来寻找下面的两个点
if per_to_side >1:
assert ValueError("per_to_side 必须小于 1!")
y_below = int((neckBelow - cutNeckHeight) * per_to_side + cutNeckHeight) # 定位 y 轴坐标
cell_left_below = [locate_width(a_thresh, y_=y_below, mode=1), y_below]
cell_right_bellow = [locate_width(a_thresh, y_=y_below, mode=2), y_below]
# 四个点全齐,开始透视变换
# 需要变换的四个点为 cell_left_above, cell_right_above, cell_left_below, cell_right_bellow
rect = np.array([cell_left_above, cell_right_above, cell_left_below, cell_right_bellow],
dtype='float32')
# 变化后的坐标点
dst = np.array([[0, 0], [toWidth, 0], [0 , toHeight], [toWidth, toHeight]],
dtype='float32')
M = cv2.getPerspectiveTransform(rect, dst)
warped = cv2.warpPerspective(image, M, (toWidth, toHeight))
# 将变换后的图像覆盖到原图上
final = cover_image(image=warped, background=image, mode=3, x=cell_left_above[0], y=cell_left_above[1])
return final
def transformationNeck2(image:np.ndarray, per_to_side:float=0.8)->np.ndarray:
"""
透视变换脖子函数,输入图像和四个点 (矩形框)
矩形框内的图像可能是不完整的 (边角有透明区域)
我们将根据透视变换将矩形框内的图像拉伸成和矩形框一样的形状。
算法分为几个步骤:选择脖子的四个点 -> 选定这四个点拉伸后的坐标 -> 透视变换 -> 覆盖原图
"""
b, g, r, a = cv2.split(image) # 这应该是一个四通道的图像
height, width = a.shape
def locate_side(image_:np.ndarray, x_:int, y_max:int) -> int:
# 寻找 x=y, 且 y <= y_max 上从下往上第一个非 0 的点,如果没找到就返回 0
y_ = 0
for y_ in range(y_max - 1, -1, -1):
if image_[y_][x_] != 0:
break
return y_
def locate_width(image_:np.ndarray, y_:int, mode, left_or_right:int=None):
# 从 y=y 这个水平线上寻找两边的非零点
# 增加 left_or_right 的原因在于为下面 check_jaw 服务
if mode==1: # 左往右
x_ = 0
if left_or_right is None:
left_or_right = 0
for x_ in range(left_or_right, width):
if image_[y_][x_] != 0:
break
else: # 右往左
x_ = width
if left_or_right is None:
left_or_right = width - 1
for x_ in range(left_or_right, -1, -1):
if image_[y_][x_] != 0:
break
return x_
def check_jaw(image_:np.ndarray, left_, right_):
"""
检查选择的点是否与截到下巴,如果截到了,就往下平移一个单位
"""
f= True # True 代表没截到下巴
# [x, y]
for x_cell in range(left_[0] + 1, right_[0]):
if image_[left_[1]][x_cell] == 0:
f = False
break
if f is True:
return left_, right_
else:
y_ = left_[1] + 2
x_left_ = locate_width(image_, y_, mode=1, left_or_right=left_[0])
x_right_ = locate_width(image_, y_, mode=2, left_or_right=right_[0])
left_, right_ = check_jaw(image_, [x_left_, y_], [x_right_, y_])
return left_, right_
# 选择脖子的四个点,核心在于选择上面的两个点,这两个点的确定的位置应该是"宽出来的"两个点
_, _ ,_, a = cv2.split(image) # 这应该是一个四通道的图像
ret,a_thresh = cv2.threshold(a,127,255,cv2.THRESH_BINARY)
y_high, y_low, x_left, x_right = get_box_pro(image=image, model=1) # 直接返回矩阵信息
y_left_side = locate_side(image_=a_thresh, x_=x_left, y_max=y_low) # 左边的点的 y 轴坐标
y_right_side = locate_side(image_=a_thresh, x_=x_right, y_max=y_low) # 右边的点的 y 轴坐标
y = min(y_left_side, y_right_side) # 将两点的坐标保持相同
cell_left_above, cell_right_above = check_jaw(a_thresh,[x_left, y], [x_right, y])
x_left, x_right = cell_left_above[0], cell_right_above[0]
# 此时我们寻找到了脖子的"宽出来的"两个点,这两个点作为上面的两个点,接下来寻找下面的两个点
if per_to_side >1:
assert ValueError("per_to_side 必须小于 1!")
# 在后面的透视变换中我会把它拉成矩形,在这里我先获取四个点的高和宽
height_ = 100 # 这个值应该是个变化的值,与拉伸的长度有关,但是现在先规定为 150
width_ = x_right - x_left # 其实也就是 cell_right_above[1] - cell_left_above[1]
y = int((y_low - y)*per_to_side + y) # 定位 y 轴坐标
cell_left_below, cell_right_bellow = ([locate_width(a_thresh, y_=y, mode=1), y], [locate_width(a_thresh, y_=y, mode=2), y])
# 四个点全齐,开始透视变换
# 寻找透视变换后的四个点,只需要变换 below 的两个点即可
# cell_left_below_final, cell_right_bellow_final = ([cell_left_above[1], y_low], [cell_right_above[1], y_low])
# 需要变换的四个点为 cell_left_above, cell_right_above, cell_left_below, cell_right_bellow
rect = np.array([cell_left_above, cell_right_above, cell_left_below, cell_right_bellow],
dtype='float32')
# 变化后的坐标点
dst = np.array([[0, 0], [width_, 0], [0 , height_], [width_, height_]],
dtype='float32')
# 计算变换矩阵
M = cv2.getPerspectiveTransform(rect, dst)
warped = cv2.warpPerspective(image, M, (width_, height_))
# a = cv2.erode(a, (10, 10))
# image = cv2.merge((r, g, b, a))
final = cover_image(image=warped, background=image, mode=3, x=cell_left_above[0], y=cell_left_above[1])
# tmp = np.zeros(image.shape)
# final = cover_image(image=warped, background=tmp, mode=3, x=cell_left_above[0], y=cell_left_above[1])
# final = cover_image(image=image, background=final, mode=3, x=0, y=0)
return final
def bestJunctionCheck(image:np.ndarray, offset:int, stepSize:int=2):
"""
最优点检测算算法输入一张脖子图片 (无论这张图片是否已经被二值化,我都认为没有被二值化),输出一个小数 (脖子最上方与衔接点位置/脖子图像长度)
与 beta 版不同的是它新增了一个阈值限定内容。
对于脖子而言,我我们首先可以定位到上面的部分,然后根据上面的这个点向下进行遍历检测。
与 beta 版类似,我们使用一个 stepSize 来用作斜率的检测
但是对于遍历检测而言,与 beta 版不同的是,我们需要对遍历的地方进行一定的限制。
限制的标准是,如果当前遍历的点的横坐标和起始点横坐标的插值超过了某个阈值,则认为是越界。
"""
point_k = 1
_, _, _, a = cv2.split(image) # 这应该是一个四通道的图像
height, width = a.shape
ret, a_thresh = cv2.threshold(a, 127, 255, cv2.THRESH_BINARY) # 将透明图层二值化
# 直接返回脖子的位置信息,修正系数为 0, get_box_pro 内部也封装了二值化,所以直接输入原图
y_high, y_low, _, _ = get_box_pro(image=image, model=1, correction_factor=0)
# 真正有用的只有上下 y 轴的两个值...
# 首先当然是确定起始点的位置,我们用同样的 scan 扫描函数进行行遍历。
def scan(y_:int, max_num:int=2):
num = 0
# 设定两个值,分别代表脖子的左边和右边
left = False
right = False
for x_ in range(width):
if a_thresh[y_][x_] != 0:
# 检测左边
if x_ < width // 2 and left is False:
num += 1
left = True
# 检测右边
elif x_ > width // 2 and right is False:
num += 1
right = True
return True if num >= max_num else False
def locate_neck_above():
"""
定位脖子的尖尖脚
"""
# y_high 就是脖子的最高点
for y_ in range(y_high, height):
if scan(y_):
return y_
y_start = locate_neck_above() # 得到遍历的初始高度
if y_low - y_start < stepSize: assert ValueError("脖子太小!")
# 然后获取一下初始的坐标点
x_left, x_right = 0, width
for x_left_ in range(0, width):
if a_thresh[y_start][x_left_] != 0:
x_left = x_left_
break
for x_right_ in range(width -1 , -1, -1):
if a_thresh[y_start][x_right_] != 0:
x_right = x_right_
break
# 接下来我定义两个生成器,首先是脖子轮廓 (向下寻找的) 生成器,每进行一次 next,生成器会返回 y+1 的脖子轮廓点
def contoursGenerator(image_:np.ndarray, y_:int, mode):
"""
这会是一个生成器,用于生成脖子两边的轮廓
y_ 是启始点的 y 坐标,每一次寻找都会让 y_+1
mode==1 说明是找左边的边,即,image_[y_][x_] == 0 且 image_[y_][x_ + 1] !=0 时跳出;
否则 当 image_[y_][x_] != 0 时,x_ - 1; 当 image_[y_][x_] == 0 且 image_[y_][x_ + 1] ==0 时 x_ + 1
mode==2 说明是找右边的边,即,image_[y_][x_] == 0 且 image_[y_][x_ - 1] !=0 时跳出
否则 当 image_[y_][x_] != 0 时,x_ + 1; 当 image_[y_][x_] == 0 且 image_[y_][x_ - 1] ==0 时 x_ - 1
"""
y_ += 1
try:
if mode == 1:
x_ = 0
while 0 <= y_ < height and 0 <= x_ < width:
while image_[y_][x_] != 0 and x_ >= 0: x_ -= 1
# 这里其实会有 bug,不过可以不管
while x_ < width and image_[y_][x_] == 0 and image_[y_][x_ + 1] == 0: x_ += 1
yield [y_, x_]
y_ += 1
elif mode == 2:
x_ = width-1
while 0 <= y_ < height and 0 <= x_ < width:
while x_ < width and image_[y_][x_] != 0: x_ += 1
while x_ >= 0 and image_[y_][x_] == 0 and image_[y_][x_ - 1] == 0: x_ -= 1
yield [y_, x_]
y_ += 1
# 当处理失败则返回 False
except IndexError:
yield False
# 然后是斜率生成器,这个生成器依赖子轮廓生成器,每一次生成轮廓后会计算斜率,另一个点的选取和 stepSize 有关
def kGenerator(image_: np.ndarray, mode):
"""
导数生成器,用来生成每一个点对应的导数
"""
y_ = y_start
# 对起始点建立一个生成器,mode=1 时是左边轮廓,mode=2 时是右边轮廓
c_generator = contoursGenerator(image_=image_, y_=y_, mode=mode)
for cell in c_generator:
# 寻找距离当前 cell 距离为 stepSize 的轮廓点
kc = contoursGenerator(image_=image_, y_=cell[0] + stepSize, mode=mode)
kCell = next(kc)
if kCell is False:
# 寻找失败
yield False, False
else:
# 寻找成功,返回当坐标点和斜率值
# 对于左边而言,斜率必然是前一个点的坐标减去后一个点的坐标
# 对于右边而言,斜率必然是后一个点的坐标减去前一个点的坐标
k = (cell[1] - kCell[1]) / stepSize if mode == 1 else (kCell[1] - cell[1]) / stepSize
yield k, cell
# 接着开始写寻找算法,需要注意的是我们是分两边选择的
def findPt(image_:np.ndarray, mode):
x_base = x_left if mode == 1 else x_right
k_generator = kGenerator(image_=image_, mode=mode)
k, cell = k_generator.__next__()
if k is False:
raise ValueError("无法找到拐点!")
k_next, cell_next = k_generator.__next__()
while k_next is not False:
cell = cell_next
# if cell[1] > x_base and mode == 2:
# x_base = cell[1]
# elif cell[1] < x_base and mode == 1:
# x_base = cell[1]
# 跳出循环的方式一:斜率超过了某个值
if k_next > point_k:
print("K out")
break
# 跳出循环的方式二:超出阈值
elif abs(cell[1] - x_base) > offset:
print("O out")
break
k_next, cell_next = k_generator.__next__()
if abs(cell[1] - x_base) > offset:
cell[0] = cell[0] - offset - 1
return cell[0]
# 先找左边的拐点:
pointY_left = findPt(image_=a_thresh, mode=1)
# 再找右边的拐点:
pointY_right = findPt(image_=a_thresh, mode=2)
point = min(pointY_right, pointY_left)
per = (point - y_high) / (y_low - y_high)
# pointX_left = next(contoursGenerator(image_=a_thresh, y_= point- 1, mode=1))[1]
# pointX_right = next(contoursGenerator(image_=a_thresh, y_=point - 1, mode=2))[1]
# return [pointX_left, point], [pointX_right, point]
return per
if __name__ == "__main__":
img = cv2.imread("./neck_temp/neck_image6.png", cv2.IMREAD_UNCHANGED)
new = transformationNeck(img)
cv2.imwrite("./1.png", new)
-39
View File
@@ -1,39 +0,0 @@
import numpy as np
from PIL import Image
import cv2
import onnxruntime
from .tensor2numpy import NNormalize, NTo_Tensor, NUnsqueeze
from .vision import image2bgr
def read_modnet_image(input_image, ref_size=512):
im = Image.fromarray(np.uint8(input_image))
width, length = im.size[0], im.size[1]
im = np.asarray(im)
im = image2bgr(im)
im = cv2.resize(im, (ref_size, ref_size), interpolation=cv2.INTER_AREA)
im = NNormalize(im, mean=np.array([0.5, 0.5, 0.5]), std=np.array([0.5, 0.5, 0.5]))
im = NUnsqueeze(NTo_Tensor(im))
return im, width, length
def get_modnet_matting(input_image, checkpoint_path="./test.onnx", ref_size=512):
print("checkpoint_path:", checkpoint_path)
sess = onnxruntime.InferenceSession(checkpoint_path)
input_name = sess.get_inputs()[0].name
output_name = sess.get_outputs()[0].name
im, width, length = read_modnet_image(input_image=input_image, ref_size=ref_size)
matte = sess.run([output_name], {input_name: im})
matte = (matte[0] * 255).astype('uint8')
matte = np.squeeze(matte)
mask = cv2.resize(matte, (width, length), interpolation=cv2.INTER_AREA)
b, g, r = cv2.split(np.uint8(input_image))
output_image = cv2.merge((b, g, r, mask))
return output_image
-2
View File
@@ -1,2 +0,0 @@
from .visualization_utils import show_bboxes
from .detector import detect_faces
-238
View File
@@ -1,238 +0,0 @@
import numpy as np
from PIL import Image
def nms(boxes, overlap_threshold=0.5, mode='union'):
"""Non-maximum suppression.
Arguments:
boxes: a float numpy array of shape [n, 5],
where each row is (xmin, ymin, xmax, ymax, score).
overlap_threshold: a float number.
mode: 'union' or 'min'.
Returns:
list with indices of the selected boxes
"""
# if there are no boxes, return the empty list
if len(boxes) == 0:
return []
# list of picked indices
pick = []
# grab the coordinates of the bounding boxes
x1, y1, x2, y2, score = [boxes[:, i] for i in range(5)]
area = (x2 - x1 + 1.0)*(y2 - y1 + 1.0)
ids = np.argsort(score) # in increasing order
while len(ids) > 0:
# grab index of the largest value
last = len(ids) - 1
i = ids[last]
pick.append(i)
# compute intersections
# of the box with the largest score
# with the rest of boxes
# left top corner of intersection boxes
ix1 = np.maximum(x1[i], x1[ids[:last]])
iy1 = np.maximum(y1[i], y1[ids[:last]])
# right bottom corner of intersection boxes
ix2 = np.minimum(x2[i], x2[ids[:last]])
iy2 = np.minimum(y2[i], y2[ids[:last]])
# width and height of intersection boxes
w = np.maximum(0.0, ix2 - ix1 + 1.0)
h = np.maximum(0.0, iy2 - iy1 + 1.0)
# intersections' areas
inter = w * h
if mode == 'min':
overlap = inter/np.minimum(area[i], area[ids[:last]])
elif mode == 'union':
# intersection over union (IoU)
overlap = inter/(area[i] + area[ids[:last]] - inter)
# delete all boxes where overlap is too big
ids = np.delete(
ids,
np.concatenate([[last], np.where(overlap > overlap_threshold)[0]])
)
return pick
def convert_to_square(bboxes):
"""Convert bounding boxes to a square form.
Arguments:
bboxes: a float numpy array of shape [n, 5].
Returns:
a float numpy array of shape [n, 5],
squared bounding boxes.
"""
square_bboxes = np.zeros_like(bboxes)
x1, y1, x2, y2 = [bboxes[:, i] for i in range(4)]
h = y2 - y1 + 1.0
w = x2 - x1 + 1.0
max_side = np.maximum(h, w)
square_bboxes[:, 0] = x1 + w*0.5 - max_side*0.5
square_bboxes[:, 1] = y1 + h*0.5 - max_side*0.5
square_bboxes[:, 2] = square_bboxes[:, 0] + max_side - 1.0
square_bboxes[:, 3] = square_bboxes[:, 1] + max_side - 1.0
return square_bboxes
def calibrate_box(bboxes, offsets):
"""Transform bounding boxes to be more like true bounding boxes.
'offsets' is one of the outputs of the nets.
Arguments:
bboxes: a float numpy array of shape [n, 5].
offsets: a float numpy array of shape [n, 4].
Returns:
a float numpy array of shape [n, 5].
"""
x1, y1, x2, y2 = [bboxes[:, i] for i in range(4)]
w = x2 - x1 + 1.0
h = y2 - y1 + 1.0
w = np.expand_dims(w, 1)
h = np.expand_dims(h, 1)
# this is what happening here:
# tx1, ty1, tx2, ty2 = [offsets[:, i] for i in range(4)]
# x1_true = x1 + tx1*w
# y1_true = y1 + ty1*h
# x2_true = x2 + tx2*w
# y2_true = y2 + ty2*h
# below is just more compact form of this
# are offsets always such that
# x1 < x2 and y1 < y2 ?
translation = np.hstack([w, h, w, h])*offsets
bboxes[:, 0:4] = bboxes[:, 0:4] + translation
return bboxes
def get_image_boxes(bounding_boxes, img, size=24):
"""Cut out boxes from the image.
Arguments:
bounding_boxes: a float numpy array of shape [n, 5].
img: an instance of PIL.Image.
size: an integer, size of cutouts.
Returns:
a float numpy array of shape [n, 3, size, size].
"""
num_boxes = len(bounding_boxes)
width, height = img.size
[dy, edy, dx, edx, y, ey, x, ex, w, h] = correct_bboxes(bounding_boxes, width, height)
img_boxes = np.zeros((num_boxes, 3, size, size), 'float32')
for i in range(num_boxes):
img_box = np.zeros((h[i], w[i], 3), 'uint8')
img_array = np.asarray(img, 'uint8')
img_box[dy[i]:(edy[i] + 1), dx[i]:(edx[i] + 1), :] =\
img_array[y[i]:(ey[i] + 1), x[i]:(ex[i] + 1), :]
# resize
img_box = Image.fromarray(img_box)
img_box = img_box.resize((size, size), Image.BILINEAR)
img_box = np.asarray(img_box, 'float32')
img_boxes[i, :, :, :] = _preprocess(img_box)
return img_boxes
def correct_bboxes(bboxes, width, height):
"""Crop boxes that are too big and get coordinates
with respect to cutouts.
Arguments:
bboxes: a float numpy array of shape [n, 5],
where each row is (xmin, ymin, xmax, ymax, score).
width: a float number.
height: a float number.
Returns:
dy, dx, edy, edx: a int numpy arrays of shape [n],
coordinates of the boxes with respect to the cutouts.
y, x, ey, ex: a int numpy arrays of shape [n],
corrected ymin, xmin, ymax, xmax.
h, w: a int numpy arrays of shape [n],
just heights and widths of boxes.
in the following order:
[dy, edy, dx, edx, y, ey, x, ex, w, h].
"""
x1, y1, x2, y2 = [bboxes[:, i] for i in range(4)]
w, h = x2 - x1 + 1.0, y2 - y1 + 1.0
num_boxes = bboxes.shape[0]
# 'e' stands for end
# (x, y) -> (ex, ey)
x, y, ex, ey = x1, y1, x2, y2
# we need to cut out a box from the image.
# (x, y, ex, ey) are corrected coordinates of the box
# in the image.
# (dx, dy, edx, edy) are coordinates of the box in the cutout
# from the image.
dx, dy = np.zeros((num_boxes,)), np.zeros((num_boxes,))
edx, edy = w.copy() - 1.0, h.copy() - 1.0
# if box's bottom right corner is too far right
ind = np.where(ex > width - 1.0)[0]
edx[ind] = w[ind] + width - 2.0 - ex[ind]
ex[ind] = width - 1.0
# if box's bottom right corner is too low
ind = np.where(ey > height - 1.0)[0]
edy[ind] = h[ind] + height - 2.0 - ey[ind]
ey[ind] = height - 1.0
# if box's top left corner is too far left
ind = np.where(x < 0.0)[0]
dx[ind] = 0.0 - x[ind]
x[ind] = 0.0
# if box's top left corner is too high
ind = np.where(y < 0.0)[0]
dy[ind] = 0.0 - y[ind]
y[ind] = 0.0
return_list = [dy, edy, dx, edx, y, ey, x, ex, w, h]
return_list = [i.astype('int32') for i in return_list]
return return_list
def _preprocess(img):
"""Preprocessing step before feeding the network.
Arguments:
img: a float numpy array of shape [h, w, c].
Returns:
a float numpy array of shape [1, c, h, w].
"""
img = img.transpose((2, 0, 1))
img = np.expand_dims(img, 0)
img = (img - 127.5)*0.0078125
return img
-166
View File
@@ -1,166 +0,0 @@
import numpy as np
from .box_utils import nms, calibrate_box, get_image_boxes, convert_to_square
from .first_stage import run_first_stage
import onnxruntime
import os
from os.path import exists
import requests
def download_img(img_url, base_dir):
print("Downloading Onnx Model in:",img_url)
r = requests.get(img_url, stream=True)
filename = img_url.split("/")[-1]
# print(r.status_code) # 返回状态码
if r.status_code == 200:
open(f'{base_dir}/{filename}', 'wb').write(r.content) # 将内容写入图片
print(f"Download Finshed -- {filename}")
del r
def detect_faces(image, min_face_size=20.0, thresholds=None, nms_thresholds=None):
"""
Arguments:
image: an instance of PIL.Image.
min_face_size: a float number.
thresholds: a list of length 3.
nms_thresholds: a list of length 3.
Returns:
two float numpy arrays of shapes [n_boxes, 4] and [n_boxes, 10],
bounding boxes and facial landmarks.
"""
if nms_thresholds is None:
nms_thresholds = [0.7, 0.7, 0.7]
if thresholds is None:
thresholds = [0.6, 0.7, 0.8]
base_url = "https://linimages.oss-cn-beijing.aliyuncs.com/"
onnx_filedirs = ["pnet.onnx", "rnet.onnx", "onet.onnx"]
# LOAD MODELS
basedir = os.path.dirname(os.path.realpath(__file__)).split("detector.py")[0]
for onnx_filedir in onnx_filedirs:
if not exists(f"{basedir}/weights"):
os.mkdir(f"{basedir}/weights")
if not exists(f"{basedir}/weights/{onnx_filedir}"):
# download onnx model
download_img(img_url=base_url+onnx_filedir, base_dir=f"{basedir}/weights")
pnet = onnxruntime.InferenceSession(f"{basedir}/weights/pnet.onnx") # Load a ONNX model
input_name_pnet = pnet.get_inputs()[0].name
output_name_pnet1 = pnet.get_outputs()[0].name
output_name_pnet2 = pnet.get_outputs()[1].name
pnet = [pnet, input_name_pnet, [output_name_pnet1, output_name_pnet2]]
rnet = onnxruntime.InferenceSession(f"{basedir}/weights/rnet.onnx") # Load a ONNX model
input_name_rnet = rnet.get_inputs()[0].name
output_name_rnet1 = rnet.get_outputs()[0].name
output_name_rnet2 = rnet.get_outputs()[1].name
rnet = [rnet, input_name_rnet, [output_name_rnet1, output_name_rnet2]]
onet = onnxruntime.InferenceSession(f"{basedir}/weights/onet.onnx") # Load a ONNX model
input_name_onet = onet.get_inputs()[0].name
output_name_onet1 = onet.get_outputs()[0].name
output_name_onet2 = onet.get_outputs()[1].name
output_name_onet3 = onet.get_outputs()[2].name
onet = [onet, input_name_onet, [output_name_onet1, output_name_onet2, output_name_onet3]]
# BUILD AN IMAGE PYRAMID
width, height = image.size
min_length = min(height, width)
min_detection_size = 12
factor = 0.707 # sqrt(0.5)
# scales for scaling the image
scales = []
# scales the image so that
# minimum size that we can detect equals to
# minimum face size that we want to detect
m = min_detection_size/min_face_size
min_length *= m
factor_count = 0
while min_length > min_detection_size:
scales.append(m*factor**factor_count)
min_length *= factor
factor_count += 1
# STAGE 1
# it will be returned
bounding_boxes = []
# run P-Net on different scales
for s in scales:
boxes = run_first_stage(image, pnet, scale=s, threshold=thresholds[0])
bounding_boxes.append(boxes)
# collect boxes (and offsets, and scores) from different scales
bounding_boxes = [i for i in bounding_boxes if i is not None]
bounding_boxes = np.vstack(bounding_boxes)
keep = nms(bounding_boxes[:, 0:5], nms_thresholds[0])
bounding_boxes = bounding_boxes[keep]
# use offsets predicted by pnet to transform bounding boxes
bounding_boxes = calibrate_box(bounding_boxes[:, 0:5], bounding_boxes[:, 5:])
# shape [n_boxes, 5]
bounding_boxes = convert_to_square(bounding_boxes)
bounding_boxes[:, 0:4] = np.round(bounding_boxes[:, 0:4])
# STAGE 2
img_boxes = get_image_boxes(bounding_boxes, image, size=24)
output = rnet[0].run([rnet[2][0], rnet[2][1]], {rnet[1]: img_boxes})
offsets = output[0] # shape [n_boxes, 4]
probs = output[1] # shape [n_boxes, 2]
keep = np.where(probs[:, 1] > thresholds[1])[0]
bounding_boxes = bounding_boxes[keep]
bounding_boxes[:, 4] = probs[keep, 1].reshape((-1,))
offsets = offsets[keep]
keep = nms(bounding_boxes, nms_thresholds[1])
bounding_boxes = bounding_boxes[keep]
bounding_boxes = calibrate_box(bounding_boxes, offsets[keep])
bounding_boxes = convert_to_square(bounding_boxes)
bounding_boxes[:, 0:4] = np.round(bounding_boxes[:, 0:4])
# STAGE 3
img_boxes = get_image_boxes(bounding_boxes, image, size=48)
if len(img_boxes) == 0:
return [], []
#img_boxes = Variable(torch.FloatTensor(img_boxes), volatile=True)
# with torch.no_grad():
# img_boxes = torch.FloatTensor(img_boxes)
# output = onet(img_boxes)
output = onet[0].run([onet[2][0], onet[2][1], onet[2][2]], {rnet[1]: img_boxes})
landmarks = output[0] # shape [n_boxes, 10]
offsets = output[1] # shape [n_boxes, 4]
probs = output[2] # shape [n_boxes, 2]
keep = np.where(probs[:, 1] > thresholds[2])[0]
bounding_boxes = bounding_boxes[keep]
bounding_boxes[:, 4] = probs[keep, 1].reshape((-1,))
offsets = offsets[keep]
landmarks = landmarks[keep]
# compute landmark points
width = bounding_boxes[:, 2] - bounding_boxes[:, 0] + 1.0
height = bounding_boxes[:, 3] - bounding_boxes[:, 1] + 1.0
xmin, ymin = bounding_boxes[:, 0], bounding_boxes[:, 1]
landmarks[:, 0:5] = np.expand_dims(xmin, 1) + np.expand_dims(width, 1)*landmarks[:, 0:5]
landmarks[:, 5:10] = np.expand_dims(ymin, 1) + np.expand_dims(height, 1)*landmarks[:, 5:10]
bounding_boxes = calibrate_box(bounding_boxes, offsets)
keep = nms(bounding_boxes, nms_thresholds[2], mode='min')
bounding_boxes = bounding_boxes[keep]
landmarks = landmarks[keep]
return bounding_boxes, landmarks
-97
View File
@@ -1,97 +0,0 @@
import math
from PIL import Image
import numpy as np
from .box_utils import nms, _preprocess
def run_first_stage(image, net, scale, threshold):
"""Run P-Net, generate bounding boxes, and do NMS.
Arguments:
image: an instance of PIL.Image.
net: an instance of pytorch's nn.Module, P-Net.
scale: a float number,
scale width and height of the image by this number.
threshold: a float number,
threshold on the probability of a face when generating
bounding boxes from predictions of the net.
Returns:
a float numpy array of shape [n_boxes, 9],
bounding boxes with scores and offsets (4 + 1 + 4).
"""
# scale the image and convert it to a float array
width, height = image.size
sw, sh = math.ceil(width*scale), math.ceil(height*scale)
img = image.resize((sw, sh), Image.BILINEAR)
img = np.asarray(img, 'float32')
img = _preprocess(img)
# with torch.no_grad():
# img = torch.FloatTensor(_preprocess(img))
output = net[0].run([net[2][0],net[2][1]], {net[1]: img})
probs = output[1][0, 1, :, :]
offsets = output[0]
# probs: probability of a face at each sliding window
# offsets: transformations to true bounding boxes
boxes = _generate_bboxes(probs, offsets, scale, threshold)
if len(boxes) == 0:
return None
keep = nms(boxes[:, 0:5], overlap_threshold=0.5)
return boxes[keep]
def _generate_bboxes(probs, offsets, scale, threshold):
"""Generate bounding boxes at places
where there is probably a face.
Arguments:
probs: a float numpy array of shape [n, m].
offsets: a float numpy array of shape [1, 4, n, m].
scale: a float number,
width and height of the image were scaled by this number.
threshold: a float number.
Returns:
a float numpy array of shape [n_boxes, 9]
"""
# applying P-Net is equivalent, in some sense, to
# moving 12x12 window with stride 2
stride = 2
cell_size = 12
# indices of boxes where there is probably a face
inds = np.where(probs > threshold)
if inds[0].size == 0:
return np.array([])
# transformations of bounding boxes
tx1, ty1, tx2, ty2 = [offsets[0, i, inds[0], inds[1]] for i in range(4)]
# they are defined as:
# w = x2 - x1 + 1
# h = y2 - y1 + 1
# x1_true = x1 + tx1*w
# x2_true = x2 + tx2*w
# y1_true = y1 + ty1*h
# y2_true = y2 + ty2*h
offsets = np.array([tx1, ty1, tx2, ty2])
score = probs[inds[0], inds[1]]
# P-Net is applied to scaled images
# so we need to rescale bounding boxes back
bounding_boxes = np.vstack([
np.round((stride*inds[1] + 1.0)/scale),
np.round((stride*inds[0] + 1.0)/scale),
np.round((stride*inds[1] + 1.0 + cell_size)/scale),
np.round((stride*inds[0] + 1.0 + cell_size)/scale),
score, offsets
])
# why one is added?
return bounding_boxes.T
@@ -1,31 +0,0 @@
from PIL import ImageDraw
def show_bboxes(img, bounding_boxes, facial_landmarks=[]):
"""Draw bounding boxes and facial landmarks.
Arguments:
img: an instance of PIL.Image.
bounding_boxes: a float numpy array of shape [n, 5].
facial_landmarks: a float numpy array of shape [n, 10].
Returns:
an instance of PIL.Image.
"""
img_copy = img.copy()
draw = ImageDraw.Draw(img_copy)
for b in bounding_boxes:
draw.rectangle([
(b[0], b[1]), (b[2], b[3])
], outline='white')
for p in facial_landmarks:
for i in range(5):
draw.ellipse([
(p[i] - 1.0, p[i + 5] - 1.0),
(p[i] + 1.0, p[i + 5] + 1.0)
], outline='blue')
return img_copy
Binary file not shown.
Binary file not shown.
Binary file not shown.
-452
View File
@@ -1,452 +0,0 @@
"""
本文件存放一些自制的简单的图像处理函数
"""
from PIL import Image
import cv2
import numpy as np
import math
import warnings
import csv
import glob
def cover_mask(image_path, mask_path, alpha=0.85, rate=0.1, if_save=True):
"""
在图片右下角盖上水印
:param image_path:
:param mask_path: 水印路径,以 PNG 方式读取
:param alpha:不透明度,默认为 0.85
:param rate:水印比例,越小水印也越小,默认为 0.1
:param if_save: 是否将裁剪后的图片保存,如果为 True,则保存并返回新图路径,否则不保存,返回截取后的图片对象
:return: 新的图片路径
"""
# 生成新的图片路径,我们默认图片后缀存在且必然包含“.”
path_len = len(image_path)
index = 0
for index in range(path_len - 1, -1, -1):
if image_path[index] == ".":
break
if 3 >= path_len - index >= 6:
raise TypeError("输入的图片格式有误!")
new_path = image_path[0:index] + "_with_mask" + image_path[index:path_len]
# 以 png 方式读取水印图
mask = Image.open(mask_path).convert('RGBA')
mask_h, mask_w = mask.size
# 以 png 的方式读取原图
im = Image.open(image_path).convert('RGBA')
# 我采取的策略是,先拷贝一张原图 im 为 base 作为基底,然后在 im 上利用 paste 函数添加水印
# 此时的水印是完全不透明的,我需要利用 blend 函数内置参数 alpha 进行不透明度调整
base = im.copy()
# layer = Image.new('RGBA', im.size, (0, 0, 0, ))
# tmp = Image.new('RGBA', im.size, (0, 0, 0, 0))
h, w = im.size
# 根据原图大小缩放水印图
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)
mh, mw = mask.size
r, g, b, a = mask.split()
im.paste(mask, (h-mh, w-mw), mask=a)
# im.show()
out = Image.blend(base, im, alpha=alpha).convert('RGB')
# out = Image.alpha_composite(im, layer).convert('RGB')
if if_save:
out.save(new_path)
return new_path
else:
return out
def check_image(image) ->np.ndarray:
"""
判断某一对象是否为图像/矩阵类型,最终返回图像/矩阵
"""
if not isinstance(image, np.ndarray):
image = cv2.imread(image, cv2.IMREAD_UNCHANGED)
return image
def get_box(image) -> list:
"""
这是一个简单的扣图后图像定位函数,不考虑噪点影响
我们使用遍历的方法,碰到非透明点以后立即返回位置坐标
:param image:图像信息,可以是图片路径,也可以是已经读取后的图像
如果传入的是图片路径,我会首先通过读取图片、二值化,然后再进行图像处理
如果传入的是图像,直接处理,不会二值化
:return: 回传一个列表,分别是图像的上下(y)左右(x)自个值
"""
image = check_image(image)
height, width, _ = image.shape
try:
b, g, r, a = cv2.split(image)
# 二值化处理
a = (a > 127).astype(np.int_)
except ValueError:
# 说明传入的是无透明图层的图像,直接返回图像尺寸
warnings.warn("你传入了一张非四通道格式的图片!")
return [0, height, 0, width]
flag1, flag2 = 0, 0
box = [0, 0, 0, 0] # 上下左右
# 采用两面夹击战术,使用 flag1 和 2 确定两面的裁剪程度
# 先得到上下
for i in range(height):
for j in range(width):
if flag1 == 0 and a[i][j] != 0:
flag1 = 1
box[0] = i
if flag2 == 0 and a[height - i -1][j] != 0:
flag2 = 1
box[1] = height - i - 1
if flag2 * flag1 == 1:
break
# 再得到左右
flag1, flag2 = 0, 0
for j in range(width):
for i in range(height):
if flag1 == 0 and a[i][j] != 0:
flag1 = 1
box[2] = j
if flag2 == 0 and a[i][width - j - 1] != 0:
flag2 = 1
box[3] = width - j - 1
if flag2 * flag1 == 1:
break
return box
def filtering(img, f, x, y, x_max, y_max, x_min, y_min, area=0, noise_size=50) ->tuple:
"""
filtering 将使用递归的方法得到一个连续图像(这个连续矩阵必须得是单通道的)的范围 (坐标)
:param img: 传入的矩阵
:param f: 和 img 相同尺寸的全零矩阵,用于标记递归递归过的点
:param x: 当前递归到的 x 轴坐标
:param y: 当前递归到的 y 轴坐标
:param x_max: 递归过程中 x 轴坐标的最大值
:param y_max: 递归过程中 y 轴坐标的最大值
:param x_min: 递归过程中 x 轴坐标的最小值
:param y_min: 递归过程中 y 轴坐标的最小值
:param area: 当前递归区域面积大小
:param noise_size: 最大递归区域面积大小,当 area 大于 noise_size 时,函数返回 (0, 1)
:return: 分两种情况,当 area 大于 noise_size 时,函数返回 (0, 1),当 area 小于等于 noise_size 时,函数返回 (box, 0)
其中 box 是连续图像的坐标和像素点面积(上下左右,面积)
理论上来讲,我们可以用这个函数递归出任一图像的形状和坐标,但是从计算机内存、计算速度上考虑,这并不是一个好的选择
所以这个函数一般用于判断和过滤噪点
"""
dire_dir = [(1, 0), (-1, 0), (0, 1), (0, -1), (1, 1), (1, -1), (-1, -1), (-1, 1)]
height, width = img.shape
f[x][y] = 1
for dire in dire_dir:
delta_x, delta_y = dire
tmp_x, tmp_y = (x + delta_x, y + delta_y)
if height > tmp_x >= 0 and width > tmp_y >= 0:
if img[tmp_x][tmp_y] != 0 and f[tmp_x][tmp_y] == 0:
f[tmp_x][tmp_y] = 1
# cv2.imshow("test", f)
# cv2.waitKey(3)
area += 1
if area > noise_size:
return 0, 1
else:
x_max = tmp_x if tmp_x > x_max else x_max
x_min = tmp_x if tmp_x < x_min else x_min
y_max = tmp_y if tmp_y > y_max else y_max
y_min = tmp_y if tmp_y < y_min else y_min
box, flag = filtering(img, f, tmp_x, tmp_y, x_max, y_max, x_min, y_min, area=area, noise_size=noise_size)
if flag == 1:
return 0, 1
else:
(x_max, x_min, y_max, y_min, area) = box
return [x_min, x_max, y_min, y_max, area], 0
def get_box_pro(image: np.ndarray, model: int = 1, correction_factor=None, thresh: int = 127):
"""
本函数能够实现输入一张四通道图像,返回图像中最大连续非透明面积的区域的矩形坐标
本函数将采用 opencv 内置函数来解析整个图像的 mask,并提供一些参数,用于读取图像的位置信息
Args:
image: 四通道矩阵图像
model: 返回值模式
correction_factor: 提供一些边缘扩张接口,输入格式为 list 或者 int:[up, down, left, right]。
举个例子,假设我们希望剪切出的矩形框左边能够偏左 1 个像素,则输入 [0, 0, 1, 0]
如果希望右边偏右 1 个像素,则输入 [0, 0, 0, 1]
如果输入为 int,则默认只会对左右两边做拓展,比如输入 2,则和 [0, 0, 2, 2] 是等效的
thresh: 二值化阈值,为了保持一些羽化效果,thresh 必须要小
Returns:
model 为 1 时,将会返回切割出的矩形框的四个坐标点信息
model 为 2 时,将会返回矩形框四边相距于原图四边的距离
"""
# ------------ 数据格式规范部分 -------------- #
# 输入必须为四通道
if correction_factor is None:
correction_factor = [0, 0, 0, 0]
if not isinstance(image, np.ndarray) or len(cv2.split(image)) != 4:
raise TypeError("输入的图像必须为四通道 np.ndarray 类型矩阵!")
# correction_factor 规范化
if isinstance(correction_factor, int):
correction_factor = [0, 0, correction_factor, correction_factor]
elif not isinstance(correction_factor, list):
raise TypeError("correction_factor 必须为 int 或者 list 类型!")
# ------------ 数据格式规范完毕 -------------- #
# 分离 mask
_, _, _, mask = cv2.split(image)
# mask 二值化处理
_, mask = cv2.threshold(mask, thresh=thresh, maxval=255, type=0)
contours, hierarchy = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
temp = np.ones(image.shape, np.uint8)*255
cv2.drawContours(temp, contours, -1, (0, 0, 255), -1)
contours_area = []
for cnt in contours:
contours_area.append(cv2.contourArea(cnt))
idx = contours_area.index(max(contours_area))
x, y, w, h = cv2.boundingRect(contours[idx]) # 框出图像
# ------------ 开始输出数据 -------------- #
height, width, _ = image.shape
y_up = y - correction_factor[0] if y - correction_factor[0] >= 0 else 0
y_down = y + h + correction_factor[1] if y + h + correction_factor[1] < height else height - 1
x_left = x - correction_factor[2] if x - correction_factor[2] >= 0 else 0
x_right = x + w + correction_factor[3] if x + w + correction_factor[3] < width else width - 1
if model == 1:
# model=1,将会返回切割出的矩形框的四个坐标点信息
return [y_up, y_down, x_left, x_right]
elif model == 2:
# model=2, 将会返回矩形框四边相距于原图四边的距离
return [y_up, height - y_down, x_left, width - x_right]
else:
raise EOFError("请选择正确的模式!")
def cut(image_path:str, box:list, if_save=True):
"""
根据 box,裁剪对应的图片区域后保存
:param image_path: 原图路径
:param box: 坐标列表,上下左右
:param if_save:是否将裁剪后的图片保存,如果为 True,则保存并返回新图路径,否则不保存,返回截取后的图片对象
:return: 新图路径或者是新图对象
"""
index = 0
path_len = len(image_path)
up, down, left, right = box
image = cv2.imread(image_path, cv2.IMREAD_UNCHANGED)
new_image = image[up: down, left: right]
if if_save:
for index in range(path_len - 1, -1, -1):
if image_path[index] == ".":
break
if 3 >= path_len - index >= 6:
raise TypeError("输入的图片格式有误!")
new_path = image_path[0:index] + "_cut" + image_path[index:path_len]
cv2.imwrite(new_path, new_image, [cv2.IMWRITE_PNG_COMPRESSION, 9])
return new_path
else:
return new_image
def zoom_image_without_change_size(image:np.ndarray, zoom_rate, interpolation=cv2.INTER_NEAREST) ->np.ndarray:
"""
在不改变原图大小的情况下,对图像进行放大,目前只支持从图像中心放大
:param image:传入的图像对象
:param zoom_rate:放大比例,单位为倍(初始为 1 倍)
:param interpolation:插值方式,与 opencv 的 resize 内置参数相对应,默认为最近邻插值
:return: 裁剪后的图像实例
"""
height, width, _ = image.shape
if zoom_rate < 1:
# zoom_rate 不能小于 1
raise ValueError("zoom_rate 不能小于 1")
height_tmp = int(height * zoom_rate)
width_tmp = int(width * zoom_rate)
image_tmp = cv2.resize(image, (height_tmp, width_tmp), interpolation=interpolation)
# 定位一下被裁剪的位置,实际上是裁剪框的左上角的点的坐标
delta_x = (width_tmp - width) // 2 # 横向
delta_y = (height_tmp - height) // 2 # 纵向
return image_tmp[delta_y : delta_y + height, delta_x : delta_x + width]
def filedir2csv(scan_filedir, csv_filedir):
file_list = glob.glob(scan_filedir+"/*")
with open(csv_filedir, "w") as csv_file:
writter = csv.writer(csv_file)
for file_dir in file_list:
writter.writerow([file_dir])
print("filedir2csv success!")
def full_ties(image_pre:np.ndarray):
height, width = image_pre.shape
# 先膨胀
kernel = np.ones((5, 5), dtype=np.uint8)
dilate = cv2.dilate(image_pre, kernel, 1)
# cv2.imshow("dilate", dilate)
def FillHole(image):
# 复制 image 图像
im_floodFill = image.copy()
# Mask 用于 floodFill,官方要求长宽 +2
mask = np.zeros((height + 2, width + 2), np.uint8)
seedPoint = (0, 0)
# floodFill 函数中的 seedPoint 对应像素必须是背景
is_break = False
for i in range(im_floodFill.shape[0]):
for j in range(im_floodFill.shape[1]):
if (im_floodFill[i][j] == 0):
seedPoint = (i, j)
is_break = True
break
if (is_break):
break
# 得到 im_floodFill 255 填充非孔洞值
cv2.floodFill(im_floodFill, mask, seedPoint, 255)
# cv2.imshow("tmp1", im_floodFill)
# 得到 im_floodFill 的逆 im_floodFill_inv
im_floodFill_inv = cv2.bitwise_not(im_floodFill)
# cv2.imshow("tmp2", im_floodFill_inv)
# 把 image、im_floodFill_inv 这两幅图像结合起来得到前景
im_out = image | im_floodFill_inv
return im_out
# 洪流算法填充
image_floodFill = FillHole(dilate)
# 填充图和原图合并
image_final = image_floodFill | image_pre
# 再腐蚀
kernel = np.ones((5, 5), np.uint8)
erosion= cv2.erode(image_final, kernel, iterations=6)
# cv2.imshow("erosion", erosion)
# 添加高斯模糊
blur = cv2.GaussianBlur(erosion, (5, 5), 2.5)
# cv2.imshow("blur", blur)
# image_final = merge_image(image_pre, erosion)
# 再与原图合并
image_final = image_pre | blur
# cv2.imshow("final", image_final)
return image_final
def cut_BiggestAreas(image):
# 裁剪出整张图轮廓最大的部分
def find_BiggestAreas(image_pre):
# 定义一个三乘三的卷积核
kernel = np.ones((3, 3), dtype=np.uint8)
# 将输入图片膨胀
# dilate = cv2.dilate(image_pre, kernel, 3)
# cv2.imshow("dilate", dilate)
# 将输入图片二值化
_, thresh = cv2.threshold(image_pre, 127, 255, cv2.THRESH_BINARY)
# cv2.imshow("thresh", thresh)
# 将二值化后的图片膨胀
dilate_afterThresh = cv2.dilate(thresh, kernel, 5)
# cv2.imshow("thresh_afterThresh", dilate_afterThresh)
# 找轮廓
contours_, hierarchy = cv2.findContours(dilate_afterThresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# 识别出最大的轮廓
# 需要注意的是,在低版本的 findContours 当中返回的结果是 tuple,不支持 pop,所以需要将其转为 pop
contours = [x for x in contours_]
area = map(cv2.contourArea, contours)
area_list = list(area)
area_max = max(area_list)
post = area_list.index(area_max)
# 将最大的区域保留,其余全部填黑
contours.pop(post)
for i in range(len(contours)):
cv2.drawContours(image_pre, contours, i, 0, cv2.FILLED)
# cv2.imshow("cut", image_pre)
return image_pre
b, g, r, a = cv2.split(image)
a_new = find_BiggestAreas(a)
new_image = cv2.merge((b, g, r, a_new))
return new_image
def locate_neck(image:np.ndarray, proportion):
"""
根据输入的图片 (四通道) 和 proportion(自上而下) 的比例,定位到相应的 y 点,然后向内收缩,直到两边的像素点不透明
"""
if image.shape[-1] != 4:
raise TypeError("请输入一张 png 格式的四通道图片!")
if proportion > 1 or proportion <=0:
raise ValueError("proportion 必须在 0~1 之间!")
_, _, _, a = cv2.split(image)
height, width = a.shape
_, a = cv2.threshold(a, 127, 255, cv2.THRESH_BINARY)
y = int(height * proportion)
x = 0
for x in range(width):
if a[y][x] == 255:
break
left = (y, x)
for x in range(width - 1, -1 , -1):
if a[y][x] == 255:
break
right = (y, x)
return left, right, right[1] - left[1]
def get_cutbox_image(input_image):
height, width = input_image.shape[0], input_image.shape[1]
y_top, y_bottom, x_left, x_right = get_box_pro(input_image, model=2)
result_image = input_image[y_top:height - y_bottom, x_left:width - x_right]
return result_image
def brightnessAdjustment(image: np.ndarray, bright_factor: int=0):
"""
图像亮度调节
:param image: 输入的图像矩阵
:param bright_factor:亮度调节因子,可正可负,没有范围限制
当 bright_factor ---> +无穷 时,图像全白
当 bright_factor ---> -无穷 时,图像全黑
:return: 处理后的图片
"""
res = np.uint8(np.clip(np.int16(image) + bright_factor, 0, 255))
return res
def contrastAdjustment(image: np.ndarray, contrast_factor: int = 0):
"""
图像对比度调节,实际上调节对比度的同时对亮度也有一定的影响
:param image: 输入的图像矩阵
:param contrast_factor:亮度调节因子,可正可负,范围在 [-100, +100] 之间
当 contrast_factor=-100 时,图像变为灰色
:return: 处理后的图片
"""
contrast_factor = 1 + min(contrast_factor, 100) / 100 if contrast_factor > 0 else 1 + max(contrast_factor,
-100) / 100
image_b = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
bright_ = image_b.mean()
res = np.uint8(np.clip(contrast_factor * (np.int16(image) - bright_) + bright_, 0, 255))
return res
class CV2Bytes(object):
@staticmethod
def byte_cv2(image_byte, flags=cv2.IMREAD_COLOR) ->np.ndarray:
"""
将传入的字节流解码为图像,当 flags 为 -1 的时候为无损解码
"""
np_arr = np.frombuffer(image_byte,np.uint8)
image = cv2.imdecode(np_arr, flags)
return image
@staticmethod
def cv2_byte(image:np.ndarray, imageType:str=".jpg"):
"""
将传入的图像解码为字节流
"""
_, image_encode = cv2.imencode(imageType, image)
image_byte = image_encode.tobytes()
return image_byte
def comb2images(src_white:np.ndarray, src_black:np.ndarray, mask:np.ndarray) -> np.ndarray:
"""输入两张图片,将这两张图片根据输入的 mask 进行叠加处理
这里并非简单的 cv2.add(),因为也考虑了羽化部分,所以需要进行一些其他的处理操作
核心的算法为:dst = (mask * src_white + (1 - mask) * src_black).astype(np.uint8)
Args:
src_white (np.ndarray): 第一张图像,代表的是 mask 中的白色区域,三通道
src_black (np.ndarray): 第二张图像,代表的是 mask 中的黑色区域,三通道
mask (np.ndarray): mask.输入为单通道,后续会归一化并转为三通道
需要注意的是这三者的尺寸应该是一样的
Returns:
np.ndarray: 返回的三通道图像
"""
# 函数内部不检查相关参数是否一样,使用的时候需要注意一下
mask = cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR).astype(np.float32) / 255
return (mask * src_white + (1 - mask) * src_black).astype(np.uint8)
-692
View File
@@ -1,692 +0,0 @@
import cv2
from PIL import Image
import numpy as np
import functools
import time
def calTime(mark):
"""
一个输出函数时间的装饰器。
:param mark: str, 可选填,如果填了就会在 print 开头加上 mark 标签。
"""
if isinstance(mark, str):
def decorater(func):
@functools.wraps(func)
def wrapper(*args, **kw):
start_time = time.time()
return_param = func(*args, **kw)
print(
"[Mark-{}] {} 函数花费的时间为 {:.2f}.".format(
mark, func.__name__, time.time() - start_time
)
)
return return_param
return wrapper
return decorater
else:
func = mark
@functools.wraps(func)
def wrapper(*args, **kw):
start_time = time.time()
return_param = func(*args, **kw)
print(
"{} 函数花费的时间为 {:.2f}.".format(
func.__name__, time.time() - start_time
)
)
return return_param
return wrapper
def ChangeImageDPI(input_path, output_path, dpi=300):
"""
改变输入图像的 dpi.
input_path: 输入图像路径
output_path: 输出图像路径
dpi:打印分辨率
"""
image = Image.open(input_path)
image.save(output_path, dpi=(dpi, dpi))
# print(1)
print(
"Your Image's DPI have been changed. The last DPI = ({},{}) ".format(dpi, dpi)
)
def IDphotos_cut(x1, y1, x2, y2, img):
"""
在图片上进行滑动裁剪,输入输出为
输入:一张图片 img,和裁剪框信息 (x1,x2,y1,y2)
输出:裁剪好的图片,然后裁剪框超出了图像范围,那么将用 0 矩阵补位
------------------------------------
x:裁剪框左上的横坐标
y:裁剪框左上的纵坐标
x2:裁剪框右下的横坐标
y2:裁剪框右下的纵坐标
crop_size:裁剪框大小
img:裁剪图像(numpy.array
output_path:裁剪图片的输出路径
------------------------------------
"""
crop_size = (y2 - y1, x2 - x1)
"""
------------------------------------
temp_x_1:裁剪框左边超出图像部分
temp_y_1:裁剪框上边超出图像部分
temp_x_2:裁剪框右边超出图像部分
temp_y_2:裁剪框下边超出图像部分
------------------------------------
"""
temp_x_1 = 0
temp_y_1 = 0
temp_x_2 = 0
temp_y_2 = 0
if y1 < 0:
temp_y_1 = abs(y1)
y1 = 0
if y2 > img.shape[0]:
temp_y_2 = y2
y2 = img.shape[0]
temp_y_2 = temp_y_2 - y2
if x1 < 0:
temp_x_1 = abs(x1)
x1 = 0
if x2 > img.shape[1]:
temp_x_2 = x2
x2 = img.shape[1]
temp_x_2 = temp_x_2 - x2
# 生成一张全透明背景
print("crop_size:", crop_size)
background_bgr = np.full((crop_size[0], crop_size[1]), 255, dtype=np.uint8)
background_a = np.full((crop_size[0], crop_size[1]), 0, dtype=np.uint8)
background = cv2.merge(
(background_bgr, background_bgr, background_bgr, background_a)
)
background[
temp_y_1 : crop_size[0] - temp_y_2, temp_x_1 : crop_size[1] - temp_x_2
] = img[y1:y2, x1:x2]
return background
def resize_image_esp(input_image, esp=2000):
"""
输入:
input_pathnumpy 图片
esp:限制的最大边长
"""
# resize 函数=>可以让原图压缩到最大边为 esp 的尺寸 (不改变比例)
width = input_image.shape[0]
length = input_image.shape[1]
max_num = max(width, length)
if max_num > esp:
print("Image resizing...")
if width == max_num:
length = int((esp / width) * length)
width = esp
else:
width = int((esp / length) * width)
length = esp
print(length, width)
im_resize = cv2.resize(
input_image, (length, width), interpolation=cv2.INTER_AREA
)
return im_resize
else:
return input_image
def resize_image_by_min(input_image, esp=600):
"""
将图像缩放为最短边至少为 esp 的图像。
:param input_image: 输入图像(OpenCV 矩阵)
:param esp: 缩放后的最短边长
:return: 缩放后的图像,缩放倍率
"""
height, width = input_image.shape[0], input_image.shape[1]
min_border = min(height, width)
if min_border < esp:
if height >= width:
new_width = esp
new_height = height * esp // width
else:
new_height = esp
new_width = width * esp // height
return (
cv2.resize(
input_image, (new_width, new_height), interpolation=cv2.INTER_AREA
),
new_height / height,
)
else:
return input_image, 1
def detect_distance(value, crop_heigh, max=0.06, min=0.04):
"""
检测人头顶与照片顶部的距离是否在适当范围内。
输入:与顶部的差值
输出:(status, move_value)
status=0 不动
status=1 人脸应向上移动(裁剪框向下移动)
status-2 人脸应向下移动(裁剪框向上移动)
---------------------------------------
value:头顶与照片顶部的距离·
crop_heigh: 裁剪框的高度
max: 距离的最大值
min: 距离的最小值
---------------------------------------
"""
value = value / crop_heigh # 头顶往上的像素占图像的比例
if min <= value <= max:
return 0, 0
elif value > max:
# 头顶往上的像素比例高于 max
move_value = value - max
move_value = int(move_value * crop_heigh)
# print("上移{}".format(move_value))
return 1, move_value
else:
# 头顶往上的像素比例低于 min
move_value = min - value
move_value = int(move_value * crop_heigh)
# print("下移{}".format(move_value))
return -1, move_value
def draw_picture_dots(image, dots, pen_size=10, pen_color=(0, 0, 255)):
"""
给一张照片上绘制点。
image: Opencv 图像矩阵
dots: 一堆点,形如 [(100,100),(150,100)]
pen_size: 画笔的大小
pen_color: 画笔的颜色
"""
if isinstance(dots, dict):
dots = [v for u, v in dots.items()]
image = image.copy()
for x, y in dots:
cv2.circle(image, (int(x), int(y)), pen_size, pen_color, -1)
return image
def draw_picture_rectangle(image, bbox, pen_size=2, pen_color=(0, 0, 255)):
image = image.copy()
x1 = int(bbox[0])
y1 = int(bbox[1])
x2 = int(bbox[2])
y2 = int(bbox[3])
cv2.rectangle(image, (x1, y1), (x2, y2), pen_color, pen_size)
return image
def generate_gradient(start_color, width, height, mode="updown"):
# 定义背景颜色
end_color = (255, 255, 255) # 白色
# 创建一个空白图像
r_out = np.zeros((height, width), dtype=int)
g_out = np.zeros((height, width), dtype=int)
b_out = np.zeros((height, width), dtype=int)
if mode == "updown":
# 生成上下渐变色
for y in range(height):
r = int(
(y / height) * end_color[0] + ((height - y) / height) * start_color[0]
)
g = int(
(y / height) * end_color[1] + ((height - y) / height) * start_color[1]
)
b = int(
(y / height) * end_color[2] + ((height - y) / height) * start_color[2]
)
r_out[y, :] = r
g_out[y, :] = g
b_out[y, :] = b
else:
# 生成中心渐变色
img = np.zeros((height, width, 3))
# 定义椭圆中心和半径
center = (width // 2, height // 2)
end_axies = max(height, width)
# 定义渐变色
end_color = (255, 255, 255)
# 绘制椭圆
for y in range(end_axies):
axes = (end_axies - y, end_axies - y)
r = int(
(y / end_axies) * end_color[0]
+ ((end_axies - y) / end_axies) * start_color[0]
)
g = int(
(y / end_axies) * end_color[1]
+ ((end_axies - y) / end_axies) * start_color[1]
)
b = int(
(y / end_axies) * end_color[2]
+ ((end_axies - y) / end_axies) * start_color[2]
)
cv2.ellipse(img, center, axes, 0, 0, 360, (b, g, r), -1)
b_out, g_out, r_out = cv2.split(np.uint64(img))
return r_out, g_out, b_out
def add_background(input_image, bgr=(0, 0, 0), mode="pure_color"):
"""
本函数的功能为为透明图像加上背景。
:param input_image: numpy.array(4 channels), 透明图像
:param bgr: tuple, 合成纯色底时的 BGR 值
:param new_background: numpy.array(3 channels),合成自定义图像底时的背景图
:return: output: 合成好的输出图像
"""
height, width = input_image.shape[0], input_image.shape[1]
b, g, r, a = cv2.split(input_image)
a_cal = a / 255
if mode == "pure_color":
# 纯色填充
b2 = np.full([height, width], bgr[0], dtype=int)
g2 = np.full([height, width], bgr[1], dtype=int)
r2 = np.full([height, width], bgr[2], dtype=int)
elif mode == "updown_gradient":
b2, g2, r2 = generate_gradient(bgr, width, height, mode="updown")
else:
b2, g2, r2 = generate_gradient(bgr, width, height, mode="center")
output = cv2.merge(
((b - b2) * a_cal + b2, (g - g2) * a_cal + g2, (r - r2) * a_cal + r2)
)
return output
def rotate_bound(image, angle):
"""
一个旋转函数,输入一张图片和一个旋转角,可以实现不损失图像信息的旋转。
- image: numpy.array(3 channels)
- angle: 旋转角(度)
"""
(h, w) = image.shape[:2]
(cX, cY) = (w / 2, h / 2)
M = cv2.getRotationMatrix2D((cX, cY), -angle, 1.0)
cos = np.abs(M[0, 0])
sin = np.abs(M[0, 1])
nW = int((h * sin) + (w * cos))
nH = int((h * cos) + (w * sin))
M[0, 2] += (nW / 2) - cX
M[1, 2] += (nH / 2) - cY
return cv2.warpAffine(image, M, (nW, nH)), cos, sin
def rotate_bound_4channels(image, a, angle):
"""
【rotate_bound_4channels 的 4 通道版本】
一个旋转函数,输入一张图片和一个旋转角,可以实现不损失图像信息的旋转。
Inputs:
- image: numpy.array(3 channels), 输入图像
- a: numpy.array(1 channels), 输入图像的 A 矩阵
- angle: 旋转角(度)
Returns:
- input_image: numpy.array(3 channels), 对 image 进行旋转后的图像
- result_image: numpy.array(4 channels), 旋转且透明的图像
- cos: float, 旋转角的余弦值
- sin: float, 旋转角的正弦值
"""
input_image, cos, sin = rotate_bound(image, angle)
new_a, _, _ = rotate_bound(a, angle) # 对做 matte 旋转,以便之后 merge
b, g, r = cv2.split(input_image)
result_image = cv2.merge((b, g, r, new_a)) # 得到抠图结果图的无损旋转结果
return input_image, result_image, cos, sin
def cover_image(image, background, x, y, mode=1):
"""
mode = 1: directly cover
mode = 2: cv2.add
mode = 3: bgra cover
"""
image = image.copy()
background = background.copy()
height1, width1 = background.shape[0], background.shape[1]
height2, width2 = image.shape[0], image.shape[1]
wuqiong_bg_y = height1 + 1
wuqiong_bg_x = width1 + 1
wuqiong_img_y = height2 + 1
wuqiong_img_x = width2 + 1
def cover_mode(
image,
background,
imgy1=0,
imgy2=-1,
imgx1=0,
imgx2=-1,
bgy1=0,
bgy2=-1,
bgx1=0,
bgx2=-1,
mode=1,
):
if mode == 1:
background[bgy1:bgy2, bgx1:bgx2] = image[imgy1:imgy2, imgx1:imgx2]
elif mode == 2:
background[bgy1:bgy2, bgx1:bgx2] = cv2.add(
background[bgy1:bgy2, bgx1:bgx2], image[imgy1:imgy2, imgx1:imgx2]
)
elif mode == 3:
b, g, r, a = cv2.split(image[imgy1:imgy2, imgx1:imgx2])
b2, g2, r2, a2 = cv2.split(background[bgy1:bgy2, bgx1:bgx2])
background[bgy1:bgy2, bgx1:bgx2, 0] = b * (a / 255) + b2 * (1 - a / 255)
background[bgy1:bgy2, bgx1:bgx2, 1] = g * (a / 255) + g2 * (1 - a / 255)
background[bgy1:bgy2, bgx1:bgx2, 2] = r * (a / 255) + r2 * (1 - a / 255)
background[bgy1:bgy2, bgx1:bgx2, 3] = cv2.add(a, a2)
return background
if x >= 0 and y >= 0:
x2 = x + width2
y2 = y + height2
if x2 <= width1 and y2 <= height1:
background = cover_mode(
image,
background,
0,
wuqiong_img_y,
0,
wuqiong_img_x,
y,
y2,
x,
x2,
mode,
)
elif x2 > width1 and y2 <= height1:
# background[y:y2, x:] = image[:, :width1 - x]
background = cover_mode(
image,
background,
0,
wuqiong_img_y,
0,
width1 - x,
y,
y2,
x,
wuqiong_bg_x,
mode,
)
elif x2 <= width1 and y2 > height1:
# background[y:, x:x2] = image[:height1 - y, :]
background = cover_mode(
image,
background,
0,
height1 - y,
0,
wuqiong_img_x,
y,
wuqiong_bg_y,
x,
x2,
mode,
)
else:
# background[y:, x:] = image[:height1 - y, :width1 - x]
background = cover_mode(
image,
background,
0,
height1 - y,
0,
width1 - x,
y,
wuqiong_bg_y,
x,
wuqiong_bg_x,
mode,
)
elif x < 0 and y >= 0:
x2 = x + width2
y2 = y + height2
if x2 <= width1 and y2 <= height1:
# background[y:y2, :x + width2] = image[:, abs(x):]
background = cover_mode(
image,
background,
0,
wuqiong_img_y,
abs(x),
wuqiong_img_x,
y,
y2,
0,
x + width2,
mode,
)
elif x2 > width1 and y2 <= height1:
background = cover_mode(
image,
background,
0,
wuqiong_img_y,
abs(x),
width1 + abs(x),
y,
y2,
0,
wuqiong_bg_x,
mode,
)
elif x2 <= 0:
pass
elif x2 <= width1 and y2 > height1:
background = cover_mode(
image,
background,
0,
height1 - y,
abs(x),
wuqiong_img_x,
y,
wuqiong_bg_y,
0,
x2,
mode,
)
else:
# background[y:, :] = image[:height1 - y, abs(x):width1 + abs(x)]
background = cover_mode(
image,
background,
0,
height1 - y,
abs(x),
width1 + abs(x),
y,
wuqiong_bg_y,
0,
wuqiong_bg_x,
mode,
)
elif x >= 0 and y < 0:
x2 = x + width2
y2 = y + height2
if y2 <= 0:
pass
if x2 <= width1 and y2 <= height1:
# background[:y2, x:x2] = image[abs(y):, :]
background = cover_mode(
image,
background,
abs(y),
wuqiong_img_y,
0,
wuqiong_img_x,
0,
y2,
x,
x2,
mode,
)
elif x2 > width1 and y2 <= height1:
# background[:y2, x:] = image[abs(y):, :width1 - x]
background = cover_mode(
image,
background,
abs(y),
wuqiong_img_y,
0,
width1 - x,
0,
y2,
x,
wuqiong_bg_x,
mode,
)
elif x2 <= width1 and y2 > height1:
# background[:, x:x2] = image[abs(y):height1 + abs(y), :]
background = cover_mode(
image,
background,
abs(y),
height1 + abs(y),
0,
wuqiong_img_x,
0,
wuqiong_bg_y,
x,
x2,
mode,
)
else:
# background[:, x:] = image[abs(y):height1 + abs(y), :width1 - abs(x)]
background = cover_mode(
image,
background,
abs(y),
height1 + abs(y),
0,
width1 - abs(x),
0,
wuqiong_bg_x,
x,
wuqiong_bg_x,
mode,
)
else:
x2 = x + width2
y2 = y + height2
if y2 <= 0 or x2 <= 0:
pass
if x2 <= width1 and y2 <= height1:
# background[:y2, :x2] = image[abs(y):, abs(x):]
background = cover_mode(
image,
background,
abs(y),
wuqiong_img_y,
abs(x),
wuqiong_img_x,
0,
y2,
0,
x2,
mode,
)
elif x2 > width1 and y2 <= height1:
# background[:y2, :] = image[abs(y):, abs(x):width1 + abs(x)]
background = cover_mode(
image,
background,
abs(y),
wuqiong_img_y,
abs(x),
width1 + abs(x),
0,
y2,
0,
wuqiong_bg_x,
mode,
)
elif x2 <= width1 and y2 > height1:
# background[:, :x2] = image[abs(y):height1 + abs(y), abs(x):]
background = cover_mode(
image,
background,
abs(y),
height1 + abs(y),
abs(x),
wuqiong_img_x,
0,
wuqiong_bg_y,
0,
x2,
mode,
)
else:
# background[:, :] = image[abs(y):height1 - abs(y), abs(x):width1 + abs(x)]
background = cover_mode(
image,
background,
abs(y),
height1 - abs(y),
abs(x),
width1 + abs(x),
0,
wuqiong_bg_y,
0,
wuqiong_bg_x,
mode,
)
return background
def image2bgr(input_image):
if len(input_image.shape) == 2:
input_image = input_image[:, :, None]
if input_image.shape[2] == 1:
result_image = np.repeat(input_image, 3, axis=2)
elif input_image.shape[2] == 4:
result_image = input_image[:, :, 0:3]
else:
result_image = input_image
return result_image
if __name__ == "__main__":
image = cv2.imread("./03.png", -1)
result_image = add_background(image, bgr=(255, 255, 255))
cv2.imwrite("test.jpg", result_image)
+1 -1
View File
@@ -2,7 +2,7 @@ from fastapi import FastAPI, UploadFile, Form
import onnxruntime
from hivision.creator.face_judgement_align import IDphotos_create
from hivision.creator.layoutCreate import generate_layout_photo, generate_layout_image
from hivisionai.hycv.vision import add_background
from hivision.creator.vision import add_background
from utils import resize_image_to_kb_base64, hex_to_rgb
import base64
import numpy as np
+1 -1
View File
@@ -5,7 +5,7 @@ import numpy as np
import onnxruntime
from utils import resize_image_to_kb, hex_to_rgb
from hivision.creator.face_judgement_align import IDphotos_create
from hivisionai.hycv.vision import add_background
from hivision.creator.vision import add_background
from hivision.creator.layoutCreate import generate_layout_photo, generate_layout_image
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
r"""
@DATE: 2024/9/5 21:39
@File: create_id_photo.py
@IDE: pycharm
@Description:
用于测试创建证件照
"""
from hivision.creator import IDCreator
import cv2
import os
now_dir = os.path.dirname(__file__)
image_path = os.path.join(os.path.dirname(now_dir), "app", "images", "test.jpg")
output_dir = os.path.join(now_dir, "temp")
image = cv2.imread(image_path)
creator = IDCreator()
result = creator(image)
cv2.imwrite(os.path.join(output_dir, "result.jpg"), result.standard)
cv2.imwrite(os.path.join(output_dir, "result_hd.jpg"), result.hd)
+1
View File
@@ -0,0 +1 @@
存放一些测试临时文件
+8 -3
View File
@@ -1,5 +1,10 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
r"""
@DATE: 2024/9/5 21:33
@File: __init__.py
@IDE: pycharm
@Description:
与核心无关的工具函数
"""
与核心代码无关的工具函数
"""
from .image_utils import *