From d3da34e80d373608dff8aa5da8ec69d26454738c Mon Sep 17 00:00:00 2001 From: esimov Date: Wed, 16 Jun 2021 16:19:18 +0300 Subject: [PATCH] fix: resolving carving issue on image enlargment (#65) --- carver.go | 2 +- cmd/caire/main.go | 2 - process.go | 48 ++++++-------- process_test.go | 2 - stackblur.go | 20 ++---- vendor/github.com/esimov/pigo/core/pigo.go | 74 ++++++++++++---------- 6 files changed, 69 insertions(+), 79 deletions(-) diff --git a/carver.go b/carver.go index 9179a53..9ad9690 100644 --- a/carver.go +++ b/carver.go @@ -122,7 +122,7 @@ func (c *Carver) ComputeSeams(img *image.NRGBA, p *Processor) error { } if p.BlurRadius > 0 { - srcImg = StackBlur(sobel, uint32(p.BlurRadius)) + srcImg = c.StackBlur(sobel, uint32(p.BlurRadius)) } else { srcImg = sobel } diff --git a/cmd/caire/main.go b/cmd/caire/main.go index e4cf005..5292afc 100644 --- a/cmd/caire/main.go +++ b/cmd/caire/main.go @@ -62,7 +62,6 @@ var ( percentage = flag.Bool("perc", false, "Reduce image by percentage") square = flag.Bool("square", false, "Reduce image to square dimensions") debug = flag.Bool("debug", false, "Use debugger") - scale = flag.Bool("scale", false, "Proportional scaling") faceDetect = flag.Bool("face", false, "Use face detection") faceAngle = flag.Float64("angle", 0.0, "Plane rotated faces angle") workers = flag.Int("conc", runtime.NumCPU(), "Number of files to process concurrently") @@ -89,7 +88,6 @@ func main() { Percentage: *percentage, Square: *square, Debug: *debug, - Scale: *scale, FaceDetect: *faceDetect, FaceAngle: *faceAngle, } diff --git a/process.go b/process.go index bafb68e..e8f0afe 100644 --- a/process.go +++ b/process.go @@ -21,8 +21,6 @@ import ( "golang.org/x/image/bmp" ) -const maxResizeWithoutScaling = 2000 - //go:embed data/facefinder var classifier embed.FS @@ -48,7 +46,6 @@ type Processor struct { Percentage bool Square bool Debug bool - Scale bool FaceDetect bool FaceAngle float64 PigoFaceDetector *pigo.Pigo @@ -92,6 +89,7 @@ func (p *Processor) Resize(img *image.NRGBA) (image.Image, error) { if p.NewHeight == 0 { newHeight = p.NewHeight } + reduce := func() error { width, height := img.Bounds().Max.X, img.Bounds().Max.Y c = NewCarver(width, height) @@ -175,28 +173,26 @@ func (p *Processor) Resize(img *image.NRGBA) (image.Image, error) { } img = c.RotateImage270(img) } else if newWidth > 0 || newHeight > 0 { - // Use this option to rescale the image proportionally prior resizing. - // First the image is scaled down preserving the image aspect ratio, + // We are trying to rescale the image proportionally prior resizing. + // First the image is scaled down or up by preserving the image aspect ratio, // then the seam carving algorithm is applied only to the remaining pixels. - // Prevent memory overflow issue in case of huge images by switching to scaling first option - if img.Bounds().Dx() > maxResizeWithoutScaling || - img.Bounds().Dy() > maxResizeWithoutScaling { - p.Scale = true - } + // Scale the width and height by the smaller factor (i.e Min(wScaleFactor, hScaleFactor)) + // Example: input: 5000x2500, scale: 2160x1080, final target: 1920x1080 + wScaleFactor := float64(c.Width) / float64(p.NewWidth) + hScaleFactor := float64(c.Height) / float64(p.NewHeight) + scaleWidth := math.Round(float64(c.Width) / math.Min(wScaleFactor, hScaleFactor)) + scaleHeight := math.Round(float64(c.Height) / math.Min(wScaleFactor, hScaleFactor)) - if p.Scale { - // Find the scale factor for width and height. - // Scale the width and height by the smaller factor (i.e Min(wScaleFactor, hScaleFactor)) - // This will scale one side directly to the target length, - // and the other proportionally larger than the target length. - // Example: input: 5000x2500, scale: 2160x1080, final target: 1920x1080 - wScaleFactor := float64(c.Width) / float64(p.NewWidth) - hScaleFactor := float64(c.Height) / float64(p.NewHeight) - scaleWidth := math.Round(float64(c.Width) / math.Min(wScaleFactor, hScaleFactor)) //post scale width - scaleHeight := math.Round(float64(c.Height) / math.Min(wScaleFactor, hScaleFactor)) // post scale height + newImg = imaging.Resize(img, int(scaleWidth), int(scaleHeight), imaging.Lanczos) - newImg = imaging.Resize(img, int(scaleWidth), int(scaleHeight), imaging.Lanczos) + dx0, dy0 := img.Bounds().Max.X, newImg.Bounds().Max.Y + dx1, dy1 := newImg.Bounds().Max.X, newImg.Bounds().Max.Y + + // Rescale the image only when it's resized both horizontally and vertically + // and the new image width or height are preserved, otherwise it might happen, that + // the generated image size does not match with the requested image size. + if !((p.NewWidth == 0 && dx0 == dx1) || (p.NewHeight == 0 && dy0 == dy1)) { // The amount needed to remove by carving. One or both of these will be 0. newWidth = int(scaleWidth) - p.NewWidth newHeight = int(scaleHeight) - p.NewHeight @@ -206,13 +202,12 @@ func (p *Processor) Resize(img *image.NRGBA) (image.Image, error) { img = dst } - // Check if the new width does not match with the rescaled image width. // Run the carver function if the desired image width is not identical with the rescaled image width. if newWidth > 0 && newWidth != img.Bounds().Max.X { - // Because scaling horizontally and vertically at the same time using the scale option - // it might happen that the scaled image exceeds the desired image size, we need to make sure + // Because of scaling horizontally and vertically at the same time it might happen + // that the scaled image exceeds the desired image size, we need to make sure // that the new width and|or height is reduced and not enlarged. - if p.NewWidth > c.Width && newWidth < p.NewWidth && img.Bounds().Max.X < p.NewWidth { + if p.NewWidth > c.Width && img.Bounds().Max.X < p.NewWidth { for x := 0; x < newWidth; x++ { if err = enlarge(); err != nil { return nil, err @@ -227,12 +222,11 @@ func (p *Processor) Resize(img *image.NRGBA) (image.Image, error) { } } } - // Check if the new height does not match with the rescaled image height. // Run the carver function if the desired image height is not identical with the rescaled image height. if newHeight > 0 && newHeight != img.Bounds().Max.Y { img = c.RotateImage90(img) // Check new height against the width of the image because the image is rotated 90deg. - if p.NewHeight > c.Height && (newHeight < p.NewHeight && img.Bounds().Max.X < p.NewHeight) { + if p.NewHeight > c.Height && img.Bounds().Max.X < p.NewHeight { for y := 0; y < newHeight; y++ { if err = enlarge(); err != nil { return nil, err diff --git a/process_test.go b/process_test.go index 3b72d3c..0235c2d 100644 --- a/process_test.go +++ b/process_test.go @@ -22,7 +22,6 @@ func reduceImageH(t *testing.T) { Percentage: false, Square: false, Debug: false, - Scale: false, } // Reduce image size horizontally for x := 0; x < newWidth; x++ { @@ -51,7 +50,6 @@ func reduceImageV(t *testing.T) { Percentage: false, Square: false, Debug: false, - Scale: false, } // Reduce image size horizontally img = c.RotateImage90(img) diff --git a/stackblur.go b/stackblur.go index 5401278..8112459 100644 --- a/stackblur.go +++ b/stackblur.go @@ -7,10 +7,10 @@ import ( "image" ) -// blurStack is a linked list containing the color value and a pointer to the next struct. -type blurStack struct { +// blurstack is a linked list containing the color value and a pointer to the next struct. +type blurstack struct { r, g, b, a uint32 - next *blurStack + next *blurstack } var mulTable = []uint32{ @@ -51,15 +51,10 @@ var shgTable = []uint32{ 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, } -// NewBlurStack is a constructor function returning a new struct of type blurStack. -func (bs *blurStack) NewBlurStack() *blurStack { - return &blurStack{bs.r, bs.g, bs.b, bs.a, bs.next} -} - // StackBlur applies a blur filter to the provided image. // The radius defines the bluring average. -func StackBlur(img *image.NRGBA, radius uint32) *image.NRGBA { - var stackEnd, stackIn, stackOut *blurStack +func (c *Carver) StackBlur(img *image.NRGBA, radius uint32) *image.NRGBA { + var stackEnd, stackIn, stackOut *blurstack var width, height = uint32(img.Bounds().Dx()), uint32(img.Bounds().Dy()) var ( div, widthMinus1, heightMinus1, radiusPlus1, sumFactor uint32 @@ -76,12 +71,11 @@ func StackBlur(img *image.NRGBA, radius uint32) *image.NRGBA { radiusPlus1 = radius + 1 sumFactor = radiusPlus1 * (radiusPlus1 + 1) / 2 - bs := blurStack{} - stackStart := bs.NewBlurStack() + stackStart := new(blurstack) stack := stackStart for i = 1; i < div; i++ { - stack.next = bs.NewBlurStack() + stack.next = new(blurstack) stack = stack.next if i == radiusPlus1 { stackEnd = stack diff --git a/vendor/github.com/esimov/pigo/core/pigo.go b/vendor/github.com/esimov/pigo/core/pigo.go index a6354e5..5071a9b 100644 --- a/vendor/github.com/esimov/pigo/core/pigo.go +++ b/vendor/github.com/esimov/pigo/core/pigo.go @@ -136,29 +136,32 @@ func (pg *Pigo) classifyRegion(r, c, s int, pixels []uint8, dim int) float32 { r = r * 256 c = c * 256 - for i := 0; i < int(pg.treeNum); i++ { - idx := 1 + if pg.treeNum > 0 { + for i := 0; i < int(pg.treeNum); i++ { + idx := 1 - for j := 0; j < int(pg.treeDepth); j++ { - x1 := ((r+int(pg.treeCodes[root+4*idx+0])*s)>>8)*dim + ((c + int(pg.treeCodes[root+4*idx+1])*s) >> 8) - x2 := ((r+int(pg.treeCodes[root+4*idx+2])*s)>>8)*dim + ((c + int(pg.treeCodes[root+4*idx+3])*s) >> 8) + for j := 0; j < int(pg.treeDepth); j++ { + x1 := ((r+int(pg.treeCodes[root+4*idx+0])*s)>>8)*dim + ((c + int(pg.treeCodes[root+4*idx+1])*s) >> 8) + x2 := ((r+int(pg.treeCodes[root+4*idx+2])*s)>>8)*dim + ((c + int(pg.treeCodes[root+4*idx+3])*s) >> 8) - bintest := func(px1, px2 uint8) int { - if px1 <= px2 { - return 1 + bintest := func(px1, px2 uint8) int { + if px1 <= px2 { + return 1 + } + return 0 } - return 0 + idx = 2*idx + bintest(pixels[x1], pixels[x2]) } - idx = 2*idx + bintest(pixels[x1], pixels[x2]) - } - out += pg.treePred[treeDepth*i+idx-treeDepth] + out += pg.treePred[treeDepth*i+idx-treeDepth] - if out <= pg.treeThreshold[i] { - return -1.0 + if out <= pg.treeThreshold[i] { + return -1.0 + } + root += 4 * treeDepth } - root += 4 * treeDepth + return out - pg.treeThreshold[pg.treeNum-1] } - return out - pg.treeThreshold[pg.treeNum-1] + return 0.0 } // classifyRotatedRegion applies the face classification function over a rotated image based on the parsed binary data. @@ -175,32 +178,35 @@ func (pg *Pigo) classifyRotatedRegion(r, c, s int, a float64, nrows, ncols int, qsin := s * qSinTable[int(32.0*a)] //s*(256.0*math.Sin(2*math.Pi*a)) qcos := s * qCosTable[int(32.0*a)] //s*(256.0*math.Cos(2*math.Pi*a)) - for i := 0; i < int(pg.treeNum); i++ { - var idx = 1 + if pg.treeNum > 0 { + for i := 0; i < int(pg.treeNum); i++ { + var idx = 1 - for j := 0; j < int(pg.treeDepth); j++ { - r1 := abs(min(nrows-1, max(0, 65536*r+qcos*int(pg.treeCodes[root+4*idx+0])-qsin*int(pg.treeCodes[root+4*idx+1]))>>16)) - c1 := abs(min(nrows-1, max(0, 65536*c+qsin*int(pg.treeCodes[root+4*idx+0])+qcos*int(pg.treeCodes[root+4*idx+1]))>>16)) + for j := 0; j < int(pg.treeDepth); j++ { + r1 := abs(min(nrows-1, max(0, 65536*r+qcos*int(pg.treeCodes[root+4*idx+0])-qsin*int(pg.treeCodes[root+4*idx+1]))>>16)) + c1 := abs(min(nrows-1, max(0, 65536*c+qsin*int(pg.treeCodes[root+4*idx+0])+qcos*int(pg.treeCodes[root+4*idx+1]))>>16)) - r2 := abs(min(nrows-1, max(0, 65536*r+qcos*int(pg.treeCodes[root+4*idx+2])-qsin*int(pg.treeCodes[root+4*idx+3]))>>16)) - c2 := abs(min(nrows-1, max(0, 65536*c+qsin*int(pg.treeCodes[root+4*idx+2])+qcos*int(pg.treeCodes[root+4*idx+3]))>>16)) + r2 := abs(min(nrows-1, max(0, 65536*r+qcos*int(pg.treeCodes[root+4*idx+2])-qsin*int(pg.treeCodes[root+4*idx+3]))>>16)) + c2 := abs(min(nrows-1, max(0, 65536*c+qsin*int(pg.treeCodes[root+4*idx+2])+qcos*int(pg.treeCodes[root+4*idx+3]))>>16)) - bintest := func(px1, px2 uint8) int { - if px1 <= px2 { - return 1 + bintest := func(px1, px2 uint8) int { + if px1 <= px2 { + return 1 + } + return 0 } - return 0 + idx = 2*idx + bintest(pixels[r1*dim+c1], pixels[r2*dim+c2]) } - idx = 2*idx + bintest(pixels[r1*dim+c1], pixels[r2*dim+c2]) - } - out += pg.treePred[treeDepth*i+idx-treeDepth] + out += pg.treePred[treeDepth*i+idx-treeDepth] - if out <= pg.treeThreshold[i] { - return -1.0 + if out <= pg.treeThreshold[i] { + return -1.0 + } + root += 4 * treeDepth } - root += 4 * treeDepth + return out - pg.treeThreshold[pg.treeNum-1] } - return out - pg.treeThreshold[pg.treeNum-1] + return 0.0 } // Detection struct contains the detection results composed of