Last active
February 1, 2024 10:52
-
-
Save wjkoh/660c97cfd3133bae936a431d20ab983e to your computer and use it in GitHub Desktop.
Go: Box blur
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package bake | |
import ( | |
"errors" | |
"image" | |
"image/draw" | |
) | |
// Try radius: 8 and numPasses: 2 with | |
// https://cdn.pixabay.com/photo/2015/09/21/14/24/zombie-949916_960_720.jpg | |
func Blur(im image.Image, radius int, numPasses int) (*image.RGBA, error) { | |
if radius < 1 { | |
return nil, errors.New("radius must be greater than 0") | |
} | |
if numPasses < 1 { | |
return nil, errors.New("numPasses must be greater than 0") | |
} | |
windowSize := 2*radius + 1 | |
if im.Bounds().Dx() < windowSize || im.Bounds().Dy() < windowSize { | |
return nil, errors.New("image is smaller than 2*radius + 1") | |
} | |
// Use RGBA, not NRGBA. We need alpha-premultiplied RGB channels. | |
src := image.NewRGBA(image.Rect(0, 0, im.Bounds().Dx(), im.Bounds().Dy())) | |
draw.Draw(src, src.Bounds(), im, im.Bounds().Min, draw.Src) | |
dst := image.NewRGBA(image.Rect(0, 0, im.Bounds().Dx(), im.Bounds().Dy())) | |
var ( | |
mean [4]float64 | |
oneOverWindowSize = 1.0 / float64(windowSize) | |
offset int | |
) | |
for i := 0; i < numPasses; i++ { | |
for y := src.Bounds().Min.Y; y < src.Bounds().Max.Y; y++ { | |
mean = [4]float64{0.0, 0.0, 0.0, 0.0} | |
for x := src.Bounds().Min.X; x < src.Bounds().Max.X; x++ { | |
if x <= radius || x >= src.Bounds().Max.X-radius { | |
mean = [4]float64{0.0, 0.0, 0.0, 0.0} | |
for dx := -radius; dx < radius+1; dx++ { | |
mirrored := x + dx | |
if mirrored < 0 { | |
mirrored = -mirrored | |
} else if mirrored >= src.Bounds().Max.X { | |
mirrored = src.Bounds().Max.X - (mirrored - src.Bounds().Max.X) - 2 | |
} | |
offset = src.PixOffset(mirrored, y) | |
add(mean[:], src.Pix[offset:offset+4]) | |
} | |
multiply(mean[:], oneOverWindowSize) | |
offset = dst.PixOffset(x, y) | |
assign(dst.Pix[offset:offset+4], mean[:]) | |
continue | |
} | |
offset = src.PixOffset(x-radius-1, y) | |
addCoeff(mean[:], src.Pix[offset:offset+4], -oneOverWindowSize) | |
offset = src.PixOffset(x+radius, y) | |
addCoeff(mean[:], src.Pix[offset:offset+4], oneOverWindowSize) | |
offset = dst.PixOffset(x, y) | |
assign(dst.Pix[offset:offset+4], mean[:]) | |
} | |
} | |
src, dst = dst, src | |
for x := src.Bounds().Min.X; x < src.Bounds().Max.X; x++ { | |
mean = [4]float64{0.0, 0.0, 0.0, 0.0} | |
for y := src.Bounds().Min.Y; y < src.Bounds().Max.Y; y++ { | |
if y <= radius || y >= src.Bounds().Max.Y-radius { | |
mean = [4]float64{0.0, 0.0, 0.0, 0.0} | |
for dy := -radius; dy < radius+1; dy++ { | |
mirrored := y + dy | |
if mirrored < 0 { | |
mirrored = -mirrored | |
} else if mirrored >= src.Bounds().Max.Y { | |
mirrored = src.Bounds().Max.Y - (mirrored - src.Bounds().Max.Y) - 2 | |
} | |
offset = src.PixOffset(x, mirrored) | |
add(mean[:], src.Pix[offset:offset+4]) | |
} | |
multiply(mean[:], oneOverWindowSize) | |
offset = dst.PixOffset(x, y) | |
assign(dst.Pix[offset:offset+4], mean[:]) | |
continue | |
} | |
offset = src.PixOffset(x, y-radius-1) | |
addCoeff(mean[:], src.Pix[offset:offset+4], -oneOverWindowSize) | |
offset = src.PixOffset(x, y+radius) | |
addCoeff(mean[:], src.Pix[offset:offset+4], oneOverWindowSize) | |
offset = dst.PixOffset(x, y) | |
assign(dst.Pix[offset:offset+4], mean[:]) | |
} | |
} | |
src, dst = dst, src | |
} | |
return src, nil | |
} | |
func assign[T uint8 | float64](a []uint8, b []T) { | |
a[0] = uint8(b[0]) | |
a[1] = uint8(b[1]) | |
a[2] = uint8(b[2]) | |
a[3] = uint8(b[3]) | |
} | |
func add(a []float64, b []uint8) { | |
a[0] = a[0] + float64(b[0]) | |
a[1] = a[1] + float64(b[1]) | |
a[2] = a[2] + float64(b[2]) | |
a[3] = a[3] + float64(b[3]) | |
} | |
func addCoeff(a []float64, b []uint8, c float64) { | |
a[0] = a[0] + float64(b[0])*c | |
a[1] = a[1] + float64(b[1])*c | |
a[2] = a[2] + float64(b[2])*c | |
a[3] = a[3] + float64(b[3])*c | |
} | |
func multiply(a []float64, b float64) { | |
a[0] = a[0] * b | |
a[1] = a[1] * b | |
a[2] = a[2] * b | |
a[3] = a[3] * b | |
} |
Author
wjkoh
commented
Nov 3, 2023
It requires 32 milliseconds to blur a 1024x1024 RGBA image with a radius of 8 pixels and two passes.
wjkoh@Woojongs-MacBook-Pro ~/bake (main)> go test -bench=. -run=BenchmarkBlur (base)
goos: darwin
goarch: arm64
pkg: github.com/cowork-ai/bake
BenchmarkBlur64-10 11364 104743 ns/op 32896 B/op 4 allocs/op
BenchmarkBlur128-10 2781 422655 ns/op 131200 B/op 4 allocs/op
BenchmarkBlur256-10 679 1774799 ns/op 524416 B/op 4 allocs/op
BenchmarkBlur512-10 163 7246958 ns/op 2097280 B/op 4 allocs/op
BenchmarkBlur1024-10 34 32612907 ns/op 8388738 B/op 4 allocs/op
PASS
ok github.com/cowork-ai/bake 8.195s
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment