Files
tencent_face_recognition/api_operations.py
T

619 lines
21 KiB
Python
Raw Normal View History

"""API 操作实现。
将原 ``tencent_cloud_client.py`` 中各 API 调用方法(搜索/检测/属性/创建人员/
删除人员/注册人脸/删除人脸/获取库信息/获取人员列表等)抽离为独立函数,
由 ``TencentCloudClient`` 委托调用。每个函数返回标准化的结果字典。
腾讯云 IAI SDK (``tencentcloud.iai.v20200303``) 的若干字段名已更新:
- ``NeedRotateCheck`` 已废弃,使用 ``NeedRotateDetection``。
- ``CreateFaceRequest`` 使用 ``Images``(列表)而非 ``Image``。
- ``Candidate`` 没有 ``PersonTag`` 字段(标签在 ``PersonGroupInfos`` 中)。
- ``FaceInfo`` 的属性位于 ``FaceAttributesInfo`` / ``FaceQualityInfo`` 子结构。
- ``GetGroupInfoResponse`` 的库标签字段是 ``Tag`` 而非 ``GroupTag``。
- ``CreateFaceResponse`` 的成功人脸 ID 列表是 ``SucFaceIds`` 而非 ``FaceIds``。
"""
from __future__ import annotations
import json
import logging
import time
import uuid
from typing import Any, Dict, List, Optional
from tencentcloud.iai.v20200303 import models
from .retry import RetryConfig, execute_with_retry
_LOGGER = logging.getLogger(__name__)
# 是否记录请求/响应载荷详情(调试用)
ENABLE_REQUEST_LOGGING = False
ENABLE_RESPONSE_LOGGING = False
ENABLE_PERFORMANCE_LOGGING = True
LOG_PAYLOAD_MAX_SIZE = 1024
def _log_request(operation: str, params: Dict[str, Any], request_id: str) -> None:
if not ENABLE_REQUEST_LOGGING:
return
safe_params = {}
for key, value in params.items():
if key in ("Image", "Images", "Url"):
safe_params[key] = f"<image_{len(str(value))}_chars>"
else:
safe_params[key] = value
try:
params_str = json.dumps(safe_params, ensure_ascii=False, default=str)
except (TypeError, ValueError):
params_str = str(safe_params)
if len(params_str) > LOG_PAYLOAD_MAX_SIZE:
params_str = params_str[:LOG_PAYLOAD_MAX_SIZE] + "...[truncated]"
_LOGGER.debug("API请求: op=%s id=%s params=%s", operation, request_id, params_str)
def _log_response(operation: str, request_id: str, response: Any, duration: float) -> None:
if ENABLE_PERFORMANCE_LOGGING:
if duration > 5.0:
_LOGGER.warning("API响应缓慢: op=%s id=%s duration=%.3fs", operation, request_id, duration)
elif duration > 2.0:
_LOGGER.info("API响应较慢: op=%s id=%s duration=%.3fs", operation, request_id, duration)
if not ENABLE_RESPONSE_LOGGING:
return
try:
resp_dict = {}
for attr in dir(response):
if attr.startswith("_") or callable(getattr(response, attr)):
continue
resp_dict[attr] = getattr(response, attr)
resp_str = json.dumps(resp_dict, ensure_ascii=False, default=str)
except Exception: # noqa: BLE001
resp_str = str(response)
if len(resp_str) > LOG_PAYLOAD_MAX_SIZE:
resp_str = resp_str[:LOG_PAYLOAD_MAX_SIZE] + "...[truncated]"
_LOGGER.debug("API响应: op=%s id=%s response=%s", operation, request_id, resp_str)
def _log_error(operation: str, request_id: str, error: Exception, duration: float, sanitize=None) -> None:
sanitize = sanitize or (lambda s: s)
_LOGGER.error(
"API错误: op=%s id=%s duration=%.3fs error_type=%s msg=%s",
operation, request_id, duration, type(error).__name__,
sanitize(str(error)),
)
# SDK 异常的 code 在 TencentCloudSDKException 上
code = getattr(error, "code", None)
if code:
_LOGGER.error("腾讯云API错误详情: op=%s id=%s code=%s", operation, request_id, code)
if _LOGGER.isEnabledFor(logging.DEBUG):
_LOGGER.debug("API错误堆栈: op=%s id=%s", operation, request_id, exc_info=True)
def _build_error_result(ex: Exception, **extra: Any) -> Dict[str, Any]:
"""构造失败结果字典。"""
from tencentcloud.common.exception.tencent_cloud_sdk_exception import (
TencentCloudSDKException,
)
result: Dict[str, Any] = {"success": False}
result.update(extra)
if isinstance(ex, TencentCloudSDKException):
result["error_code"] = getattr(ex, "code", None)
result["error_message"] = getattr(ex, "message", str(ex))
else:
result["error_code"] = None
result["error_message"] = str(ex)
result["error"] = str(ex)
return result
def _run(
client,
operation_name: str,
build_request,
send,
parse_response,
*,
retry_config: Optional[RetryConfig] = None,
sanitize=None,
success_extra: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""统一的 API 调用包装:构建请求 -> 发送 -> 解析响应,带重试。
- ``build_request()`` 返回 ``(models.XxxRequest, params_dict)``。
- ``send(req)`` 调用 client 的实际 SDK 方法,返回 response。
- ``parse_response(resp)`` 返回业务结果字典(成功部分)。
"""
def api_call() -> Dict[str, Any]:
request_id = str(uuid.uuid4())
start = time.time()
try:
req, params = build_request()
_log_request(operation_name, params, request_id)
resp = send(req)
duration = time.time() - start
_log_response(operation_name, request_id, resp, duration)
result = {"success": True}
if success_extra:
result.update(success_extra)
result.update(parse_response(resp))
return result
except Exception as ex: # noqa: BLE001
duration = time.time() - start
_log_error(operation_name, request_id, ex, duration, sanitize)
# 抛出交由 execute_with_retry 决定是否重试
raise
return execute_with_retry(
api_call, operation_name, config=retry_config, sanitize_error=sanitize
)
# --- 各操作实现 -----------------------------------------------------------
def search_faces(
client,
*,
image_base64: str,
group_ids: List[str],
max_face_num: int = 1,
min_face_size: int = 34,
max_user_num: int = 5,
quality_control: int = 1,
need_rotate_check: int = 1,
face_match_threshold: float = 60.0,
retry_config: Optional[RetryConfig] = None,
sanitize=None,
) -> Dict[str, Any]:
"""搜索人脸。"""
def build_request():
req = models.SearchFacesRequest()
params = {
"NeedPersonInfo": 1,
"Image": image_base64,
"MaxFaceNum": max_face_num,
"MinFaceSize": min_face_size,
"MaxPersonNum": max_user_num,
"QualityControl": quality_control,
"NeedRotateDetection": need_rotate_check,
"FaceMatchThreshold": face_match_threshold,
"GroupIds": group_ids,
}
req.from_json_string(json.dumps(params))
return req, params
def parse_response(resp):
results: List[Dict[str, Any]] = []
for result in getattr(resp, "Results", []) or []:
face_result: Dict[str, Any] = {
"face_id": None,
"candidates": [],
"face_rect": _face_rect_dict(getattr(result, "FaceRect", None)),
}
for candidate in getattr(result, "Candidates", []) or []:
face_result["candidates"].append({
"person_id": getattr(candidate, "PersonId", None),
"person_name": getattr(candidate, "PersonName", None),
"score": getattr(candidate, "Score", None),
"face_id": getattr(candidate, "FaceId", None),
"gender": getattr(candidate, "Gender", None),
"person_group_infos": _person_group_infos(
getattr(candidate, "PersonGroupInfos", None)
),
})
results.append(face_result)
return {"faces": results, "face_count": len(results)}
return _run(
client, "人脸搜索", build_request,
lambda req: client.SearchFaces(req), parse_response,
retry_config=retry_config, sanitize=sanitize,
)
def detect_faces(
client,
*,
image_base64: str,
max_face_num: int = 1,
min_face_size: int = 34,
need_rotate_check: int = 1,
retry_config: Optional[RetryConfig] = None,
sanitize=None,
) -> Dict[str, Any]:
"""检测人脸(不返回属性)。"""
def build_request():
req = models.DetectFaceRequest()
params = {
"Image": image_base64,
"MaxFaceNum": max_face_num,
"MinFaceSize": min_face_size,
"NeedRotateDetection": need_rotate_check,
}
req.from_json_string(json.dumps(params))
return req, params
def parse_response(resp):
results = []
for info in getattr(resp, "FaceInfos", []) or []:
results.append({
"x": getattr(info, "X", 0),
"y": getattr(info, "Y", 0),
"width": getattr(info, "Width", 0),
"height": getattr(info, "Height", 0),
})
return {"faces": results, "face_count": len(results),
"image_width": getattr(resp, "ImageWidth", 0),
"image_height": getattr(resp, "ImageHeight", 0)}
return _run(
client, "人脸检测", build_request,
lambda req: client.DetectFace(req), parse_response,
retry_config=retry_config, sanitize=sanitize,
)
def get_face_attributes(
client,
*,
image_base64: str,
max_face_num: int = 1,
need_rotate_check: int = 1,
retry_config: Optional[RetryConfig] = None,
sanitize=None,
) -> Dict[str, Any]:
"""获取人脸属性。"""
def build_request():
req = models.DetectFaceRequest()
params = {
"Image": image_base64,
"MaxFaceNum": max_face_num,
"NeedRotateDetection": need_rotate_check,
"NeedFaceAttributes": 1,
"NeedQualityDetection": 1,
}
req.from_json_string(json.dumps(params))
return req, params
def parse_response(resp):
results = []
for info in getattr(resp, "FaceInfos", []) or []:
attrs = getattr(info, "FaceAttributesInfo", None)
quality = getattr(info, "FaceQualityInfo", None)
results.append({
"x": getattr(info, "X", 0),
"y": getattr(info, "Y", 0),
"width": getattr(info, "Width", 0),
"height": getattr(info, "Height", 0),
"gender": getattr(attrs, "Gender", None) if attrs else None,
"age": getattr(attrs, "Age", None) if attrs else None,
"expression": getattr(attrs, "Expression", None) if attrs else None,
"beauty": getattr(attrs, "Beauty", None) if attrs else None,
"glass": getattr(attrs, "Glass", None) if attrs else None,
"pitch": getattr(attrs, "Pitch", None) if attrs else None,
"yaw": getattr(attrs, "Yaw", None) if attrs else None,
"roll": getattr(attrs, "Roll", None) if attrs else None,
"eye_open": getattr(attrs, "EyeOpen", None) if attrs else None,
"quality_score": getattr(quality, "Score", None) if quality else None,
"quality_brightness": getattr(quality, "Brightness", None) if quality else None,
"quality_sharpness": getattr(quality, "Sharpness", None) if quality else None,
})
return {"faces": results, "face_count": len(results),
"image_width": getattr(resp, "ImageWidth", 0),
"image_height": getattr(resp, "ImageHeight", 0)}
return _run(
client, "获取人脸属性", build_request,
lambda req: client.DetectFace(req), parse_response,
retry_config=retry_config, sanitize=sanitize,
)
def create_person(
client,
*,
person_id: str,
person_name: str,
group_id: str,
image_base64: Optional[str] = None,
gender: Optional[int] = None,
person_tag: Optional[str] = None,
quality_control: int = 1,
need_rotate_check: int = 1,
retry_config: Optional[RetryConfig] = None,
sanitize=None,
) -> Dict[str, Any]:
"""创建人员。"""
def build_request():
req = models.CreatePersonRequest()
params: Dict[str, Any] = {
"PersonId": person_id,
"PersonName": person_name,
"GroupId": group_id,
"QualityControl": quality_control,
"NeedRotateDetection": need_rotate_check,
}
if image_base64:
params["Image"] = image_base64
if gender is not None:
params["Gender"] = gender
# ``PersonTag`` 字段在 SDK 中已废弃,人员备注需通过
# ``PersonExDescriptionInfos``(外部描述列表)存储。
if person_tag is not None:
params["PersonExDescriptionInfos"] = [
{
"PersonExDescriptionIndex": 0,
"PersonExDescription": person_tag,
}
]
req.from_json_string(json.dumps(params))
return req, params
def parse_response(resp):
return {
"person_id": getattr(resp, "PersonId", person_id),
"face_id": getattr(resp, "FaceId", ""),
"face_rect": _face_rect_dict(getattr(resp, "FaceRect", None)),
"face_model_version": getattr(resp, "FaceModelVersion", ""),
}
return _run(
client, "创建人员", build_request,
lambda req: client.CreatePerson(req), parse_response,
retry_config=retry_config, sanitize=sanitize,
)
def delete_person(
client,
*,
person_id: str,
retry_config: Optional[RetryConfig] = None,
sanitize=None,
) -> Dict[str, Any]:
"""删除人员。"""
def build_request():
req = models.DeletePersonRequest()
params = {"PersonId": person_id}
req.from_json_string(json.dumps(params))
return req, params
return _run(
client, "删除人员", build_request,
lambda req: client.DeletePerson(req), lambda resp: {"person_id": person_id},
retry_config=retry_config, sanitize=sanitize,
)
def create_face(
client,
*,
person_id: str,
image_base64: str,
quality_control: int = 1,
need_rotate_check: int = 1,
retry_config: Optional[RetryConfig] = None,
sanitize=None,
) -> Dict[str, Any]:
"""为人员注册人脸。
``CreateFaceRequest`` 接受 ``Images``(列表),单张图片包装为单元素列表。
"""
def build_request():
req = models.CreateFaceRequest()
params = {
"PersonId": person_id,
"Images": [image_base64],
"QualityControl": quality_control,
"NeedRotateDetection": need_rotate_check,
}
req.from_json_string(json.dumps(params))
return req, params
def parse_response(resp):
return {
"person_id": person_id,
"face_ids": list(getattr(resp, "SucFaceIds", []) or []),
"face_rects": [_face_rect_dict(r) for r in (getattr(resp, "SucFaceRects", []) or [])],
"suc_face_num": getattr(resp, "SucFaceNum", 0),
"ret_code": getattr(resp, "RetCode", None),
"face_model_version": getattr(resp, "FaceModelVersion", ""),
}
return _run(
client, "注册人脸", build_request,
lambda req: client.CreateFace(req), parse_response,
retry_config=retry_config, sanitize=sanitize,
)
def delete_face(
client,
*,
person_id: str,
face_id: str,
retry_config: Optional[RetryConfig] = None,
sanitize=None,
) -> Dict[str, Any]:
"""删除人脸。``DeleteFaceRequest`` 接受 ``FaceIds``(列表)。"""
def build_request():
req = models.DeleteFaceRequest()
params = {"PersonId": person_id, "FaceIds": [face_id]}
req.from_json_string(json.dumps(params))
return req, params
return _run(
client, "删除人脸", build_request,
lambda req: client.DeleteFace(req),
lambda resp: {"person_id": person_id, "face_id": face_id,
"suc_indexes": list(getattr(resp, "SucIndexes", []) or [])},
retry_config=retry_config, sanitize=sanitize,
)
def get_group_info(
client,
*,
group_id: str,
retry_config: Optional[RetryConfig] = None,
sanitize=None,
) -> Dict[str, Any]:
"""获取人员库信息。"""
def build_request():
req = models.GetGroupInfoRequest()
params = {"GroupId": group_id}
req.from_json_string(json.dumps(params))
return req, params
def parse_response(resp):
return {
"group_id": getattr(resp, "GroupId", group_id),
"group_name": getattr(resp, "GroupName", ""),
# SDK 字段名为 Tag(库标签),原代码误用 GroupTag
"group_tag": getattr(resp, "Tag", ""),
"face_model_version": getattr(resp, "FaceModelVersion", ""),
"creation_timestamp": getattr(resp, "CreationTimestamp", 0),
}
return _run(
client, "获取人员库信息", build_request,
lambda req: client.GetGroupInfo(req), parse_response,
retry_config=retry_config, sanitize=sanitize,
)
def get_person_list(
client,
*,
group_id: str,
limit: int = 100,
offset: int = 0,
retry_config: Optional[RetryConfig] = None,
sanitize=None,
) -> Dict[str, Any]:
"""获取人员列表(单页)。"""
def build_request():
req = models.GetPersonListRequest()
params = {"GroupId": group_id, "Limit": limit, "Offset": offset}
req.from_json_string(json.dumps(params))
return req, params
def parse_response(resp):
persons = []
for info in getattr(resp, "PersonInfos", []) or []:
persons.append({
"person_id": getattr(info, "PersonId", ""),
"person_name": getattr(info, "PersonName", ""),
"gender": getattr(info, "Gender", 0),
"face_ids": list(getattr(info, "FaceIds", []) or []),
"creation_timestamp": getattr(info, "CreationTimestamp", 0),
})
return {
"persons": persons,
"person_num": getattr(resp, "PersonNum", len(persons)),
"face_model_version": getattr(resp, "FaceModelVersion", ""),
}
return _run(
client, "获取人员列表", build_request,
lambda req: client.GetPersonList(req), parse_response,
retry_config=retry_config, sanitize=sanitize,
)
def get_person_list_all(
client,
*,
group_id: str,
limit: int = 100,
retry_config: Optional[RetryConfig] = None,
sanitize=None,
) -> Dict[str, Any]:
"""获取人员列表(自动分页获取全部)。"""
all_persons: List[Dict[str, Any]] = []
offset = 0
face_model_version = ""
while True:
page = get_person_list(
client, group_id=group_id, limit=limit, offset=offset,
retry_config=retry_config, sanitize=sanitize,
)
if not page.get("success", False):
return page
persons = page.get("persons", [])
all_persons.extend(persons)
face_model_version = page.get("face_model_version", face_model_version)
# 单页不足 limit 视为末页
if len(persons) < limit:
break
offset += limit
# 防御:person_num 已达上限则停止
person_num = page.get("person_num", 0)
if person_num and len(all_persons) >= person_num:
break
return {
"success": True,
"persons": all_persons,
"person_count": len(all_persons),
"face_model_version": face_model_version,
"error": None,
"error_code": None,
"error_message": None,
}
def verify_credentials(client, retry_config: Optional[RetryConfig] = None) -> bool:
"""验证凭据是否有效,通过调用 GetGroupList 轻量测试。
成功返回 True;失败抛出对应的 ``TencentFaceRecognitionError``。
"""
def api_call() -> bool:
req = models.GetGroupListRequest()
req.from_json_string(json.dumps({"Limit": 1, "Offset": 0}))
client.GetGroupList(req)
return True
execute_with_retry(api_call, "凭据验证", config=retry_config)
_LOGGER.info("凭据验证成功")
return True
# --- 辅助 -----------------------------------------------------------------
def _face_rect_dict(face_rect) -> Optional[Dict[str, Any]]:
"""将 SDK 的 FaceRect 对象转为字典。"""
if face_rect is None:
return None
return {
"x": getattr(face_rect, "X", 0),
"y": getattr(face_rect, "Y", 0),
"width": getattr(face_rect, "Width", 0),
"height": getattr(face_rect, "Height", 0),
}
def _person_group_infos(infos) -> List[Dict[str, Any]]:
"""将 Candidate.PersonGroupInfos 转为列表字典。"""
if not infos:
return []
result = []
for info in infos:
result.append({
"group_id": getattr(info, "GroupId", ""),
"person_ex_descriptions": list(getattr(info, "PersonExDescriptions", []) or []),
})
return result