topfans/backend/gateway/service/compositor/compose.go
Lenticular Studio Agent 67cf3d4177 chore: 清理 laserCompositor 微服务残留
- 删除已弃用的 compositor_client.go
- 删除激光合成微服务代码
- 添加 gateway 合成控制器和测试文件
- 添加 Dify prompt 补丁脚本

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-23 22:44:03 +08:00

221 lines
6.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package compositor 镭射卡 6 层合成引擎(纯图像处理,无外部依赖)
//
// 从旧 microservice backend/services/laserCompositor 迁入,
// 去掉了 HTTP 服务器/OSS 上传/独立部署等基础设施,仅保留核心合成逻辑。
package compositor
import (
"bytes"
"fmt"
"image"
"image/color"
"image/draw"
"image/png"
"math"
)
// ComposeRequest 合成请求
type ComposeRequest struct {
BackgroundURL string `json:"background_url"`
CutoutURL string `json:"cutout_url"`
OverlayURL string `json:"overlay_url"`
GratingConfig GratingConfig `json:"grating_config"`
ExportWidth int `json:"export_width"`
ExportHeight int `json:"export_height"`
VariantIndex int `json:"variant_index"`
}
func (r *ComposeRequest) CanvasW() int {
if r.ExportWidth <= 0 {
return 450
}
return r.ExportWidth
}
func (r *ComposeRequest) CanvasH() int {
if r.ExportHeight <= 0 {
return 600
}
return r.ExportHeight
}
// Compose 执行 6 层合成,返回 PNG bytes
//
// 图层顺序(从底到顶):
// 1. AI 艺术背景
// 2. 金属反光 (DrawMetallicSheen)
// 3. 彩虹衍射光栅色带 (DrawDiffractionStripes)
// 4. AI 装饰层(如有)
// 5. 光栅 finish胶片颗粒 + 磨砂哑光 (DrawGratingFinish)
// 6. 人物抠图(最顶层)
func Compose(req ComposeRequest) ([]byte, error) {
cw := req.CanvasW()
ch := req.CanvasH()
canvas := image.NewNRGBA(image.Rect(0, 0, cw, ch))
// Layer 1: 背景图(降级到纯色底)
bgLoaded := false
if req.BackgroundURL != "" {
if img, err := DownloadImage(req.BackgroundURL); err == nil {
drawCover(canvas, img, cw, ch)
bgLoaded = true
}
}
if !bgLoaded {
bgColor := HexToColor(req.GratingConfig.BackdropTone)
fillSolid(canvas, bgColor)
}
// Layer 2: 金属反光梯度(在背景之上,作为彩虹光栅的反射底)
DrawMetallicSheen(canvas, req.GratingConfig)
// Layer 3: 彩虹衍射光栅色带(核心镭射效果)
DrawDiffractionStripes(canvas, req.GratingConfig, req.VariantIndex)
// Layer 4: AI 装饰层(如有)— 在人物后面
if req.OverlayURL != "" {
if img, err := DownloadImage(req.OverlayURL); err == nil {
overlay := ExtractAlpha(img)
drawScreenOverlay(canvas, overlay)
}
}
// Layer 5: 光栅 finish胶片颗粒 + 磨砂哑光)
DrawGratingFinish(canvas, req.VariantIndex)
// Layer 6: 人物抠图 — 最上层,清晰可见
if req.CutoutURL != "" {
if img, err := DownloadDecodePNG(req.CutoutURL); err == nil {
blended := drawCutoutOver(canvas, img, cw, ch)
fmt.Printf("[compositor] cutout drawn: src=%dx%d, dest=%dx%d, blended_pixels=%d\n",
img.Bounds().Dx(), img.Bounds().Dy(), cw, ch, blended)
} else {
fmt.Printf("[compositor] cutout download failed: %v, URL_prefix=%s\n",
err, safePrefix(req.CutoutURL, 60))
}
}
var buf bytes.Buffer
if err := png.Encode(&buf, canvas); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func fillSolid(dst *image.NRGBA, c color.NRGBA) {
b := dst.Bounds()
for y := b.Min.Y; y < b.Max.Y; y++ {
for x := b.Min.X; x < b.Max.X; x++ {
dst.SetNRGBA(x, y, c)
}
}
}
// drawCover 缩放并居中绘制图像(用于背景层,不关心 alpha 混合细节)
func drawCover(dst *image.NRGBA, src image.Image, cw, ch int) {
iw := src.Bounds().Dx()
ih := src.Bounds().Dy()
if iw <= 0 || ih <= 0 {
return
}
scale := math.Max(float64(cw)/float64(iw), float64(ch)/float64(ih))
dw := int(float64(iw) * scale)
dh := int(float64(ih) * scale)
dx := (cw - dw) / 2
dy := (ch - dh) / 2
draw.Draw(dst, image.Rect(dx, dy, dx+dw, dy+dh), src, src.Bounds().Min, draw.Over)
}
// drawCutoutOver 将 PNG 抠图叠加到画布上,逐像素手动 NRGBA alpha 混合
// 替代 draw.Draw 避免 Go 标准库对 NRGBA 非预乘 alpha 的潜在混合问题
func drawCutoutOver(dst *image.NRGBA, src image.Image, cw, ch int) int {
iw := src.Bounds().Dx()
ih := src.Bounds().Dy()
if iw <= 0 || ih <= 0 {
return 0
}
scale := math.Max(float64(cw)/float64(iw), float64(ch)/float64(ih))
dw := int(float64(iw) * scale)
dh := int(float64(ih) * scale)
dx := (cw - dw) / 2
dy := (ch - dh) / 2
blended := 0
for sy := 0; sy < ih; sy++ {
for sx := 0; sx < iw; sx++ {
// 按比例映射到目标坐标
tx := dx + int(float64(sx)*float64(dw)/float64(iw))
ty := dy + int(float64(sy)*float64(dh)/float64(ih))
if tx < 0 || tx >= cw || ty < 0 || ty >= ch {
continue
}
sr, sg, sb, sa := src.At(sx, sy).RGBA()
// 归一化到 0-1源 alpha 阈值过滤
srcA := float64(sa) / 65535.0
if srcA < 0.005 {
continue
}
blended++
srcR := float64(sr) / 65535.0
srcG := float64(sg) / 65535.0
srcB := float64(sb) / 65535.0
// 读取目标像素NRGBA 非预乘)
di := dst.PixOffset(tx, ty)
dstR := float64(dst.Pix[di]) / 255.0
dstG := float64(dst.Pix[di+1]) / 255.0
dstB := float64(dst.Pix[di+2]) / 255.0
dstA := float64(dst.Pix[di+3]) / 255.0
// NRGBA over 合成: Co = Cs*As + Cd*Ad*(1-As), Ao = As + Ad*(1-As)
outA := srcA + dstA*(1-srcA)
if outA < 0.002 {
dst.Pix[di] = 0
dst.Pix[di+1] = 0
dst.Pix[di+2] = 0
dst.Pix[di+3] = 0
continue
}
outR := (srcR*srcA + dstR*dstA*(1-srcA)) / outA
outG := (srcG*srcA + dstG*dstA*(1-srcA)) / outA
outB := (srcB*srcA + dstB*dstA*(1-srcA)) / outA
dst.Pix[di] = uint8(clamp01(outR) * 255)
dst.Pix[di+1] = uint8(clamp01(outG) * 255)
dst.Pix[di+2] = uint8(clamp01(outB) * 255)
dst.Pix[di+3] = uint8(clamp01(outA) * 255)
}
}
return blended
}
func drawScreenOverlay(dst *image.NRGBA, overlay *image.NRGBA) {
ob := overlay.Bounds()
db := dst.Bounds()
for y := ob.Min.Y; y < ob.Max.Y; y++ {
for x := ob.Min.X; x < ob.Max.X; x++ {
if x < db.Min.X || x >= db.Max.X || y < db.Min.Y || y >= db.Max.Y {
continue
}
oc := overlay.NRGBAAt(x, y)
if oc.A == 0 {
continue
}
alpha := float64(oc.A) / 255.0
applyScreenPixel(dst, x, y,
float64(oc.R)/255.0,
float64(oc.G)/255.0,
float64(oc.B)/255.0,
alpha,
)
}
}
}
func safePrefix(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n] + "..."
}