217 lines
5.9 KiB
Go
217 lines
5.9 KiB
Go
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] + "..."
|
||
}
|