From 8ad9ee53617bad6979958cbd749d793b5d32bcb8 Mon Sep 17 00:00:00 2001 From: Saxon Nelson-Milton Date: Tue, 30 Mar 2021 14:06:00 +1030 Subject: [PATCH] device/raspivid & revid/config: exposed more raspivid parameters, namely contrast, sharpness, exposure value, ISO and AWBGains --- device/raspivid/raspivid.go | 163 +++++++++++++++++++++-------- device/raspivid/raspivid_test.go | 173 +++++++++++++++++++++++++++++++ revid/config/config.go | 15 +++ revid/config/variables.go | 48 +++++++++ 4 files changed, 358 insertions(+), 41 deletions(-) diff --git a/device/raspivid/raspivid.go b/device/raspivid/raspivid.go index 6be1785c..8d1985ab 100644 --- a/device/raspivid/raspivid.go +++ b/device/raspivid/raspivid.go @@ -32,6 +32,7 @@ import ( "io" "io/ioutil" "os/exec" + "strconv" "strings" "bitbucket.org/ausocean/av/codec/codecutil" @@ -58,6 +59,11 @@ const ( defaultRaspividQuantization = 30 defaultRaspividBitrate = 4800 defaultRaspividFramerate = 25 + defaultRaspividSharpness = 0 + defaultRaspividContrast = 0 + defaultRaspividISO = 100 + defaultRaspividEV = 0 + defaultRaspividAWBGains = "1.0,1.0" ) // Configuration errors. @@ -74,6 +80,11 @@ var ( errBadExposure = errors.New("exposure bad or unset, defaulting") errBadAutoWhiteBalance = errors.New("auto white balance 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. @@ -196,65 +207,75 @@ func (r *Raspivid) Set(c config.Config) error { 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) { errs = append(errs, errBadAutoWhiteBalance) 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 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 // configuration set using the Set method then call the raspivid command, // piping the video output from which the Read method will read from. func (r *Raspivid) Start() 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), - "--exposure", fmt.Sprint(r.cfg.Exposure), - "--awb", fmt.Sprint(r.cfg.AutoWhiteBalance), + args, err := r.createArgs() + if err != nil { + return fmt.Errorf("could not create raspivid args: %w", err) } - 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.cmd = exec.Command("raspivid", args...) - var err error r.out, err = r.cmd.StdoutPipe() if err != nil { return fmt.Errorf("could not pipe command output: %w", err) @@ -325,3 +346,63 @@ func (r *Raspivid) Stop() error { func (r *Raspivid) IsRunning() bool { 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 +} diff --git a/device/raspivid/raspivid_test.go b/device/raspivid/raspivid_test.go index f6fa2ec5..5c9ca37b 100644 --- a/device/raspivid/raspivid_test.go +++ b/device/raspivid/raspivid_test.go @@ -70,3 +70,176 @@ func TestIsRunning(t *testing.T) { 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 +} diff --git a/revid/config/config.go b/revid/config/config.go index 012c8b46..ebb9b6bc 100644 --- a/revid/config/config.go +++ b/revid/config/config.go @@ -102,6 +102,9 @@ type Config struct { // defined at the start of the file. AutoWhiteBalance string + // AWBGains sets the blue and red channel gains of an image/video capture device. + AWBGains string + BitDepth uint // Sample bit depth. Bitrate uint // Bitrate specifies the bitrate for constant bitrate in kbps. Brightness uint @@ -127,11 +130,17 @@ type Config struct { // clips by default. 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 // are defined in the exported []string ExposureModes defined at the start // of the file. 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. 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. 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. // This must be set for revid to work correctly. Logger Logger @@ -234,6 +246,9 @@ type Config struct { SampleRate uint // Samples a second (Hz). 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 // timelapse snaps. 100 represents minimal compression and 0 represents the most // compression. diff --git a/revid/config/variables.go b/revid/config/variables.go index 0edc815a..6c8f8992 100644 --- a/revid/config/variables.go +++ b/revid/config/variables.go @@ -40,6 +40,7 @@ import ( // Config map Keys. const ( KeyAutoWhiteBalance = "AutoWhiteBalance" + KeyAWBGains = "AWBGains" KeyBitDepth = "BitDepth" KeyBitrate = "Bitrate" KeyBrightness = "Brightness" @@ -49,7 +50,9 @@ const ( KeyCBR = "CBR" KeyClipDuration = "ClipDuration" KeyChannels = "Channels" + KeyContrast = "Contrast" KeyExposure = "Exposure" + KeyEV = "EV" KeyFileFPS = "FileFPS" KeyFilters = "Filters" KeyFrameRate = "FrameRate" @@ -59,6 +62,7 @@ const ( KeyInput = "Input" KeyInputCodec = "InputCodec" KeyInputPath = "InputPath" + KeyISO = "ISO" KeyLogging = "logging" KeyLoop = "Loop" KeyMinFPS = "MinFPS" @@ -86,6 +90,7 @@ const ( KeyRTPAddress = "RTPAddress" KeySampleRate = "SampleRate" KeySaturation = "Saturation" + KeySharpness = "Sharpness" KeyJPEGQuality = "JPEGQuality" KeySuppress = "Suppress" KeyTimelapseDuration = "TimelapseDuration" @@ -141,6 +146,11 @@ var Variables = []struct { Type_: "enum:off,auto,sun,cloud,shade,tungsten,fluorescent,incandescent,flash,horizon", Update: func(c *Config, v string) { c.AutoWhiteBalance = v }, }, + { + Name: KeyAWBGains, + Type_: typeString, + Update: func(c *Config, v string) { c.AWBGains = v }, + }, { Name: KeyBitDepth, Type_: typeUint, @@ -210,6 +220,28 @@ var Variables = []struct { Type_: typeUint, 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, Type_: "enum:auto,night,nightpreview,backlight,spotlight,sports,snow,beach,verylong,fixedfps,antishake,fireworks", @@ -327,6 +359,11 @@ var Variables = []struct { Type_: typeString, 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, Type_: "enum:Debug,Info,Warning,Error,Fatal", @@ -577,6 +614,17 @@ var Variables = []struct { 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, Type_: typeUint,