Files
caire/process.go

702 lines
18 KiB
Go

package caire
import (
"embed"
"errors"
"fmt"
"image"
"image/color"
"image/color/palette"
"image/draw"
"image/gif"
"image/jpeg"
"image/png"
"io"
"math"
"os"
"path/filepath"
"strings"
"github.com/disintegration/imaging"
"github.com/esimov/caire/utils"
pigo "github.com/esimov/pigo/core"
"golang.org/x/image/bmp"
)
//go:embed data/facefinder
var classifier embed.FS
var (
g *gif.GIF
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 {
Resize(*image.NRGBA) (image.Image, error)
}
// shrinkFn is a generic function used to shrink an image.
type shrinkFn func(*Carver, *image.NRGBA) (*image.NRGBA, error)
// enlargeFn is a generic function used to enlarge an image.
type enlargeFn func(*Carver, *image.NRGBA) (*image.NRGBA, error)
// Processor options
type Processor struct {
SobelThreshold int
BlurRadius int
NewWidth int
NewHeight int
Percentage bool
Square bool
Debug bool
Preview bool
FaceDetect bool
ShapeType string
SeamColor string
MaskPath string
RMaskPath string
Mask image.Image
RMask image.Image
FaceAngle float64
PigoFaceDetector *pigo.Pigo
Spinner *utils.Spinner
vRes bool
}
var (
shrinkHorizFn shrinkFn
shrinkVertFn shrinkFn
enlargeHorizFn enlargeFn
enlargeVertFn enlargeFn
)
// resize implements the Resize method of the Carver interface.
// It returns the concrete resize operation method.
func resize(s SeamCarver, img *image.NRGBA) (image.Image, error) {
return s.Resize(img)
}
// Resize 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 c = NewCarver(img.Bounds().Dx(), img.Bounds().Dy())
var (
newImg image.Image
newWidth int
newHeight int
pw, ph int
err error
)
rCount = 0
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
}
if p.NewHeight != 0 && len(p.MaskPath) > 0 {
p.Mask = c.RotateImage90(p.Mask.(*image.NRGBA))
}
if p.NewHeight != 0 && len(p.RMaskPath) > 0 {
p.RMask = c.RotateImage90(p.RMask.(*image.NRGBA))
}
// 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 this way the horizontal and vertical seams are merged together seamlessly.
shrinkHorizFn = func(c *Carver, 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(c, img)
if err != nil {
return nil, err
}
if p.NewHeight > 0 && p.NewHeight != dy {
if p.NewHeight <= dy {
img, _ = shrinkVertFn(c, img)
} else {
img, _ = enlargeVertFn(c, img)
}
} else {
img, _ = shrinkHorizFn(c, img)
}
}
rCount++
return img, nil
}
// enlargeHorizFn calls itself recursively to enlarge the image horizontally.
enlargeHorizFn = func(c *Carver, 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(c, img)
if err != nil {
return nil, err
}
if p.NewHeight > 0 && p.NewHeight != dy {
if p.NewHeight <= dy {
img, _ = shrinkVertFn(c, img)
} else {
img, _ = enlargeVertFn(c, img)
}
} else {
img, _ = enlargeHorizFn(c, img)
}
}
rCount++
return img, nil
}
// 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 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 dx > p.NewHeight {
img, err = p.shrink(c, img)
if err != nil {
return nil, err
}
if resizeBothSide {
img = c.RotateImage270(img)
}
if p.NewWidth > 0 && p.NewWidth != dy {
if p.NewWidth <= dy {
img, _ = shrinkHorizFn(c, img)
} else {
img, _ = enlargeHorizFn(c, img)
}
} else {
img, _ = shrinkVertFn(c, img)
}
} else {
if resizeBothSide {
img = c.RotateImage270(img)
}
}
rCount++
return img, nil
}
// 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 dx < p.NewHeight {
img, err = p.enlarge(c, img)
if err != nil {
return nil, err
}
if resizeBothSide {
img = c.RotateImage270(img)
}
if p.NewWidth > 0 && p.NewWidth != dy {
if p.NewWidth <= dy {
img, _ = shrinkHorizFn(c, img)
} else {
img, _ = enlargeHorizFn(c, img)
}
} else {
img, _ = enlargeVertFn(c, img)
}
} else {
if resizeBothSide {
img = c.RotateImage270(img)
}
}
rCount++
return img, nil
}
if p.NewWidth != 0 && p.NewHeight != 0 {
resizeBothSide = true
}
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)
if newImg != nil {
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()
if nw > nh {
pw = nw - nh
ph = 0
} else {
ph = nh - nw
pw = 0
}
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's 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, _ = enlargeHorizFn(c, img)
} else {
img, _ = shrinkHorizFn(c, img)
}
}
// 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 {
img, _ = enlargeVertFn(c, img)
} else {
img, _ = shrinkVertFn(c, img)
}
if !resizeBothSide {
img = c.RotateImage270(img)
}
}
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)
} else {
newImg = imaging.Resize(img, 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 {
img = 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
// Instantiate a new Pigo object in case the face detection option is used.
p.PigoFaceDetector = pigo.NewPigo()
if p.FaceDetect {
cascadeFile, err := classifier.ReadFile("data/facefinder")
if err != nil {
return fmt.Errorf("error reading the cascade file: %v", err)
}
// 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.PigoFaceDetector, err = p.PigoFaceDetector.Unpack(cascadeFile)
if err != nil {
return fmt.Errorf("error unpacking the cascade file: %v", err)
}
}
src, _, err := image.Decode(r)
if err != nil {
return err
}
img := p.imgToNRGBA(src)
if len(p.MaskPath) > 0 {
mf, err := os.Open(p.MaskPath)
if err != nil {
return fmt.Errorf("could not open the mask file: %v", err)
}
ctype, err := utils.DetectContentType(mf.Name())
if err != nil {
return err
}
if !strings.Contains(ctype.(string), "image") {
return fmt.Errorf("the mask should be an image file")
}
mask, _, err := image.Decode(mf)
if err != nil {
return fmt.Errorf("could not decode the mask file: %v", err)
}
p.Mask = p.imgToNRGBA(mask)
}
if len(p.RMaskPath) > 0 {
rmf, err := os.Open(p.RMaskPath)
if err != nil {
return fmt.Errorf("could not open the mask file: %v", err)
}
ctype, err := utils.DetectContentType(rmf.Name())
if err != nil {
return err
}
if !strings.Contains(ctype.(string), "image") {
return fmt.Errorf("the mask should be an image file")
}
rmask, _, err := image.Decode(rmf)
if err != nil {
return fmt.Errorf("could not decode the mask file: %v", err)
}
p.RMask = p.imgToNRGBA(rmask)
}
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 := w.(type) {
case *os.File:
ext := filepath.Ext(w.Name())
switch ext {
case "", ".jpg", ".jpeg":
res, err := resize(p, img)
if err != nil {
return err
}
return jpeg.Encode(w, res, &jpeg.Options{Quality: 100})
case ".png":
res, err := resize(p, img)
if err != nil {
return err
}
return png.Encode(w, res)
case ".bmp":
res, err := resize(p, img)
if err != nil {
return err
}
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.Name(), g)
default:
return errors.New("unsupported image format")
}
default:
res, err := resize(p, img)
if err != nil {
return err
}
return jpeg.Encode(w, res, &jpeg.Options{Quality: 100})
}
}
// shrink reduces the image dimension either horizontally or vertically.
func (p *Processor) shrink(c *Carver, 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 len(p.MaskPath) > 0 {
p.Mask = c.RemoveSeam(p.Mask.(*image.NRGBA), seams, false)
}
if len(p.RMaskPath) > 0 {
p.RMask = c.RemoveSeam(p.RMask.(*image.NRGBA), seams, false)
}
if isGif {
p.encodeImgToGif(c, img, g)
}
go func() {
select {
case imgWorker <- worker{
carver: c,
img: img,
}:
case <-errs:
return
}
}()
return img, nil
}
// enlarge increases the image dimension either horizontally or vertically.
func (p *Processor) enlarge(c *Carver, 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 isGif {
p.encodeImgToGif(c, img, g)
}
go func() {
select {
case imgWorker <- worker{
carver: c,
img: img,
}:
case <-errs:
return
}
}()
return img, nil
}
// imgToNRGBA converts any image type to *image.NRGBA with min-point at (0, 0).
func (p *Processor) imgToNRGBA(img image.Image) *image.NRGBA {
srcBounds := img.Bounds()
if srcBounds.Min.X == 0 && srcBounds.Min.Y == 0 {
if src0, ok := img.(*image.NRGBA); ok {
return src0
}
}
srcMinX := srcBounds.Min.X
srcMinY := srcBounds.Min.Y
dstBounds := srcBounds.Sub(srcBounds.Min)
dstW := dstBounds.Dx()
dstH := dstBounds.Dy()
dst := image.NewNRGBA(dstBounds)
switch src := img.(type) {
case *image.NRGBA:
rowSize := srcBounds.Dx() * 4
for dstY := 0; dstY < dstH; dstY++ {
di := dst.PixOffset(0, dstY)
si := src.PixOffset(srcMinX, srcMinY+dstY)
for dstX := 0; dstX < dstW; dstX++ {
copy(dst.Pix[di:di+rowSize], src.Pix[si:si+rowSize])
}
}
case *image.YCbCr:
for dstY := 0; dstY < dstH; dstY++ {
di := dst.PixOffset(0, dstY)
for dstX := 0; dstX < dstW; dstX++ {
srcX := srcMinX + dstX
srcY := srcMinY + dstY
siy := src.YOffset(srcX, srcY)
sic := src.COffset(srcX, srcY)
r, g, b := color.YCbCrToRGB(src.Y[siy], src.Cb[sic], src.Cr[sic])
dst.Pix[di+0] = r
dst.Pix[di+1] = g
dst.Pix[di+2] = b
dst.Pix[di+3] = 0xff
di += 4
}
}
default:
for dstY := 0; dstY < dstH; dstY++ {
di := dst.PixOffset(0, dstY)
for dstX := 0; dstX < dstW; dstX++ {
c := color.NRGBAModel.Convert(img.At(srcMinX+dstX, srcMinY+dstY)).(color.NRGBA)
dst.Pix[di+0] = c.R
dst.Pix[di+1] = c.G
dst.Pix[di+2] = c.B
dst.Pix[di+3] = c.A
di += 4
}
}
}
return dst
}
// 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)
}
// writeGifToFile writes the encoded Gif file to the destination file.
func writeGifToFile(path string, g *gif.GIF) error {
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
return gif.EncodeAll(f, g)
}