Skip to content

Commit

Permalink
Add unsharp mask (#34)
Browse files Browse the repository at this point in the history
* Add unsharp mask func

* Add unsharp mask amount

* Add unsharp mask doc

* Add unsharp mask tests

* Add unsharp mask to README
  • Loading branch information
anthonynsimon authored Sep 23, 2016
1 parent a6f662f commit 35fd190
Show file tree
Hide file tree
Showing 3 changed files with 178 additions and 0 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,11 @@ func main() {

![example](https://anthonynsimon.github.io/projects/bild/sobel.jpg)

### Unsharp Mask
result := effect.UnsharpMask(img, 0.6, 1.2)

![example](https://anthonynsimon.github.io/projects/bild/unsharpmask.jpg)


## Histogram
import "github.com/anthonynsimon/bild/histogram"
Expand Down
46 changes: 46 additions & 0 deletions effect/effect.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/anthonynsimon/bild/adjust"
"github.com/anthonynsimon/bild/blend"
"github.com/anthonynsimon/bild/blur"
"github.com/anthonynsimon/bild/clone"
"github.com/anthonynsimon/bild/convolution"
"github.com/anthonynsimon/bild/math/f64"
Expand Down Expand Up @@ -139,6 +140,51 @@ func Sharpen(src image.Image) *image.RGBA {
return convolution.Convolve(src, &k, &convolution.Options{Bias: 0, Wrap: false})
}

// UnsharpMask returns a copy of the image with its high-frecuency components amplified.
// Parameter radius corresponds to the radius to be samples per pixel.
// Parameter amount is the normalized strength of the effect. A value of 0.0 will leave
// the image untouched and a value of 1.0 will fully apply the unsharp mask.
func UnsharpMask(img image.Image, radius, amount float64) *image.RGBA {
amount = f64.Clamp(amount, 0, 10)

blurred := blur.Gaussian(img, 5*radius) // scale radius by matching factor

bounds := img.Bounds()
src := clone.AsRGBA(img)
dst := image.NewRGBA(bounds)
w, h := bounds.Dx(), bounds.Dy()

parallel.Line(h, func(start, end int) {
for y := start; y < end; y++ {
for x := 0; x < w; x++ {
pos := y*dst.Stride + x*4

r := float64(src.Pix[pos+0])
g := float64(src.Pix[pos+1])
b := float64(src.Pix[pos+2])
a := float64(src.Pix[pos+3])

rBlur := float64(blurred.Pix[pos+0])
gBlur := float64(blurred.Pix[pos+1])
bBlur := float64(blurred.Pix[pos+2])
aBlur := float64(blurred.Pix[pos+3])

r = r + (r-rBlur)*amount
g = g + (g-gBlur)*amount
b = b + (b-bBlur)*amount
a = a + (a-aBlur)*amount

dst.Pix[pos+0] = uint8(f64.Clamp(r, 0, 255))
dst.Pix[pos+1] = uint8(f64.Clamp(g, 0, 255))
dst.Pix[pos+2] = uint8(f64.Clamp(b, 0, 255))
dst.Pix[pos+3] = uint8(f64.Clamp(a, 0, 255))
}
}
})

return dst
}

// Sobel returns an image emphasising edges using an approximation to the Sobel–Feldman operator.
func Sobel(src image.Image) *image.RGBA {

Expand Down
127 changes: 127 additions & 0 deletions effect/effect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,133 @@ func TestSharpen(t *testing.T) {
}
}

func TestUnsharpMask(t *testing.T) {
cases := []struct {
value image.Image
radius float64
amount float64
expected *image.RGBA
}{
{
radius: 0.0,
amount: 0.0,
value: &image.RGBA{
Rect: image.Rect(0, 0, 3, 3),
Stride: 3 * 4,
Pix: []uint8{
0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x40, 0x40, 0xFF,
0x40, 0x80, 0x80, 0xFF, 0xB2, 0x00, 0xCA, 0xFF, 0x80, 0x40, 0x80, 0xFF,
0x00, 0x00, 0xAA, 0xFF, 0xFF, 0xCC, 0xCC, 0xFF, 0xFF, 0x00, 0xAA, 0xFF,
},
},
expected: &image.RGBA{
Rect: image.Rect(0, 0, 3, 3),
Stride: 3 * 4,
Pix: []uint8{
0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x40, 0x40, 0xFF,
0x40, 0x80, 0x80, 0xFF, 0xB2, 0x00, 0xCA, 0xFF, 0x80, 0x40, 0x80, 0xFF,
0x00, 0x00, 0xAA, 0xFF, 0xFF, 0xCC, 0xCC, 0xFF, 0xFF, 0x00, 0xAA, 0xFF,
},
},
},
{
radius: 10.0,
amount: 0.0,
value: &image.RGBA{
Rect: image.Rect(0, 0, 3, 3),
Stride: 3 * 4,
Pix: []uint8{
0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x40, 0x40, 0xFF,
0x40, 0x80, 0x80, 0xFF, 0xB2, 0x00, 0xCA, 0xFF, 0x80, 0x40, 0x80, 0xFF,
0x00, 0x00, 0xAA, 0xFF, 0xFF, 0xCC, 0xCC, 0xFF, 0xFF, 0x00, 0xAA, 0xFF,
},
},
expected: &image.RGBA{
Rect: image.Rect(0, 0, 3, 3),
Stride: 3 * 4,
Pix: []uint8{
0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x40, 0x40, 0xFF,
0x40, 0x80, 0x80, 0xFF, 0xB2, 0x00, 0xCA, 0xFF, 0x80, 0x40, 0x80, 0xFF,
0x00, 0x00, 0xAA, 0xFF, 0xFF, 0xCC, 0xCC, 0xFF, 0xFF, 0x00, 0xAA, 0xFF,
},
},
},
{
radius: 0.0,
amount: 10.0,
value: &image.RGBA{
Rect: image.Rect(0, 0, 3, 3),
Stride: 3 * 4,
Pix: []uint8{
0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x40, 0x40, 0xFF,
0x40, 0x80, 0x80, 0xFF, 0xB2, 0x00, 0xCA, 0xFF, 0x80, 0x40, 0x80, 0xFF,
0x00, 0x00, 0xAA, 0xFF, 0xFF, 0xCC, 0xCC, 0xFF, 0xFF, 0x00, 0xAA, 0xFF,
},
},
expected: &image.RGBA{
Rect: image.Rect(0, 0, 3, 3),
Stride: 3 * 4,
Pix: []uint8{
0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x40, 0x40, 0xFF,
0x40, 0x80, 0x80, 0xFF, 0xB2, 0x00, 0xCA, 0xFF, 0x80, 0x40, 0x80, 0xFF,
0x00, 0x00, 0xAA, 0xFF, 0xFF, 0xCC, 0xCC, 0xFF, 0xFF, 0x00, 0xAA, 0xFF,
},
},
},
{
radius: 1.0,
amount: 1.0,
value: &image.RGBA{
Rect: image.Rect(0, 0, 3, 3),
Stride: 3 * 4,
Pix: []uint8{
0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x40, 0x40, 0xFF,
0x40, 0x80, 0x80, 0xFF, 0xB2, 0x00, 0xCA, 0xFF, 0x80, 0x40, 0x80, 0xFF,
0x00, 0x00, 0xAA, 0xFF, 0xFF, 0xCC, 0xCC, 0xFF, 0xFF, 0x00, 0xAA, 0xFF,
},
},
expected: &image.RGBA{
Rect: image.Rect(0, 0, 3, 3),
Stride: 3 * 4,
Pix: []uint8{
0xFF, 0x0, 0x0, 0xFF, 0xFF, 0xFF, 0x0, 0xFF, 0xFF, 0x47, 0x25, 0xFF,
0x0, 0xD2, 0x9A, 0xFF, 0xA9, 0x0, 0xFF, 0xFF, 0x36, 0x4D, 0x93, 0xFF,
0x0, 0x0, 0xDA, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0, 0xD4, 0xFF,
},
},
},
{
radius: 1.0,
amount: 0.5,
value: &image.RGBA{
Rect: image.Rect(0, 0, 3, 3),
Stride: 3 * 4,
Pix: []uint8{
0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x40, 0x40, 0xFF,
0x40, 0x80, 0x80, 0xFF, 0xB2, 0x00, 0xCA, 0xFF, 0x80, 0x40, 0x80, 0xFF,
0x00, 0x00, 0xAA, 0xFF, 0xFF, 0xCC, 0xCC, 0xFF, 0xFF, 0x00, 0xAA, 0xFF,
},
},
expected: &image.RGBA{
Rect: image.Rect(0, 0, 3, 3),
Stride: 3 * 4,
Pix: []uint8{
0xFF, 0x0, 0x0, 0xFF, 0xFF, 0xFF, 0x0, 0xFF, 0xFF, 0x43, 0x32, 0xFF,
0xB, 0xA9, 0x8D, 0xFF, 0xAD, 0x0, 0xFA, 0xFF, 0x5B, 0x46, 0x89, 0xFF,
0x0, 0x0, 0xC2, 0xFF, 0xFF, 0xFF, 0xF3, 0xFF, 0xFF, 0x0, 0xBF, 0xFF,
},
},
},
}

for _, c := range cases {
actual := UnsharpMask(c.value, c.radius, c.amount)
if !util.RGBAImageEqual(actual, c.expected) {
t.Errorf("%s:\nexpected: %v\nactual: %v\n", "UnsharpMask", util.RGBAToString(c.expected), util.RGBAToString(actual))
}
}
}

func BenchmarkMedian1(b *testing.B) {
img := image.NewRGBA(image.Rect(0, 0, 256, 256))
for n := 0; n < b.N; n++ {
Expand Down

0 comments on commit 35fd190

Please sign in to comment.