diff --git a/carver.go b/carver.go index 09977a5..1932fb3 100644 --- a/carver.go +++ b/carver.go @@ -75,8 +75,6 @@ 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(p *Processor, img *image.NRGBA) (*image.NRGBA, error) { - var srcImg *image.NRGBA - width, height := img.Bounds().Dx(), img.Bounds().Dy() sobel = c.SobelDetector(img, float64(p.SobelThreshold)) @@ -215,8 +213,13 @@ func (c *Carver) ComputeSeams(p *Processor, img *image.NRGBA) (*image.NRGBA, err } } + var srcImg *image.NRGBA if p.BlurRadius > 0 { - srcImg = c.StackBlur(sobel, uint32(p.BlurRadius)) + srcImg = image.NewNRGBA(img.Bounds()) + err := Stackblur(srcImg, sobel, uint32(p.BlurRadius)) + if err != nil { + return nil, fmt.Errorf("error bluring the image: %w", err) + } } else { srcImg = sobel } diff --git a/carver_test.go b/carver_test.go index 53aef10..67427f9 100644 --- a/carver_test.go +++ b/carver_test.go @@ -318,7 +318,9 @@ func TestCarver_ShouldNotRemoveFaceZone(t *testing.T) { pixels := rgbToGrayscale(img) sobel := c.SobelDetector(img, float64(p.SobelThreshold)) - img = c.StackBlur(sobel, uint32(p.BlurRadius)) + + err = Stackblur(img, sobel, uint32(p.BlurRadius)) + assert.NoError(t, err) cParams := pigo.CascadeParams{ MinSize: 100, diff --git a/go.mod b/go.mod index 5bc7032..7e5b776 100644 --- a/go.mod +++ b/go.mod @@ -6,9 +6,9 @@ require ( gioui.org v0.8.0 github.com/disintegration/imaging v1.6.2 github.com/esimov/pigo v1.4.5 - github.com/stretchr/testify v1.8.1 + github.com/stretchr/testify v1.10.0 golang.org/x/exp v0.0.0-20240707233637-46b078467d37 - golang.org/x/image v0.18.0 + golang.org/x/image v0.23.0 golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 ) @@ -18,7 +18,8 @@ require ( github.com/go-text/typesetting v0.2.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/exp/shiny v0.0.0-20240707233637-46b078467d37 // indirect - golang.org/x/sys v0.22.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/text v0.21.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index dd8d6cd..7a4be23 100644 --- a/go.sum +++ b/go.sum @@ -5,7 +5,6 @@ gioui.org v0.8.0/go.mod h1:vEMmpxMOd/iwJhXvGVIzWEbxMWhnMQ9aByOGQdlQ8rc= gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ= gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA= gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= @@ -18,35 +17,35 @@ github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+q github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0= github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/exp v0.0.0-20240707233637-46b078467d37 h1:uLDX+AfeFCct3a2C7uIWBKMJIR3CJMhcgfrUAqjRK6w= golang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/exp/shiny v0.0.0-20240707233637-46b078467d37 h1:SOSg7+sueresE4IbmmGM60GmlIys+zNX63d6/J4CMtU= golang.org/x/exp/shiny v0.0.0-20240707233637-46b078467d37/go.mod h1:3F+MieQB7dRYLTmnncoFbb1crS5lfQoTfDgQy6K4N0o= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= -golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= +golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= +golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201107080550-4d91cf3a1aaf/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20191110171634-ad39bd3f0407/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc= golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/stackblur.go b/stackblur.go index e1243cc..3b6e5f6 100644 --- a/stackblur.go +++ b/stackblur.go @@ -1,16 +1,18 @@ -// Go implementation of StackBlur algorithm described here: +// Go implementation of the StackBlur algorithm // http://incubator.quasimondo.com/processing/fast_blur_deluxe.php package caire import ( + "errors" "image" + "image/color" ) -// 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,11 +53,86 @@ var shgTable = []uint32{ 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, } -// StackBlur applies a blur filter to the provided image. -// The radius defines the bluring average. -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()) +// Stackblur takes the source image and returns it's blurred version by applying the blur radius defined as parameter. The destination image must be a image.NRGBA. +func Stackblur(dst, src image.Image, radius uint32) error { + // Limit the maximum blur radius to 255 to avoid overflowing the multable. + if int(radius) >= len(mulTable) { + radius = uint32(len(mulTable) - 1) + } + + if radius < 1 { + return errors.New("blur radius must be greater than 0") + } + + img, ok := dst.(*image.NRGBA) + if !ok { + return errors.New("the destination image must be image.NRGBA") + } + + process(img, src, radius) + return nil +} + +func process(dst *image.NRGBA, src image.Image, radius uint32) { + srcBounds := src.Bounds() + srcMinX := srcBounds.Min.X + srcMinY := srcBounds.Min.Y + + dstBounds := srcBounds.Sub(srcBounds.Min) + dstW := dstBounds.Dx() + dstH := dstBounds.Dy() + + switch src0 := src.(type) { + case *image.NRGBA: + rowSize := srcBounds.Dx() * 4 + for dstY := 0; dstY < dstH; dstY++ { + di := src0.PixOffset(0, dstY) + si := src0.PixOffset(srcMinX, srcMinY+dstY) + for dstX := 0; dstX < dstW; dstX++ { + copy(dst.Pix[di:di+rowSize], src0.Pix[si:si+rowSize]) + } + } + case *image.YCbCr: + for dstY := 0; dstY < dstH; dstY++ { + di := dst.PixOffset(0, dstY) + for dstX := 0; dstX < dstW; dstX++ { + srcX := srcMinX + dstX + srcY := srcMinY + dstY + siy := src0.YOffset(srcX, srcY) + sic := src0.COffset(srcX, srcY) + r, g, b := color.YCbCrToRGB(src0.Y[siy], src0.Cb[sic], src0.Cr[sic]) + dst.Pix[di+0] = r + dst.Pix[di+1] = g + dst.Pix[di+2] = b + dst.Pix[di+3] = 0xff + di += 4 + } + } + default: + for dstY := 0; dstY < dstH; dstY++ { + di := dst.PixOffset(0, dstY) + for dstX := 0; dstX < dstW; dstX++ { + c := color.NRGBAModel.Convert(src.At(srcMinX+dstX, srcMinY+dstY)).(color.NRGBA) + dst.Pix[di+0] = c.R + dst.Pix[di+1] = c.G + dst.Pix[di+2] = c.B + dst.Pix[di+3] = c.A + di += 4 + } + } + } + + blurImage(dst, radius) +} + +func blurImage(src *image.NRGBA, radius uint32) { + var ( + stackEnd *blurStack + stackIn *blurStack + stackOut *blurStack + ) + + var width, height = uint32(src.Bounds().Dx()), uint32(src.Bounds().Dy()) var ( div, widthMinus1, heightMinus1, radiusPlus1, sumFactor uint32 x, y, i, p, yp, yi, yw, @@ -65,26 +142,17 @@ func (c *Carver) StackBlur(img *image.NRGBA, radius uint32) *image.NRGBA { pr, pg, pb, pa uint32 ) - // Limit the maximum blur radius to 255, otherwise it overflows the multable length - // and will panic with and index out of range error. - if int(radius) >= len(mulTable) { - radius = uint32(len(mulTable) - 1) - } - if radius < 1 { - radius = 1 - } - div = radius + radius + 1 widthMinus1 = width - 1 heightMinus1 = height - 1 radiusPlus1 = radius + 1 sumFactor = radiusPlus1 * (radiusPlus1 + 1) / 2 - stackStart := new(blurstack) + stackStart := new(blurStack) stack := stackStart for i = 1; i < div; i++ { - stack.next = new(blurstack) + stack.next = new(blurStack) stack = stack.next if i == radiusPlus1 { stackEnd = stack @@ -98,10 +166,10 @@ func (c *Carver) StackBlur(img *image.NRGBA, radius uint32) *image.NRGBA { for y = 0; y < height; y++ { rInSum, gInSum, bInSum, aInSum, rSum, gSum, bSum, aSum = 0, 0, 0, 0, 0, 0, 0, 0 - pr = uint32(img.Pix[yi]) - pg = uint32(img.Pix[yi+1]) - pb = uint32(img.Pix[yi+2]) - pa = uint32(img.Pix[yi+3]) + pr = uint32(src.Pix[yi]) + pg = uint32(src.Pix[yi+1]) + pb = uint32(src.Pix[yi+2]) + pa = uint32(src.Pix[yi+3]) rOutSum = radiusPlus1 * pr gOutSum = radiusPlus1 * pg @@ -131,10 +199,10 @@ func (c *Carver) StackBlur(img *image.NRGBA, radius uint32) *image.NRGBA { diff = i } p = yi + (diff << 2) - pr = uint32(img.Pix[p]) - pg = uint32(img.Pix[p+1]) - pb = uint32(img.Pix[p+2]) - pa = uint32(img.Pix[p+3]) + pr = uint32(src.Pix[p]) + pg = uint32(src.Pix[p+1]) + pb = uint32(src.Pix[p+2]) + pa = uint32(src.Pix[p+3]) stack.r = pr stack.g = pg @@ -158,16 +226,16 @@ func (c *Carver) StackBlur(img *image.NRGBA, radius uint32) *image.NRGBA { for x = 0; x < width; x++ { pa = (aSum * mulSum) >> shgSum - img.Pix[yi+3] = uint8(pa) + src.Pix[yi+3] = uint8(pa) if pa != 0 { - img.Pix[yi] = uint8((rSum * mulSum) >> shgSum) - img.Pix[yi+1] = uint8((gSum * mulSum) >> shgSum) - img.Pix[yi+2] = uint8((bSum * mulSum) >> shgSum) + src.Pix[yi] = uint8((rSum * mulSum) >> shgSum) + src.Pix[yi+1] = uint8((gSum * mulSum) >> shgSum) + src.Pix[yi+2] = uint8((bSum * mulSum) >> shgSum) } else { - img.Pix[yi] = 0 - img.Pix[yi+1] = 0 - img.Pix[yi+2] = 0 + src.Pix[yi] = 0 + src.Pix[yi+1] = 0 + src.Pix[yi+2] = 0 } rSum -= rOutSum @@ -187,10 +255,10 @@ func (c *Carver) StackBlur(img *image.NRGBA, radius uint32) *image.NRGBA { } p = (yw + p) << 2 - stackIn.r = uint32(img.Pix[p]) - stackIn.g = uint32(img.Pix[p+1]) - stackIn.b = uint32(img.Pix[p+2]) - stackIn.a = uint32(img.Pix[p+3]) + stackIn.r = uint32(src.Pix[p]) + stackIn.g = uint32(src.Pix[p+1]) + stackIn.b = uint32(src.Pix[p+2]) + stackIn.a = uint32(src.Pix[p+3]) rInSum += stackIn.r gInSum += stackIn.g @@ -230,10 +298,10 @@ func (c *Carver) StackBlur(img *image.NRGBA, radius uint32) *image.NRGBA { rInSum, gInSum, bInSum, aInSum, rSum, gSum, bSum, aSum = 0, 0, 0, 0, 0, 0, 0, 0 yi = x << 2 - pr = uint32(img.Pix[yi]) - pg = uint32(img.Pix[yi+1]) - pb = uint32(img.Pix[yi+2]) - pa = uint32(img.Pix[yi+3]) + pr = uint32(src.Pix[yi]) + pg = uint32(src.Pix[yi+1]) + pb = uint32(src.Pix[yi+2]) + pa = uint32(src.Pix[yi+3]) rOutSum = radiusPlus1 * pr gOutSum = radiusPlus1 * pg @@ -259,10 +327,10 @@ func (c *Carver) StackBlur(img *image.NRGBA, radius uint32) *image.NRGBA { for i = 1; i <= radius; i++ { yi = (yp + x) << 2 - pr = uint32(img.Pix[yi]) - pg = uint32(img.Pix[yi+1]) - pb = uint32(img.Pix[yi+2]) - pa = uint32(img.Pix[yi+3]) + pr = uint32(src.Pix[yi]) + pg = uint32(src.Pix[yi+1]) + pb = uint32(src.Pix[yi+2]) + pa = uint32(src.Pix[yi+3]) stack.r = pr stack.g = pg @@ -293,16 +361,16 @@ func (c *Carver) StackBlur(img *image.NRGBA, radius uint32) *image.NRGBA { for y = 0; y < height; y++ { p = yi << 2 pa = (aSum * mulSum) >> shgSum - img.Pix[p+3] = uint8(pa) + src.Pix[p+3] = uint8(pa) if pa > 0 { - img.Pix[p] = uint8((rSum * mulSum) >> shgSum) - img.Pix[p+1] = uint8((gSum * mulSum) >> shgSum) - img.Pix[p+2] = uint8((bSum * mulSum) >> shgSum) + src.Pix[p] = uint8((rSum * mulSum) >> shgSum) + src.Pix[p+1] = uint8((gSum * mulSum) >> shgSum) + src.Pix[p+2] = uint8((bSum * mulSum) >> shgSum) } else { - img.Pix[p] = 0 - img.Pix[p+1] = 0 - img.Pix[p+2] = 0 + src.Pix[p] = 0 + src.Pix[p+1] = 0 + src.Pix[p+2] = 0 } rSum -= rOutSum @@ -322,10 +390,10 @@ func (c *Carver) StackBlur(img *image.NRGBA, radius uint32) *image.NRGBA { } p = (x + (p * width)) << 2 - stackIn.r = uint32(img.Pix[p]) - stackIn.g = uint32(img.Pix[p+1]) - stackIn.b = uint32(img.Pix[p+2]) - stackIn.a = uint32(img.Pix[p+3]) + stackIn.r = uint32(src.Pix[p]) + stackIn.g = uint32(src.Pix[p+1]) + stackIn.b = uint32(src.Pix[p+2]) + stackIn.a = uint32(src.Pix[p+3]) rInSum += stackIn.r gInSum += stackIn.g @@ -359,5 +427,4 @@ func (c *Carver) StackBlur(img *image.NRGBA, radius uint32) *image.NRGBA { yi += width } } - return img }