Files
mediadevices/pkg/io/video/scale.go
2020-10-29 00:04:12 -07:00

195 lines
5.4 KiB
Go

package video
import (
"errors"
"image"
"golang.org/x/image/draw"
)
var (
scalerTestAlgos = map[string]Scaler{
"NearestNeighbor": ScalerNearestNeighbor,
}
scalerBenchAlgos = map[string]Scaler{
"NearestNeighbor": ScalerNearestNeighbor,
"ApproxBiLinear": ScalerApproxBiLinear,
"BiLinear": ScalerBiLinear,
}
)
// Scaler represents scaling algorithm
type Scaler draw.Scaler
// List of scaling algorithms
var (
ScalerNearestNeighbor = Scaler(draw.NearestNeighbor)
ScalerApproxBiLinear = Scaler(draw.ApproxBiLinear)
ScalerBiLinear = Scaler(draw.BiLinear)
ScalerCatmullRom = Scaler(draw.CatmullRom)
)
var errUnsupportedImageType = errors.New("scaling: unsupported image type")
// Scale returns video scaling transform.
// Setting scaler=nil to use default scaler. (ScalerNearestNeighbor)
// Negative width or height value will keep the aspect ratio of incoming image.
//
// Note: computation cost to scale YCbCr format is 10 times higher than RGB
// due to the implementation in x/image/draw package.
func Scale(width, height int, scaler Scaler) TransformFunc {
return func(r Reader) Reader {
scalerCached := ScalerNearestNeighbor
if scaler != nil {
scalerCached = scaler
}
// cacheScaler makes cached version of Scaler if available.
cacheScaler := func(dRect, sRect image.Rectangle) {}
if kernel, ok := scaler.(interface {
NewScaler(int, int, int, int) draw.Scaler
}); ok {
cacheScaler = func(dRect, sRect image.Rectangle) {
scalerCached = kernel.NewScaler(dRect.Dx(), dRect.Dy(), sRect.Dx(), sRect.Dy())
}
}
var rect image.Rectangle
var imgScaled image.Image
if width > 0 && height > 0 {
rect = image.Rect(0, 0, width, height)
} else if width <= 0 && height <= 0 {
panic("Both width and height are negative!")
}
// updateRect updates output image size
updateRect := func(r image.Rectangle) {}
if height <= 0 {
updateRect = func(r image.Rectangle) {
h := r.Dy() * width / r.Dx()
rect = image.Rect(0, 0, width, h)
}
} else if width <= 0 {
updateRect = func(r image.Rectangle) {
w := r.Dx() * height / r.Dy()
rect = image.Rect(0, 0, w, height)
}
}
// fixedRect returns Rectangle of chroma plane
fixedRect := func(rect image.Rectangle, sr image.YCbCrSubsampleRatio) image.Rectangle {
switch sr {
case image.YCbCrSubsampleRatio444:
case image.YCbCrSubsampleRatio422:
rect.Max.X /= 2
case image.YCbCrSubsampleRatio420:
rect.Max.X /= 2
rect.Max.Y /= 2
}
return rect
}
src := &rgbLikeYCbCr{y: &image.Gray{}, cb: &image.Gray{}, cr: &image.Gray{}}
dst := &rgbLikeYCbCr{y: &image.Gray{}, cb: &image.Gray{}, cr: &image.Gray{}}
// ycbcrRealloc reallocs image.YCbCr if needed
ycbcrRealloc := func(i1 *image.YCbCr) {
if imgScaled == nil || imgScaled.ColorModel() != i1.ColorModel() {
updateRect(i1.Rect)
cacheScaler(rect, i1.Rect)
imgScaled = image.NewYCbCr(rect, i1.SubsampleRatio)
}
imgDst := imgScaled.(*image.YCbCr)
if imgDst.Rect.Dx() != i1.Rect.Dx() || imgDst.Rect.Dy() != i1.Rect.Dy() {
updateRect(i1.Rect)
cacheScaler(rect, i1.Rect)
}
yDx := rect.Dx()
yDy := rect.Dy()
cRect := fixedRect(rect, i1.SubsampleRatio)
cDx := cRect.Dx()
cDy := cRect.Dx()
yLen := yDx * yDy
cLen := cDx * cDy
if len(imgDst.Y) < yLen {
if cap(imgDst.Y) < yLen {
imgDst.Y = make([]uint8, yLen)
}
imgDst.Y = imgDst.Y[:yLen]
}
if len(imgDst.Cr) < cLen {
if cap(imgDst.Cr) < cLen {
imgDst.Cr = make([]uint8, cLen)
}
imgDst.Cr = imgDst.Cr[:cLen]
}
if len(imgDst.Cb) < cLen {
if cap(imgDst.Cb) < cLen {
imgDst.Cb = make([]uint8, cLen)
}
imgDst.Cb = imgDst.Cb[:cLen]
}
*dst.y = image.Gray{Pix: imgDst.Y, Stride: imgDst.YStride, Rect: rect}
*dst.cb = image.Gray{Pix: imgDst.Cb, Stride: imgDst.CStride, Rect: cRect}
*dst.cr = image.Gray{Pix: imgDst.Cr, Stride: imgDst.CStride, Rect: cRect}
}
// ycbcrRealloc reallocs image.RGBA if needed
rgbaRealloc := func(i1 *image.RGBA) {
if imgScaled == nil || imgScaled.ColorModel() != i1.ColorModel() {
updateRect(i1.Rect)
cacheScaler(rect, i1.Rect)
imgScaled = image.NewRGBA(rect)
}
imgDst := imgScaled.(*image.RGBA)
if imgDst.Rect.Dx() != i1.Rect.Dx() || imgDst.Rect.Dy() != i1.Rect.Dy() {
updateRect(i1.Rect)
cacheScaler(rect, i1.Rect)
}
l := 4 * rect.Dx() * rect.Dy()
if len(imgDst.Pix) < l {
if cap(imgDst.Pix) < l {
imgDst.Pix = make([]uint8, l)
}
imgDst.Pix = imgDst.Pix[:l]
}
}
return ReaderFunc(func() (image.Image, func(), error) {
img, _, err := r.Read()
if err != nil {
return nil, func() {}, err
}
switch v := img.(type) {
case *image.RGBA:
rgbaRealloc(v)
dst := imgScaled.(*image.RGBA)
scalerCached.Scale(dst, rect, v, v.Rect, draw.Src, nil)
cloned := *dst // clone metadata
return &cloned, func() {}, nil
case *image.YCbCr:
ycbcrRealloc(v)
// Scale each plane
*src.y = image.Gray{Pix: v.Y, Stride: v.YStride, Rect: v.Rect}
*src.cb = image.Gray{
Pix: v.Cb, Stride: v.CStride, Rect: fixedRect(v.Rect, v.SubsampleRatio),
}
*src.cr = image.Gray{
Pix: v.Cr, Stride: v.CStride, Rect: fixedRect(v.Rect, v.SubsampleRatio),
}
scalerCached.Scale(dst, dst.Bounds(), src, src.Bounds(), draw.Src, nil)
cloned := *(imgScaled.(*image.YCbCr)) // clone metadata
return &cloned, func() {}, nil
default:
return nil, func() {}, errUnsupportedImageType
}
})
}
}