Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optional face detection with gocv #35

Open
wants to merge 29 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b035707
Change the imports in Go code, reflecting the fork
svkoskin Apr 11, 2018
9fa7dac
Declare and use an interface for edge, skin and saturation detectors
svkoskin Apr 12, 2018
fa369ab
Allow Analyzer users to override Detectors
svkoskin Apr 12, 2018
654bf38
Include a tool for debug runs (based on README)
svkoskin Apr 11, 2018
d6b4b4c
Restore face detection deleted in 'f81c194e7d11e4d031b45c072aea29043c…
svkoskin Apr 12, 2018
41573b1
gocv api
svkoskin Apr 12, 2018
e15f810
Reset skin bias
svkoskin Apr 12, 2018
96dc0c1
Describe example usage of gocv features
svkoskin Apr 12, 2018
a3a40ff
Restore imports
svkoskin Apr 16, 2018
6da9bd3
Revert "Restore imports"
svkoskin Jun 5, 2018
0b7246f
Tests: Remove a noisy debug print
svkoskin May 30, 2018
15bd6ac
Make analyse a method of smartcropAnalyzer
svkoskin Jun 4, 2018
964bc81
Introduce a new interface and a field to describe EdgeDetector
svkoskin Jun 4, 2018
d607a1b
Alter face detector initialization
svkoskin Jun 4, 2018
ff0aec3
Detectors shall provide their Bias and Weight
svkoskin Jun 4, 2018
9acfd05
FaceDetector: Return bias and weight
svkoskin Jun 4, 2018
f8c5cd0
Revert "Reset skin bias"
svkoskin Jun 4, 2018
cef7d23
Satisfy the new Detect interface in detectors
svkoskin Jun 5, 2018
751d4f9
Make scoring work with the new detector structure
svkoskin Jun 5, 2018
fe4a524
FaceDetector: Satisfy the new interface
svkoskin Jun 4, 2018
fc94470
Restore debug output functionality
svkoskin Jun 5, 2018
9b8c1bb
Extract logger
svkoskin Apr 24, 2018
d2c290a
Restore imports
svkoskin Apr 16, 2018
bbd2e71
Skip building gocv-pieces in Travis CI for now
svkoskin Jun 5, 2018
1961e95
Face detector: Mark faces with a filled circle instead of a filled re…
svkoskin Jun 6, 2018
f09e3d5
Debug: Try to blend colors a bit
svkoskin Jun 7, 2018
2f3124c
Face detector: Fix initialization of gocv.Mat, the constructor has di…
svkoskin Jun 7, 2018
4908101
Fix FaceDetector init example
svkoskin Jun 7, 2018
58f6608
Travis CI: Try to skip building gocv and OpenCV
svkoskin Jun 7, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ before_install:
# - sudo apt-get update -qq
# - sudo apt-get install libcv-dev libopencv-dev libopencv-contrib-dev libhighgui-dev libopencv-photo-dev libopencv-imgproc-dev libopencv-stitching-dev libopencv-superres-dev libopencv-ts-dev libopencv-videostab-dev

install:
# It's complicated to get OpenCV built on each platform, so exclude the OpenCV-based detector and its dependencies this way.
- go get -t $(go list ./... | grep -v gocv | xargs)

script:
- go test -v -tags ci ./...
- if [[ $TRAVIS_GO_VERSION == 1.9* ]]; then $GOPATH/bin/goveralls -service=travis-ci; fi
Expand Down
92 changes: 88 additions & 4 deletions debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,94 @@ import (
"path/filepath"
)

