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:
64
carver.go
64
carver.go
@@ -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()
|
||||
|
@@ -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",
|
||||
|
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.
|
||||
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
2
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
|
||||
|
72
process.go
72
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()
|
||||
|
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+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
|
||||
}
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user