You've already forked VptPassiveAdapter
feat(api): 添加图像处理配置和服务器端图像处理功能
- 新增 CompressionConfig、ThumbnailConfig 和 ImageProcessingConfig 结构体用于图像处理配置 - 实现 GetEffectiveConfig 方法提供图像处理配置的默认值 - 在 UploadConfig 中添加 ImageProcessing 字段传递服务器配置 - 移除客户端本地缩略图生成功能,改用服务器端处理 - 添加 UploadFaceDataWithProcessing 函数实现带图像处理的上传流程 - 实现 configToImageOptions 函数将服务器配置转换为图像处理选项 - 在 util/image.go 中添加完整的图像处理功能,支持裁切和缩放模式 - 更新依赖添加 golang.org/x/image 用于高质量图像缩放 - 添加 .claude 到 .gitignore 文件
This commit is contained in:
@@ -69,12 +69,64 @@ func ProxyRequest(ctx context.Context, method, path string, body io.Reader, head
|
||||
|
||||
// Upload Proxy Structures
|
||||
|
||||
// CompressionConfig 压缩配置
|
||||
type CompressionConfig struct {
|
||||
Enabled bool `json:"enabled"` // 是否启用压缩
|
||||
Quality int `json:"quality"` // 压缩质量 (1-100)
|
||||
}
|
||||
|
||||
// ThumbnailConfig 缩略图配置
|
||||
type ThumbnailConfig struct {
|
||||
Mode string `json:"mode"` // 缩略图模式: "cut"(裁切) 或 "shrink"(缩放)
|
||||
ShrinkRatio float64 `json:"shrinkRatio"` // 缩放/裁切比例 (0-1)
|
||||
Quality int `json:"quality"` // 缩略图压缩质量 (1-100)
|
||||
}
|
||||
|
||||
// ImageProcessingConfig 图像处理配置
|
||||
type ImageProcessingConfig struct {
|
||||
SourceCompression *CompressionConfig `json:"sourceCompression,omitempty"` // 原图压缩配置
|
||||
Thumbnail *ThumbnailConfig `json:"thumbnail,omitempty"` // 缩略图配置
|
||||
}
|
||||
|
||||
// GetEffectiveConfig 返回有效的图像处理配置(应用默认值)
|
||||
// 默认值:原图不压缩;缩略图裁切模式,比例0.5,质量75
|
||||
func (c *ImageProcessingConfig) GetEffectiveConfig() *ImageProcessingConfig {
|
||||
effective := &ImageProcessingConfig{
|
||||
SourceCompression: &CompressionConfig{
|
||||
Enabled: false, // 默认不压缩原图
|
||||
Quality: 100,
|
||||
},
|
||||
Thumbnail: &ThumbnailConfig{
|
||||
Mode: "cut", // 默认裁切模式
|
||||
ShrinkRatio: 0.5, // 默认比例0.5
|
||||
Quality: 75, // 默认质量75
|
||||
},
|
||||
}
|
||||
|
||||
if c == nil {
|
||||
return effective
|
||||
}
|
||||
|
||||
// 覆盖原图压缩配置
|
||||
if c.SourceCompression != nil {
|
||||
effective.SourceCompression = c.SourceCompression
|
||||
}
|
||||
|
||||
// 覆盖缩略图配置
|
||||
if c.Thumbnail != nil {
|
||||
effective.Thumbnail = c.Thumbnail
|
||||
}
|
||||
|
||||
return effective
|
||||
}
|
||||
|
||||
type UploadConfig struct {
|
||||
TaskID int64 `json:"taskId"`
|
||||
FaceUploadURL string `json:"faceUploadUrl"`
|
||||
ThumbnailUploadURL string `json:"thumbnailUploadUrl"`
|
||||
SourceUploadURL string `json:"sourceUploadUrl"`
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
TaskID int64 `json:"taskId"`
|
||||
FaceUploadURL string `json:"faceUploadUrl"`
|
||||
ThumbnailUploadURL string `json:"thumbnailUploadUrl"`
|
||||
SourceUploadURL string `json:"sourceUploadUrl"`
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
ImageProcessing *ImageProcessingConfig `json:"imageProcessing,omitempty"` // 图像处理配置
|
||||
}
|
||||
|
||||
type ProxyResponse struct {
|
||||
|
||||
@@ -3,7 +3,6 @@ package api
|
||||
import (
|
||||
"ZhenTuLocalPassiveAdapter/config"
|
||||
"ZhenTuLocalPassiveAdapter/logger"
|
||||
"ZhenTuLocalPassiveAdapter/util"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
@@ -177,32 +176,21 @@ func HandleUploadFaces(c *gin.Context) {
|
||||
pos.RightBtmY = *face.RightBtmY
|
||||
}
|
||||
|
||||
// 3. Generate Thumbnail (if Source Image and Coordinates exist)
|
||||
var thumbImg []byte
|
||||
if len(srcImg) > 0 && pos.LeftTopX > 0 && pos.LeftTopY > 0 { // Basic validation
|
||||
// Use Source Image to generate thumbnail
|
||||
// The thumbnail is 1/2 original size, centered on face
|
||||
var err error
|
||||
thumbImg, err = util.GenerateThumbnailFromCoords(srcImg, pos.LeftTopX, pos.LeftTopY, pos.RightBtmX, pos.RightBtmY)
|
||||
if err != nil {
|
||||
logger.Error("Failed to generate thumbnail", zap.String("faceId", face.FaceID), zap.Error(err))
|
||||
// Continue without thumbnail? or fail?
|
||||
// Usually continue, but log error.
|
||||
}
|
||||
}
|
||||
// 3. Construct Face Rectangle for image processing
|
||||
faceRect := [4]int{pos.LeftTopX, pos.LeftTopY, pos.RightBtmX, pos.RightBtmY}
|
||||
|
||||
// 4. Execute Upload Flow
|
||||
// 4. Execute Upload Flow with Server-side Image Processing Configuration
|
||||
scenicId := config.Config.Viid.ScenicId
|
||||
|
||||
go func(fID, dID string, fData, tData, sData []byte, p FacePositionInfo) {
|
||||
go func(fID, dID string, fData, sData []byte, rect [4]int, p FacePositionInfo) {
|
||||
// Create a detached context with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
if err := UploadFaceData(ctx, scenicId, dID, fData, tData, sData, p); err != nil {
|
||||
if err := UploadFaceDataWithProcessing(ctx, scenicId, dID, fData, sData, rect, p); err != nil {
|
||||
logger.Error("Failed to process face upload", zap.String("faceId", fID), zap.Error(err))
|
||||
}
|
||||
}(face.FaceID, deviceID, faceImg, thumbImg, srcImg, pos)
|
||||
}(face.FaceID, deviceID, faceImg, srcImg, faceRect, pos)
|
||||
}
|
||||
|
||||
// Respond Success
|
||||
|
||||
@@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"ZhenTuLocalPassiveAdapter/logger"
|
||||
"ZhenTuLocalPassiveAdapter/util"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
@@ -9,9 +10,109 @@ import (
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
// UploadFaceData implements the coordinated upload flow: Get Config -> Upload -> Notify
|
||||
// UploadFaceDataWithProcessing 带图像处理的上传流程
|
||||
// 根据服务器返回的配置处理图像后上传
|
||||
func UploadFaceDataWithProcessing(ctx context.Context, scenicId int64, deviceNo string, faceImg, srcImg []byte, faceRect [4]int, facePos FacePositionInfo) error {
|
||||
// 1. 获取上传配置(包含图像处理配置)
|
||||
uploadConfig, err := GetUploadConfig(ctx, scenicId, deviceNo)
|
||||
if err != nil {
|
||||
logger.Error("获取上传配置失败", zap.String("deviceNo", deviceNo), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info("获取到VIID任务ID",
|
||||
zap.Int64("taskId", uploadConfig.TaskID),
|
||||
zap.Bool("hasImageProcessing", uploadConfig.ImageProcessing != nil))
|
||||
|
||||
// 2. 将服务器配置转换为图像处理选项
|
||||
imgOpts := configToImageOptions(uploadConfig.ImageProcessing)
|
||||
|
||||
// 3. 处理图像
|
||||
processedImages, err := util.ProcessImages(faceImg, srcImg, faceRect, imgOpts)
|
||||
if err != nil {
|
||||
SubmitFailure(ctx, uploadConfig.TaskID, "IMAGE_PROCESSING_FAILED", err.Error())
|
||||
logger.Error("图像处理失败", zap.Int64("taskId", uploadConfig.TaskID), zap.Error(err))
|
||||
return fmt.Errorf("image processing failed: %w", err)
|
||||
}
|
||||
|
||||
logger.Debug("图像处理完成",
|
||||
zap.Int64("taskId", uploadConfig.TaskID),
|
||||
zap.Int("faceSize", len(processedImages.FaceData)),
|
||||
zap.Int("thumbSize", len(processedImages.ThumbData)),
|
||||
zap.Int("sourceSize", len(processedImages.SourceData)))
|
||||
|
||||
// 4. 并行上传到OSS
|
||||
g, subCtx := errgroup.WithContext(ctx)
|
||||
|
||||
// 上传人脸图
|
||||
g.Go(func() error {
|
||||
if len(processedImages.FaceData) > 0 {
|
||||
if err := UploadFileToOSS(subCtx, uploadConfig.FaceUploadURL, processedImages.FaceData, "image/jpeg"); err != nil {
|
||||
return fmt.Errorf("upload face image failed: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// 上传缩略图
|
||||
g.Go(func() error {
|
||||
if len(processedImages.ThumbData) > 0 {
|
||||
if err := UploadFileToOSS(subCtx, uploadConfig.ThumbnailUploadURL, processedImages.ThumbData, "image/jpeg"); err != nil {
|
||||
return fmt.Errorf("upload thumbnail image failed: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// 上传原图
|
||||
g.Go(func() error {
|
||||
if len(processedImages.SourceData) > 0 {
|
||||
if err := UploadFileToOSS(subCtx, uploadConfig.SourceUploadURL, processedImages.SourceData, "image/jpeg"); err != nil {
|
||||
return fmt.Errorf("upload source image failed: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
SubmitFailure(ctx, uploadConfig.TaskID, "UPLOAD_FAILED", err.Error())
|
||||
logger.Error("文件上传失败", zap.Int64("taskId", uploadConfig.TaskID), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 5. 提交结果
|
||||
if err := SubmitResult(ctx, uploadConfig.TaskID, facePos); err != nil {
|
||||
logger.Error("提交结果失败", zap.Int64("taskId", uploadConfig.TaskID), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info("VIID任务完成", zap.Int64("taskId", uploadConfig.TaskID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// configToImageOptions 将服务器配置转换为图像处理选项
|
||||
func configToImageOptions(config *ImageProcessingConfig) *util.ImageProcessingOptions {
|
||||
// 应用默认值
|
||||
effectiveConfig := config.GetEffectiveConfig()
|
||||
|
||||
opts := &util.ImageProcessingOptions{
|
||||
// 原图压缩配置
|
||||
SourceCompressionEnabled: effectiveConfig.SourceCompression.Enabled,
|
||||
SourceQuality: effectiveConfig.SourceCompression.Quality,
|
||||
|
||||
// 缩略图配置
|
||||
ThumbnailMode: effectiveConfig.Thumbnail.Mode,
|
||||
ThumbnailShrinkRatio: effectiveConfig.Thumbnail.ShrinkRatio,
|
||||
ThumbnailQuality: effectiveConfig.Thumbnail.Quality,
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
// UploadFaceData 已有的上传流程(向后兼容)
|
||||
// 已废弃:请使用 UploadFaceDataWithProcessing
|
||||
func UploadFaceData(ctx context.Context, scenicId int64, deviceNo string, faceImg, thumbImg, srcImg []byte, facePos FacePositionInfo) error {
|
||||
// 1. Get Upload Config
|
||||
// 1. 获取上传配置
|
||||
uploadConfig, err := GetUploadConfig(ctx, scenicId, deviceNo)
|
||||
if err != nil {
|
||||
logger.Error("获取上传配置失败", zap.String("deviceNo", deviceNo), zap.Error(err))
|
||||
@@ -20,10 +121,10 @@ func UploadFaceData(ctx context.Context, scenicId int64, deviceNo string, faceIm
|
||||
|
||||
logger.Info("获取到VIID任务ID", zap.Int64("taskId", uploadConfig.TaskID))
|
||||
|
||||
// 2. Parallel Upload to OSS
|
||||
// 2. 并行上传到OSS
|
||||
g, subCtx := errgroup.WithContext(ctx)
|
||||
|
||||
// Upload Face Image
|
||||
// 上传人脸图
|
||||
g.Go(func() error {
|
||||
if len(faceImg) > 0 {
|
||||
if err := UploadFileToOSS(subCtx, uploadConfig.FaceUploadURL, faceImg, "image/jpeg"); err != nil {
|
||||
@@ -33,7 +134,7 @@ func UploadFaceData(ctx context.Context, scenicId int64, deviceNo string, faceIm
|
||||
return nil
|
||||
})
|
||||
|
||||
// Upload Thumbnail Image
|
||||
// 上传缩略图
|
||||
g.Go(func() error {
|
||||
if len(thumbImg) > 0 {
|
||||
if err := UploadFileToOSS(subCtx, uploadConfig.ThumbnailUploadURL, thumbImg, "image/jpeg"); err != nil {
|
||||
@@ -43,7 +144,7 @@ func UploadFaceData(ctx context.Context, scenicId int64, deviceNo string, faceIm
|
||||
return nil
|
||||
})
|
||||
|
||||
// Upload Source Image
|
||||
// 上传原图
|
||||
g.Go(func() error {
|
||||
if len(srcImg) > 0 {
|
||||
if err := UploadFileToOSS(subCtx, uploadConfig.SourceUploadURL, srcImg, "image/jpeg"); err != nil {
|
||||
@@ -54,13 +155,12 @@ func UploadFaceData(ctx context.Context, scenicId int64, deviceNo string, faceIm
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
// Report failure
|
||||
SubmitFailure(ctx, uploadConfig.TaskID, "UPLOAD_FAILED", err.Error())
|
||||
logger.Error("文件上传失败", zap.Int64("taskId", uploadConfig.TaskID), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 3. Submit Result
|
||||
// 3. 提交结果
|
||||
if err := SubmitResult(ctx, uploadConfig.TaskID, facePos); err != nil {
|
||||
logger.Error("提交结果失败", zap.Int64("taskId", uploadConfig.TaskID), zap.Error(err))
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user