func debugOutput(debug bool, img *image.RGBA, debugType string) {
if debug {
writeImage("png", img, "./smartcrop_"+debugType+".png")
// debugImage carries debug output image and has methods for updating and writing it
type DebugImage struct {
img *image.RGBA
colors []color.RGBA
nextColorIdx int
}

func NewDebugImage(bounds image.Rectangle) *DebugImage {
di := DebugImage{}

// Set up the actual image
di.img = image.NewRGBA(bounds)
for x := bounds.Min.X; x < bounds.Max.X; x++ {
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
di.img.Set(x, y, color.Black)
}
}

// Set up an array of colors used for debug outputs
di.colors = []color.RGBA{
{0, 255, 0, 255}, // default edges
{255, 0, 0, 255}, // default skin
{0, 0, 255, 255}, // default saturation
{255, 128, 0, 255}, // a few extra...
{128, 0, 128, 255},
{64, 255, 255, 255},
{255, 64, 255, 255},
{255, 255, 64, 255},
{255, 255, 255, 255},
}
di.nextColorIdx = 0
return &di
}

func (di *DebugImage) popNextColor() color.RGBA {
c := di.colors[di.nextColorIdx]
di.nextColorIdx++

// Wrap around if necessary (if someone ever implements and sets a tenth detector)
if di.nextColorIdx >= len(di.colors) {
di.nextColorIdx = 0
}
return c
}

func scaledColorComponent(factor uint8, oldComponent uint8, newComponent uint8) uint8 {
if factor < 1 {
return oldComponent
}

return uint8(bounds(((float64(factor)/255.0*float64(newComponent))+float64(oldComponent))/2.0) * 2.0)
}

func (di *DebugImage) AddDetected(d [][]uint8) {
baseColor := di.popNextColor()

minX := di.img.Bounds().Min.X
minY := di.img.Bounds().Min.Y

maxX := di.img.Bounds().Max.X
maxY := di.img.Bounds().Max.Y
if maxX > len(d) {
maxX = len(d)
}
if maxY > len(d[0]) {
maxY = len(d[0])
}

for x := minX; x < maxX; x++ {
for y := minY; y < maxY; y++ {
if d[x][y] > 0 {
c := di.img.RGBAAt(x, y)
nc := color.RGBA{}
nc.R = scaledColorComponent(d[x][y], c.R, baseColor.R)
nc.G = scaledColorComponent(d[x][y], c.G, baseColor.G)
nc.B = scaledColorComponent(d[x][y], c.B, baseColor.B)
nc.A = 255

di.img.SetRGBA(x, y, nc)
}
}
}
}

func (di *DebugImage) DebugOutput(debugType string) {
writeImage("png", di.img, "./smartcrop_"+debugType+".png")
}

func writeImage(imgtype string, img image.Image, name string) error {
if err := os.MkdirAll(filepath.Dir(name), 0755); err != nil {
panic(err)
Expand Down Expand Up @@ -82,7 +164,9 @@ func writeImageToPng(img image.Image, name string) error {
return png.Encode(fso, img)
}

func drawDebugCrop(topCrop Crop, o *image.RGBA) {
func (di *DebugImage) DrawDebugCrop(topCrop Crop) {
o := di.img

width := o.Bounds().Dx()
height := o.Bounds().Dy()

Expand Down
125 changes: 125 additions & 0 deletions gocv/face.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// +build !ci

package gocv

import (
"fmt"
"image"
"os"

"gocv.io/x/gocv"

sclogger "github.com/muesli/smartcrop/logger"
)

type FaceDetector struct {
FaceDetectionHaarCascadeFilepath string
Logger *sclogger.Logger
}

func (d *FaceDetector) Name() string {
return "face"
}

func (d *FaceDetector) Bias() float64 {
return 0.9
}

func (d *FaceDetector) Weight() float64 {
return 1.8
}

func (d *FaceDetector) Detect(img *image.RGBA) ([][]uint8, error) {
res := make([][]uint8, img.Bounds().Dx())
for x := range res {
res[x] = make([]uint8, img.Bounds().Dy())
}

if img == nil {
return res, fmt.Errorf("img can't be nil")
}
if d.FaceDetectionHaarCascadeFilepath == "" {
return res, fmt.Errorf("FaceDetector's FaceDetectionHaarCascadeFilepath not specified")
}

_, err := os.Stat(d.FaceDetectionHaarCascadeFilepath)
if err != nil {
return res, err
}

classifier := gocv.NewCascadeClassifier()
defer classifier.Close()
if !classifier.Load(d.FaceDetectionHaarCascadeFilepath) {
return res, fmt.Errorf("FaceDetector failed loading cascade file")
}

// image.NRGBA-compatible params
cvMat, err := gocv.NewMatFromBytes(img.Rect.Dy(), img.Rect.Dx(), gocv.MatTypeCV8UC4, img.Pix)
defer cvMat.Close()
if err != nil {
return res, err
}

faces := classifier.DetectMultiScale(cvMat)

if d.Logger.DebugMode == true {
d.Logger.Log.Printf("Number of faces detected: %d\n", len(faces))
}

for _, face := range faces {
// Upper left corner of detected face-rectangle
x := face.Min.X
y := face.Min.Y

width := face.Dx()
height := face.Dy()

if d.Logger.DebugMode == true {
d.Logger.Log.Printf("Face: x: %d y: %d w: %d h: %d\n", x, y, width, height)
}

drawAFilledCircle(res, x+(width/2), y+(height/2), width/2)
}
return res, nil
}

func drawAFilledCircle(pix [][]uint8, x0, y0, r int) {
x := r - 1
y := 0
dx := 1
dy := 1
err := dx - (r << 1)

for {
if x < y {
return
}

for i := -x; i <= x; i++ {
putPixel(pix, x0+i, y0+y)
putPixel(pix, x0+i, y0-y)
putPixel(pix, x0+y, y0+i)
putPixel(pix, x0-y, y0+i)
}

if err <= 0 {
y++
err += dy
dy += 2
} else {
x--
dx += 2
err += dx - (r << 1)
}
}
}

func putPixel(pix [][]uint8, x, y int) {
if x >= len(pix) {
return
}
if y >= len(pix[x]) {
return
}
pix[x][y] = uint8(255)
}
11 changes: 11 additions & 0 deletions logger/logger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package logger

import (
"log"
)

// Logger contains a logger.
type Logger struct {
DebugMode bool
Log *log.Logger
}
46 changes: 46 additions & 0 deletions smartcrop-rundebug/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package main

import (
"fmt"
"image"
_ "image/jpeg"
_ "image/png"
"log"
"os"

"github.com/muesli/smartcrop"
sclogger "github.com/muesli/smartcrop/logger"
"github.com/muesli/smartcrop/nfnt"
// "github.com/muesli/smartcrop/gocv"
)

func main() {
if len(os.Args) < 2 {
fmt.Println("Please give me an argument")
os.Exit(1)
}

f, _ := os.Open(os.Args[1])
img, _, _ := image.Decode(f)

l := sclogger.Logger{
DebugMode: true,
Log: log.New(os.Stderr, "", 0),
}

analyzer := smartcrop.NewAnalyzerWithLogger(nfnt.NewDefaultResizer(), l)

/*
To replace skin detection with gocv-based face detection:

analyzer.SetDetectors([]smartcrop.Detector{
&gocv.FaceDetector{"./cascade.xml", &l},
&smartcrop.SaturationDetector{},
})
*/

topCrop, _ := analyzer.FindBestCrop(img, 300, 200)

// The crop will have the requested aspect ratio, but you need to copy/scale it yourself
fmt.Printf("Top crop: %+v\n", topCrop)
}
Loading