device/raspivid & revid/config: exposed more raspivid parameters, namely contrast, sharpness, exposure value, ISO and AWBGains

This commit is contained in:
Saxon Nelson-Milton 2021-03-30 14:06:00 +10:30
parent d096434e1e
commit 8ad9ee5361
4 changed files with 358 additions and 41 deletions

View File

@ -32,6 +32,7 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"os/exec" "os/exec"
"strconv"
"strings" "strings"
"bitbucket.org/ausocean/av/codec/codecutil" "bitbucket.org/ausocean/av/codec/codecutil"
@ -58,6 +59,11 @@ const (
defaultRaspividQuantization = 30 defaultRaspividQuantization = 30
defaultRaspividBitrate = 4800 defaultRaspividBitrate = 4800
defaultRaspividFramerate = 25 defaultRaspividFramerate = 25
defaultRaspividSharpness = 0
defaultRaspividContrast = 0
defaultRaspividISO = 100
defaultRaspividEV = 0
defaultRaspividAWBGains = "1.0,1.0"
) )
// Configuration errors. // Configuration errors.
@ -74,6 +80,11 @@ var (
errBadExposure = errors.New("exposure bad or unset, defaulting") errBadExposure = errors.New("exposure bad or unset, defaulting")
errBadAutoWhiteBalance = errors.New("auto white balance bad or unset, defaulting") errBadAutoWhiteBalance = errors.New("auto white balance bad or unset, defaulting")
errBadQuantization = errors.New("quantization bad or unset, defaulting") errBadQuantization = errors.New("quantization bad or unset, defaulting")
errBadAWBGains = errors.New("auto white balance gains bad or unset, defaulting")
errBadEV = errors.New("exposure value bad or unset, defaulting")
errBadContrast = errors.New("contrast bad or unset, defaulting")
errBadSharpness = errors.New("sharpness bad or unset, defaulting")
errBadISO = errors.New("iso bad or unset, defaulting")
) )
// Possible modes for raspivid --exposure parameter. // Possible modes for raspivid --exposure parameter.
@ -196,65 +207,75 @@ func (r *Raspivid) Set(c config.Config) error {
c.Exposure = defaultRaspividExposure c.Exposure = defaultRaspividExposure
} }
if c.EV < -10 || c.EV > 10 {
errs = append(errs, errBadEV)
c.EV = defaultRaspividEV
}
if c.Contrast < -100 || c.Contrast > 100 {
errs = append(errs, errBadContrast)
c.Contrast = defaultRaspividContrast
}
if c.Sharpness < -100 || c.Sharpness > 100 {
errs = append(errs, errBadSharpness)
c.Sharpness = defaultRaspividSharpness
}
if c.AutoWhiteBalance == "" || !sliceutils.ContainsString(AutoWhiteBalanceModes[:], c.AutoWhiteBalance) { if c.AutoWhiteBalance == "" || !sliceutils.ContainsString(AutoWhiteBalanceModes[:], c.AutoWhiteBalance) {
errs = append(errs, errBadAutoWhiteBalance) errs = append(errs, errBadAutoWhiteBalance)
c.AutoWhiteBalance = defaultRaspividAutoWhiteBalance c.AutoWhiteBalance = defaultRaspividAutoWhiteBalance
} }
if !goodAWBGains(c.AWBGains) {
errs = append(errs, errBadAWBGains)
c.AWBGains = defaultRaspividAWBGains
}
if c.ISO == 0 || c.ISO < 100 || c.ISO > 800 {
errs = append(errs, errBadISO)
c.ISO = defaultRaspividISO
}
r.cfg = c r.cfg = c
return errs return errs
} }
func goodAWBGains(g string) bool {
parts := strings.Split(g, ",")
if len(parts) != 2 {
return false
}
bg, err := strconv.ParseFloat(parts[0], 64)
if err != nil {
return false
}
rg, err := strconv.ParseFloat(parts[1], 64)
if err != nil {
return false
}
if bg < 0 || rg < 0 {
return false
}
return true
}
// Start will prepare the arguments for the raspivid command using the // Start will prepare the arguments for the raspivid command using the
// configuration set using the Set method then call the raspivid command, // configuration set using the Set method then call the raspivid command,
// piping the video output from which the Read method will read from. // piping the video output from which the Read method will read from.
func (r *Raspivid) Start() error { func (r *Raspivid) Start() error {
const disabled = "0" args, err := r.createArgs()
args := []string{ if err != nil {
"--output", "-", return fmt.Errorf("could not create raspivid args: %w", err)
"--nopreview",
"--timeout", disabled,
"--width", fmt.Sprint(r.cfg.Width),
"--height", fmt.Sprint(r.cfg.Height),
"--bitrate", fmt.Sprint(r.cfg.Bitrate * 1000), // Convert from kbps to bps.
"--framerate", fmt.Sprint(r.cfg.FrameRate),
"--rotation", fmt.Sprint(r.cfg.Rotation),
"--brightness", fmt.Sprint(r.cfg.Brightness),
"--saturation", fmt.Sprint(r.cfg.Saturation),
"--exposure", fmt.Sprint(r.cfg.Exposure),
"--awb", fmt.Sprint(r.cfg.AutoWhiteBalance),
} }
if r.cfg.HorizontalFlip {
args = append(args, "--hflip")
}
if r.cfg.VerticalFlip {
args = append(args, "--vflip")
}
if r.cfg.HorizontalFlip {
args = append(args, "--hflip")
}
switch r.cfg.InputCodec {
default:
return fmt.Errorf("revid: invalid input codec: %v", r.cfg.InputCodec)
case codecutil.H264:
args = append(args,
"--codec", "H264",
"--inline",
"--intra", fmt.Sprint(r.cfg.MinFrames),
)
if !r.cfg.CBR {
args = append(args, "-qp", fmt.Sprint(r.cfg.Quantization))
}
case codecutil.MJPEG:
args = append(args, "--codec", "MJPEG")
}
r.cfg.Logger.Log(logger.Info, pkg+"raspivid args", "raspividArgs", strings.Join(args, " ")) r.cfg.Logger.Log(logger.Info, pkg+"raspivid args", "raspividArgs", strings.Join(args, " "))
r.cmd = exec.Command("raspivid", args...) r.cmd = exec.Command("raspivid", args...)
var err error
r.out, err = r.cmd.StdoutPipe() r.out, err = r.cmd.StdoutPipe()
if err != nil { if err != nil {
return fmt.Errorf("could not pipe command output: %w", err) return fmt.Errorf("could not pipe command output: %w", err)
@ -325,3 +346,63 @@ func (r *Raspivid) Stop() error {
func (r *Raspivid) IsRunning() bool { func (r *Raspivid) IsRunning() bool {
return r.isRunning return r.isRunning
} }
func (r *Raspivid) createArgs() ([]string, error) {
const disabled = "0"
args := []string{
"--output", "-",
"--nopreview",
"--timeout", disabled,
"--width", fmt.Sprint(r.cfg.Width),
"--height", fmt.Sprint(r.cfg.Height),
"--bitrate", fmt.Sprint(r.cfg.Bitrate * 1000), // Convert from kbps to bps.
"--framerate", fmt.Sprint(r.cfg.FrameRate),
"--rotation", fmt.Sprint(r.cfg.Rotation),
"--brightness", fmt.Sprint(r.cfg.Brightness),
"--saturation", fmt.Sprint(r.cfg.Saturation),
"--sharpness", fmt.Sprint(r.cfg.Sharpness),
"--contrast", fmt.Sprint(r.cfg.Contrast),
"--awb", fmt.Sprint(r.cfg.AutoWhiteBalance),
"--exposure", fmt.Sprint(r.cfg.Exposure),
}
if r.cfg.ISO != defaultRaspividISO {
args = append(args, []string{"--ISO", fmt.Sprint(r.cfg.ISO)}...)
}
if r.cfg.Exposure == "off" {
args = append(args, []string{"--ev", fmt.Sprint(r.cfg.EV)}...)
}
if r.cfg.AutoWhiteBalance == "off" {
args = append(args, []string{"--awbgains", fmt.Sprint(r.cfg.AWBGains)}...)
}
if r.cfg.HorizontalFlip {
args = append(args, "--hflip")
}
if r.cfg.VerticalFlip {
args = append(args, "--vflip")
}
if r.cfg.HorizontalFlip {
args = append(args, "--hflip")
}
switch r.cfg.InputCodec {
default:
return []string{}, fmt.Errorf("revid: invalid input codec: %v", r.cfg.InputCodec)
case codecutil.H264:
args = append(args,
"--codec", "H264",
"--inline",
"--intra", fmt.Sprint(r.cfg.MinFrames),
)
if !r.cfg.CBR {
args = append(args, "-qp", fmt.Sprint(r.cfg.Quantization))
}
case codecutil.MJPEG:
args = append(args, "--codec", "MJPEG")
}
return args, nil
}

View File

@ -70,3 +70,176 @@ func TestIsRunning(t *testing.T) {
t.Error("device is running, when it should not be") t.Error("device is running, when it should not be")
} }
} }
func TestGoodAWBGains(t *testing.T) {
tests := []struct {
gains string
expect bool
}{
{gains: "-0.6,1.7", expect: false},
{gains: "0.6,-1.6", expect: false},
{gains: "1.3,0.3", expect: true},
{gains: "0.8,", expect: false},
{gains: "0.3", expect: false},
{gains: "0,0", expect: true},
{gains: ",1.4", expect: false},
}
for i, test := range tests {
got := goodAWBGains(test.gains)
if got != test.expect {
t.Errorf("did not get get expected result for test: %d\nWant: %v, Got: %v\n", i, test.expect, got)
}
}
}
func TestCreateArgs(t *testing.T) {
tests := []struct {
cfg config.Config
want []string
}{
{
cfg: config.Config{
Height: 1080,
Width: 1440,
Bitrate: 1000,
FrameRate: 25,
Rotation: 45,
InputCodec: codecutil.H264,
Brightness: 50,
Saturation: 20,
Contrast: 30,
Sharpness: -30,
AutoWhiteBalance: "auto",
Exposure: "auto",
EV: 3,
AWBGains: "0.9,1.2",
ISO: 300,
CBR: true,
},
want: []string{
"--output", "-",
"--nopreview",
"--timeout", "0",
"--width", "1440",
"--height", "1080",
"--bitrate", "1000000", // Convert from kbps to bps.
"--framerate", "25",
"--rotation", "45",
"--brightness", "50",
"--saturation", "20",
"--sharpness", "-30",
"--contrast", "30",
"--awb", "auto",
"--exposure", "auto",
"--ISO", "300",
"--codec", "H264",
"--inline",
"--intra", "0",
},
},
{
cfg: config.Config{
Height: 1080,
Width: 1440,
Bitrate: 1000,
FrameRate: 25,
Rotation: 45,
InputCodec: codecutil.H264,
Brightness: 50,
Saturation: 20,
Contrast: 30,
Sharpness: -30,
AutoWhiteBalance: "off",
Exposure: "off",
EV: 3,
AWBGains: "0.9,1.2",
ISO: 300,
CBR: true,
},
want: []string{
"--output", "-",
"--nopreview",
"--timeout", "0",
"--width", "1440",
"--height", "1080",
"--bitrate", "1000000", // Convert from kbps to bps.
"--framerate", "25",
"--rotation", "45",
"--brightness", "50",
"--saturation", "20",
"--sharpness", "-30",
"--contrast", "30",
"--awb", "off",
"--exposure", "off",
"--ISO", "300",
"--ev", "3",
"--awbgains", "0.9,1.2",
"--codec", "H264",
"--inline",
"--intra", "0",
},
},
{
cfg: config.Config{
Height: 1080,
Width: 1440,
Bitrate: 1000,
FrameRate: 25,
Rotation: 45,
InputCodec: codecutil.H264,
Brightness: 50,
Saturation: 20,
Contrast: 30,
Sharpness: -30,
AutoWhiteBalance: "off",
Exposure: "off",
EV: 3,
ISO: 100,
AWBGains: "0.9,1.2",
CBR: true,
},
want: []string{
"--output", "-",
"--nopreview",
"--timeout", "0",
"--width", "1440",
"--height", "1080",
"--bitrate", "1000000", // Convert from kbps to bps.
"--framerate", "25",
"--rotation", "45",
"--brightness", "50",
"--saturation", "20",
"--sharpness", "-30",
"--contrast", "30",
"--awb", "off",
"--exposure", "off",
"--ev", "3",
"--awbgains", "0.9,1.2",
"--codec", "H264",
"--inline",
"--intra", "0",
},
},
}
for i, test := range tests {
got, err := (&Raspivid{cfg: test.cfg}).createArgs()
if err != nil {
t.Fatalf("did not expect error from createArgs: %v", err)
}
if !cmpStrSlice(got, test.want) {
t.Errorf("did not get expected args list for test: %d\nGot: %v\nWant: %v", i, got, test.want)
}
}
}
func cmpStrSlice(a, b []string) bool {
for i, v := range a {
if v != b[i] {
return false
}
}
return true
}

View File

@ -102,6 +102,9 @@ type Config struct {
// defined at the start of the file. // defined at the start of the file.
AutoWhiteBalance string AutoWhiteBalance string
// AWBGains sets the blue and red channel gains of an image/video capture device.
AWBGains string
BitDepth uint // Sample bit depth. BitDepth uint // Sample bit depth.
Bitrate uint // Bitrate specifies the bitrate for constant bitrate in kbps. Bitrate uint // Bitrate specifies the bitrate for constant bitrate in kbps.
Brightness uint Brightness uint
@ -127,11 +130,17 @@ type Config struct {
// clips by default. // clips by default.
ClipDuration time.Duration ClipDuration time.Duration
// Contrast is the contrast of captured video/images from a capture device.
Contrast int
// Exposure defines the exposure mode used by the Raspivid input. Valid modes // Exposure defines the exposure mode used by the Raspivid input. Valid modes
// are defined in the exported []string ExposureModes defined at the start // are defined in the exported []string ExposureModes defined at the start
// of the file. // of the file.
Exposure string Exposure string
// EV is the exposure value for image/video capture devices.
EV int
FileFPS uint // Defines the rate at which frames from a file source are processed. FileFPS uint // Defines the rate at which frames from a file source are processed.
Filters []uint // Defines the methods of filtering to be used in between lexing and encoding. Filters []uint // Defines the methods of filtering to be used in between lexing and encoding.
@ -174,6 +183,9 @@ type Config struct {
// defined if File input is to be used. // defined if File input is to be used.
InputPath string InputPath string
// ISO sets the image/video capture device's sensitivity to light.
ISO uint
// Logger holds an implementation of the Logger interface as defined in revid.go. // Logger holds an implementation of the Logger interface as defined in revid.go.
// This must be set for revid to work correctly. // This must be set for revid to work correctly.
Logger Logger Logger Logger
@ -234,6 +246,9 @@ type Config struct {
SampleRate uint // Samples a second (Hz). SampleRate uint // Samples a second (Hz).
Saturation int Saturation int
// Sharpness is the sharpness of capture image/video from a capture device.
Sharpness int
// JPEGQuality is a value 0-100 inclusive, controlling JPEG compression of the // JPEGQuality is a value 0-100 inclusive, controlling JPEG compression of the
// timelapse snaps. 100 represents minimal compression and 0 represents the most // timelapse snaps. 100 represents minimal compression and 0 represents the most
// compression. // compression.

View File

@ -40,6 +40,7 @@ import (
// Config map Keys. // Config map Keys.
const ( const (
KeyAutoWhiteBalance = "AutoWhiteBalance" KeyAutoWhiteBalance = "AutoWhiteBalance"
KeyAWBGains = "AWBGains"
KeyBitDepth = "BitDepth" KeyBitDepth = "BitDepth"
KeyBitrate = "Bitrate" KeyBitrate = "Bitrate"
KeyBrightness = "Brightness" KeyBrightness = "Brightness"
@ -49,7 +50,9 @@ const (
KeyCBR = "CBR" KeyCBR = "CBR"
KeyClipDuration = "ClipDuration" KeyClipDuration = "ClipDuration"
KeyChannels = "Channels" KeyChannels = "Channels"
KeyContrast = "Contrast"
KeyExposure = "Exposure" KeyExposure = "Exposure"
KeyEV = "EV"
KeyFileFPS = "FileFPS" KeyFileFPS = "FileFPS"
KeyFilters = "Filters" KeyFilters = "Filters"
KeyFrameRate = "FrameRate" KeyFrameRate = "FrameRate"
@ -59,6 +62,7 @@ const (
KeyInput = "Input" KeyInput = "Input"
KeyInputCodec = "InputCodec" KeyInputCodec = "InputCodec"
KeyInputPath = "InputPath" KeyInputPath = "InputPath"
KeyISO = "ISO"
KeyLogging = "logging" KeyLogging = "logging"
KeyLoop = "Loop" KeyLoop = "Loop"
KeyMinFPS = "MinFPS" KeyMinFPS = "MinFPS"
@ -86,6 +90,7 @@ const (
KeyRTPAddress = "RTPAddress" KeyRTPAddress = "RTPAddress"
KeySampleRate = "SampleRate" KeySampleRate = "SampleRate"
KeySaturation = "Saturation" KeySaturation = "Saturation"
KeySharpness = "Sharpness"
KeyJPEGQuality = "JPEGQuality" KeyJPEGQuality = "JPEGQuality"
KeySuppress = "Suppress" KeySuppress = "Suppress"
KeyTimelapseDuration = "TimelapseDuration" KeyTimelapseDuration = "TimelapseDuration"
@ -141,6 +146,11 @@ var Variables = []struct {
Type_: "enum:off,auto,sun,cloud,shade,tungsten,fluorescent,incandescent,flash,horizon", Type_: "enum:off,auto,sun,cloud,shade,tungsten,fluorescent,incandescent,flash,horizon",
Update: func(c *Config, v string) { c.AutoWhiteBalance = v }, Update: func(c *Config, v string) { c.AutoWhiteBalance = v },
}, },
{
Name: KeyAWBGains,
Type_: typeString,
Update: func(c *Config, v string) { c.AWBGains = v },
},
{ {
Name: KeyBitDepth, Name: KeyBitDepth,
Type_: typeUint, Type_: typeUint,
@ -210,6 +220,28 @@ var Variables = []struct {
Type_: typeUint, Type_: typeUint,
Update: func(c *Config, v string) { c.Channels = parseUint(KeyChannels, v, c) }, Update: func(c *Config, v string) { c.Channels = parseUint(KeyChannels, v, c) },
}, },
{
Name: KeyContrast,
Type_: typeInt,
Update: func(c *Config, v string) {
_v, err := strconv.Atoi(v)
if err != nil {
c.Logger.Log(logger.Warning, "invalid contrast param", "value", v)
}
c.Contrast = _v
},
},
{
Name: KeyEV,
Type_: typeInt,
Update: func(c *Config, v string) {
_v, err := strconv.Atoi(v)
if err != nil {
c.Logger.Log(logger.Warning, "invalid EV param", "value", v)
}
c.EV = _v
},
},
{ {
Name: KeyExposure, Name: KeyExposure,
Type_: "enum:auto,night,nightpreview,backlight,spotlight,sports,snow,beach,verylong,fixedfps,antishake,fireworks", Type_: "enum:auto,night,nightpreview,backlight,spotlight,sports,snow,beach,verylong,fixedfps,antishake,fireworks",
@ -327,6 +359,11 @@ var Variables = []struct {
Type_: typeString, Type_: typeString,
Update: func(c *Config, v string) { c.InputPath = v }, Update: func(c *Config, v string) { c.InputPath = v },
}, },
{
Name: KeyISO,
Type_: typeUint,
Update: func(c *Config, v string) { c.ISO = parseUint(KeyISO, v, c) },
},
{ {
Name: KeyLogging, Name: KeyLogging,
Type_: "enum:Debug,Info,Warning,Error,Fatal", Type_: "enum:Debug,Info,Warning,Error,Fatal",
@ -577,6 +614,17 @@ var Variables = []struct {
c.Saturation = _v c.Saturation = _v
}, },
}, },
{
Name: KeySharpness,
Type_: typeInt,
Update: func(c *Config, v string) {
_v, err := strconv.Atoi(v)
if err != nil {
c.Logger.Log(logger.Warning, "invalid Sharpness param", "value", v)
}
c.Sharpness = _v
},
},
{ {
Name: KeyJPEGQuality, Name: KeyJPEGQuality,
Type_: typeUint, Type_: typeUint,