|
|
from PIL import Image |
|
|
from insightface.app import FaceAnalysis |
|
|
import numpy as np |
|
|
import os |
|
|
from pathlib import Path |
|
|
import time |
|
|
import argparse |
|
|
import cv2 |
|
|
|
|
|
class FaceInference: |
|
|
"""人脸检测推理类,封装insightface的推理功能""" |
|
|
|
|
|
def __init__(self, det_thresh=0.5, det_size=(640, 640), ctx_id=0): |
|
|
""" |
|
|
初始化人脸检测器 |
|
|
|
|
|
Args: |
|
|
det_thresh: 检测阈值 |
|
|
det_size: 检测图像尺寸 |
|
|
ctx_id: GPU设备ID,如果为-1则使用CPU,否则使用GPU |
|
|
""" |
|
|
|
|
|
if ctx_id == -1: |
|
|
providers = ['CPUExecutionProvider'] |
|
|
provider_options = [{}] |
|
|
ctx_id = -1 |
|
|
else: |
|
|
providers = ['CUDAExecutionProvider', 'CPUExecutionProvider'] |
|
|
provider_options = [{"device_id": str(ctx_id)}, {}] |
|
|
|
|
|
self.face_analysis = FaceAnalysis( |
|
|
allowed_modules=['detection'], |
|
|
providers=providers, |
|
|
provider_options=provider_options, |
|
|
) |
|
|
|
|
|
self.face_analysis.prepare(ctx_id=ctx_id, det_thresh=det_thresh, det_size=det_size) |
|
|
|
|
|
def _make_square_bbox(self, x1, y1, x2, y2, image_width, image_height): |
|
|
""" |
|
|
将矩形bbox转换为方形bbox,保持人脸比例不变 |
|
|
|
|
|
Args: |
|
|
x1, y1, x2, y2: 原始bbox坐标 |
|
|
image_width, image_height: 图像尺寸 |
|
|
|
|
|
Returns: |
|
|
tuple: (new_x1, new_y1, new_x2, new_y2) 方形bbox坐标 |
|
|
""" |
|
|
|
|
|
center_x = (x1 + x2) / 2 |
|
|
center_y = (y1 + y2) / 2 |
|
|
width = x2 - x1 |
|
|
height = y2 - y1 |
|
|
|
|
|
|
|
|
square_size = max(width, height) |
|
|
|
|
|
|
|
|
half_size = square_size / 2 |
|
|
new_x1 = center_x - half_size |
|
|
new_y1 = center_y - half_size |
|
|
new_x2 = center_x + half_size |
|
|
new_y2 = center_y + half_size |
|
|
|
|
|
|
|
|
if new_x1 < 0: |
|
|
new_x1 = 0 |
|
|
new_x2 = square_size |
|
|
if new_y1 < 0: |
|
|
new_y1 = 0 |
|
|
new_y2 = square_size |
|
|
if new_x2 > image_width: |
|
|
new_x2 = image_width |
|
|
new_x1 = image_width - square_size |
|
|
if new_y2 > image_height: |
|
|
new_y2 = image_height |
|
|
new_y1 = image_height - square_size |
|
|
|
|
|
|
|
|
new_x1 = max(0, new_x1) |
|
|
new_y1 = max(0, new_y1) |
|
|
new_x2 = min(image_width, new_x2) |
|
|
new_y2 = min(image_height, new_y2) |
|
|
|
|
|
return new_x1, new_y1, new_x2, new_y2 |
|
|
|
|
|
def infer_from_array(self, image_array, n=None): |
|
|
""" |
|
|
对输入numpy数组进行人脸检测推理 |
|
|
|
|
|
Args: |
|
|
image_array: numpy数组,形状为[H, W, 3],值范围为0-255 |
|
|
n: 选择前n个最大的人脸,如果为None则选择所有人脸 |
|
|
|
|
|
Returns: |
|
|
dict: 包含检测结果的字典,格式为: |
|
|
{ |
|
|
'faces': 检测到的人脸列表, |
|
|
'bboxes': bbox列表,每个元素为[x, y, width, height], |
|
|
'masks': mask列表,每个元素为单通道mask图像, |
|
|
'masked_images': masked图像列表,每个元素为应用mask后的图像, |
|
|
'image_shape': 原始图像的形状 (height, width, channels) |
|
|
} |
|
|
如果未检测到人脸,返回中心区域矩形作为默认bbox |
|
|
""" |
|
|
try: |
|
|
if image_array is None: |
|
|
print("错误:输入图像数组为空") |
|
|
return {} |
|
|
|
|
|
|
|
|
if len(image_array.shape) != 3 or image_array.shape[2] != 3: |
|
|
print(f"错误:图像数组形状不正确,期望[H, W, 3],实际{image_array.shape}") |
|
|
return {} |
|
|
|
|
|
|
|
|
if image_array.dtype != np.uint8: |
|
|
image_array = image_array.astype(np.uint8) |
|
|
|
|
|
faces = self.face_analysis.get(image_array) |
|
|
height, width = image_array.shape[:2] |
|
|
|
|
|
if not faces: |
|
|
return { |
|
|
'faces': [], |
|
|
'bboxes': [], |
|
|
'masks': [], |
|
|
'masked_images': [], |
|
|
'image_shape': image_array.shape |
|
|
} |
|
|
|
|
|
|
|
|
if n is not None and n > 0: |
|
|
|
|
|
faces_with_area = [(face, (face['bbox'][2] - face['bbox'][0]) * (face['bbox'][3] - face['bbox'][1])) for face in faces] |
|
|
faces_with_area.sort(key=lambda x: x[1], reverse=True) |
|
|
faces = [face for face, _ in faces_with_area[:n]] |
|
|
|
|
|
|
|
|
|
|
|
faces = sorted(faces, key=lambda x: x['bbox'][0]) |
|
|
|
|
|
|
|
|
bboxes = [] |
|
|
masks = [] |
|
|
masked_images = [] |
|
|
|
|
|
for i, face in enumerate(faces): |
|
|
bbox = face['bbox'] |
|
|
x1, y1, x2, y2 = bbox[0], bbox[1], bbox[2], bbox[3] |
|
|
|
|
|
|
|
|
square_x1, square_y1, square_x2, square_y2 = self._make_square_bbox( |
|
|
x1, y1, x2, y2, width, height |
|
|
) |
|
|
|
|
|
|
|
|
mask = np.zeros(image_array.shape[:2], dtype=np.uint8) |
|
|
mask[int(square_y1):int(square_y2), int(square_x1):int(square_x2)] = 1.0 |
|
|
|
|
|
|
|
|
masked_image = image_array.copy() |
|
|
masked_image = cv2.bitwise_and(masked_image, masked_image, mask=mask) |
|
|
|
|
|
bboxes.append([square_x1, square_y1, square_x2 - square_x1, square_y2 - square_y1]) |
|
|
masks.append(mask) |
|
|
masked_images.append(masked_image) |
|
|
|
|
|
|
|
|
|
|
|
return { |
|
|
'faces': faces, |
|
|
'bboxes': bboxes, |
|
|
'masks': masks, |
|
|
'masked_images': masked_images, |
|
|
'image_shape': image_array.shape |
|
|
} |
|
|
|
|
|
except Exception as e: |
|
|
print(f"处理图像数组时出错: {str(e)}") |
|
|
|
|
|
if 'image_array' in locals() and image_array is not None: |
|
|
return { |
|
|
'faces': [], |
|
|
'bboxes': [], |
|
|
'masks': [], |
|
|
'masked_images': [], |
|
|
'image_shape': image_array.shape |
|
|
} |
|
|
|
|
|
return {} |
|
|
|
|
|
def infer(self, image_path, n=None): |
|
|
""" |
|
|
对输入图像进行人脸检测推理 |
|
|
|
|
|
Args: |
|
|
image_path: 图像文件路径或图片 |
|
|
n: 选择前n个最大的人脸,如果为None则选择所有人脸 |
|
|
|
|
|
Returns: |
|
|
dict: 包含检测结果的字典,格式为: |
|
|
{ |
|
|
'faces': 检测到的人脸列表, |
|
|
'bboxes': bbox列表,每个元素为[x, y, width, height], |
|
|
'masks': mask列表,每个元素为单通道mask图像, |
|
|
'masked_images': masked图像列表,每个元素为应用mask后的图像, |
|
|
'image_shape': 原始图像的形状 (height, width, channels) |
|
|
} |
|
|
如果未检测到人脸,返回中心区域矩形作为默认bbox |
|
|
""" |
|
|
try: |
|
|
image = cv2.imread(image_path) |
|
|
if image is None: |
|
|
print(f"错误:无法读取图像 {image_path}") |
|
|
|
|
|
return {} |
|
|
|
|
|
faces = self.face_analysis.get(image) |
|
|
height, width = image.shape[:2] |
|
|
|
|
|
if not faces: |
|
|
print(f"警告:图像 {os.path.basename(image_path)} 中未检测到人脸,使用中心区域作为默认方形bbox") |
|
|
|
|
|
|
|
|
min_dim = min(width, height) |
|
|
square_size = min_dim // 2 |
|
|
center_x, center_y = width // 2, height // 2 |
|
|
|
|
|
x1 = center_x - square_size // 2 |
|
|
y1 = center_y - square_size // 2 |
|
|
x2 = x1 + square_size |
|
|
y2 = y1 + square_size |
|
|
|
|
|
|
|
|
x1 = max(0, x1) |
|
|
y1 = max(0, y1) |
|
|
x2 = min(width, x2) |
|
|
y2 = min(height, y2) |
|
|
|
|
|
|
|
|
mask = np.zeros(image.shape[:2], dtype=np.uint8) |
|
|
mask[int(y1):int(y2), int(x1):int(x2)] = 1.0 |
|
|
|
|
|
|
|
|
masked_image = image.copy() |
|
|
masked_image = cv2.bitwise_and(masked_image, masked_image, mask=mask) |
|
|
|
|
|
return { |
|
|
'faces': [], |
|
|
'bboxes': [[x1, y1, x2 - x1, y2 - y1]], |
|
|
'masks': [mask], |
|
|
'masked_images': [masked_image], |
|
|
'image_shape': image.shape |
|
|
} |
|
|
|
|
|
|
|
|
if n is not None and n > 0: |
|
|
|
|
|
faces_with_area = [(face, (face['bbox'][2] - face['bbox'][0]) * (face['bbox'][3] - face['bbox'][1])) for face in faces] |
|
|
faces_with_area.sort(key=lambda x: x[1], reverse=True) |
|
|
faces = [face for face, _ in faces_with_area[:n]] |
|
|
|
|
|
|
|
|
faces = sorted(faces, key=lambda x: x['bbox'][0]) |
|
|
|
|
|
|
|
|
bboxes = [] |
|
|
masks = [] |
|
|
masked_images = [] |
|
|
|
|
|
for i, face in enumerate(faces): |
|
|
bbox = face['bbox'] |
|
|
x1, y1, x2, y2 = bbox[0], bbox[1], bbox[2], bbox[3] |
|
|
|
|
|
|
|
|
square_x1, square_y1, square_x2, square_y2 = self._make_square_bbox( |
|
|
x1, y1, x2, y2, width, height |
|
|
) |
|
|
|
|
|
|
|
|
mask = np.zeros(image.shape[:2], dtype=np.uint8) |
|
|
mask[int(square_y1):int(square_y2), int(square_x1):int(square_x2)] = 1.0 |
|
|
|
|
|
|
|
|
masked_image = image.copy() |
|
|
masked_image = cv2.bitwise_and(masked_image, masked_image, mask=mask) |
|
|
|
|
|
bboxes.append([square_x1, square_y1, square_x2 - square_x1, square_y2 - square_y1]) |
|
|
masks.append(mask) |
|
|
masked_images.append(masked_image) |
|
|
|
|
|
return { |
|
|
'faces': faces, |
|
|
'bboxes': bboxes, |
|
|
'masks': masks, |
|
|
'masked_images': masked_images, |
|
|
'image_shape': image.shape |
|
|
} |
|
|
|
|
|
except Exception as e: |
|
|
print(f"处理图像 {image_path} 时出错: {str(e)}") |
|
|
|
|
|
if 'image' in locals() and image is not None: |
|
|
height, width = image.shape[:2] |
|
|
|
|
|
|
|
|
min_dim = min(width, height) |
|
|
square_size = min_dim // 2 |
|
|
center_x, center_y = width // 2, height // 2 |
|
|
|
|
|
x1 = center_x - square_size // 2 |
|
|
y1 = center_y - square_size // 2 |
|
|
x2 = x1 + square_size |
|
|
y2 = y1 + square_size |
|
|
|
|
|
|
|
|
x1 = max(0, x1) |
|
|
y1 = max(0, y1) |
|
|
x2 = min(width, x2) |
|
|
y2 = min(height, y2) |
|
|
|
|
|
|
|
|
mask = np.zeros(image.shape[:2], dtype=np.uint8) |
|
|
mask[int(y1):int(y2), int(x1):int(x2)] = 1.0 |
|
|
|
|
|
|
|
|
masked_image = image.copy() |
|
|
masked_image = cv2.bitwise_and(masked_image, masked_image, mask=mask) |
|
|
|
|
|
return { |
|
|
'faces': [], |
|
|
'bboxes': [[x1, y1, x2 - x1, y2 - y1]], |
|
|
'masks': [mask], |
|
|
'masked_images': [masked_image], |
|
|
'image_shape': image.shape |
|
|
} |
|
|
|
|
|
return {} |
|
|
|
|
|
|
|
|
class FaceProcessor: |
|
|
def __init__(self, det_thresh=0.5, det_size=(640, 640)): |
|
|
self.face_analysis = FaceAnalysis(allowed_modules=['detection']) |
|
|
self.face_analysis.prepare(ctx_id=0, det_thresh=det_thresh, det_size=det_size) |
|
|
|
|
|
def _make_square_bbox(self, x1, y1, x2, y2, image_width, image_height): |
|
|
""" |
|
|
将矩形bbox转换为方形bbox,保持人脸比例不变 |
|
|
|
|
|
Args: |
|
|
x1, y1, x2, y2: 原始bbox坐标 |
|
|
image_width, image_height: 图像尺寸 |
|
|
|
|
|
Returns: |
|
|
tuple: (new_x1, new_y1, new_x2, new_y2) 方形bbox坐标 |
|
|
""" |
|
|
|
|
|
center_x = (x1 + x2) / 2 |
|
|
center_y = (y1 + y2) / 2 |
|
|
width = x2 - x1 |
|
|
height = y2 - y1 |
|
|
|
|
|
|
|
|
square_size = max(width, height) |
|
|
|
|
|
|
|
|
half_size = square_size / 2 |
|
|
new_x1 = center_x - half_size |
|
|
new_y1 = center_y - half_size |
|
|
new_x2 = center_x + half_size |
|
|
new_y2 = center_y + half_size |
|
|
|
|
|
|
|
|
if new_x1 < 0: |
|
|
new_x1 = 0 |
|
|
new_x2 = square_size |
|
|
if new_y1 < 0: |
|
|
new_y1 = 0 |
|
|
new_y2 = square_size |
|
|
if new_x2 > image_width: |
|
|
new_x2 = image_width |
|
|
new_x1 = image_width - square_size |
|
|
if new_y2 > image_height: |
|
|
new_y2 = image_height |
|
|
new_y1 = image_height - square_size |
|
|
|
|
|
|
|
|
new_x1 = max(0, new_x1) |
|
|
new_y1 = max(0, new_y1) |
|
|
new_x2 = min(image_width, new_x2) |
|
|
new_y2 = min(image_height, new_y2) |
|
|
|
|
|
return new_x1, new_y1, new_x2, new_y2 |
|
|
|
|
|
def get_face_bbox_and_mask(self, image): |
|
|
faces = self.face_analysis.get(image) |
|
|
if not faces: |
|
|
print("警告:图像中未检测到人脸。") |
|
|
return None, None, None |
|
|
|
|
|
|
|
|
faces = sorted(faces, key=lambda x: x['bbox'][0]) |
|
|
|
|
|
height, width = image.shape[:2] |
|
|
bboxes = [] |
|
|
masks = [] |
|
|
masked_images = [] |
|
|
|
|
|
for i, face in enumerate(faces): |
|
|
bbox = face['bbox'] |
|
|
x1, y1, x2, y2 = bbox[0], bbox[1], bbox[2], bbox[3] |
|
|
|
|
|
|
|
|
square_x1, square_y1, square_x2, square_y2 = self._make_square_bbox( |
|
|
x1, y1, x2, y2, width, height |
|
|
) |
|
|
|
|
|
|
|
|
mask = np.zeros(image.shape[:2], dtype=np.uint8) |
|
|
mask[int(square_y1):int(square_y2), int(square_x1):int(square_x2)] = 1.0 |
|
|
|
|
|
|
|
|
masked_image = image.copy() |
|
|
masked_image = cv2.bitwise_and(masked_image, masked_image, mask=mask) |
|
|
|
|
|
bboxes.append([square_x1, square_y1, square_x2 - square_x1, square_y2 - square_y1]) |
|
|
masks.append(mask) |
|
|
masked_images.append(masked_image) |
|
|
|
|
|
return bboxes, masks, masked_images |
|
|
|
|
|
def main(): |
|
|
parser = argparse.ArgumentParser(description='Process images to detect faces and save bbox, mask, and masked images.') |
|
|
parser.add_argument('--input_dir', type=str, default="./data/bbox_test_input", help='Directory containing input images.') |
|
|
parser.add_argument('--bbox_output_dir', type=str, default="./temp/bbox", help='Directory to save bbox npy files.') |
|
|
parser.add_argument('--mask_output_dir', type=str, default="./temp/mask", help='Directory to save mask images.') |
|
|
parser.add_argument('--masked_image_output_dir', type=str, default="./temp/masked_images", help='Directory to save masked images.') |
|
|
args = parser.parse_args() |
|
|
|
|
|
|
|
|
os.makedirs(args.bbox_output_dir, exist_ok=True) |
|
|
os.makedirs(args.mask_output_dir, exist_ok=True) |
|
|
os.makedirs(args.masked_image_output_dir, exist_ok=True) |
|
|
|
|
|
|
|
|
face_processor = FaceProcessor() |
|
|
|
|
|
|
|
|
supported_formats = {'.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.tif'} |
|
|
|
|
|
|
|
|
image_files = [] |
|
|
for file in os.listdir(args.input_dir): |
|
|
if Path(file).suffix.lower() in supported_formats: |
|
|
image_files.append(file) |
|
|
|
|
|
if not image_files: |
|
|
print(f"警告:在目录 {args.input_dir} 中未找到支持的图像文件") |
|
|
return |
|
|
|
|
|
|
|
|
for image_file in image_files: |
|
|
image_path = os.path.join(args.input_dir, image_file) |
|
|
|
|
|
|
|
|
image = cv2.imread(image_path) |
|
|
if image is None: |
|
|
print(f" 错误:无法读取图像 {image_path}") |
|
|
continue |
|
|
|
|
|
|
|
|
bboxes, masks, masked_images = face_processor.get_face_bbox_and_mask(image) |
|
|
|
|
|
if bboxes is None: |
|
|
print(f" 跳过:未检测到人脸") |
|
|
continue |
|
|
|
|
|
|
|
|
base_name = Path(image_file).stem |
|
|
|
|
|
|
|
|
bbox_file = os.path.join(args.bbox_output_dir, f"{base_name}_bbox.npy") |
|
|
np.save(bbox_file, np.array(bboxes)) |
|
|
|
|
|
|
|
|
for i, (mask, masked_image) in enumerate(zip(masks, masked_images)): |
|
|
|
|
|
mask_file = os.path.join(args.mask_output_dir, f"{base_name}_face{i+1}_mask.png") |
|
|
cv2.imwrite(mask_file, mask) |
|
|
|
|
|
|
|
|
masked_image_file = os.path.join(args.masked_image_output_dir, f"{base_name}_face{i+1}_masked.png") |
|
|
cv2.imwrite(masked_image_file, masked_image) |
|
|
|
|
|
print(f" 已保存人脸{i+1}的mask: {mask_file}") |
|
|
print(f" 已保存人脸{i+1}的masked图像: {masked_image_file}") |
|
|
|
|
|
print(f"\n处理完成!") |
|
|
print(f"bbox文件保存在: {args.bbox_output_dir}") |
|
|
print(f"mask文件保存在: {args.mask_output_dir}") |
|
|
print(f"masked图像保存在: {args.masked_image_output_dir}") |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
main() |
|
|
|