From 9612a69f9de863f8d4db788dad193d74dbea2fdf Mon Sep 17 00:00:00 2001 From: shiyu Date: Tue, 17 Jun 2025 16:18:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=A3=81=E5=89=AA?= =?UTF-8?q?=E4=BA=BA=E8=84=B8=E5=9B=BE=E7=89=87=E5=8A=9F=E8=83=BD=EF=BC=8C?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=BA=BA=E8=84=B8=E7=AE=A1=E7=90=86=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E4=BB=A5=E6=94=AF=E6=8C=81=E8=A3=81=E5=89=AA=E8=B7=AF?= =?UTF-8?q?=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Models/DataBase/Face.cs | 2 + .../FaceRecognitionTaskProcessor.cs | 49 +++++- Services/Management/FaceManagementService.cs | 11 +- Web/src/pages/explore/Index.tsx | 151 ++++++++++-------- 4 files changed, 142 insertions(+), 71 deletions(-) diff --git a/Models/DataBase/Face.cs b/Models/DataBase/Face.cs index 376446c..bd214f6 100644 --- a/Models/DataBase/Face.cs +++ b/Models/DataBase/Face.cs @@ -15,6 +15,8 @@ public class Face : BaseModel [Range(0.0, 1.0)] public double FaceConfidence { get; set; } + public string? CroppedImagePath { get; set; } + public int PictureId { get; set; } [ForeignKey("PictureId")] diff --git a/Services/Background/Processors/FaceRecognitionTaskProcessor.cs b/Services/Background/Processors/FaceRecognitionTaskProcessor.cs index feffc6c..7e1233e 100644 --- a/Services/Background/Processors/FaceRecognitionTaskProcessor.cs +++ b/Services/Background/Processors/FaceRecognitionTaskProcessor.cs @@ -4,6 +4,9 @@ using Foxel.Services.Storage; using Microsoft.EntityFrameworkCore; using System.Text.Json; using System.Text.Json.Serialization; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Formats.Jpeg; namespace Foxel.Services.Background.Processors { @@ -162,8 +165,15 @@ namespace Foxel.Services.Background.Processors // 保存人脸数据到数据库 if (faceRecognitionResult?.Result != null && faceRecognitionResult.Result.Any()) { + // 确保人脸保存目录存在 + var faceImagesDir = Path.Combine(Directory.GetCurrentDirectory(), "Uploads", "faces"); + Directory.CreateDirectory(faceImagesDir); + foreach (var faceResult in faceRecognitionResult.Result) { + // 裁剪人脸图片 + var croppedImagePath = await CropAndSaveFaceImageAsync(tempImagePath, faceResult.FacialArea, faceImagesDir); + var face = new Face { PictureId = pictureId, @@ -172,7 +182,8 @@ namespace Foxel.Services.Background.Processors Y = faceResult.FacialArea.Y, W = faceResult.FacialArea.W, H = faceResult.FacialArea.H, - FaceConfidence = faceResult.FaceConfidence + FaceConfidence = faceResult.FaceConfidence, + CroppedImagePath = croppedImagePath }; dbContext.Faces.Add(face); @@ -282,5 +293,41 @@ namespace Foxel.Services.Background.Processors _logger.LogWarning("尝试在 FaceRecognitionProcessor 中更新不存在的任务状态: TaskId={TaskId}", taskId); } } + + private async Task CropAndSaveFaceImageAsync(string originalImagePath, FacialAreaResponse facialArea, string saveDirectory) + { + try + { + using var originalImage = await Image.LoadAsync(originalImagePath); + + // 确保裁剪区域在图片范围内 + var cropX = Math.Max(0, facialArea.X); + var cropY = Math.Max(0, facialArea.Y); + var cropWidth = Math.Min(facialArea.W, originalImage.Width - cropX); + var cropHeight = Math.Min(facialArea.H, originalImage.Height - cropY); + + if (cropWidth <= 0 || cropHeight <= 0) + { + throw new Exception("无效的人脸区域坐标"); + } + + var cropRect = new Rectangle(cropX, cropY, cropWidth, cropHeight); + + // 生成唯一文件名 + var fileName = $"face_{Guid.NewGuid()}.jpg"; + var filePath = Path.Combine(saveDirectory, fileName); + + // 使用 ImageSharp 裁剪并保存 + using var croppedImage = originalImage.Clone(ctx => ctx.Crop(cropRect)); + await croppedImage.SaveAsJpegAsync(filePath, new JpegEncoder { Quality = 90 }); + + return Path.Combine("Uploads", "faces", fileName); + } + catch (Exception ex) + { + _logger.LogError(ex, "裁剪人脸图片失败"); + return string.Empty; + } + } } } diff --git a/Services/Management/FaceManagementService.cs b/Services/Management/FaceManagementService.cs index df491c5..ff91418 100644 --- a/Services/Management/FaceManagementService.cs +++ b/Services/Management/FaceManagementService.cs @@ -4,12 +4,14 @@ using Foxel.Models.Response.Picture; using Foxel.Services.Mapping; using Foxel.Api.Management; using Microsoft.EntityFrameworkCore; +using Foxel.Services.Configuration; namespace Foxel.Services.Management; public class FaceManagementService( IDbContextFactory contextFactory, IMappingService mappingService, + IConfigService configService, ILogger logger) : IFaceManagementService { public async Task> GetFaceClustersAsync(int page = 1, int pageSize = 20) @@ -22,7 +24,7 @@ public class FaceManagementService( { Cluster = c, FaceCount = dbContext.Faces.Count(f => f.ClusterId == c.Id), - ThumbnailPath = dbContext.Faces + ThumbnailPath = configService["AppSettings:ServerUrl"] + dbContext.Faces .Where(f => f.ClusterId == c.Id) .Include(f => f.Picture) .OrderByDescending(f => f.CreatedAt) @@ -195,11 +197,10 @@ public class FaceManagementService( { Cluster = c, FaceCount = dbContext.Faces.Count(f => f.ClusterId == c.Id && f.Picture.UserId == userId), - ThumbnailPath = dbContext.Faces - .Where(f => f.ClusterId == c.Id && f.Picture.UserId == userId) - .Include(f => f.Picture) + ThumbnailPath = configService["AppSettings:ServerUrl"]+ dbContext.Faces + .Where(f => f.ClusterId == c.Id && f.Picture.UserId == userId && !string.IsNullOrEmpty(f.CroppedImagePath)) .OrderByDescending(f => f.CreatedAt) - .Select(f => f.Picture.ThumbnailPath) + .Select(f => f.CroppedImagePath) .FirstOrDefault() }) .Where(x => x.FaceCount > 0) diff --git a/Web/src/pages/explore/Index.tsx b/Web/src/pages/explore/Index.tsx index 7c616a2..6690dd6 100644 --- a/Web/src/pages/explore/Index.tsx +++ b/Web/src/pages/explore/Index.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useCallback } from 'react'; import { Card, Button, Space, Modal, message, Typography, - Row, Col, Image, Form, Input, Avatar, + Row, Col, Image, Form, Input, Avatar, Tag, Tooltip, Spin, Empty, Statistic, Select, Grid } from 'antd'; @@ -40,7 +40,7 @@ const FaceExplore: React.FC = () => { const [targetCluster, setTargetCluster] = useState(null); const [clusterPictures, setClusterPictures] = useState([]); const [picturesLoading, setPicturesLoading] = useState(false); - + const [editForm] = Form.useForm(); const [mergeForm] = Form.useForm(); @@ -118,12 +118,12 @@ const FaceExplore: React.FC = () => { const handleEditOk = async () => { if (!editingCluster) return; - + try { const values = await editForm.validateFields(); setLoading(true); const response = await updateMyCluster(editingCluster.id, values); - + if (response.success) { message.success('更新聚类信息成功'); setIsEditModalVisible(false); @@ -140,12 +140,12 @@ const FaceExplore: React.FC = () => { const handleMergeOk = async () => { if (!targetCluster) return; - + try { const values = await mergeForm.validateFields(); setLoading(true); const response = await mergeMyUserClusters(targetCluster.id, values.sourceClusterId); - + if (response.success) { message.success('合并聚类成功'); setIsMergeModalVisible(false); @@ -189,31 +189,33 @@ const FaceExplore: React.FC = () => { hoverable className="cluster-card" cover={ -
{cluster.thumbnailPath ? ( - {cluster.name} ) : ( - } - style={{ backgroundColor: '#e6f7ff' }} + } + style={{ + backgroundColor: '#e6f7ff', + border: '3px solid #fff', + boxShadow: '0 2px 8px rgba(0,0,0,0.1)' + }} /> )}
@@ -247,8 +249,8 @@ const FaceExplore: React.FC = () => { )} {cluster.description && ( - {cluster.description} @@ -281,16 +283,16 @@ const FaceExplore: React.FC = () => { - -