From 02c6d4de69cb00624918166436f6a090a53315ae Mon Sep 17 00:00:00 2001 From: esimov Date: Mon, 27 Dec 2021 10:57:36 +0200 Subject: [PATCH] feat: support for binary file masks #68 --- carver.go | 66 ++++++++++++++++++++++++++++---- cmd/caire/main.go | 5 +++ draw.go | 4 +- gui.go | 2 + process.go | 72 ++++++++++++++++++++++++++++++----- sobel.go | 2 +- utils/download.go | 6 +-- utils/{utils.go => format.go} | 0 8 files changed, 135 insertions(+), 22 deletions(-) rename utils/{utils.go => format.go} (100%) diff --git a/carver.go b/carver.go index 123b813..406c7f5 100644 --- a/carver.go +++ b/carver.go @@ -13,8 +13,8 @@ import ( const maxFaceDetAttempts = 20 var ( - faceDetAttempts int - usedSeams []UsedSeams + usedSeams []UsedSeams + detAttempts int ) // Carver is the main entry struct having as parameters the newly generated image width, height and seam points. @@ -71,13 +71,13 @@ func (c *Carver) set(x, y int, px float64) { // // - the minimum energy level is calculated by summing up the current pixel value // with the minimum pixel value of the neighboring pixels from the previous row. -func (c *Carver) ComputeSeams(img *image.NRGBA, p *Processor) error { +func (c *Carver) ComputeSeams(p *Processor, img *image.NRGBA) error { var srcImg *image.NRGBA width, height := img.Bounds().Dx(), img.Bounds().Dy() sobel := c.SobelDetector(img, float64(p.SobelThreshold)) - if p.FaceDetect && faceDetAttempts < maxFaceDetAttempts { + if p.FaceDetect && detAttempts < maxFaceDetAttempts { var ratio float64 if width < height { @@ -115,11 +115,11 @@ func (c *Carver) ComputeSeams(img *image.NRGBA, p *Processor) error { if len(faces) == 0 { // Retry detecting faces for a certain amount of time. - if faceDetAttempts < maxFaceDetAttempts { - faceDetAttempts++ + if detAttempts < maxFaceDetAttempts { + detAttempts++ } } else { - faceDetAttempts = 0 + detAttempts = 0 } // Range over all the detected faces and draw a white rectangle mask over each of them. @@ -137,11 +137,63 @@ func (c *Carver) ComputeSeams(img *image.NRGBA, p *Processor) error { } } + // Traverse the pixel data of the binary file used for protecting the regions + // which we do not want to be altered by the seam carver, + // obtain the white patches and apply it to the sobel image. + if len(p.MaskPath) > 0 && p.Mask != nil { + for x := 0; x < width; x++ { + for y := 0; y < height; y++ { + r, g, b, a := p.Mask.At(x, y).RGBA() + if r>>8 == 0xff && g>>8 == 0xff && b>>8 == 0xff { + sobel.Set(x, y, color.RGBA{ + R: uint8(r >> 8), + G: uint8(g >> 8), + B: uint8(b >> 8), + A: uint8(a >> 8), + }) + } + } + } + } + + // Traverse the pixel data of the binary file used for protecting the regions + // we do not want to be altered by the seam carver, obtain the white patches, + // but this time inverse the colors to black and merge it back to the sobel image. + if len(p.RMaskPath) > 0 && p.RMask != nil { + dx, dy := p.RMask.Bounds().Max.X, p.RMask.Bounds().Max.Y + for x := 0; x < dx; x++ { + for y := 0; y < dy; y++ { + r, g, b, a := p.RMask.At(x, y).RGBA() + if r>>8 == 0xff && g>>8 == 0xff && b>>8 == 0xff { + sobel.Set(x, y, color.RGBA{ + R: uint8(0x0 & r >> 8), + G: uint8(0x0 & g >> 8), + B: uint8(0x0 & b >> 8), + A: uint8(a >> 8), + }) + } else { + sr, sg, sb, _ := sobel.At(x, y).RGBA() + r = uint32(min(int(sr>>8+sr>>8/2), 0xff)) + g = uint32(min(int(sg>>8+sg>>8/2), 0xff)) + b = uint32(min(int(sb>>8+sb>>8/2), 0xff)) + + sobel.Set(x, y, color.RGBA{ + R: uint8(r), + G: uint8(g), + B: uint8(b), + A: uint8(a >> 8), + }) + } + } + } + } + if p.BlurRadius > 0 { srcImg = c.StackBlur(sobel, uint32(p.BlurRadius)) } else { srcImg = sobel } + for x := 0; x < c.Width; x++ { for y := 0; y < c.Height; y++ { r, _, _, a := srcImg.At(x, y).RGBA() diff --git a/cmd/caire/main.go b/cmd/caire/main.go index 47daa6f..347a77c 100644 --- a/cmd/caire/main.go +++ b/cmd/caire/main.go @@ -64,6 +64,8 @@ var ( square = flag.Bool("square", false, "Reduce image to square dimensions") debug = flag.Bool("debug", false, "Use debugger") preview = flag.Bool("preview", true, "Show GUI window") + maskPath = flag.String("mask", "", "Mask file path") // path to the binary file used for protecting the regions to not be removed. + rMaskPath = flag.String("rmask", "", "Remove mask file path") // path to the binary file used for removing the unwanted regions. faceDetect = flag.Bool("face", false, "Use face detection") faceAngle = flag.Float64("angle", 0.0, "Face rotation angle") workers = flag.Int("conc", runtime.NumCPU(), "Number of files to process concurrently") @@ -91,6 +93,8 @@ func main() { Preview: *preview, FaceDetect: *faceDetect, FaceAngle: *faceAngle, + MaskPath: *maskPath, + RMaskPath: *rMaskPath, } defaultMsg := fmt.Sprintf("%s %s", @@ -423,6 +427,7 @@ func printStatus(fname string, err error) { utils.DecorateText("\nError resizing the image: %s", utils.ErrorMessage), utils.DecorateText(fmt.Sprintf("\n\tReason: %v\n", err.Error()), utils.DefaultMessage), ) + os.Exit(0) } else { if fname != pipeName { fmt.Fprintf(os.Stderr, fmt.Sprintf("\nThe resized image has been saved as: %s %s\n\n", diff --git a/draw.go b/draw.go index fafaa44..3373029 100644 --- a/draw.go +++ b/draw.go @@ -23,6 +23,7 @@ const ( // It receives as parameters the shape type, the seam (x,y) coordinate and a size. func (g *Gui) DrawSeam(shape shapeType, x, y, s float64) { r := getRatio(g.cfg.window.w, g.cfg.window.h) + switch shape { case circle: g.drawCircle(x*r, y*r, s) @@ -129,8 +130,7 @@ func (g *Gui) getFillColor() color.Color { return g.cfg.color.fill } -// getRatio resizes the image but retain the aspect ratio in case the -// image width and height is greater than the predefined window. +// getRatio returns the image aspect ratio. func getRatio(w, h float64) float64 { var r float64 = 1 if w > maxScreenX && h > maxScreenY { diff --git a/gui.go b/gui.go index 9c26221..9d08371 100644 --- a/gui.go +++ b/gui.go @@ -88,6 +88,8 @@ func (g *Gui) initWindow(w, h int) { func (g *Gui) getWindowSize() (float64, float64) { w, h := g.cfg.window.w, g.cfg.window.h + // retains the aspect ratio in case the image width and height + // is greater than the predefined window. r := getRatio(w, h) if w > maxScreenX && h > maxScreenY { w = float64(w) * r diff --git a/process.go b/process.go index 7979517..ffeab6c 100644 --- a/process.go +++ b/process.go @@ -15,6 +15,7 @@ import ( "math" "os" "path/filepath" + "strings" "github.com/disintegration/imaging" "github.com/esimov/caire/utils" @@ -66,6 +67,10 @@ type Processor struct { Debug bool Preview bool FaceDetect bool + MaskPath string + RMaskPath string + Mask image.Image + RMask image.Image FaceAngle float64 PigoFaceDetector *pigo.Pigo Spinner *utils.Spinner @@ -81,9 +86,9 @@ var ( enlargeVertFn enlargeFn ) -// Resize implements the Resize method of the Carver interface. +// 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) { +func resize(s SeamCarver, img *image.NRGBA) (image.Image, error) { return s.Resize(img) } @@ -418,6 +423,48 @@ func (p *Processor) Process(r io.Reader, w io.Writer) error { } img := p.imgToNRGBA(src) + if len(p.MaskPath) > 0 { + mf, err := os.Open(p.MaskPath) + if err != nil { + return errors.New(fmt.Sprintf("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 errors.New("the mask should be an image file.") + } + + mask, _, err := image.Decode(mf) + if err != nil { + return errors.New(fmt.Sprintf("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 errors.New(fmt.Sprintf("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 errors.New("the mask should be an image file.") + } + + rmask, _, err := image.Decode(rmf) + if err != nil { + return errors.New(fmt.Sprintf("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 @@ -445,19 +492,19 @@ func (p *Processor) Process(r io.Reader, w io.Writer) error { ext := filepath.Ext(w.(*os.File).Name()) switch ext { case "", ".jpg", ".jpeg": - res, err := Resize(p, img) + 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) + res, err := resize(p, img) if err != nil { return err } return png.Encode(w, res) case ".bmp": - res, err := Resize(p, img) + res, err := resize(p, img) if err != nil { return err } @@ -465,7 +512,7 @@ func (p *Processor) Process(r io.Reader, w io.Writer) error { case ".gif": g = new(gif.GIF) isGif = true - _, err := Resize(p, img) + _, err := resize(p, img) if err != nil { return err } @@ -474,7 +521,7 @@ func (p *Processor) Process(r io.Reader, w io.Writer) error { return errors.New("unsupported image format") } default: - res, err := Resize(p, img) + res, err := resize(p, img) if err != nil { return err } @@ -486,12 +533,19 @@ func (p *Processor) Process(r io.Reader, w io.Writer) error { 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(img, p); err != nil { + if err := c.ComputeSeams(p, img); err != nil { return nil, err } seams := c.FindLowestEnergySeams() 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) } @@ -513,7 +567,7 @@ func (p *Processor) shrink(c *Carver, img *image.NRGBA) (*image.NRGBA, error) { 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(img, p); err != nil { + if err := c.ComputeSeams(p, img); err != nil { return nil, err } seams := c.FindLowestEnergySeams() diff --git a/sobel.go b/sobel.go index 2fe76be..8dd1ea9 100644 --- a/sobel.go +++ b/sobel.go @@ -82,7 +82,7 @@ func (c *Carver) SobelDetector(img *image.NRGBA, threshold float64) *image.NRGBA dst.Pix[idx] = uint8(edges[idx]) dst.Pix[idx+1] = uint8(edges[idx+1]) dst.Pix[idx+2] = uint8(edges[idx+2]) - dst.Pix[idx+3] = 255 + dst.Pix[idx+3] = 0xff } return dst } diff --git a/utils/download.go b/utils/download.go index 4df7342..a878582 100644 --- a/utils/download.go +++ b/utils/download.go @@ -36,7 +36,7 @@ func DownloadImage(url string) (*os.File, error) { if err != nil { return nil, errors.New(fmt.Sprintf("unable to copy the source URI into the destination file")) } - ctype, err := detectContentType(tmpfile.Name()) + ctype, err := DetectContentType(tmpfile.Name()) if err != nil { return nil, err } @@ -61,8 +61,8 @@ func IsValidUrl(uri string) bool { return true } -// detectContentType detects the file type by reading MIME type information of the file content. -func detectContentType(fname string) (interface{}, error) { +// DetectContentType detects the file type by reading MIME type information of the file content. +func DetectContentType(fname string) (interface{}, error) { file, err := os.Open(fname) if err != nil { return nil, err diff --git a/utils/utils.go b/utils/format.go similarity index 100% rename from utils/utils.go rename to utils/format.go