Replaced FrameData.Image() with FrameData.GuessImageFormat() + now handling premultiplied image formats

This commit is contained in:
Quentin Renard
2024-01-26 11:21:21 +01:00
parent 6e42185a28
commit 7c941fea03
5 changed files with 394 additions and 143 deletions

View File

@@ -59,7 +59,7 @@ func (f *Frame) SetColorRange(r ColorRange) {
} }
func (f *Frame) Data() *FrameData { func (f *Frame) Data() *FrameData {
return newFrameData(f) return NewFrameData(newFrameDataFrame(f))
} }
func (f *Frame) Height() int { func (f *Frame) Height() int {

View File

@@ -9,43 +9,62 @@ import (
"strings" "strings"
) )
type frameDataImageFormat int type FrameDataFrame interface {
Height() int
const ( ImageBufferSize(align int) (int, error)
frameDataImageFormatNone frameDataImageFormat = iota ImageCopyToBuffer(b []byte, align int) (int, error)
frameDataImageFormatNRGBA Linesize(i int) int
frameDataImageFormatNYCbCrA PixelFormat() PixelFormat
frameDataImageFormatYCbCr PlaneBytes(i int) []byte
) Width() int
func frameDataImageFormatFromPixelFormat(pf PixelFormat) frameDataImageFormat {
// Switch on pixel format
switch pf {
// NRGBA
case PixelFormatRgba:
return frameDataImageFormatNRGBA
// NYCbCrA
case PixelFormatYuva420P,
PixelFormatYuva422P,
PixelFormatYuva444P:
return frameDataImageFormatNYCbCrA
// YCbCr
case PixelFormatYuv410P,
PixelFormatYuv411P, PixelFormatYuvj411P,
PixelFormatYuv420P, PixelFormatYuvj420P,
PixelFormatYuv422P, PixelFormatYuvj422P,
PixelFormatYuv440P, PixelFormatYuvj440P,
PixelFormatYuv444P, PixelFormatYuvj444P:
return frameDataImageFormatYCbCr
}
return frameDataImageFormatNone
} }
type FrameData struct { var _ FrameDataFrame = (*frameDataFrame)(nil)
type frameDataFrame struct {
f *Frame f *Frame
} }
func newFrameData(f *Frame) *FrameData { func newFrameDataFrame(f *Frame) *frameDataFrame {
return &frameDataFrame{f: f}
}
func (f *frameDataFrame) Height() int {
return f.f.Height()
}
func (f *frameDataFrame) ImageBufferSize(align int) (int, error) {
return f.f.ImageBufferSize(align)
}
func (f *frameDataFrame) ImageCopyToBuffer(b []byte, align int) (int, error) {
return f.f.ImageCopyToBuffer(b, align)
}
func (f *frameDataFrame) Linesize(i int) int {
return f.f.Linesize()[i]
}
func (f *frameDataFrame) PixelFormat() PixelFormat {
return f.f.PixelFormat()
}
func (f *frameDataFrame) PlaneBytes(i int) []byte {
return bytesFromC(func(size *cUlong) *C.uint8_t {
*size = cUlong(int(f.f.c.linesize[i]) * f.f.Height())
return f.f.c.data[i]
})
}
func (f *frameDataFrame) Width() int {
return f.f.Width()
}
type FrameData struct {
f FrameDataFrame
}
func NewFrameData(f FrameDataFrame) *FrameData {
return &FrameData{f: f} return &FrameData{f: f}
} }
@@ -76,11 +95,33 @@ func (d *FrameData) Bytes(align int) ([]byte, error) {
return nil, errors.New("astiav: frame type not implemented") return nil, errors.New("astiav: frame type not implemented")
} }
func (d *FrameData) planeBytes(i int) []byte { // Always returns non-premultiplied formats when dealing with alpha channels, however this might not
return bytesFromC(func(size *cUlong) *C.uint8_t { // always be accurate. In this case, use your own format in .ToImage()
*size = cUlong(int(d.f.c.linesize[i]) * d.f.Height()) func (d *FrameData) GuessImageFormat() (image.Image, error) {
return d.f.c.data[i] switch d.f.PixelFormat() {
}) case PixelFormatGray8:
return &image.Gray{}, nil
case PixelFormatGray16Be:
return &image.Gray16{}, nil
case PixelFormatRgb0, PixelFormat0Rgb, PixelFormatRgb4, PixelFormatRgb8:
return &image.RGBA{}, nil
case PixelFormatRgba:
return &image.NRGBA{}, nil
case PixelFormatRgba64Be:
return &image.NRGBA64{}, nil
case PixelFormatYuva420P,
PixelFormatYuva422P,
PixelFormatYuva444P:
return &image.NYCbCrA{}, nil
case PixelFormatYuv410P,
PixelFormatYuv411P, PixelFormatYuvj411P,
PixelFormatYuv420P, PixelFormatYuvj420P,
PixelFormatYuv422P, PixelFormatYuvj422P,
PixelFormatYuv440P, PixelFormatYuvj440P,
PixelFormatYuv444P, PixelFormatYuvj444P:
return &image.YCbCr{}, nil
}
return nil, fmt.Errorf("astiav: pixel format %s not handled by Go", d.f.PixelFormat())
} }
func (d *FrameData) imageYCbCrSubsampleRatio() image.YCbCrSubsampleRatio { func (d *FrameData) imageYCbCrSubsampleRatio() image.YCbCrSubsampleRatio {
@@ -101,98 +142,74 @@ func (d *FrameData) imageYCbCrSubsampleRatio() image.YCbCrSubsampleRatio {
} }
func (d *FrameData) copyPlaneBytes(i int, s *[]uint8) { func (d *FrameData) copyPlaneBytes(i int, s *[]uint8) {
b := d.planeBytes(0) b := d.f.PlaneBytes(i)
if len(b) > cap(*s) { if len(b) > cap(*s) {
*s = make([]uint8, len(b)) *s = make([]uint8, len(b))
} }
copy(*s, b) copy(*s, b)
} }
func (d *FrameData) toImageNRGBA(i *image.NRGBA) { func (d *FrameData) toImagePix(pix *[]uint8, stride *int, rect *image.Rectangle) {
d.copyPlaneBytes(0, &i.Pix) d.copyPlaneBytes(0, pix)
if v := d.f.Linesize()[0]; i.Stride != v { if v := d.f.Linesize(0); *stride != v {
i.Stride = v *stride = v
} }
if w, h := d.f.Width(), d.f.Height(); i.Rect.Dy() != w || i.Rect.Dx() != h { if w, h := d.f.Width(), d.f.Height(); rect.Dy() != w || rect.Dx() != h {
i.Rect = image.Rect(0, 0, w, h) *rect = image.Rect(0, 0, w, h)
} }
} }
func (d *FrameData) toImageYCbCr(i *image.YCbCr) { func (d *FrameData) toImageYCbCr(y, cb, cr *[]uint8, yStride, cStride *int, subsampleRatio *image.YCbCrSubsampleRatio, rect *image.Rectangle) {
d.copyPlaneBytes(0, &i.Y) d.copyPlaneBytes(0, y)
d.copyPlaneBytes(1, &i.Cb) d.copyPlaneBytes(1, cb)
d.copyPlaneBytes(2, &i.Cr) d.copyPlaneBytes(2, cr)
if v := d.f.Linesize()[0]; i.YStride != v { if v := d.f.Linesize(0); *yStride != v {
i.YStride = v *yStride = v
} }
if v := d.f.Linesize()[1]; i.CStride != v { if v := d.f.Linesize(1); *cStride != v {
i.CStride = v *cStride = v
} }
if v := d.imageYCbCrSubsampleRatio(); i.SubsampleRatio != v { if v := d.imageYCbCrSubsampleRatio(); *subsampleRatio != v {
i.SubsampleRatio = v *subsampleRatio = v
} }
if w, h := d.f.Width(), d.f.Height(); i.Rect.Dy() != w || i.Rect.Dx() != h { if w, h := d.f.Width(), d.f.Height(); rect.Dy() != w || rect.Dx() != h {
i.Rect = image.Rect(0, 0, w, h) *rect = image.Rect(0, 0, w, h)
} }
} }
func (d *FrameData) toImageNYCbCrA(i *image.NYCbCrA) { func (d *FrameData) toImageYCbCrA(y, cb, cr, a *[]uint8, yStride, cStride, aStride *int, subsampleRatio *image.YCbCrSubsampleRatio, rect *image.Rectangle) {
d.toImageYCbCr(&i.YCbCr) d.toImageYCbCr(y, cb, cr, yStride, cStride, subsampleRatio, rect)
d.copyPlaneBytes(3, &i.A) d.copyPlaneBytes(3, a)
if v := d.f.Linesize()[3]; i.AStride != v { if v := d.f.Linesize(3); *aStride != v {
i.AStride = v *aStride = v
} }
} }
func (d *FrameData) Image() (image.Image, error) {
// Switch on image format
switch frameDataImageFormatFromPixelFormat(d.f.PixelFormat()) {
// NRGBA
case frameDataImageFormatNRGBA:
i := &image.NRGBA{}
d.toImageNRGBA(i)
return i, nil
// NYCbCrA
case frameDataImageFormatNYCbCrA:
i := &image.NYCbCrA{}
d.toImageNYCbCrA(i)
return i, nil
// YCbCr
case frameDataImageFormatYCbCr:
i := &image.YCbCr{}
d.toImageYCbCr(i)
return i, nil
}
return nil, fmt.Errorf("astiav: %s pixel format not handled by the Go standard image package", d.f.PixelFormat())
}
func (d *FrameData) ToImage(dst image.Image) error { func (d *FrameData) ToImage(dst image.Image) error {
// Switch on image format if v, ok := dst.(*image.Alpha); ok {
switch frameDataImageFormatFromPixelFormat(d.f.PixelFormat()) { d.toImagePix(&v.Pix, &v.Stride, &v.Rect)
// NRGBA } else if v, ok := dst.(*image.Alpha16); ok {
case frameDataImageFormatNRGBA: d.toImagePix(&v.Pix, &v.Stride, &v.Rect)
i, ok := dst.(*image.NRGBA) } else if v, ok := dst.(*image.CMYK); ok {
if !ok { d.toImagePix(&v.Pix, &v.Stride, &v.Rect)
return errors.New("astiav: image should be *image.NRGBA") } else if v, ok := dst.(*image.Gray); ok {
d.toImagePix(&v.Pix, &v.Stride, &v.Rect)
} else if v, ok := dst.(*image.Gray16); ok {
d.toImagePix(&v.Pix, &v.Stride, &v.Rect)
} else if v, ok := dst.(*image.NRGBA); ok {
d.toImagePix(&v.Pix, &v.Stride, &v.Rect)
} else if v, ok := dst.(*image.NRGBA64); ok {
d.toImagePix(&v.Pix, &v.Stride, &v.Rect)
} else if v, ok := dst.(*image.NYCbCrA); ok {
d.toImageYCbCrA(&v.Y, &v.Cb, &v.Cr, &v.A, &v.YStride, &v.CStride, &v.AStride, &v.SubsampleRatio, &v.Rect)
} else if v, ok := dst.(*image.RGBA); ok {
d.toImagePix(&v.Pix, &v.Stride, &v.Rect)
} else if v, ok := dst.(*image.RGBA64); ok {
d.toImagePix(&v.Pix, &v.Stride, &v.Rect)
} else if v, ok := dst.(*image.YCbCr); ok {
d.toImageYCbCr(&v.Y, &v.Cb, &v.Cr, &v.YStride, &v.CStride, &v.SubsampleRatio, &v.Rect)
} else {
return errors.New("astiav: image format is not handled")
} }
d.toImageNRGBA(i)
return nil
// NYCbCrA
case frameDataImageFormatNYCbCrA:
i, ok := dst.(*image.NYCbCrA)
if !ok {
return errors.New("astiav: image should be *image.NYCbCrA")
}
d.toImageNYCbCrA(i)
return nil
// YCbCr
case frameDataImageFormatYCbCr:
i, ok := dst.(*image.YCbCr)
if !ok {
return errors.New("astiav: image should be *image.YCbCr")
}
d.toImageYCbCr(i)
return nil return nil
} }
return fmt.Errorf("astiav: %s pixel format not handled by the Go standard image package", d.f.PixelFormat())
}

