feat: support for binary file masks #68

This commit is contained in:
esimov
2021-12-27 10:57:36 +02:00
parent 04511d2267
commit 02c6d4de69
8 changed files with 135 additions and 22 deletions

View File

@@ -13,8 +13,8 @@ import (
const maxFaceDetAttempts = 20 const maxFaceDetAttempts = 20
var ( 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. // 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 // - 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. // 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 var srcImg *image.NRGBA
width, height := img.Bounds().Dx(), img.Bounds().Dy() width, height := img.Bounds().Dx(), img.Bounds().Dy()
sobel := c.SobelDetector(img, float64(p.SobelThreshold)) sobel := c.SobelDetector(img, float64(p.SobelThreshold))
if p.FaceDetect && faceDetAttempts < maxFaceDetAttempts { if p.FaceDetect && detAttempts < maxFaceDetAttempts {
var ratio float64 var ratio float64
if width < height { if width < height {
@@ -115,11 +115,11 @@ func (c *Carver) ComputeSeams(img *image.NRGBA, p *Processor) error {
if len(faces) == 0 { if len(faces) == 0 {
// Retry detecting faces for a certain amount of time. // Retry detecting faces for a certain amount of time.
if faceDetAttempts < maxFaceDetAttempts { if detAttempts < maxFaceDetAttempts {
faceDetAttempts++ detAttempts++
} }
} else { } else {
faceDetAttempts = 0 detAttempts = 0
} }
// Range over all the detected faces and draw a white rectangle mask over each of them. // 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 { if p.BlurRadius > 0 {
srcImg = c.StackBlur(sobel, uint32(p.BlurRadius)) srcImg = c.StackBlur(sobel, uint32(p.BlurRadius))
} else { } else {
srcImg = sobel srcImg = sobel
} }
for x := 0; x < c.Width; x++ { for x := 0; x < c.Width; x++ {
for y := 0; y < c.Height; y++ { for y := 0; y < c.Height; y++ {
r, _, _, a := srcImg.At(x, y).RGBA() r, _, _, a := srcImg.At(x, y).RGBA()

View File

@@ -64,6 +64,8 @@ var (
square = flag.Bool("square", false, "Reduce image to square dimensions") square = flag.Bool("square", false, "Reduce image to square dimensions")
debug = flag.Bool("debug", false, "Use debugger") debug = flag.Bool("debug", false, "Use debugger")
preview = flag.Bool("preview", true, "Show GUI window") 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") faceDetect = flag.Bool("face", false, "Use face detection")
faceAngle = flag.Float64("angle", 0.0, "Face rotation angle") faceAngle = flag.Float64("angle", 0.0, "Face rotation angle")
workers = flag.Int("conc", runtime.NumCPU(), "Number of files to process concurrently") workers = flag.Int("conc", runtime.NumCPU(), "Number of files to process concurrently")
@@ -91,6 +93,8 @@ func main() {
Preview: *preview, Preview: *preview,
FaceDetect: *faceDetect, FaceDetect: *faceDetect,
FaceAngle: *faceAngle, FaceAngle: *faceAngle,
MaskPath: *maskPath,
RMaskPath: *rMaskPath,
} }
defaultMsg := fmt.Sprintf("%s %s", 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("\nError resizing the image: %s", utils.ErrorMessage),
utils.DecorateText(fmt.Sprintf("\n\tReason: %v\n", err.Error()), utils.DefaultMessage), utils.DecorateText(fmt.Sprintf("\n\tReason: %v\n", err.Error()), utils.DefaultMessage),
) )
os.Exit(0)
} else { } else {
if fname != pipeName { if fname != pipeName {
fmt.Fprintf(os.Stderr, fmt.Sprintf("\nThe resized 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",

View File

@@ -23,6 +23,7 @@ const (
// It receives as parameters the shape type, the seam (x,y) coordinate and a size. // 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) { func (g *Gui) DrawSeam(shape shapeType, x, y, s float64) {
r := getRatio(g.cfg.window.w, g.cfg.window.h) r := getRatio(g.cfg.window.w, g.cfg.window.h)
switch shape { switch shape {
case circle: case circle:
g.drawCircle(x*r, y*r, s) g.drawCircle(x*r, y*r, s)
@@ -129,8 +130,7 @@ func (g *Gui) getFillColor() color.Color {
return g.cfg.color.fill return g.cfg.color.fill
} }
// getRatio resizes the image but retain the aspect ratio in case the // getRatio returns the image aspect ratio.
// image width and height is greater than the predefined window.
func getRatio(w, h float64) float64 { func getRatio(w, h float64) float64 {
var r float64 = 1 var r float64 = 1
if w > maxScreenX && h > maxScreenY { if w > maxScreenX && h > maxScreenY {

2
gui.go
View File

@@ -88,6 +88,8 @@ func (g *Gui) initWindow(w, h int) {
func (g *Gui) getWindowSize() (float64, float64) { func (g *Gui) getWindowSize() (float64, float64) {
w, h := g.cfg.window.w, g.cfg.window.h 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) r := getRatio(w, h)
if w > maxScreenX && h > maxScreenY { if w > maxScreenX && h > maxScreenY {
w = float64(w) * r w = float64(w) * r

View File

@@ -15,6 +15,7 @@ import (
"math" "math"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/disintegration/imaging" "github.com/disintegration/imaging"
"github.com/esimov/caire/utils" "github.com/esimov/caire/utils"
@@ -66,6 +67,10 @@ type Processor struct {
Debug bool Debug bool
Preview bool Preview bool
FaceDetect bool FaceDetect bool
MaskPath string
RMaskPath string
Mask image.Image
RMask image.Image
FaceAngle float64 FaceAngle float64
PigoFaceDetector *pigo.Pigo PigoFaceDetector *pigo.Pigo
Spinner *utils.Spinner Spinner *utils.Spinner
@@ -81,9 +86,9 @@ var (
enlargeVertFn enlargeFn 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. // 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) return s.Resize(img)
} }
@@ -418,6 +423,48 @@ func (p *Processor) Process(r io.Reader, w io.Writer) error {
} }
img := p.imgToNRGBA(src) 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 { if p.Preview {
guiWidth := img.Bounds().Max.X guiWidth := img.Bounds().Max.X
guiHeight := img.Bounds().Max.Y 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()) ext := filepath.Ext(w.(*os.File).Name())
switch ext { switch ext {
case "", ".jpg", ".jpeg": case "", ".jpg", ".jpeg":
res, err := Resize(p, img) res, err := resize(p, img)
if err != nil { if err != nil {
return err return err
} }
return jpeg.Encode(w, res, &jpeg.Options{Quality: 100}) return jpeg.Encode(w, res, &jpeg.Options{Quality: 100})
case ".png": case ".png":
res, err := Resize(p, img) res, err := resize(p, img)
if err != nil { if err != nil {
return err return err
} }
return png.Encode(w, res) return png.Encode(w, res)
case ".bmp": case ".bmp":
res, err := Resize(p, img) res, err := resize(p, img)
if err != nil { if err != nil {
return err return err
} }
@@ -465,7 +512,7 @@ func (p *Processor) Process(r io.Reader, w io.Writer) error {
case ".gif": case ".gif":
g = new(gif.GIF) g = new(gif.GIF)
isGif = true isGif = true
_, err := Resize(p, img) _, err := resize(p, img)
if err != nil { if err != nil {
return err return err
} }
@@ -474,7 +521,7 @@ func (p *Processor) Process(r io.Reader, w io.Writer) error {
return errors.New("unsupported image format") return errors.New("unsupported image format")
} }
default: default:
res, err := Resize(p, img) res, err := resize(p, img)
if err != nil { if err != nil {
return err 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) { func (p *Processor) shrink(c *Carver, img *image.NRGBA) (*image.NRGBA, error) {
width, height := img.Bounds().Max.X, img.Bounds().Max.Y width, height := img.Bounds().Max.X, img.Bounds().Max.Y
c = NewCarver(width, height) c = NewCarver(width, height)
if err := c.ComputeSeams(img, p); err != nil { if err := c.ComputeSeams(p, img); err != nil {
return nil, err return nil, err
} }
seams := c.FindLowestEnergySeams() seams := c.FindLowestEnergySeams()
img = c.RemoveSeam(img, seams, p.Debug) 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 { if isGif {
p.encodeImgToGif(c, img, g) 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) { func (p *Processor) enlarge(c *Carver, img *image.NRGBA) (*image.NRGBA, error) {
width, height := img.Bounds().Max.X, img.Bounds().Max.Y width, height := img.Bounds().Max.X, img.Bounds().Max.Y
c = NewCarver(width, height) c = NewCarver(width, height)
if err := c.ComputeSeams(img, p); err != nil { if err := c.ComputeSeams(p, img); err != nil {
return nil, err return nil, err
} }
seams := c.FindLowestEnergySeams() seams := c.FindLowestEnergySeams()

View File

@@ -82,7 +82,7 @@ func (c *Carver) SobelDetector(img *image.NRGBA, threshold float64) *image.NRGBA
dst.Pix[idx] = uint8(edges[idx]) dst.Pix[idx] = uint8(edges[idx])
dst.Pix[idx+1] = uint8(edges[idx+1]) dst.Pix[idx+1] = uint8(edges[idx+1])
dst.Pix[idx+2] = uint8(edges[idx+2]) dst.Pix[idx+2] = uint8(edges[idx+2])
dst.Pix[idx+3] = 255 dst.Pix[idx+3] = 0xff
} }
return dst return dst
} }

View File

@@ -36,7 +36,7 @@ func DownloadImage(url string) (*os.File, error) {
if err != nil { if err != nil {
return nil, errors.New(fmt.Sprintf("unable to copy the source URI into the destination file")) 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 { if err != nil {
return nil, err return nil, err
} }
@@ -61,8 +61,8 @@ func IsValidUrl(uri string) bool {
return true return true
} }
// detectContentType detects the file type by reading MIME type information of the file content. // DetectContentType detects the file type by reading MIME type information of the file content.
func detectContentType(fname string) (interface{}, error) { func DetectContentType(fname string) (interface{}, error) {
file, err := os.Open(fname) file, err := os.Open(fname)
if err != nil { if err != nil {
return nil, err return nil, err