Рефакторинг: выделение get_crop_coordinates(), интеграция с Django API

This commit is contained in:
2026-08-14 09:37:35 +07:00
parent c7ae88a3e0
commit 707dcf3437
+79 -81
View File
@@ -1,84 +1,42 @@
import io import io
import cv2 import cv2
import os
import logging import logging
import numpy as np import numpy as np
import requests
from PIL import Image, ImageFilter, ImageEnhance from PIL import Image, ImageFilter, ImageEnhance
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def prepare_ad_photo(input_path: str, output_path: str, target_size: int = 96) -> dict: # Настройки API
try: API_URL = "https://profile.sibcem.ru/SibCemProfile/api/photo-bot/upload/"
# 1. Загружаем изображение для OpenCV API_KEY = "sibcem_bot_2026_a7f3k9m2x8q1w5e4r6t"
if isinstance(input_path, bytes):
img = cv2.imdecode(np.frombuffer(input_path, np.uint8), cv2.IMREAD_COLOR)
else: def get_crop_coordinates(image_bytes: bytes) -> tuple | None:
img = cv2.imread(input_path) """
Ищет лицо на фото и возвращает координаты квадратного кропа.
Returns:
(crop_x1, crop_y1, crop_x2, crop_y2) или None если лицо не найдено.
"""
img = cv2.imdecode(np.frombuffer(image_bytes, np.uint8), cv2.IMREAD_COLOR)
if img is None: if img is None:
return {"success": False, "error": "Не удалось прочитать файл изображения."} return None
if isinstance(input_path, bytes):
img = cv2.imdecode(np.frombuffer(input_path, np.uint8), cv2.IMREAD_COLOR)
else:
img = cv2.imread(input_path)
if img is None:
return {"success": False, "error": "Не удалось прочитать файл изображения."}
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 2. Загружаем каскад Хаара для поиска лица
cascade_path = cv2.data.haarcascades + 'haarcascade_frontalface_default.xml' cascade_path = cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
face_cascade = cv2.CascadeClassifier(cascade_path) face_cascade = cv2.CascadeClassifier(cascade_path)
# 3. Ищем лица
# Оставляем оптимальное значение minNeighbors=8 для баланса
faces = face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=8, minSize=(50, 50)) faces = face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=8, minSize=(50, 50))
if len(faces) == 0: if len(faces) == 0:
return {"success": False, "error": "Лицо не найдено! Пожалуйста, сделайте фото анфас при хорошем освещении."} return None
if len(faces) > 1: if len(faces) > 1:
faces = sorted(faces, key=lambda f: f[2] * f[3], reverse=True) faces = sorted(faces, key=lambda f: f[2] * f[3], reverse=True)
x, y, w, h = faces[0] x, y, w, h = faces[0]
# =========================================================
# АБСОЛЮТНАЯ ЗАЩИТА ОТ ГРАФИКИ (НОРМАЛИЗОВАННАЯ МАТЕМАТИКА)
# =========================================================
# 1. Вырезаем центральную зону лица
face_roi_original = gray[y + int(h*0.2):y + int(h*0.8), x + int(w*0.2):x + int(w*0.8)]
# 2. Сжимаем до 100x100 пикселей.
# Это уравнивает 4K-фотографии и мелкие картинки, делая метрики стабильными.
face_roi = cv2.resize(face_roi_original, (100, 100))
std_dev = np.std(face_roi)
laplacian_var = cv2.Laplacian(face_roi, cv2.CV_64F).var()
unique_colors = len(np.unique(face_roi))
logger.info(f"Анализ нормализованного лица: std_dev={std_dev:.2f}, laplacian={laplacian_var:.2f}, colors={unique_colors}")
# ПРОВЕРКА 1: Плоские, замыленные картинки
if std_dev < 10.0 or laplacian_var < 30.0:
logger.warning(f"Заблокирован плоский рисунок/замыливание: std={std_dev:.1f}, lap={laplacian_var:.1f}")
return {"success": False, "error": "Изображение слишком размыто или имеет плоскую текстуру. Отправьте реальное фото."}
# ПРОВЕРКА 2: Векторы, комиксы и жесткий 2D-арт (Умный порог резкости + цвета)
# У реального человека на хорошую камеру laplacian может доходить до 500-600,
# но при этом всегда будет богатая палитра цветов (шум матрицы).
# У мультиков высокая резкость всегда сопровождается бедной палитрой.
if laplacian_var > 850.0 or (laplacian_var > 400.0 and unique_colors < 150):
logger.warning(f"Заблокирован резкий 2D-арт: lap={laplacian_var:.1f}, colors={unique_colors}")
return {"success": False, "error": "Изображение похоже на графический арт или сильно отфильтровано. Пожалуйста, загрузите естественное фото."}
# ПРОВЕРКА 3: 3D-рендеры и нейросети (Мало шума)
# На реальной фотографии (даже с хорошим светом) шум матрицы телефона
# даст минимум 110-120 уникальных оттенков на 10000 пикселей.
if unique_colors < 105:
logger.warning(f"Заблокирован 3D-рендер по палитре: unique_colors={unique_colors}")
return {"success": False, "error": "Изображение распознано как 3D-модель или рисунок (неестественная палитра). Ждем реальное фото!"}
# =========================================================
# 4. Кадрируем (делаем квадрат с отступами вокруг лица)
margin = int(w * 0.3) margin = int(w * 0.3)
y1 = max(0, y - int(margin * 1.5)) y1 = max(0, y - int(margin * 1.5))
y2 = min(img.shape[0], y + h + margin) y2 = min(img.shape[0], y + h + margin)
@@ -92,36 +50,76 @@ def prepare_ad_photo(input_path: str, output_path: str, target_size: int = 96) -
crop_x2 = crop_x1 + side crop_x2 = crop_x1 + side
crop_y2 = crop_y1 + side crop_y2 = crop_y1 + side
# 5. Обработка через Pillow return (crop_x1, crop_y1, crop_x2, crop_y2)
pil_img = Image.open(io.BytesIO(input_path))
# ВАЖНО: Убираем альфа-канал (прозрачность), иначе BMP выдаст ошибку
if pil_img.mode in ('RGBA', 'P', 'LA'):
background = Image.new('RGB', pil_img.size, (255, 255, 255))
background.paste(pil_img, mask=pil_img.split()[3] if pil_img.mode == 'RGBA' else None)
pil_img = background
elif pil_img.mode != 'RGB':
pil_img = pil_img.convert('RGB')
cropped_img = pil_img.crop((crop_x1, crop_y1, crop_x2, crop_y2)) def _upload_to_api(image_bytes: bytes, user_id: str, coords: tuple) -> dict:
"""
Отправляет фото и координаты в Django API.
Возвращает JSON от сервера.
"""
x1, y1, x2, y2 = coords
try:
response = requests.post(
API_URL,
headers={"X-API-Key": API_KEY},
data={
"user_id": user_id,
"crop_x1": x1,
"crop_y1": y1,
"crop_x2": x2,
"crop_y2": y2,
},
files={
"image": ("photo.jpg", image_bytes, "image/jpeg")
},
timeout=30
)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
logger.error(f"Ошибка API: {e}")
return {"status": "error", "reason": f"Ошибка сети: {str(e)}"}
except Exception as e:
logger.error(f"Ошибка отправки в API: {e}")
return {"status": "error", "reason": f"Ошибка: {str(e)}"}
# Сжимаем до нужного размера (96x96)
final_img = cropped_img.resize((target_size, target_size), Image.Resampling.LANCZOS)
# Добавляем немного резкости, чтобы лицо не мылилось при 96px def prepare_ad_photo(input_path: str, user_id: str, output_path: str = None, target_size: int = 96) -> dict:
final_img = final_img.filter(ImageFilter.UnsharpMask(radius=1.5, percent=100, threshold=3)) """
Отправляет фото на сервер через API.
output_path и target_size больше не используются (оставлены для обратной совместимости).
"""
try:
# 1. Читаем байты
if isinstance(input_path, bytes):
image_bytes = input_path
else:
with open(input_path, 'rb') as f:
image_bytes = f.read()
# 6. Сохраняем в формате BMP # 2. Ищем лицо и получаем координаты
final_img.save(output_path, format="BMP") coords = get_crop_coordinates(image_bytes)
if not coords:
return {"success": False, "error": "Лицо не найдено! Пожалуйста, сделайте фото анфас при хорошем освещении."}
# 7. Проверяем финальный вес # 3. Отправляем в API
size_kb = os.path.getsize(output_path) / 1024 api_result = _upload_to_api(image_bytes, user_id, coords)
if size_kb > 99: # 4. Возвращаем результат
return {"success": False, "error": f"Сбой: размер файла ({size_kb:.1f} KB) превысил лимит в 99 KB."} if api_result.get("status") == "ok":
return {
return {"success": True, "size_kb": round(size_kb, 1)} "success": True,
"photo_id": api_result.get("photo_id"),
"verdict": api_result.get("verdict"),
"message": api_result.get("message"),
}
else:
return {
"success": False,
"error": api_result.get("reason", "Неизвестная ошибка API")
}
except Exception as e: except Exception as e:
logger.error(f"Ошибка в photo_processor: {e}") logger.error(f"Ошибка в prepare_ad_photo: {e}")
return {"success": False, "error": str(e)} return {"success": False, "error": str(e)}