Files
silver_pose/v2/internal/pose/preprocess.go
T

88 lines
3.2 KiB
Go

package pose
import (
"fmt"
"math"
)
const letterboxPadding = byte(114)
// PreprocessBGR reproduces the fixed-shape Ultralytics letterbox contract:
// BGR source pixels are resized with bilinear interpolation, padded in 114
// gray, converted to RGB/CHW and normalised to [0, 1].
func PreprocessBGR(frame []byte, width, height, target int) ([]float32, LetterboxTransform, error) {
if width <= 0 || height <= 0 || target <= 0 {
return nil, LetterboxTransform{}, fmt.Errorf("frame width, height and target must be positive")
}
if len(frame) != width*height*3 {
return nil, LetterboxTransform{}, fmt.Errorf("BGR frame length = %d, want %d", len(frame), width*height*3)
}
scale := math.Min(float64(target)/float64(width), float64(target)/float64(height))
resizedWidth := int(math.Round(float64(width) * scale))
resizedHeight := int(math.Round(float64(height) * scale))
padWidth := target - resizedWidth
padHeight := target - resizedHeight
padLeft := int(math.Round(float64(padWidth)/2.0 - 0.1))
padTop := int(math.Round(float64(padHeight)/2.0 - 0.1))
transform := LetterboxTransform{
OriginalWidth: width, OriginalHeight: height, InputSize: target,
ResizedWidth: resizedWidth, ResizedHeight: resizedHeight,
PadLeft: padLeft, PadTop: padTop, Scale: float32(scale),
}
plane := target * target
input := make([]float32, 3*plane)
padding := float32(letterboxPadding) / 255.0
for index := range input {
input[index] = padding
}
for y := 0; y < resizedHeight; y++ {
sourceY := clampFloat((float64(y)+0.5)*float64(height)/float64(resizedHeight)-0.5, 0, float64(height-1))
y0 := int(math.Floor(sourceY))
y1 := minInt(y0+1, height-1)
yWeight := float32(sourceY - float64(y0))
for x := 0; x < resizedWidth; x++ {
sourceX := clampFloat((float64(x)+0.5)*float64(width)/float64(resizedWidth)-0.5, 0, float64(width-1))
x0 := int(math.Floor(sourceX))
x1 := minInt(x0+1, width-1)
xWeight := float32(sourceX - float64(x0))
leftTop := (y0*width + x0) * 3
rightTop := (y0*width + x1) * 3
leftBottom := (y1*width + x0) * 3
rightBottom := (y1*width + x1) * 3
destination := (padTop+y)*target + padLeft + x
for sourceChannel := 0; sourceChannel < 3; sourceChannel++ {
value := bilinear(
frame[leftTop+sourceChannel], frame[rightTop+sourceChannel],
frame[leftBottom+sourceChannel], frame[rightBottom+sourceChannel],
xWeight, yWeight,
)
// BGR source maps to RGB tensor planes.
tensorChannel := 2 - sourceChannel
// cv2.resize writes uint8 pixels before Ultralytics converts the
// image to float; round here to retain that observable contract.
input[tensorChannel*plane+destination] = float32(math.Round(float64(value))) / 255.0
}
}
}
return input, transform, nil
}
func bilinear(topLeft, topRight, bottomLeft, bottomRight byte, xWeight, yWeight float32) float32 {
top := float32(topLeft)*(1-xWeight) + float32(topRight)*xWeight
bottom := float32(bottomLeft)*(1-xWeight) + float32(bottomRight)*xWeight
return top*(1-yWeight) + bottom*yWeight
}
func clampFloat(value, minimum, maximum float64) float64 {
return math.Max(minimum, math.Min(maximum, value))
}
func minInt(left, right int) int {
if left < right {
return left
}
return right
}