c7cb0f0602
Co-authored-by: jmorganca <jmorganca@gmail.com> Co-authored-by: Michael Yang <mxyng@pm.me> Co-authored-by: Jesse Gross <jesse@ollama.com>
240 lines
6 KiB
Go
240 lines
6 KiB
Go
package imageproc
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
_ "image/jpeg"
|
|
_ "image/png"
|
|
"math"
|
|
"slices"
|
|
|
|
"golang.org/x/image/draw"
|
|
)
|
|
|
|
func GetSupportedAspectRatios(maxTiles int) []image.Point {
|
|
ratios := []image.Point{}
|
|
|
|
for w := range maxTiles {
|
|
for h := range maxTiles {
|
|
if (w+1)*(h+1) <= maxTiles {
|
|
ratios = append(ratios, image.Point{w + 1, h + 1})
|
|
}
|
|
}
|
|
}
|
|
|
|
return ratios
|
|
}
|
|
|
|
func clip(a, a_min, a_max int) int {
|
|
if a < a_min {
|
|
return a_min
|
|
} else if a > a_max {
|
|
return a_max
|
|
}
|
|
|
|
return a
|
|
}
|
|
|
|
func getImageSizeFitToCanvas(imageSize, canvasSize image.Point, tileSize int) image.Point {
|
|
targetWidth := clip(imageSize.X, tileSize, canvasSize.X)
|
|
targetHeight := clip(imageSize.Y, tileSize, canvasSize.Y)
|
|
|
|
scaleWidth := float64(targetWidth) / float64(imageSize.X)
|
|
scaleHeight := float64(targetHeight) / float64(imageSize.Y)
|
|
|
|
var w, h int
|
|
|
|
if scaleWidth < scaleHeight {
|
|
w = targetWidth
|
|
h = min(int(math.Floor(float64(imageSize.Y)*scaleWidth)), targetHeight)
|
|
} else {
|
|
w = min(int(math.Floor(float64(imageSize.X)*scaleHeight)), targetWidth)
|
|
h = targetHeight
|
|
}
|
|
|
|
return image.Point{w, h}
|
|
}
|
|
|
|
func getOptimalTiledCanvas(imageSize image.Point, maxImageTiles, tileSize int) image.Point {
|
|
possibleTileArrangements := GetSupportedAspectRatios(maxImageTiles)
|
|
possibleCanvasSizes := []image.Point{}
|
|
for _, pta := range possibleTileArrangements {
|
|
possibleCanvasSizes = append(possibleCanvasSizes, image.Point{pta.X * tileSize, pta.Y * tileSize})
|
|
}
|
|
|
|
scales := []float64{}
|
|
|
|
for _, pcs := range possibleCanvasSizes {
|
|
scaleHeight := float64(pcs.Y) / float64(imageSize.Y)
|
|
scaleWidth := float64(pcs.X) / float64(imageSize.X)
|
|
|
|
if scaleWidth > scaleHeight {
|
|
scales = append(scales, scaleHeight)
|
|
} else {
|
|
scales = append(scales, scaleWidth)
|
|
}
|
|
}
|
|
|
|
var minUpscale float64
|
|
var maxDownscale float64
|
|
var upscale bool
|
|
|
|
for _, s := range scales {
|
|
if s > 1.0 {
|
|
upscale = true
|
|
if minUpscale == 0 {
|
|
minUpscale = s
|
|
} else {
|
|
minUpscale = math.Min(minUpscale, s)
|
|
}
|
|
} else {
|
|
maxDownscale = math.Max(maxDownscale, s)
|
|
}
|
|
}
|
|
|
|
selectedScale := maxDownscale
|
|
if upscale {
|
|
selectedScale = minUpscale
|
|
}
|
|
|
|
var selectedCanvas image.Point
|
|
for n, pcs := range possibleCanvasSizes {
|
|
if scales[n] == selectedScale {
|
|
// choose the smallest possible canvas
|
|
if selectedCanvas.X == 0 && selectedCanvas.Y == 0 {
|
|
selectedCanvas = pcs
|
|
} else if pcs.X*pcs.Y < selectedCanvas.X*selectedCanvas.Y {
|
|
selectedCanvas = pcs
|
|
}
|
|
}
|
|
}
|
|
return selectedCanvas
|
|
}
|
|
|
|
func splitToTiles(img image.Image, numTilesSize image.Point) []image.Image {
|
|
b := img.Bounds()
|
|
width := b.Max.X - b.Min.X
|
|
height := b.Max.Y - b.Min.Y
|
|
tileHeight := height / numTilesSize.Y
|
|
tileWidth := width / numTilesSize.X
|
|
|
|
images := []image.Image{}
|
|
|
|
for h := range numTilesSize.Y {
|
|
for w := range numTilesSize.X {
|
|
rect := image.Rect(tileWidth*w, tileHeight*h, tileWidth*(w+1), tileHeight*(h+1))
|
|
images = append(images, img.(interface {
|
|
SubImage(image.Rectangle) image.Image
|
|
}).SubImage(rect))
|
|
}
|
|
}
|
|
|
|
return images
|
|
}
|
|
|
|
// remove the "alpha" channel by drawing over a prefilled image
|
|
func compositeImage(img image.Image) image.Image {
|
|
dst := image.NewRGBA(img.Bounds())
|
|
|
|
white := color.RGBA{255, 255, 255, 255}
|
|
draw.Draw(dst, dst.Bounds(), &image.Uniform{white}, image.Point{}, draw.Src)
|
|
draw.Draw(dst, dst.Bounds(), img, img.Bounds().Min, draw.Over)
|
|
|
|
return dst
|
|
}
|
|
|
|
func ResizeImage(img image.Image, format string, outputSize image.Point, maxImageTiles int) (image.Image, image.Point) {
|
|
if format == "png" {
|
|
img = compositeImage(img)
|
|
}
|
|
|
|
b := img.Bounds()
|
|
tileSize := outputSize.Y
|
|
|
|
canvasSize := getOptimalTiledCanvas(b.Max, maxImageTiles, tileSize)
|
|
aspectRatio := image.Point{canvasSize.X / tileSize, canvasSize.Y / tileSize}
|
|
newSize := getImageSizeFitToCanvas(b.Max, canvasSize, tileSize)
|
|
|
|
dst := image.NewRGBA(image.Rect(0, 0, newSize.X, newSize.Y))
|
|
|
|
// scaling choices:
|
|
// NearestNeighbor fast, blocky output
|
|
// ApproxBiLinear fast, medium quality
|
|
// BiLinear slow, high quality
|
|
// CatmullRom very slow, very high quality
|
|
draw.BiLinear.Scale(dst, dst.Rect, img, b, draw.Over, nil)
|
|
|
|
return dst, aspectRatio
|
|
}
|
|
|
|
func PadImage(img image.Image, outputSize, aspectRatio image.Point) image.Image {
|
|
paddedSize := image.Point{
|
|
X: outputSize.X * aspectRatio.X,
|
|
Y: outputSize.Y * aspectRatio.Y,
|
|
}
|
|
|
|
dst := image.NewRGBA(image.Rect(0, 0, paddedSize.X, paddedSize.Y))
|
|
draw.Draw(dst, img.Bounds(), img, image.Point{0, 0}, draw.Over)
|
|
|
|
return dst
|
|
}
|
|
|
|
func PackImages(img image.Image, aspectRatio image.Point, mean, std [3]float32) []float32 {
|
|
subImages := splitToTiles(img, aspectRatio)
|
|
|
|
var pixelVals []float32
|
|
|
|
for _, subImg := range subImages {
|
|
bounds := subImg.Bounds()
|
|
var rVals, gVals, bVals []float32
|
|
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
|
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
|
c := subImg.At(x, y)
|
|
r, g, b, _ := c.RGBA()
|
|
rVal := float32(r>>8) / 255.0
|
|
gVal := float32(g>>8) / 255.0
|
|
bVal := float32(b>>8) / 255.0
|
|
|
|
rVal = (rVal - mean[0]) / std[0]
|
|
gVal = (gVal - mean[1]) / std[1]
|
|
bVal = (bVal - mean[2]) / std[2]
|
|
|
|
rVals = append(rVals, rVal)
|
|
gVals = append(gVals, gVal)
|
|
bVals = append(bVals, bVal)
|
|
}
|
|
}
|
|
pixelVals = append(pixelVals, rVals...)
|
|
pixelVals = append(pixelVals, gVals...)
|
|
pixelVals = append(pixelVals, bVals...)
|
|
}
|
|
|
|
return pixelVals
|
|
}
|
|
|
|
func Preprocess(imageData []byte) ([]float32, int, error) {
|
|
// todo: need guard in here for bad image data
|
|
|
|
// mllama values
|
|
outputSize := image.Point{560, 560}
|
|
maxTiles := 4
|
|
|
|
// clip values
|
|
mean := [3]float32{0.48145466, 0.4578275, 0.40821073}
|
|
std := [3]float32{0.26862954, 0.26130258, 0.27577711}
|
|
|
|
img, format, err := image.Decode(bytes.NewReader(imageData))
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("failed to decode image: %w", err)
|
|
}
|
|
|
|
newImage, aspectRatio := ResizeImage(img, format, outputSize, maxTiles)
|
|
newImage = PadImage(newImage, outputSize, aspectRatio)
|
|
|
|
data := PackImages(newImage, aspectRatio, mean, std)
|
|
aspectRatioIndex := slices.Index(GetSupportedAspectRatios(maxTiles), aspectRatio) + 1
|
|
|
|
return data, aspectRatioIndex, nil
|
|
}
|