topfans/backend/services/laserCompositor/compositor/engine.go
2026-06-03 22:19:22 +08:00

217 lines
5.9 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
import (
"bytes"
"fmt"
"image"
"image/color"
"image/draw"
"image/png"
"math"
"github.com/topfans/laserCompositor/loader"
)
// 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"`
OutputOSSKey string `json:"output_oss_key"`
}
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. 金属反光 → 3. 光栅色带 → 4. AI 装饰 → 5. 磨砂 → 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 bgImg, err := loader.LoadImage(req.BackgroundURL); err == nil {
drawCover(canvas, bgImg, cw, ch)
bgLoaded = true
}
}
if !bgLoaded {
bgColor := HexToColor(req.GratingConfig.BackdropTone)
fillSolid(canvas, bgColor)
}
// Layer 1.5: 金属反光梯度
DrawMetallicSheen(canvas, req.GratingConfig)
// Layer 3: 彩虹光栅色带
DrawDiffractionStripes(canvas, req.GratingConfig, req.VariantIndex)
// Layer 4: AI 装饰层screen 混合,白底转透明)
if req.OverlayURL != "" {
if img, err := loader.LoadImage(req.OverlayURL); err == nil {
overlay := loader.ExtractAlpha(img)
drawScreenOverlay(canvas, overlay)
}
}
// Layer 5: 磨砂 finish
DrawGratingFinish(canvas, req.VariantIndex)
// Layer 6 (最顶层): 人物抠图 — 在所有效果之上,确保人像不被遮挡
if req.CutoutURL != "" {
if img, err := loader.LoadDecodePNG(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))
}
} else {
fmt.Println("[compositor] WARNING: CutoutURL is empty, person will not be overlaid")
}
// 编码为 PNG
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 的潜在混合问题
// 返回实际混合的像素数alpha > 0.005
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] + "..."
}