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