feat: implemented process GUI preview mode #70

This commit is contained in:
esimov
2021-11-29 10:30:09 +02:00
parent 206e52a20b
commit ef14aca0b9
5 changed files with 292 additions and 66 deletions
+35 -8
View File
@@ -9,7 +9,13 @@ import (
pigo "github.com/esimov/pigo/core"
)
var usedSeams []UsedSeams
// maxFaceDetAttempts defines the maximum number of attempts of face detections,
const maxFaceDetAttempts = 20
var (
faceDetAttempts int
usedSeams []UsedSeams
)
// Carver is the main entry struct having as parameters the newly generated image width, height and seam points.
type Carver struct {
@@ -69,12 +75,22 @@ func (c *Carver) ComputeSeams(img *image.NRGBA, p *Processor) error {
width, height := img.Bounds().Dx(), img.Bounds().Dy()
sobel := c.SobelDetector(img, float64(p.SobelThreshold))
if p.FaceDetect {
// Transform the image to a pixel array.
if p.FaceDetect && faceDetAttempts < maxFaceDetAttempts {
var ratio float64
if width < height {
ratio = float64(width) / float64(height)
} else {
ratio = float64(height) / float64(width)
}
minSize := float64(min(width, height)) * ratio / 3
// Transform the image to pixel array.
pixels := c.rgbToGrayscale(img)
cParams := pigo.CascadeParams{
MinSize: 100,
MaxSize: max(width, height),
MinSize: int(minSize),
MaxSize: min(width, height),
ShiftFactor: 0.1,
ScaleFactor: 1.1,
@@ -85,13 +101,24 @@ func (c *Carver) ComputeSeams(img *image.NRGBA, p *Processor) error {
Dim: width,
},
}
if p.vRes {
p.FaceAngle = 0.5
}
// Run the classifier over the obtained leaf nodes and return the detection results.
// The result contains quadruplets representing the row, column, scale and detection score.
faces := p.PigoFaceDetector.RunCascade(cParams, p.FaceAngle)
// Calculate the intersection over union (IoU) of two clusters.
faces = p.PigoFaceDetector.ClusterDetections(faces, 0.2)
faces = p.PigoFaceDetector.ClusterDetections(faces, 0.1)
if len(faces) == 0 {
// Retry detecting faces for a certain amount of time.
if faceDetAttempts < maxFaceDetAttempts {
faceDetAttempts++
}
} else {
faceDetAttempts = 0
}
// Range over all the detected faces and draw a white rectangle mask over each of them.
// We need to trick the sobel detector to consider them as important image parts.
@@ -236,7 +263,7 @@ func (c *Carver) AddSeam(img *image.NRGBA, seams []Seam, debug bool) *image.NRGB
y := seam.Y
for x := 0; x < bounds.Max.X; x++ {
if seam.X == x {
if debug == true {
if debug {
dst.Set(x, y, color.RGBA{255, 0, 0, 255})
continue
}
+8 -5
View File
@@ -62,8 +62,9 @@ var (
percentage = flag.Bool("perc", false, "Reduce image by percentage")
square = flag.Bool("square", false, "Reduce image to square dimensions")
debug = flag.Bool("debug", false, "Use debugger")
preview = flag.Bool("preview", true, "Show preview window")
faceDetect = flag.Bool("face", false, "Use face detection")
faceAngle = flag.Float64("angle", 0.0, "Plane rotated faces angle")
faceAngle = flag.Float64("angle", 0.0, "Face rotation angle")
workers = flag.Int("conc", runtime.NumCPU(), "Number of files to process concurrently")
// Common file related variable
@@ -88,6 +89,7 @@ func main() {
Percentage: *percentage,
Square: *square,
Debug: *debug,
Preview: *preview,
FaceDetect: *faceDetect,
FaceAngle: *faceAngle,
}
@@ -155,6 +157,7 @@ func main() {
)
}
}
proc.Preview = false
// Limit the concurrently running workers to maxWorkers.
if *workers <= 0 || *workers > maxWorkers {
@@ -231,17 +234,17 @@ func walkDir(
// Close the paths channel after Walk returns.
defer close(pathChan)
errChan <- filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
errChan <- filepath.Walk(src, func(path string, f os.FileInfo, err error) error {
isFileSupported := false
if err != nil {
return err
}
if !info.Mode().IsRegular() {
if !f.Mode().IsRegular() {
return nil
}
// Get the file base name.
fx := filepath.Ext(info.Name())
fx := filepath.Ext(f.Name())
for _, ext := range srcExts {
if ext == fx {
isFileSupported = true
@@ -402,7 +405,7 @@ func printStatus(fname string, err error) {
)
} else {
if fname != pipeName {
fmt.Fprintf(os.Stderr, fmt.Sprintf("\nThe new image has been saved as: %s %s\n\n",
fmt.Fprintf(os.Stderr, fmt.Sprintf("\nThe resized image has been saved as: %s %s\n\n",
utils.DecorateText(filepath.Base(fname), utils.SuccessMessage),
utils.DefaultColor,
))
+1 -1
View File
@@ -6,7 +6,7 @@ import (
)
// Grayscale converts the image to grayscale mode.
func (c *Carver) Grayscale(src *image.NRGBA) *image.NRGBA {
func (p *Processor) Grayscale(src *image.NRGBA) *image.NRGBA {
dx, dy := src.Bounds().Max.X, src.Bounds().Max.Y
dst := image.NewNRGBA(src.Bounds())
+104
View File
@@ -0,0 +1,104 @@
package caire
import (
"image"
"math"
"gioui.org/app"
"gioui.org/io/key"
"gioui.org/io/system"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/paint"
"gioui.org/unit"
"gioui.org/widget"
)
const (
MaxScreenX = 1366
MaxScreenY = 768
)
// showPreview spawn a new Gio GUI window and updates its content
// with the resized image recived from a channel.
func (p *Processor) showPreview(
workerChan <-chan worker,
errChan chan<- error,
guiParams struct {
width int
height int
},
) {
width, height := guiParams.width, guiParams.height
newWidth, newHeight := float64(width), float64(height)
// Resize the image but retain the aspect ratio in case the
// image width and height is greater than the predefined window.
if width > MaxScreenX && height > MaxScreenY {
widthRatio := float64(MaxScreenX) / float64(width)
heightRatio := float64(MaxScreenY) / float64(height)
ratio := math.Min(widthRatio, heightRatio)
newWidth = float64(width) * ratio
newHeight = float64(height) * ratio
}
// Create a new window.
w := app.NewWindow(
app.Title("Image resize in progress..."),
app.Size(unit.Px(float32(newWidth)), unit.Px(float32(newHeight))),
)
for err := range p.run(w, workerChan) {
errChan <- err
}
}
// run the Gio main thread until a DestroyEvent or an ESC key event is captured.
func (p *Processor) run(w *app.Window, workerChan <-chan worker) chan error {
var (
ops op.Ops
img image.Image
)
err := make(chan error)
go func() {
for {
select {
case e := <-w.Events():
switch e := e.(type) {
case system.FrameEvent:
gtx := layout.NewContext(&ops, e)
w.Invalidate()
if img != nil {
src := paint.NewImageOp(img)
src.Add(gtx.Ops)
imgWidget := widget.Image{
Src: src,
Scale: 1 / float32(gtx.Px(unit.Dp(1))),
Fit: widget.Contain,
}
imgWidget.Layout(gtx)
}
e.Frame(gtx.Ops)
case key.Event:
switch e.Name {
case key.NameEscape:
w.Close()
}
case system.DestroyEvent:
err <- e.Err
break
}
case worker := <-workerChan:
img = worker.img
if p.vRes {
img = worker.carver.RotateImage270(img.(*image.NRGBA))
}
w.Invalidate()
}
}
}()
return err
}
+144 -52
View File
@@ -2,6 +2,7 @@ package caire
import (
"embed"
"errors"
"fmt"
"image"
"image/color"
@@ -17,7 +18,6 @@ import (
"github.com/disintegration/imaging"
pigo "github.com/esimov/pigo/core"
"github.com/pkg/errors"
"golang.org/x/image/bmp"
)
@@ -26,12 +26,22 @@ var classifier embed.FS
var (
g *gif.GIF
xCount int
yCount int
resizeBothSide = false // used to tell that the image is resized both verticlaly and horizontally
rCount int
resizeBothSide = false // the image is resized both verticlaly and horizontally
isGif = false
)
var (
imgWorker = make(chan worker) // channel used to transfer the image to the GUI
errs = make(chan error)
)
// worker struct contains all the information needed for transfering the resized image to the Gio GUI.
type worker struct {
carver *Carver
img *image.NRGBA
}
// SeamCarver interface defines the Resize method.
// This needs to be implemented by every struct which declares a Resize method.
type SeamCarver interface {
@@ -53,9 +63,12 @@ type Processor struct {
Percentage bool
Square bool
Debug bool
Preview bool
FaceDetect bool
FaceAngle float64
PigoFaceDetector *pigo.Pigo
vRes bool
}
var (
@@ -83,7 +96,7 @@ func (p *Processor) Resize(img *image.NRGBA) (image.Image, error) {
pw, ph int
err error
)
xCount, yCount = 0, 0
rCount = 0
if p.NewWidth > c.Width {
newWidth = p.NewWidth - (p.NewWidth - (p.NewWidth - c.Width))
@@ -104,11 +117,11 @@ func (p *Processor) Resize(img *image.NRGBA) (image.Image, error) {
newHeight = p.NewHeight
}
// shrinkHorizFn calls itself iteratively and shrink the image horizontally.
// shrinkHorizFn calls itself recursively to shrink the image horizontally.
// If the image is resized on both X and Y axis it calls the shrink and enlarge
// function intermitently up until the desired dimension is reached.
// We are opting for this solution instead of resizing the image secventially,
// because we can merge more seamlessly together the horizontal and vertical seams.
// because this way the horizontal and vertical seams are merged together seamlessly.
shrinkHorizFn = func(c *Carver, img *image.NRGBA) (*image.NRGBA, error) {
dx, dy := img.Bounds().Dx(), img.Bounds().Dy()
if dx > p.NewWidth {
@@ -126,11 +139,11 @@ func (p *Processor) Resize(img *image.NRGBA) (image.Image, error) {
img, _ = shrinkHorizFn(c, img)
}
}
xCount++
rCount++
return img, nil
}
// enlargeHorizFn calls itself iteratively and enlarge the image horizontally.
// enlargeHorizFn calls itself recursively to enlarge the image horizontally.
enlargeHorizFn = func(c *Carver, img *image.NRGBA) (*image.NRGBA, error) {
dx, dy := img.Bounds().Dx(), img.Bounds().Dy()
if dx < p.NewWidth {
@@ -148,20 +161,23 @@ func (p *Processor) Resize(img *image.NRGBA) (image.Image, error) {
img, _ = enlargeHorizFn(c, img)
}
}
rCount++
return img, nil
}
// shrinkVertFn calls itself iteratively and shrink the image vertically.
// shrinkVertFn calls itself recursively to shrink the image vertically.
shrinkVertFn = func(c *Carver, img *image.NRGBA) (*image.NRGBA, error) {
p.vRes = true
dx, dy := img.Bounds().Dx(), img.Bounds().Dy()
// If the image is resized on both side we need to rotate the image
// each time we are invoking the shrink function.
// Otherwise if we are resizing the image on one side only we can invoke
// the rotating function only once, right before calling this function.
// If the image is resized both horizontally and vertically we need
// to rotate the image each time we are invoking the shrink function.
// Otherwise we rotate the image only once, right before calling this function.
if resizeBothSide {
dx, dy = img.Bounds().Dy(), img.Bounds().Dx()
img = c.RotateImage90(img)
}
if dy > p.NewHeight {
if dx > p.NewHeight {
img, err = p.shrink(c, img)
if err != nil {
return nil, err
@@ -169,8 +185,8 @@ func (p *Processor) Resize(img *image.NRGBA) (image.Image, error) {
if resizeBothSide {
img = c.RotateImage270(img)
}
if p.NewWidth > 0 && p.NewWidth != dx {
if p.NewWidth <= dx {
if p.NewWidth > 0 && p.NewWidth != dy {
if p.NewWidth <= dy {
img, _ = shrinkHorizFn(c, img)
} else {
img, _ = enlargeHorizFn(c, img)
@@ -183,17 +199,20 @@ func (p *Processor) Resize(img *image.NRGBA) (image.Image, error) {
img = c.RotateImage270(img)
}
}
yCount++
rCount++
return img, nil
}
// shrinkVertFn calls itself iteratively and enlarge the image vertically.
// enlargeVertFn calls itself recursively to enlarge the image vertically.
enlargeVertFn = func(c *Carver, img *image.NRGBA) (*image.NRGBA, error) {
p.vRes = true
dx, dy := img.Bounds().Dx(), img.Bounds().Dy()
if resizeBothSide {
dx, dy = img.Bounds().Dy(), img.Bounds().Dx()
img = c.RotateImage90(img)
}
if dy < p.NewHeight {
if dx < p.NewHeight {
img, err = p.enlarge(c, img)
if err != nil {
return nil, err
@@ -201,8 +220,8 @@ func (p *Processor) Resize(img *image.NRGBA) (image.Image, error) {
if resizeBothSide {
img = c.RotateImage270(img)
}
if p.NewWidth > 0 && p.NewWidth != dx {
if p.NewWidth <= dx {
if p.NewWidth > 0 && p.NewWidth != dy {
if p.NewWidth <= dy {
img, _ = shrinkHorizFn(c, img)
} else {
img, _ = enlargeHorizFn(c, img)
@@ -215,6 +234,7 @@ func (p *Processor) Resize(img *image.NRGBA) (image.Image, error) {
img = c.RotateImage270(img)
}
}
rCount++
return img, nil
}
@@ -223,16 +243,23 @@ func (p *Processor) Resize(img *image.NRGBA) (image.Image, error) {
}
if p.Percentage || p.Square {
// When square option is used the image will be resized to a square based on the shortest edge.
pw = c.Width - c.Height
ph = c.Height - c.Width
// In case pw and ph is zero, it means that the target image is square.
// In this case we can simply resize the image without running the carving operation.
if pw == 0 && ph == 0 {
return imaging.Resize(img, p.NewWidth, p.NewHeight, imaging.Lanczos), nil
if p.Percentage && pw == 0 && ph == 0 {
pw = c.Width - int(float64(c.Width)-(float64(p.NewWidth)/100*float64(c.Width)))
ph = c.Height - int(float64(c.Height)-(float64(p.NewHeight)/100*float64(c.Height)))
p.NewWidth = absint(c.Width - pw)
p.NewHeight = absint(c.Height - ph)
resImgSize := min(p.NewWidth, p.NewHeight)
return imaging.Resize(img, resImgSize, 0, imaging.Lanczos), nil
}
// When the square option is used the image will be resized to a square based on the shortest edge.
if p.Square {
// Calling the image rescale method only when both a new width and height is provided.
if p.NewWidth != 0 && p.NewHeight != 0 {
@@ -268,11 +295,14 @@ func (p *Processor) Resize(img *image.NRGBA) (image.Image, error) {
pw = c.Width - int(float64(c.Width)-(float64(p.NewWidth)/100*float64(c.Width)))
ph = c.Height - int(float64(c.Height)-(float64(p.NewHeight)/100*float64(c.Height)))
p.NewWidth = absint(c.Width - pw)
p.NewHeight = absint(c.Height - ph)
if pw > newWidth || ph > newHeight {
return nil, errors.New("the generated image size should be less than the original image size")
if p.NewWidth != 0 {
p.NewWidth = absint(c.Width - pw)
}
if p.NewHeight != 0 {
p.NewHeight = absint(c.Height - ph)
}
if pw >= c.Width || ph >= c.Height {
return nil, errors.New("cannot use the percentage flag for image enlargement")
}
}
}
@@ -312,22 +342,16 @@ func (p *Processor) Resize(img *image.NRGBA) (image.Image, error) {
// Run the carver function if the desired image height is not identical with the rescaled image height.
if newHeight > 0 && p.NewHeight != c.Height {
if !resizeBothSide {
img = c.RotateImage90(img)
}
if p.NewHeight > c.Height {
if !resizeBothSide {
img = c.RotateImage90(img)
}
img, _ = enlargeVertFn(c, img)
if !resizeBothSide {
img = c.RotateImage270(img)
}
} else {
if !resizeBothSide {
img = c.RotateImage90(img)
}
img, _ = shrinkVertFn(c, img)
if !resizeBothSide {
img = c.RotateImage270(img)
}
}
if !resizeBothSide {
img = c.RotateImage270(img)
}
}
@@ -385,13 +409,34 @@ func (p *Processor) Process(r io.Reader, w io.Writer) error {
}
}
g = new(gif.GIF)
src, _, err := image.Decode(r)
if err != nil {
return err
}
img := p.imgToNRGBA(src)
if p.Preview {
guiWidth := img.Bounds().Max.X
guiHeight := img.Bounds().Max.Y
if p.NewWidth > guiWidth {
guiWidth = p.NewWidth
}
if p.NewHeight > guiHeight {
guiHeight = p.NewHeight
}
guiParams := struct {
width int
height int
}{
width: guiWidth,
height: guiHeight,
}
// Lunch Gio GUI thread.
go p.showPreview(imgWorker, errs, guiParams)
}
switch w.(type) {
case *os.File:
ext := filepath.Ext(w.(*os.File).Name())
@@ -415,12 +460,13 @@ func (p *Processor) Process(r io.Reader, w io.Writer) error {
}
return bmp.Encode(w, res)
case ".gif":
g = new(gif.GIF)
isGif = true
_, err := Resize(p, img)
if err != nil {
return err
}
return writeGifToFile(w.(*os.File).Name())
return writeGifToFile(w.(*os.File).Name(), g)
default:
return errors.New("unsupported image format")
}
@@ -445,8 +491,19 @@ func (p *Processor) shrink(c *Carver, img *image.NRGBA) (*image.NRGBA, error) {
img = c.RemoveSeam(img, seams, p.Debug)
if isGif {
g = encodeImageToGif(img)
p.encodeImgToGif(c, img, g)
}
go func() {
select {
case imgWorker <- worker{
carver: c,
img: img,
}:
case <-errs:
return
}
}()
return img, nil
}
@@ -460,6 +517,21 @@ func (p *Processor) enlarge(c *Carver, img *image.NRGBA) (*image.NRGBA, error) {
seams := c.FindLowestEnergySeams()
img = c.AddSeam(img, seams, p.Debug)
if isGif {
p.encodeImgToGif(c, img, g)
}
go func() {
select {
case imgWorker <- worker{
carver: c,
img: img,
}:
case <-errs:
return
}
}()
return img, nil
}
@@ -521,19 +593,39 @@ func (p *Processor) imgToNRGBA(img image.Image) *image.NRGBA {
return dst
}
// encodeImageToGif encodes the provided image to a Gif file.
func encodeImageToGif(src image.Image) *gif.GIF {
bounds := src.Bounds()
dst := image.NewPaletted(image.Rect(0, 0, bounds.Dx()-xCount, bounds.Dy()-yCount), palette.Plan9)
// encodeImgToGif encodes the provided image to a Gif file.
func (p *Processor) encodeImgToGif(c *Carver, src image.Image, g *gif.GIF) {
dx, dy := src.Bounds().Max.X, src.Bounds().Max.Y
dst := image.NewPaletted(image.Rect(0, 0, dx, dy), palette.Plan9)
if p.NewHeight != 0 {
dst = image.NewPaletted(image.Rect(0, 0, dy, dx), palette.Plan9)
}
if p.NewWidth > dx {
dx += rCount
g.Config.Width = dst.Bounds().Max.X + 1
g.Config.Height = dst.Bounds().Max.Y + 1
} else {
dx -= rCount
}
if p.NewHeight > dx {
dx += rCount
g.Config.Width = dst.Bounds().Max.X + 1
g.Config.Height = dst.Bounds().Max.Y + 1
} else {
dx -= rCount
}
if p.NewHeight != 0 {
src = c.RotateImage270(src.(*image.NRGBA))
}
draw.Draw(dst, src.Bounds(), src, image.Point{}, draw.Src)
g.Image = append(g.Image, dst)
g.Delay = append(g.Delay, 0)
return g
}
// writeGifToFile writes the encoded Gif file to the destination file.
func writeGifToFile(path string) error {
func writeGifToFile(path string, g *gif.GIF) error {
f, err := os.Create(path)
if err != nil {
return err