mirror of
https://github.com/esimov/caire.git
synced 2025-09-26 20:41:14 +08:00
574 lines
14 KiB
Go
574 lines
14 KiB
Go
package caire
|
|
|
|
import (
|
|
_ "embed"
|
|
"errors"
|
|
"fmt"
|
|
"image"
|
|
"image/draw"
|
|
"io"
|
|
"math"
|
|
|
|
"github.com/disintegration/imaging"
|
|
"github.com/esimov/caire/utils"
|
|
pigo "github.com/esimov/pigo/core"
|
|
)
|
|
|
|
//go:embed data/facefinder
|
|
var cascadeFile []byte
|
|
|
|
var (
|
|
resizeXY = false // the image is resized both vertically and horizontally
|
|
|
|
imgWorker = make(chan worker) // channel used to transfer the image to the GUI
|
|
errs = make(chan error)
|
|
)
|
|
|
|
var _ SeamCarver = (*Processor)(nil)
|
|
|
|
// worker struct contains all the information needed for transferring the resized image to the Gio GUI.
|
|
type worker struct {
|
|
img *image.NRGBA
|
|
mask *image.NRGBA
|
|
seams []Seam
|
|
done bool
|
|
}
|
|
|
|
// shrinkFn is a generic function used to shrink an image.
|
|
type shrinkFn func(*image.NRGBA) (*image.NRGBA, error)
|
|
|
|
// enlargeFn is a generic function used to enlarge an image.
|
|
type enlargeFn func(*image.NRGBA) (*image.NRGBA, error)
|
|
|
|
// Processor options
|
|
type Processor struct {
|
|
FaceAngle float64
|
|
SeamColor string
|
|
MaskPath string
|
|
RMaskPath string
|
|
ShapeType ShapeType
|
|
SobelThreshold int
|
|
BlurRadius int
|
|
NewWidth int
|
|
NewHeight int
|
|
FaceDetector *pigo.Pigo
|
|
Spinner *utils.Spinner
|
|
Mask *image.NRGBA
|
|
RMask *image.NRGBA
|
|
DebugMask *image.NRGBA
|
|
Percentage bool
|
|
Square bool
|
|
Debug bool
|
|
Preview bool
|
|
FaceDetect bool
|
|
vRes bool
|
|
}
|
|
|
|
var (
|
|
shrinkHorizFn shrinkFn
|
|
shrinkVertFn shrinkFn
|
|
enlargeHorizFn enlargeFn
|
|
enlargeVertFn enlargeFn
|
|
)
|
|
|
|
// Carve is the main entry point for the image resize operation.
|
|
// The new image can be resized either horizontally or vertically (or both).
|
|
// Depending on the provided options the image can be either reduced or enlarged.
|
|
func (p *Processor) Resize(img *image.NRGBA) (image.Image, error) {
|
|
var (
|
|
newImg image.Image
|
|
newWidth int
|
|
newHeight int
|
|
pw, ph int
|
|
err error
|
|
)
|
|
c := NewCarver(img.Bounds().Dx(), img.Bounds().Dy())
|
|
|
|
if p.NewWidth > c.Width {
|
|
newWidth = p.NewWidth - (p.NewWidth - (p.NewWidth - c.Width))
|
|
} else {
|
|
newWidth = c.Width - (c.Width - (c.Width - p.NewWidth))
|
|
}
|
|
|
|
if p.NewHeight > c.Height {
|
|
newHeight = p.NewHeight - (p.NewHeight - (p.NewHeight - c.Height))
|
|
} else {
|
|
newHeight = c.Height - (c.Height - (c.Height - p.NewHeight))
|
|
}
|
|
|
|
if p.NewWidth == 0 {
|
|
newWidth = p.NewWidth
|
|
}
|
|
if p.NewHeight == 0 {
|
|
newHeight = p.NewHeight
|
|
}
|
|
|
|
// 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 intermittently up until the desired dimension is reached.
|
|
// I opted for this solution instead of resizing the image sequentially,
|
|
// to avoid resulting visual artefacts, since this way
|
|
// the horizontal and vertical seams are merged together seamlessly.
|
|
shrinkHorizFn = func(img *image.NRGBA) (*image.NRGBA, error) {
|
|
p.vRes = false
|
|
dx, dy := img.Bounds().Dx(), img.Bounds().Dy()
|
|
if dx > p.NewWidth {
|
|
img, err = p.shrink(img)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if p.NewHeight > 0 && p.NewHeight != dy {
|
|
if p.NewHeight <= dy {
|
|
img, err = shrinkVertFn(img)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
img, err = enlargeVertFn(img)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
} else {
|
|
img, err = shrinkHorizFn(img)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
return img, nil
|
|
}
|
|
|
|
// enlargeHorizFn calls itself recursively to enlarge the image horizontally.
|
|
enlargeHorizFn = func(img *image.NRGBA) (*image.NRGBA, error) {
|
|
p.vRes = false
|
|
dx, dy := img.Bounds().Dx(), img.Bounds().Dy()
|
|
if dx < p.NewWidth {
|
|
img, err = p.enlarge(img)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if p.NewHeight > 0 && p.NewHeight != dy {
|
|
if p.NewHeight <= dy {
|
|
img, err = shrinkVertFn(img)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
img, err = enlargeVertFn(img)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
} else {
|
|
img, err = enlargeHorizFn(img)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
return img, nil
|
|
}
|
|
|
|
// shrinkVertFn calls itself recursively to shrink the image vertically.
|
|
shrinkVertFn = func(img *image.NRGBA) (*image.NRGBA, error) {
|
|
p.vRes = true
|
|
dx, dy := img.Bounds().Dx(), img.Bounds().Dy()
|
|
|
|
// If the image is resized both horizontally and vertically we need
|
|
// to rotate the image each time when invoking the shrink function.
|
|
// Otherwise we rotate the image only once, right before calling this function.
|
|
if resizeXY {
|
|
dx, dy = img.Bounds().Dy(), img.Bounds().Dx()
|
|
img = rotateImage90(img)
|
|
}
|
|
if dx > p.NewHeight {
|
|
img, err = p.shrink(img)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resizeXY {
|
|
img = rotateImage270(img)
|
|
}
|
|
if p.NewWidth > 0 && p.NewWidth != dy {
|
|
if p.NewWidth <= dy {
|
|
img, err = shrinkHorizFn(img)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
img, err = enlargeHorizFn(img)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
} else {
|
|
img, err = shrinkVertFn(img)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
} else {
|
|
if resizeXY {
|
|
img = rotateImage270(img)
|
|
}
|
|
}
|
|
return img, nil
|
|
}
|
|
|
|
// enlargeVertFn calls itself recursively to enlarge the image vertically.
|
|
enlargeVertFn = func(img *image.NRGBA) (*image.NRGBA, error) {
|
|
p.vRes = true
|
|
dx, dy := img.Bounds().Dx(), img.Bounds().Dy()
|
|
|
|
if resizeXY {
|
|
dx, dy = img.Bounds().Dy(), img.Bounds().Dx()
|
|
img = rotateImage90(img)
|
|
}
|
|
if dx < p.NewHeight {
|
|
img, err = p.enlarge(img)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resizeXY {
|
|
img = rotateImage270(img)
|
|
}
|
|
if p.NewWidth > 0 && p.NewWidth != dy {
|
|
if p.NewWidth <= dy {
|
|
img, err = shrinkHorizFn(img)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
img, err = enlargeHorizFn(img)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
} else {
|
|
img, err = enlargeVertFn(img)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
} else {
|
|
if resizeXY {
|
|
img = rotateImage270(img)
|
|
}
|
|
}
|
|
return img, nil
|
|
}
|
|
|
|
if p.Percentage || p.Square {
|
|
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 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 = utils.Abs(c.Width - pw)
|
|
p.NewHeight = utils.Abs(c.Height - ph)
|
|
|
|
resImgSize := utils.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 {
|
|
p.NewWidth = utils.Min(p.NewWidth, p.NewHeight)
|
|
p.NewHeight = p.NewWidth
|
|
|
|
newImg = p.calculateFitness(img, c)
|
|
dst := image.NewNRGBA(newImg.Bounds())
|
|
draw.Draw(dst, newImg.Bounds(), newImg, image.Point{}, draw.Src)
|
|
img = dst
|
|
|
|
nw, nh := img.Bounds().Dx(), img.Bounds().Dy()
|
|
|
|
p.NewWidth = utils.Min(nw, nh)
|
|
p.NewHeight = p.NewWidth
|
|
} else {
|
|
return nil, errors.New("please provide a new WIDTH and HEIGHT when using the square option")
|
|
}
|
|
}
|
|
|
|
// Use the Percentage flag only for shrinking the image.
|
|
if p.Percentage {
|
|
// Calculate the new image size based on the provided percentage.
|
|
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)))
|
|
|
|
if p.NewWidth != 0 {
|
|
p.NewWidth = utils.Abs(c.Width - pw)
|
|
}
|
|
if p.NewHeight != 0 {
|
|
p.NewHeight = utils.Abs(c.Height - ph)
|
|
}
|
|
if pw >= c.Width || ph >= c.Height {
|
|
return nil, errors.New("cannot use the percentage flag for image enlargement")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Rescale the image when it is resized both horizontally and vertically.
|
|
// First the image is scaled down or up by preserving the image aspect ratio,
|
|
// then the seam carving algorithm is applied only to the remaining pixels.
|
|
|
|
// Scale the width and height by the smaller factor (i.e Min(wScaleFactor, hScaleFactor))
|
|
// Example: input: 5000x2500, scale: 2160x1080, final target: 1920x1080
|
|
if (c.Width > p.NewWidth && c.Height > p.NewHeight) &&
|
|
(p.NewWidth != 0 && p.NewHeight != 0) {
|
|
|
|
newImg = p.calculateFitness(img, c)
|
|
|
|
dx0, dy0 := img.Bounds().Max.X, img.Bounds().Max.Y
|
|
dx1, dy1 := newImg.Bounds().Max.X, newImg.Bounds().Max.Y
|
|
|
|
// Rescale the image when the new image width or height are preserved, otherwise
|
|
// it might happen, that the generated image size does not match with the requested image size.
|
|
if !((p.NewWidth == 0 && dx0 == dx1) || (p.NewHeight == 0 && dy0 == dy1)) {
|
|
dst := image.NewNRGBA(newImg.Bounds())
|
|
draw.Draw(dst, newImg.Bounds(), newImg, image.Point{}, draw.Src)
|
|
img = dst
|
|
}
|
|
}
|
|
|
|
// Run the carver function if the desired image width is not identical with the rescaled image width.
|
|
if newWidth > 0 && p.NewWidth != c.Width {
|
|
if p.NewWidth > c.Width {
|
|
img, err = enlargeHorizFn(img)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
img, err = shrinkHorizFn(img)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 !resizeXY {
|
|
img = rotateImage90(img)
|
|
|
|
if p.Mask != nil {
|
|
p.Mask = rotateImage90(p.Mask)
|
|
}
|
|
if p.RMask != nil {
|
|
p.RMask = rotateImage90(p.RMask)
|
|
}
|
|
}
|
|
if p.NewHeight > c.Height {
|
|
img, err = enlargeVertFn(img)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
img, err = shrinkVertFn(img)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if !resizeXY {
|
|
img = rotateImage270(img)
|
|
|
|
if p.Mask != nil {
|
|
p.Mask = rotateImage270(p.Mask)
|
|
}
|
|
if p.RMask != nil {
|
|
p.RMask = rotateImage270(p.RMask)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Signal that the process is done and no more data is sent through the channel.
|
|
go func() {
|
|
imgWorker <- worker{
|
|
img: nil,
|
|
mask: nil,
|
|
seams: nil,
|
|
done: true,
|
|
}
|
|
}()
|
|
|
|
return img, nil
|
|
}
|
|
|
|
// calculateFitness iteratively try to find the best image aspect ratio for the rescale.
|
|
func (p *Processor) calculateFitness(img *image.NRGBA, c *Carver) *image.NRGBA {
|
|
var (
|
|
w = float64(c.Width)
|
|
h = float64(c.Height)
|
|
nw = float64(p.NewWidth)
|
|
nh = float64(p.NewHeight)
|
|
newImg *image.NRGBA
|
|
)
|
|
wsf := w / nw
|
|
hsf := h / nh
|
|
sw := math.Round(w / math.Min(wsf, hsf))
|
|
sh := math.Round(h / math.Min(wsf, hsf))
|
|
|
|
if sw <= sh {
|
|
newImg = imaging.Resize(img, 0, int(sw), imaging.Lanczos)
|
|
if p.Mask != nil {
|
|
p.Mask = imaging.Resize(p.Mask, 0, int(sw), imaging.Lanczos)
|
|
}
|
|
if p.RMask != nil {
|
|
p.RMask = imaging.Resize(p.RMask, 0, int(sw), imaging.Lanczos)
|
|
}
|
|
} else {
|
|
newImg = imaging.Resize(img, 0, int(sh), imaging.Lanczos)
|
|
if p.Mask != nil {
|
|
p.Mask = imaging.Resize(p.Mask, 0, int(sh), imaging.Lanczos)
|
|
}
|
|
if p.RMask != nil {
|
|
p.RMask = imaging.Resize(p.RMask, 0, int(sh), imaging.Lanczos)
|
|
}
|
|
}
|
|
dx, dy := newImg.Bounds().Max.X, newImg.Bounds().Max.Y
|
|
c.Width = dx
|
|
c.Height = dy
|
|
|
|
if int(sw) < p.NewWidth || int(sh) < p.NewHeight {
|
|
newImg = p.calculateFitness(newImg, c)
|
|
}
|
|
return newImg
|
|
}
|
|
|
|
// Process encodes the resized image into an io.Writer interface.
|
|
// We are using the io package, since we can provide different input and output types,
|
|
// as long as they implement the io.Reader and io.Writer interface.
|
|
func (p *Processor) Process(r io.Reader, w io.Writer) error {
|
|
var err error
|
|
|
|
if p.FaceDetect {
|
|
// Instantiate a new Pigo object in case the face detection option is used.
|
|
p.FaceDetector = pigo.NewPigo()
|
|
|
|
// Unpack the binary file. This will return the number of cascade trees,
|
|
// the tree depth, the threshold and the prediction from tree's leaf nodes.
|
|
p.FaceDetector, err = p.FaceDetector.Unpack(cascadeFile)
|
|
if err != nil {
|
|
return fmt.Errorf("error unpacking the cascade file: %v", err)
|
|
}
|
|
}
|
|
|
|
if p.NewWidth != 0 && p.NewHeight != 0 {
|
|
resizeXY = true
|
|
}
|
|
|
|
src, _, err := image.Decode(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
img := 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
|
|
}
|
|
if resizeXY {
|
|
guiWidth = int(maxScreenX)
|
|
guiHeight = int(maxScreenY)
|
|
}
|
|
|
|
guiWindow := struct {
|
|
width int
|
|
height int
|
|
}{
|
|
width: guiWidth,
|
|
height: guiHeight,
|
|
}
|
|
// Lunch Gio GUI thread.
|
|
go p.showPreview(imgWorker, errs, guiWindow)
|
|
}
|
|
|
|
return encodeImg(p, w, img)
|
|
}
|
|
|
|
// shrink reduces the image dimension either horizontally or vertically.
|
|
func (p *Processor) shrink(img *image.NRGBA) (*image.NRGBA, error) {
|
|
width, height := img.Bounds().Max.X, img.Bounds().Max.Y
|
|
c := NewCarver(width, height)
|
|
|
|
if _, err := c.ComputeSeams(p, img); err != nil {
|
|
return nil, err
|
|
}
|
|
seams := c.FindLowestEnergySeams(p)
|
|
img = c.RemoveSeam(img, seams, p.Debug)
|
|
|
|
if p.Mask != nil {
|
|
p.Mask = c.RemoveSeam(p.Mask, seams, false)
|
|
draw.Draw(p.DebugMask, img.Bounds(), p.Mask, image.Point{}, draw.Over)
|
|
}
|
|
|
|
if p.RMask != nil {
|
|
p.RMask = c.RemoveSeam(p.RMask, seams, false)
|
|
draw.Draw(p.DebugMask, img.Bounds(), p.RMask, image.Point{}, draw.Over)
|
|
}
|
|
|
|
go func() {
|
|
select {
|
|
case imgWorker <- worker{
|
|
img: img,
|
|
mask: p.DebugMask,
|
|
seams: c.Seams,
|
|
done: false,
|
|
}:
|
|
case <-errs:
|
|
return
|
|
}
|
|
}()
|
|
return img, nil
|
|
}
|
|
|
|
// enlarge increases the image dimension either horizontally or vertically.
|
|
func (p *Processor) enlarge(img *image.NRGBA) (*image.NRGBA, error) {
|
|
width, height := img.Bounds().Max.X, img.Bounds().Max.Y
|
|
c := NewCarver(width, height)
|
|
|
|
if _, err := c.ComputeSeams(p, img); err != nil {
|
|
return nil, err
|
|
}
|
|
seams := c.FindLowestEnergySeams(p)
|
|
img = c.AddSeam(img, seams, p.Debug)
|
|
|
|
if p.Mask != nil {
|
|
p.Mask = c.AddSeam(p.Mask, seams, false)
|
|
p.DebugMask = p.Mask
|
|
}
|
|
|
|
if p.RMask != nil {
|
|
p.RMask = c.AddSeam(p.RMask, seams, false)
|
|
p.DebugMask = p.RMask
|
|
}
|
|
|
|
go func() {
|
|
select {
|
|
case imgWorker <- worker{
|
|
img: img,
|
|
mask: p.DebugMask,
|
|
seams: c.Seams,
|
|
done: false,
|
|
}:
|
|
case <-errs:
|
|
return
|
|
}
|
|
}()
|
|
return img, nil
|
|
}
|