Perf improvements on stackblur algorithm

This commit is contained in:
esimov
2025-05-02 11:13:15 +03:00
parent aeab3fbb82
commit 8d0ec6281a
5 changed files with 155 additions and 83 deletions

View File

@@ -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
}

View File

@@ -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,

9
go.mod
View File

@@ -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
)

31
go.sum
View File

@@ -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=

View File

@@ -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
}