fix: resolving carving issue on image enlargment (#65)

This commit is contained in:
esimov
2021-06-16 16:19:18 +03:00
parent 9b306380da
commit d3da34e80d
6 changed files with 69 additions and 79 deletions

View File

@@ -122,7 +122,7 @@ func (c *Carver) ComputeSeams(img *image.NRGBA, p *Processor) error {
} }
if p.BlurRadius > 0 { if p.BlurRadius > 0 {
srcImg = StackBlur(sobel, uint32(p.BlurRadius)) srcImg = c.StackBlur(sobel, uint32(p.BlurRadius))
} else { } else {
srcImg = sobel srcImg = sobel
} }

View File

@@ -62,7 +62,6 @@ var (
percentage = flag.Bool("perc", false, "Reduce image by percentage") percentage = flag.Bool("perc", false, "Reduce image by percentage")
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")
scale = flag.Bool("scale", false, "Proportional scaling")
faceDetect = flag.Bool("face", false, "Use face detection") faceDetect = flag.Bool("face", false, "Use face detection")
faceAngle = flag.Float64("angle", 0.0, "Plane rotated faces angle") faceAngle = flag.Float64("angle", 0.0, "Plane rotated faces 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")
@@ -89,7 +88,6 @@ func main() {
Percentage: *percentage, Percentage: *percentage,
Square: *square, Square: *square,
Debug: *debug, Debug: *debug,
Scale: *scale,
FaceDetect: *faceDetect, FaceDetect: *faceDetect,
FaceAngle: *faceAngle, FaceAngle: *faceAngle,
} }

View File