View File

@@ -2,51 +2,285 @@ package astiav_test
import ( import (
"image" "image"
"image/png"
"os"
"testing" "testing"
"github.com/asticode/go-astiav" "github.com/asticode/go-astiav"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
type frameDataFrame struct {
height int
imageBytes []byte
linesizes []int
pixelFormat astiav.PixelFormat
planesBytes [][]byte
width int
}
var _ astiav.FrameDataFrame = (*frameDataFrame)(nil)
func (f *frameDataFrame) Height() int {
return f.height
}
func (f *frameDataFrame) ImageBufferSize(align int) (int, error) {
return len(f.imageBytes), nil
}
func (f *frameDataFrame) ImageCopyToBuffer(b []byte, align int) (int, error) {
copy(b, f.imageBytes)
return len(f.imageBytes), nil
}
func (f *frameDataFrame) Linesize(i int) int {
return f.linesizes[i]
}
func (f *frameDataFrame) PixelFormat() astiav.PixelFormat {
return f.pixelFormat
}
func (f *frameDataFrame) PlaneBytes(i int) []byte {
return f.planesBytes[i]
}
func (f *frameDataFrame) Width() int {
return f.width
}
func TestFrameData(t *testing.T) { func TestFrameData(t *testing.T) {
f := &frameDataFrame{}
fd := astiav.NewFrameData(f)
for _, v := range []struct { for _, v := range []struct {
ext string err bool
i image.Image i image.Image
name string pfs []astiav.PixelFormat
}{ }{
{ {
ext: "png", i: &image.Gray{},
i: &image.NRGBA{}, pfs: []astiav.PixelFormat{astiav.PixelFormatGray8},
name: "image-rgba", },
{
i: &image.Gray16{},
pfs: []astiav.PixelFormat{astiav.PixelFormatGray16Be},
},
{
i: &image.RGBA{},
pfs: []astiav.PixelFormat{
astiav.PixelFormatRgb0,
astiav.PixelFormat0Rgb,
astiav.PixelFormatRgb4,
astiav.PixelFormatRgb8,
},
},
{
i: &image.NRGBA{},
pfs: []astiav.PixelFormat{astiav.PixelFormatRgba},
},
{
i: &image.NRGBA64{},
pfs: []astiav.PixelFormat{astiav.PixelFormatRgba64Be},
},
{
i: &image.NYCbCrA{},
pfs: []astiav.PixelFormat{
astiav.PixelFormatYuva420P,
astiav.PixelFormatYuva422P,
astiav.PixelFormatYuva444P,
},
},
{
i: &image.YCbCr{},
pfs: []astiav.PixelFormat{
astiav.PixelFormatYuv410P,
astiav.PixelFormatYuv411P,
astiav.PixelFormatYuvj411P,
astiav.PixelFormatYuv420P,
astiav.PixelFormatYuvj420P,
astiav.PixelFormatYuv422P,
astiav.PixelFormatYuvj422P,
astiav.PixelFormatYuv440P,
astiav.PixelFormatYuvj440P,
astiav.PixelFormatYuv444P,
astiav.PixelFormatYuvj444P,
},
},
{
err: true,
pfs: []astiav.PixelFormat{astiav.PixelFormatAbgr},
}, },
// TODO Find a way to test yuv and yuva even though result seems to change randomly
} { } {
// We use a closure to ease closing files for _, pf := range v.pfs {
func() { f.pixelFormat = pf
f, err := globalHelper.inputLastFrame(v.name+"."+v.ext, astiav.MediaTypeVideo) i, err := fd.GuessImageFormat()
require.NoError(t, err) if v.err {
fd := f.Data() require.Error(t, err)
} else {
b1, err := fd.Bytes(1) require.IsType(t, v.i, i)
require.NoError(t, err) }
}
b2, err := os.ReadFile("testdata/" + v.name + "-bytes") }
require.NoError(t, err)
require.Equal(t, b1, b2) f.imageBytes = []byte{0, 1, 2, 3}
_, err := fd.Bytes(0)
f1, err := os.Open("testdata/" + v.name + "." + v.ext) require.Error(t, err)
require.NoError(t, err) f.height = 1
defer f1.Close() f.width = 2
b, err := fd.Bytes(0)
i1, err := fd.Image() require.NoError(t, err)
require.NoError(t, err) require.Equal(t, f.imageBytes, b)
require.NoError(t, fd.ToImage(v.i))
i2, err := png.Decode(f1) for _, v := range []struct {
require.NoError(t, err) e image.Image
require.Equal(t, i1, i2) err bool
require.Equal(t, v.i, i2) i image.Image
}() linesizes []int
pixelFormat astiav.PixelFormat
planesBytes [][]byte
}{
{
e: &image.Alpha{
Pix: []byte{0, 1, 2, 3},
Stride: 1,
Rect: image.Rect(0, 0, 2, 1),
},
i: &image.Alpha{},
linesizes: []int{1},
pixelFormat: astiav.PixelFormatRgba,
planesBytes: [][]byte{{0, 1, 2, 3}},
},
{
e: &image.Alpha16{
Pix: []byte{0, 1, 2, 3},
Stride: 1,
Rect: image.Rect(0, 0, 2, 1),
},
i: &image.Alpha16{},
linesizes: []int{1},
pixelFormat: astiav.PixelFormatRgba,
planesBytes: [][]byte{{0, 1, 2, 3}},
},
{
e: &image.CMYK{
Pix: []byte{0, 1, 2, 3},
Stride: 1,
Rect: image.Rect(0, 0, 2, 1),
},
i: &image.CMYK{},
linesizes: []int{1},
pixelFormat: astiav.PixelFormatRgba,
planesBytes: [][]byte{{0, 1, 2, 3}},
},
{
e: &image.Gray{
Pix: []byte{0, 1, 2, 3},
Stride: 1,
Rect: image.Rect(0, 0, 2, 1),
},
i: &image.Gray{},
linesizes: []int{1},
pixelFormat: astiav.PixelFormatRgba,
planesBytes: [][]byte{{0, 1, 2, 3}},
},
{
e: &image.Gray16{
Pix: []byte{0, 1, 2, 3},
Stride: 1,
Rect: image.Rect(0, 0, 2, 1),
},
i: &image.Gray16{},
linesizes: []int{1},
pixelFormat: astiav.PixelFormatRgba,
planesBytes: [][]byte{{0, 1, 2, 3}},
},
{
e: &image.NRGBA{
Pix: []byte{0, 1, 2, 3},
Stride: 1,
Rect: image.Rect(0, 0, 2, 1),
},
i: &image.NRGBA{},
linesizes: []int{1},
pixelFormat: astiav.PixelFormatRgba,
planesBytes: [][]byte{{0, 1, 2, 3}},
},
{
e: &image.NRGBA64{
Pix: []byte{0, 1, 2, 3},
Stride: 1,
Rect: image.Rect(0, 0, 2, 1),
},
i: &image.NRGBA64{},
linesizes: []int{1},
pixelFormat: astiav.PixelFormatRgba,
planesBytes: [][]byte{{0, 1, 2, 3}},
},
{
e: &image.NYCbCrA{
A: []byte{6, 7},
AStride: 4,
YCbCr: image.YCbCr{
Y: []byte{0, 1},
Cb: []byte{2, 3},
Cr: []byte{4, 5},
YStride: 1,
CStride: 2,
SubsampleRatio: image.YCbCrSubsampleRatio444,
Rect: image.Rect(0, 0, 2, 1),
},
},
i: &image.NYCbCrA{},
linesizes: []int{1, 2, 3, 4},
pixelFormat: astiav.PixelFormatYuv444P,
planesBytes: [][]byte{{0, 1}, {2, 3}, {4, 5}, {6, 7}},
},
{
e: &image.RGBA{
Pix: []byte{0, 1, 2, 3},
Stride: 1,
Rect: image.Rect(0, 0, 2, 1),
},
i: &image.RGBA{},
linesizes: []int{1},
pixelFormat: astiav.PixelFormatRgba,
planesBytes: [][]byte{{0, 1, 2, 3}},
},
{
e: &image.RGBA64{
Pix: []byte{0, 1, 2, 3},
Stride: 1,
Rect: image.Rect(0, 0, 2, 1),
},
i: &image.RGBA64{},
linesizes: []int{1},
pixelFormat: astiav.PixelFormatRgba,
planesBytes: [][]byte{{0, 1, 2, 3}},
},
{
e: &image.YCbCr{
Y: []byte{0, 1},
Cb: []byte{2, 3},
Cr: []byte{4, 5},
YStride: 1,
CStride: 2,
SubsampleRatio: image.YCbCrSubsampleRatio420,
Rect: image.Rect(0, 0, 2, 1),
},
i: &image.YCbCr{},
linesizes: []int{1, 2, 3},
pixelFormat: astiav.PixelFormatYuv420P,
planesBytes: [][]byte{{0, 1}, {2, 3}, {4, 5}},
},
} {
f.linesizes = v.linesizes
f.pixelFormat = v.pixelFormat
f.planesBytes = v.planesBytes
err = fd.ToImage(v.i)
if v.err {
require.Error(t, err)
} else {
require.Equal(t, v.e, v.i)
}
} }
} }

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB