feat: add facepp
CI / changes (push) Successful in 1m31s
CI / ad (push) Successful in 26s
CI / admin (push) Successful in 17s
CI / analysis (push) Successful in 24s
CI / bff (push) Successful in 16s
CI / chore (push) Successful in 14s
CI / equipment (push) Successful in 15s
CI / express (push) Successful in 13s
CI / factory (push) Successful in 13s
CI / message (push) Successful in 10s
CI / order (push) Successful in 10s
CI / payment (push) Successful in 11s
CI / percutaneous (push) Successful in 11s
CI / product (push) Successful in 12s
CI / record (push) Successful in 12s
CI / sale (push) Successful in 11s
CI / store (push) Successful in 11s
CI / support (push) Successful in 11s
CI / task (push) Successful in 10s
CI / user (push) Successful in 18s
CI / water (push) Successful in 13s
CI / wecom (push) Successful in 13s
CI / ws (push) Successful in 6s

This commit is contained in:
zzw
2026-10-27 17:17:43 +08:00
parent e1f28df1e1
commit 747ab25325
12 changed files with 1133 additions and 5 deletions
@@ -0,0 +1,249 @@
package callback
import (
"encoding/json"
"errors"
"fmt"
"image/color"
"math/rand"
"os"
"path/filepath"
"strings"
"time"
"lone-services/pkg/facepp"
"lone-services/pkg/modelbase"
pkgoss "lone-services/pkg/oss"
"lone-services/pkg/utils"
"lone-services/services/analysis/internal/dao"
"lone-services/services/analysis/internal/model"
"github.com/zeromicro/go-zero/core/logx"
)
var faceppMapStroke = map[string]color.RGBA{
dao.FaceppMapTypeAcne: {R: 0xFE, G: 0x01, B: 0x00, A: 0xFF},
dao.FaceppMapTypeClosedComedones: {R: 0x00, G: 0xFF, B: 0x00, A: 0xFF},
dao.FaceppMapTypeBrownSpot: {R: 0x7E, G: 0x2A, B: 0x28, A: 0xFF},
dao.FaceppMapTypeMole: {R: 0x00, G: 0x66, B: 0xFF, A: 0xFF},
}
func RunFacepp(taskID, finalDir string) {
defer func() {
if r := recover(); r != nil {
logx.Errorf("facepp panic task_id=%s: %v", taskID, r)
}
}()
originalPath := filepath.Join(finalDir, "cross_mid.jpg")
if _, err := os.Stat(originalPath); err != nil {
if os.IsNotExist(err) {
logx.Infof("skip facepp: cross_mid.jpg not found in %s", finalDir)
return
}
logx.Errorf("stat cross_mid.jpg failed: %v", err)
return
}
expandPath, err := expandImageToTemp(originalPath, defaultExpandRatio)
if err != nil {
logx.Errorf("expand image failed: %v", err)
return
}
defer os.Remove(expandPath)
trainPath, err := saveFaceppTrainImage(expandPath)
if err != nil {
logx.Errorf("save facepp train image failed: %v", err)
return
}
client := facepp.NewClient(
utils.GetConfigString("facepp.api_key"),
utils.GetConfigString("facepp.api_secret"),
)
resp, analyzeErr := client.SkinAnalyzeFromFile(expandPath, facepp.SkinAnalyzeOptions{
ReturnMarks: facepp.DefaultReturnMarks,
})
resultPath := utils.StringEmpty
resultWeb := utils.StringEmpty
leftWeb := utils.StringEmpty
rightWeb := utils.StringEmpty
if analyzeErr != nil {
logx.Errorf("facepp skin analyze failed: %v", analyzeErr)
} else if resp != nil {
var annErr error
resultPath, resultWeb, annErr = saveFaceppTrainAnnotations(trainPath, resp.Result)
if annErr != nil {
logx.Errorf("save facepp train annotations failed: %v", annErr)
}
base := strings.TrimSuffix(filepath.Base(trainPath), filepath.Ext(trainPath))
_, leftWeb, err = saveFaceppJSON(base, "_left", resp.LeftSideResult)
if err != nil {
logx.Errorf("save facepp left json failed: %v", err)
}
_, rightWeb, err = saveFaceppJSON(base, "_right", resp.RightSideResult)
if err != nil {
logx.Errorf("save facepp right json failed: %v", err)
}
if mapErr := saveFaceppMaps(taskID, expandPath, resp.Result); mapErr != nil {
logx.Errorf("save facepp maps failed: %v", mapErr)
}
}
if err = saveFaceppRecord(taskID, originalPath, trainPath, resultPath, resultWeb, leftWeb, rightWeb, resp, analyzeErr); err != nil {
logx.Errorf("save facepp record failed: %v", err)
}
}
func saveFaceppRecord(taskID, originalPath, expandPath, resultPath, resultWeb, leftWeb, rightWeb string, resp *facepp.SkinAnalyzeResponse, analyzeErr error) error {
ossResultPath := utils.StringEmpty
if resultPath != utils.StringEmpty {
uploaded, err := uploadJSONToOSS(resultPath)
if err != nil {
logx.Errorf("upload facepp json to oss failed: %v", err)
} else {
ossResultPath = uploaded
}
}
record := dao.FaceppAnalysis{
TaskId: taskID,
OriginalPath: originalPath,
ExpandPath: expandPath,
Result: resultWeb,
LeftSideResult: leftWeb,
RightSideResult: rightWeb,
ResultPath: resultPath,
ResultOssPath: ossResultPath,
CreatedAt: utils.Now(),
}
if resp != nil {
record.RequestId = resp.RequestID
record.FaceTop = resp.FaceRectangle.Top
record.FaceLeft = resp.FaceRectangle.Left
record.FaceWidth = resp.FaceRectangle.Width
record.FaceHeight = resp.FaceRectangle.Height
record.TimeUsed = resp.TimeUsed
record.ErrorMessage = resp.ErrorMessage
}
if record.ErrorMessage == utils.StringEmpty && analyzeErr != nil {
record.ErrorMessage = analyzeErr.Error()
}
m := model.FaceppAnalysisModel{}.Init()
return m.Create(&record)
}
func uploadJSONToOSS(localPath string) (string, error) {
data, err := os.ReadFile(localPath)
if err != nil {
return utils.StringEmpty, err
}
fileDir := utils.GetConfigString("oss.fileDir")
fileName := fmt.Sprintf("%d%d.json", time.Now().UnixNano(), rand.Intn(10000))
res, err := pkgoss.Upload(pkgoss.UploadInput{
Filename: fileName,
Content: data,
ContentType: "application/json",
Dir: fileDir,
})
if err != nil {
return utils.StringEmpty, err
}
return res.URL, nil
}
func saveFaceppMaps(taskID, imagePath string, result json.RawMessage) error {
if imagePath == utils.StringEmpty {
return errors.New("facepp map image path empty")
}
now := utils.Now()
for _, meta := range dao.FaceppMapTypeMeta {
keys := facepp.MapTypeKeys[meta.Type]
pixelBoxes, err := facepp.ExtractPixelBoxes(result, keys...)
if err != nil {
return fmt.Errorf("extract %s boxes: %w", meta.Type, err)
}
tmpFile, err := os.CreateTemp("", "facepp-map-*.jpg")
if err != nil {
return err
}
tmpPath := tmpFile.Name()
_ = tmpFile.Close()
stroke := faceppMapStroke[meta.Type]
imgW, imgH, err := facepp.DrawBoxesOnFile(imagePath, tmpPath, pixelBoxes, stroke, 3)
if err != nil {
_ = os.Remove(tmpPath)
return fmt.Errorf("draw %s: %w", meta.Type, err)
}
ossURL, uploadErr := uploadFaceppImageToOSS(tmpPath)
_ = os.Remove(tmpPath)
if uploadErr != nil {
return fmt.Errorf("upload %s: %w", meta.Type, uploadErr)
}
if err = upsertFaceppMapImage(dao.FaceppMap{
TaskId: taskID,
Type: meta.Type,
Path: ossURL,
ImageWidth: imgW,
ImageHeight: imgH,
CreatedAt: now,
UpdatedAt: now,
}); err != nil {
return err
}
}
return nil
}
func upsertFaceppMapImage(record dao.FaceppMap) error {
m := model.FaceppMapModel{}.Init()
var existing dao.FaceppMap
w := modelbase.Params{
Eq: map[string]string{
"task_id": record.TaskId,
"type": record.Type,
},
}
if err := m.GetOne(w, &existing); err != nil {
return err
}
if existing.Id > utils.NumberZero {
_, err := m.Edit(w, map[string]interface{}{
"path": record.Path,
"image_width": record.ImageWidth,
"image_height": record.ImageHeight,
"updated_at": record.UpdatedAt,
})
return err
}
return m.Create(&record)
}
func uploadFaceppImageToOSS(localPath string) (string, error) {
data, err := os.ReadFile(localPath)
if err != nil {
return utils.StringEmpty, err
}
imageDir := utils.GetConfigString("oss.imageDir")
fileName := fmt.Sprintf("%d%d.jpg", time.Now().UnixNano(), rand.Intn(10000))
res, err := pkgoss.Upload(pkgoss.UploadInput{
Filename: fileName,
Content: data,
ContentType: "image/jpeg",
Dir: imageDir,
})
if err != nil {
return utils.StringEmpty, err
}
return res.URL, nil
}
@@ -0,0 +1,168 @@
package callback
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"image"
"image/color"
_ "image/jpeg"
_ "image/png"
"io"
"math/rand"
"os"
"path/filepath"
"strings"
"time"
"lone-services/pkg/facepp"
"lone-services/pkg/utils"
"github.com/disintegration/imaging"
)
const defaultExpandRatio = 0.15
func expandImageToTemp(srcPath string, ratio float64) (string, error) {
if ratio <= 0 {
ratio = defaultExpandRatio
}
img, err := imaging.Open(srcPath)
if err != nil {
return utils.StringEmpty, err
}
img = imaging.Rotate90(img)
w := img.Bounds().Dx()
h := img.Bounds().Dy()
newW := int(float64(w) * (1 + ratio))
newH := int(float64(h) * (1 + ratio))
canvas := imaging.New(newW, newH, color.Black)
out := imaging.PasteCenter(canvas, img)
ext := strings.ToLower(filepath.Ext(srcPath))
if ext == utils.StringEmpty {
ext = ".jpg"
}
tmpFile, err := os.CreateTemp("", "expand-*"+ext)
if err != nil {
return utils.StringEmpty, err
}
tmpPath := tmpFile.Name()
if err = tmpFile.Close(); err != nil {
_ = os.Remove(tmpPath)
return utils.StringEmpty, err
}
if err = imaging.Save(out, tmpPath); err != nil {
_ = os.Remove(tmpPath)
return utils.StringEmpty, err
}
return tmpPath, nil
}
func saveFaceppTrainImage(srcPath string) (string, error) {
saveDir := utils.GetConfigString("facepp.train_image_path")
if saveDir == utils.StringEmpty {
saveDir = "/data/facepp/train/images"
}
if err := os.MkdirAll(saveDir, os.ModePerm); err != nil {
return utils.StringEmpty, err
}
filename := fmt.Sprintf("%d_%d.jpg", time.Now().UnixMilli(), rand.Intn(1000000))
dstPath := filepath.Join(saveDir, filename)
if err := copyLocalFile(srcPath, dstPath); err != nil {
return utils.StringEmpty, err
}
return dstPath, nil
}
func saveFaceppTrainAnnotations(trainPath string, result json.RawMessage) (string, string, error) {
if trainPath == utils.StringEmpty {
return utils.StringEmpty, utils.StringEmpty, errors.New("train image path is empty")
}
base := strings.TrimSuffix(filepath.Base(trainPath), filepath.Ext(trainPath))
origPath, webPath, err := saveFaceppJSON(base, utils.StringEmpty, result)
if err != nil {
return utils.StringEmpty, utils.StringEmpty, err
}
labelDir := utils.GetConfigString("facepp.train_label_path")
if labelDir == utils.StringEmpty {
labelDir = "/data/facepp/train/labels"
}
if err = os.MkdirAll(labelDir, os.ModePerm); err != nil {
return origPath, webPath, err
}
imgW, imgH, err := imageSize(trainPath)
if err != nil {
return origPath, webPath, err
}
yolo, err := facepp.ResultToYOLO(result, imgW, imgH)
if err != nil {
return origPath, webPath, err
}
if err = os.WriteFile(filepath.Join(labelDir, base+".txt"), []byte(yolo), 0644); err != nil {
return origPath, webPath, err
}
return origPath, webPath, nil
}
func saveFaceppJSON(base, suffix string, raw json.RawMessage) (string, string, error) {
if len(raw) == 0 || string(raw) == "null" {
return utils.StringEmpty, utils.StringEmpty, nil
}
jsonDir := utils.GetConfigString("facepp.train_json_path")
if jsonDir == utils.StringEmpty {
jsonDir = "/data/facepp/train/json"
}
if err := os.MkdirAll(jsonDir, os.ModePerm); err != nil {
return utils.StringEmpty, utils.StringEmpty, err
}
origPath := filepath.Join(jsonDir, base+suffix+".json")
if err := writePrettyJSON(origPath, raw); err != nil {
return utils.StringEmpty, utils.StringEmpty, err
}
return origPath, filepath.Base(origPath), nil
}
func writePrettyJSON(path string, raw json.RawMessage) error {
if len(raw) == 0 || string(raw) == "null" {
raw = json.RawMessage("{}")
}
var buf bytes.Buffer
if err := json.Indent(&buf, raw, "", " "); err != nil {
buf.Reset()
buf.Write(raw)
}
return os.WriteFile(path, buf.Bytes(), 0644)
}
func imageSize(path string) (int, int, error) {
f, err := os.Open(path)
if err != nil {
return 0, 0, err
}
defer f.Close()
cfg, _, err := image.DecodeConfig(f)
if err != nil {
return 0, 0, err
}
return cfg.Width, cfg.Height, nil
}
func copyLocalFile(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, in)
return err
}
@@ -55,8 +55,8 @@ func ProcessUpload(ctx context.Context, payload analysisoss.CallbackPayload) err
logx.WithContext(ctx).Errorf("cache uploaded lights failed: %v", err)
}
if utils.GetConfigInt("switch.facepp") == utils.NumberOne {
logx.WithContext(ctx).Infof("facepp skip task_id=%s", payload.TaskId)
if utils.GetConfigInt("switch.facepp") == utils.NumberOne && coverPath != utils.StringEmpty {
go RunFacepp(payload.TaskId, pathRes.FinalDir)
}
if utils.GetConfigInt("switch.analysis") == utils.NumberOne {
+17 -3
View File
@@ -23,9 +23,23 @@ var FaceppMapTypeMeta = []FaceppMapMeta{
}
type FaceppAnalysis struct {
Id int64 `gorm:"column:id;primaryKey" json:"id"`
TaskId string `gorm:"column:task_id" json:"task_id"`
ResultOssPath string `gorm:"column:result_oss_path" json:"result_oss_path"`
Id int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
TaskId string `gorm:"column:task_id" json:"task_id"`
OriginalPath string `gorm:"column:original_path" json:"original_path"`
ExpandPath string `gorm:"column:expand_path" json:"expand_path"`
RequestId string `gorm:"column:request_id" json:"request_id"`
Result string `gorm:"column:result" json:"result"`
LeftSideResult string `gorm:"column:left_side_result" json:"left_side_result"`
RightSideResult string `gorm:"column:right_side_result" json:"right_side_result"`
ResultPath string `gorm:"column:result_path" json:"result_path"`
ResultOssPath string `gorm:"column:result_oss_path" json:"result_oss_path"`
FaceTop int `gorm:"column:face_top" json:"face_top"`
FaceLeft int `gorm:"column:face_left" json:"face_left"`
FaceWidth int `gorm:"column:face_width" json:"face_width"`
FaceHeight int `gorm:"column:face_height" json:"face_height"`
TimeUsed int `gorm:"column:time_used" json:"time_used"`
ErrorMessage string `gorm:"column:error_message" json:"error_message"`
CreatedAt utils.CustomTime `gorm:"column:created_at" json:"created_at"`
}
type FaceppMap struct {