@@ -21,8 +21,6 @@ import (
"golang.org/x/image/bmp" "golang.org/x/image/bmp"
) )
const maxResizeWithoutScaling = 2000
//go:embed data/facefinder //go:embed data/facefinder
var classifier embed.FS var classifier embed.FS
@@ -48,7 +46,6 @@ type Processor struct {
Percentage bool Percentage bool
Square bool Square bool
Debug bool Debug bool
Scale bool
FaceDetect bool FaceDetect bool
FaceAngle float64 FaceAngle float64
PigoFaceDetector *pigo.Pigo PigoFaceDetector *pigo.Pigo
@@ -92,6 +89,7 @@ func (p *Processor) Resize(img *image.NRGBA) (image.Image, error) {
if p.NewHeight == 0 { if p.NewHeight == 0 {
newHeight = p.NewHeight newHeight = p.NewHeight
} }
reduce := func() error { reduce := func() 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)
@@ -175,28 +173,26 @@ func (p *Processor) Resize(img *image.NRGBA) (image.Image, error) {
} }
img = c.RotateImage270(img) img = c.RotateImage270(img)
} else if newWidth > 0 || newHeight > 0 { } else if newWidth > 0 || newHeight > 0 {
// Use this option to rescale the image proportionally prior resizing. // We are trying to rescale the image proportionally prior resizing.
// First the image is scaled down preserving the image aspect ratio, // 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. // 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 // Scale the width and height by the smaller factor (i.e Min(wScaleFactor, hScaleFactor))
if img.Bounds().Dx() > maxResizeWithoutScaling || // Example: input: 5000x2500, scale: 2160x1080, final target: 1920x1080
img.Bounds().Dy() > maxResizeWithoutScaling { wScaleFactor := float64(c.Width) / float64(p.NewWidth)
p.Scale = true 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 { newImg = imaging.Resize(img, int(scaleWidth), int(scaleHeight), imaging.Lanczos)
// 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) 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. // The amount needed to remove by carving. One or both of these will be 0.
newWidth = int(scaleWidth) - p.NewWidth newWidth = int(scaleWidth) - p.NewWidth
newHeight = int(scaleHeight) - p.NewHeight newHeight = int(scaleHeight) - p.NewHeight
@@ -206,13 +202,12 @@ func (p *Processor) Resize(img *image.NRGBA) (image.Image, error) {
img = dst 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. // 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 { if newWidth > 0 && newWidth != img.Bounds().Max.X {
// Because scaling horizontally and vertically at the same time using the scale option // Because of scaling horizontally and vertically at the same time it might happen
// it might happen that the scaled image exceeds the desired image size, we need to make sure // 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. // 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++ { for x := 0; x < newWidth; x++ {
if err = enlarge(); err != nil { if err = enlarge(); err != nil {
return nil, err 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. // 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 { if newHeight > 0 && newHeight != img.Bounds().Max.Y {
img = c.RotateImage90(img) img = c.RotateImage90(img)
// Check new height against the width of the image because the image is rotated 90deg. // 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++ { for y := 0; y < newHeight; y++ {
if err = enlarge(); err != nil { if err = enlarge(); err != nil {
return nil, err return nil, err

View File

@@ -22,7 +22,6 @@ func reduceImageH(t *testing.T) {
Percentage: false, Percentage: false,
Square: false, Square: false,
Debug: false, Debug: false,
Scale: false,
} }
// Reduce image size horizontally // Reduce image size horizontally
for x := 0; x < newWidth; x++ { for x := 0; x < newWidth; x++ {
@@ -51,7 +50,6 @@ func reduceImageV(t *testing.T) {
Percentage: false, Percentage: false,
Square: false, Square: false,
Debug: false, Debug: false,
Scale: false,
} }
// Reduce image size horizontally // Reduce image size horizontally
img = c.RotateImage90(img) img = c.RotateImage90(img)

View File

@@ -7,10 +7,10 @@ import (
"image" "image"
) )
// blurStack is a linked list containing the color value and a pointer to the next struct. // blurstack is a linked list containing the color value and a pointer to the next struct.
type blurStack struct { type blurstack struct {
r, g, b, a uint32 r, g, b, a uint32
next *blurStack next *blurstack
} }
var mulTable = []uint32{ 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, 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. // StackBlur applies a blur filter to the provided image.
// The radius defines the bluring average. // The radius defines the bluring average.
func StackBlur(img *image.NRGBA, radius uint32) *image.NRGBA { func (c *Carver) StackBlur(img *image.NRGBA, radius uint32) *image.NRGBA {
var stackEnd, stackIn, stackOut *blurStack var stackEnd, stackIn, stackOut *blurstack
var width, height = uint32(img.Bounds().Dx()), uint32(img.Bounds().Dy()) var width, height = uint32(img.Bounds().Dx()), uint32(img.Bounds().Dy())
var ( var (
div, widthMinus1, heightMinus1, radiusPlus1, sumFactor uint32 div, widthMinus1, heightMinus1, radiusPlus1, sumFactor uint32
@@ -76,12 +71,11 @@ func StackBlur(img *image.NRGBA, radius uint32) *image.NRGBA {
radiusPlus1 = radius + 1 radiusPlus1 = radius + 1
sumFactor = radiusPlus1 * (radiusPlus1 + 1) / 2 sumFactor = radiusPlus1 * (radiusPlus1 + 1) / 2
bs := blurStack{} stackStart := new(blurstack)
stackStart := bs.NewBlurStack()
stack := stackStart stack := stackStart
for i = 1; i < div; i++ { for i = 1; i < div; i++ {
stack.next = bs.NewBlurStack() stack.next = new(blurstack)
stack = stack.next stack = stack.next
if i == radiusPlus1 { if i == radiusPlus1 {
stackEnd = stack stackEnd = stack

View File

@@ -136,29 +136,32 @@ func (pg *Pigo) classifyRegion(r, c, s int, pixels []uint8, dim int) float32 {
r = r * 256 r = r * 256
c = c * 256 c = c * 256
for i := 0; i < int(pg.treeNum); i++ { if pg.treeNum > 0 {
idx := 1 for i := 0; i < int(pg.treeNum); i++ {
idx := 1
for j := 0; j < int(pg.treeDepth); j++ { 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) 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) 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 { bintest := func(px1, px2 uint8) int {
if px1 <= px2 { if px1 <= px2 {
return 1 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] { if out <= pg.treeThreshold[i] {
return -1.0 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. // 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)) 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)) qcos := s * qCosTable[int(32.0*a)] //s*(256.0*math.Cos(2*math.Pi*a))
for i := 0; i < int(pg.treeNum); i++ { if pg.treeNum > 0 {
var idx = 1 for i := 0; i < int(pg.treeNum); i++ {
var idx = 1
for j := 0; j < int(pg.treeDepth); j++ { 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)) 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)) 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)) 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)) 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 { bintest := func(px1, px2 uint8) int {
if px1 <= px2 { if px1 <= px2 {
return 1 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] { if out <= pg.treeThreshold[i] {
return -1.0 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 // Detection struct contains the detection results composed of