mirror of
https://github.com/esimov/caire.git
synced 2025-09-26 20:41:14 +08:00
feat: support for binary file masks #68
This commit is contained in:
66
carver.go
66
carver.go
@@ -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()
|
||||||
|
@@ -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",
|
||||||
|
4
draw.go
4
draw.go
@@ -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
2
gui.go
@@ -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
|
||||||
|
72
process.go
72
process.go
@@ -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()
|
||||||
|
2
sobel.go
2
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] = 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
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
Reference in New Issue
Block a user