1/*Package effect provides the functionality to manipulate images to achieve various looks.*/ 2package effect 3 4import ( 5 "image" 6 "image/color" 7 "math" 8 9 "github.com/anthonynsimon/bild/adjust" 10 "github.com/anthonynsimon/bild/blend" 11 "github.com/anthonynsimon/bild/blur" 12 "github.com/anthonynsimon/bild/clone" 13 "github.com/anthonynsimon/bild/convolution" 14 "github.com/anthonynsimon/bild/math/f64" 15 "github.com/anthonynsimon/bild/parallel" 16 "github.com/anthonynsimon/bild/util" 17) 18 19// Invert returns a negated version of the image. 20func Invert(src image.Image) *image.RGBA { 21 fn := func(c color.RGBA) color.RGBA { 22 return color.RGBA{255 - c.R, 255 - c.G, 255 - c.B, c.A} 23 } 24 25 img := adjust.Apply(src, fn) 26 27 return img 28} 29 30// Grayscale returns a copy of the image in Grayscale using the weights 31// 0.3R + 0.6G + 0.1B as a heuristic. 32func Grayscale(img image.Image) *image.RGBA { 33 return GrayscaleWithWeights(img, 0.3, 0.6, 0.1) 34} 35 36// GrayscaleWithWeights returns a copy of the image in Grayscale using the given weights. 37// The weights should be in the range 0.0 to 1.0 inclusive. 38func GrayscaleWithWeights(img image.Image, r, g, b float64) *image.RGBA { 39 src := clone.AsRGBA(img) 40 bounds := src.Bounds() 41 srcW, srcH := bounds.Dx(), bounds.Dy() 42 43 if bounds.Empty() { 44 return &image.RGBA{} 45 } 46 47 dst := image.NewRGBA(bounds) 48 49 parallel.Line(srcH, func(start, end int) { 50 for y := start; y < end; y++ { 51 for x := 0; x < srcW; x++ { 52 pos := y*src.Stride + x*4 53 54 c := r*float64(src.Pix[pos+0]) + g*float64(src.Pix[pos+1]) + b*float64(src.Pix[pos+2]) 55 k := uint8(c + 0.5) 56 dst.Pix[pos] = k 57 dst.Pix[pos+1] = k 58 dst.Pix[pos+2] = k 59 dst.Pix[pos+3] = src.Pix[pos+3] 60 } 61 } 62 }) 63 64 return dst 65} 66 67// Sepia returns a copy of the image in Sepia tone. 68func Sepia(img image.Image) *image.RGBA { 69 fn := func(c color.RGBA) color.RGBA { 70 // Cache values as float64 71 var fc [3]float64 72 fc[0] = float64(c.R) 73 fc[1] = float64(c.G) 74 fc[2] = float64(c.B) 75 76 // Calculate out color based on heuristic 77 outRed := fc[0]*0.393 + fc[1]*0.769 + fc[2]*0.189 78 outGreen := fc[0]*0.349 + fc[1]*0.686 + fc[2]*0.168 79 outBlue := fc[0]*0.272 + fc[1]*0.534 + fc[2]*0.131 80 81 // Clamp ceiled values before returning 82 return color.RGBA{ 83 R: uint8(f64.Clamp(outRed+0.5, 0, 255)), 84 G: uint8(f64.Clamp(outGreen+0.5, 0, 255)), 85 B: uint8(f64.Clamp(outBlue+0.5, 0, 255)), 86 A: c.A, 87 } 88 } 89 90 dst := adjust.Apply(img, fn) 91 92 return dst 93} 94 95// EdgeDetection returns a copy of the image with its edges highlighted. 96func EdgeDetection(src image.Image, radius float64) *image.RGBA { 97 if radius <= 0 { 98 return image.NewRGBA(src.Bounds()) 99 } 100 101 length := int(math.Ceil(2*radius + 1)) 102 k := convolution.NewKernel(length, length) 103 104 for x := 0; x < length; x++ { 105 for y := 0; y < length; y++ { 106 v := -1.0 107 if x == length/2 && y == length/2 { 108 v = float64(length*length) - 1 109 } 110 k.Matrix[y*length+x] = v 111 112 } 113 } 114 return convolution.Convolve(src, k, &convolution.Options{Bias: 0, Wrap: false, KeepAlpha: true}) 115} 116 117// Emboss returns a copy of the image in which each pixel has been 118// replaced either by a highlight or a shadow representation. 119func Emboss(src image.Image) *image.RGBA { 120 k := convolution.Kernel{ 121 Matrix: []float64{ 122 -1, -1, 0, 123 -1, 0, 1, 124 0, 1, 1, 125 }, 126 Width: 3, 127 Height: 3, 128 } 129 130 return convolution.Convolve(src, &k, &convolution.Options{Bias: 128, Wrap: false, KeepAlpha: true}) 131} 132 133// Sharpen returns a sharpened copy of the image by detecting its edges and adding it to the original. 134func Sharpen(src image.Image) *image.RGBA { 135 k := convolution.Kernel{ 136 Matrix: []float64{ 137 0, -1, 0, 138 -1, 5, -1, 139 0, -1, 0, 140 }, 141 Width: 3, 142 Height: 3, 143 } 144 145 return convolution.Convolve(src, &k, &convolution.Options{Bias: 0, Wrap: false}) 146} 147 148// UnsharpMask returns a copy of the image with its high-frecuency components amplified. 149// Parameter radius corresponds to the radius to be samples per pixel. 150// Parameter amount is the normalized strength of the effect. A value of 0.0 will leave 151// the image untouched and a value of 1.0 will fully apply the unsharp mask. 152func UnsharpMask(img image.Image, radius, amount float64) *image.RGBA { 153 amount = f64.Clamp(amount, 0, 10) 154 155 blurred := blur.Gaussian(img, 5*radius) // scale radius by matching factor 156 157 bounds := img.Bounds() 158 src := clone.AsRGBA(img) 159 dst := image.NewRGBA(bounds) 160 w, h := bounds.Dx(), bounds.Dy() 161 162 parallel.Line(h, func(start, end int) { 163 for y := start; y < end; y++ { 164 for x := 0; x < w; x++ { 165 pos := y*dst.Stride + x*4 166 167 r := float64(src.Pix[pos+0]) 168 g := float64(src.Pix[pos+1]) 169 b := float64(src.Pix[pos+2]) 170 a := float64(src.Pix[pos+3]) 171 172 rBlur := float64(blurred.Pix[pos+0]) 173 gBlur := float64(blurred.Pix[pos+1]) 174 bBlur := float64(blurred.Pix[pos+2]) 175 aBlur := float64(blurred.Pix[pos+3]) 176 177 r = r + (r-rBlur)*amount 178 g = g + (g-gBlur)*amount 179 b = b + (b-bBlur)*amount 180 a = a + (a-aBlur)*amount 181 182 dst.Pix[pos+0] = uint8(f64.Clamp(r, 0, 255)) 183 dst.Pix[pos+1] = uint8(f64.Clamp(g, 0, 255)) 184 dst.Pix[pos+2] = uint8(f64.Clamp(b, 0, 255)) 185 dst.Pix[pos+3] = uint8(f64.Clamp(a, 0, 255)) 186 } 187 } 188 }) 189 190 return dst 191} 192 193// Sobel returns an image emphasising edges using an approximation to the Sobel–Feldman operator. 194func Sobel(src image.Image) *image.RGBA { 195 196 hk := convolution.Kernel{ 197 Matrix: []float64{ 198 1, 2, 1, 199 0, 0, 0, 200 -1, -2, -1, 201 }, 202 Width: 3, 203 Height: 3, 204 } 205 206 vk := convolution.Kernel{ 207 Matrix: []float64{ 208 -1, 0, 1, 209 -2, 0, 2, 210 -1, 0, 1, 211 }, 212 Width: 3, 213 Height: 3, 214 } 215 216 vSobel := convolution.Convolve(src, &vk, &convolution.Options{Bias: 0, Wrap: false, KeepAlpha: true}) 217 hSobel := convolution.Convolve(src, &hk, &convolution.Options{Bias: 0, Wrap: false, KeepAlpha: true}) 218 219 return blend.Add(blend.Multiply(vSobel, vSobel), blend.Multiply(hSobel, hSobel)) 220} 221 222// Median returns a new image in which each pixel is the median of its neighbors. 223// The parameter radius corresponds to the radius of the neighbor area to be searched, 224// for example a radius of R will result in a search window length of 2R+1 for each dimension. 225func Median(img image.Image, radius float64) *image.RGBA { 226 fn := func(neighbors []color.RGBA) color.RGBA { 227 util.SortRGBA(neighbors, 0, len(neighbors)-1) 228 return neighbors[len(neighbors)/2] 229 } 230 231 result := spatialFilter(img, radius, fn) 232 233 return result 234} 235 236// Dilate picks the local maxima from the neighbors of each pixel and returns the resulting image. 237// The parameter radius corresponds to the radius of the neighbor area to be searched, 238// for example a radius of R will result in a search window length of 2R+1 for each dimension. 239func Dilate(img image.Image, radius float64) *image.RGBA { 240 fn := func(neighbors []color.RGBA) color.RGBA { 241 util.SortRGBA(neighbors, 0, len(neighbors)-1) 242 return neighbors[len(neighbors)-1] 243 } 244 245 result := spatialFilter(img, radius, fn) 246 247 return result 248} 249 250// Erode picks the local minima from the neighbors of each pixel and returns the resulting image. 251// The parameter radius corresponds to the radius of the neighbor area to be searched, 252// for example a radius of R will result in a search window length of 2R+1 for each dimension. 253func Erode(img image.Image, radius float64) *image.RGBA { 254 fn := func(neighbors []color.RGBA) color.RGBA { 255 util.SortRGBA(neighbors, 0, len(neighbors)-1) 256 return neighbors[0] 257 } 258 259 result := spatialFilter(img, radius, fn) 260 261 return result 262} 263 264// spatialFilter goes through each pixel on an image collecting its neighbors and picking one 265// based on the function provided. The resulting image is then returned. 266// The parameter radius corresponds to the radius of the neighbor area to be searched, 267// for example a radius of R will result in a search window length of 2R+1 for each dimension. 268// The parameter pickerFn is the function that receives the list of neighbors and returns the selected 269// neighbor to be used for the resulting image. 270func spatialFilter(img image.Image, radius float64, pickerFn func(neighbors []color.RGBA) color.RGBA) *image.RGBA { 271 if radius <= 0 { 272 return clone.AsRGBA(img) 273 } 274 275 padding := int(radius + 1.5) 276 src := clone.Pad(img, padding, padding, clone.EdgeExtend) 277 278 kernelSize := int(2*radius + 1.5) 279 280 bounds := img.Bounds() 281 dst := image.NewRGBA(bounds) 282 283 w, h := bounds.Dx(), bounds.Dy() 284 neighborsCount := kernelSize * kernelSize 285 286 parallel.Line(h, func(start, end int) { 287 for y := start + padding; y < end+padding; y++ { 288 for x := padding; x < w+padding; x++ { 289 290 neighbors := make([]color.RGBA, neighborsCount) 291 i := 0 292 for ky := 0; ky < kernelSize; ky++ { 293 for kx := 0; kx < kernelSize; kx++ { 294 ix := x - kernelSize>>1 + kx 295 iy := y - kernelSize>>1 + ky 296 297 ipos := iy*src.Stride + ix*4 298 neighbors[i] = color.RGBA{ 299 R: src.Pix[ipos+0], 300 G: src.Pix[ipos+1], 301 B: src.Pix[ipos+2], 302 A: src.Pix[ipos+3], 303 } 304 i++ 305 } 306 } 307 308 c := pickerFn(neighbors) 309 310 pos := (y-padding)*dst.Stride + (x-padding)*4 311 dst.Pix[pos+0] = c.R 312 dst.Pix[pos+1] = c.G 313 dst.Pix[pos+2] = c.B 314 dst.Pix[pos+3] = c.A 315 } 316 } 317 }) 318 319 return dst 320} 321