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
var (
faceDetAttempts int
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()

View File

@@ -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",

View File

@@ -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 {

2
gui.go
View File

@@ -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

View File

@@ -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()

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+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
}

View File

@@ -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