colors.go

  1package gift
  2
  3import (
  4	"image"
  5	"image/draw"
  6	"math"
  7)
  8
  9func prepareLut(lutSize int, fn func(float32) float32) []float32 {
 10	lut := make([]float32, lutSize)
 11	q := 1 / float32(lutSize-1)
 12	for v := 0; v < lutSize; v++ {
 13		u := float32(v) * q
 14		lut[v] = fn(u)
 15	}
 16	return lut
 17}
 18
 19func getFromLut(lut []float32, u float32) float32 {
 20	v := int(u*float32(len(lut)-1) + 0.5)
 21	return lut[v]
 22}
 23
 24type colorchanFilter struct {
 25	fn  func(float32) float32
 26	lut bool
 27}
 28
 29func (p *colorchanFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) {
 30	dstBounds = image.Rect(0, 0, srcBounds.Dx(), srcBounds.Dy())
 31	return
 32}
 33
 34func (p *colorchanFilter) Draw(dst draw.Image, src image.Image, options *Options) {
 35	if options == nil {
 36		options = &defaultOptions
 37	}
 38
 39	srcb := src.Bounds()
 40	dstb := dst.Bounds()
 41	pixGetter := newPixelGetter(src)
 42	pixSetter := newPixelSetter(dst)
 43
 44	var useLut bool
 45	var lut []float32
 46
 47	useLut = false
 48	if p.lut {
 49		var lutSize int
 50
 51		it := pixGetter.it
 52		if it == itNRGBA || it == itRGBA || it == itGray || it == itYCbCr {
 53			lutSize = 0xff + 1
 54		} else {
 55			lutSize = 0xffff + 1
 56		}
 57
 58		numCalculations := srcb.Dx() * srcb.Dy() * 3
 59		if numCalculations > lutSize*2 {
 60			useLut = true
 61			lut = prepareLut(lutSize, p.fn)
 62		}
 63	}
 64
 65	parallelize(options.Parallelization, srcb.Min.Y, srcb.Max.Y, func(pmin, pmax int) {
 66		for y := pmin; y < pmax; y++ {
 67			for x := srcb.Min.X; x < srcb.Max.X; x++ {
 68				px := pixGetter.getPixel(x, y)
 69				if useLut {
 70					px.r = getFromLut(lut, px.r)
 71					px.g = getFromLut(lut, px.g)
 72					px.b = getFromLut(lut, px.b)
 73				} else {
 74					px.r = p.fn(px.r)
 75					px.g = p.fn(px.g)
 76					px.b = p.fn(px.b)
 77				}
 78				pixSetter.setPixel(dstb.Min.X+x-srcb.Min.X, dstb.Min.Y+y-srcb.Min.Y, px)
 79			}
 80		}
 81	})
 82}
 83
 84// Invert creates a filter that negates the colors of an image.
 85func Invert() Filter {
 86	return &colorchanFilter{
 87		fn: func(x float32) float32 {
 88			return 1 - x
 89		},
 90		lut: false,
 91	}
 92}
 93
 94// ColorspaceSRGBToLinear creates a filter that converts the colors of an image from sRGB to linear RGB.
 95func ColorspaceSRGBToLinear() Filter {
 96	return &colorchanFilter{
 97		fn: func(x float32) float32 {
 98			if x <= 0.04045 {
 99				return x / 12.92
100			}
101			return float32(math.Pow(float64((x+0.055)/1.055), 2.4))
102		},
103		lut: true,
104	}
105}
106
107// ColorspaceLinearToSRGB creates a filter that converts the colors of an image from linear RGB to sRGB.
108func ColorspaceLinearToSRGB() Filter {
109	return &colorchanFilter{
110		fn: func(x float32) float32 {
111			if x <= 0.0031308 {
112				return x * 12.92
113			}
114			return float32(1.055*math.Pow(float64(x), 1/2.4) - 0.055)
115		},
116		lut: true,
117	}
118}
119
120// Gamma creates a filter that performs a gamma correction on an image.
121// The gamma parameter must be positive. Gamma = 1 gives the original image.
122// Gamma less than 1 darkens the image and gamma greater than 1 lightens it.
123func Gamma(gamma float32) Filter {
124	e := 1 / maxf32(gamma, 1.0e-5)
125	return &colorchanFilter{
126		fn: func(x float32) float32 {
127			return powf32(x, e)
128		},
129		lut: true,
130	}
131}
132
133func sigmoid(a, b, x float32) float32 {
134	return 1 / (1 + expf32(b*(a-x)))
135}
136
137// Sigmoid creates a filter that changes the contrast of an image using a sigmoidal function and returns the adjusted image.
138// It's a non-linear contrast change useful for photo adjustments as it preserves highlight and shadow detail.
139// The midpoint parameter is the midpoint of contrast that must be between 0 and 1, typically 0.5.
140// The factor parameter indicates how much to increase or decrease the contrast, typically in range (-10, 10).
141// If the factor parameter is positive the image contrast is increased otherwise the contrast is decreased.
142//
143// Example:
144//
145//	g := gift.New(
146//		gift.Sigmoid(0.5, 5),
147//	)
148//	dst := image.NewRGBA(g.Bounds(src.Bounds()))
149//	g.Draw(dst, src)
150//
151func Sigmoid(midpoint, factor float32) Filter {
152	a := minf32(maxf32(midpoint, 0), 1)
153	b := absf32(factor)
154	sig0 := sigmoid(a, b, 0)
155	sig1 := sigmoid(a, b, 1)
156	e := float32(1.0e-5)
157
158	return &colorchanFilter{
159		fn: func(x float32) float32 {
160			if factor == 0 {
161				return x
162			} else if factor > 0 {
163				sig := sigmoid(a, b, x)
164				return (sig - sig0) / (sig1 - sig0)
165			} else {
166				arg := minf32(maxf32((sig1-sig0)*x+sig0, e), 1-e)
167				return a - logf32(1/arg-1)/b
168			}
169		},
170		lut: true,
171	}
172}
173
174// Contrast creates a filter that changes the contrast of an image.
175// The percentage parameter must be in range (-100, 100). The percentage = 0 gives the original image.
176// The percentage = -100 gives solid grey image. The percentage = 100 gives an overcontrasted image.
177func Contrast(percentage float32) Filter {
178	if percentage == 0 {
179		return &copyimageFilter{}
180	}
181
182	p := 1 + minf32(maxf32(percentage, -100), 100)/100
183
184	return &colorchanFilter{
185		fn: func(x float32) float32 {
186			if 0 <= p && p <= 1 {
187				return 0.5 + (x-0.5)*p
188			} else if 1 < p && p < 2 {
189				return 0.5 + (x-0.5)*(1/(2.0-p))
190			} else {
191				if x < 0.5 {
192					return 0
193				}
194				return 1
195			}
196		},
197		lut: false,
198	}
199}
200
201// Brightness creates a filter that changes the brightness of an image.
202// The percentage parameter must be in range (-100, 100). The percentage = 0 gives the original image.
203// The percentage = -100 gives solid black image. The percentage = 100 gives solid white image.
204func Brightness(percentage float32) Filter {
205	if percentage == 0 {
206		return &copyimageFilter{}
207	}
208
209	shift := minf32(maxf32(percentage, -100), 100) / 100
210
211	return &colorchanFilter{
212		fn: func(x float32) float32 {
213			return x + shift
214		},
215		lut: false,
216	}
217}
218
219type colorFilter struct {
220	fn func(pixel) pixel
221}
222
223func (p *colorFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) {
224	dstBounds = image.Rect(0, 0, srcBounds.Dx(), srcBounds.Dy())
225	return
226}
227
228func (p *colorFilter) Draw(dst draw.Image, src image.Image, options *Options) {
229	if options == nil {
230		options = &defaultOptions
231	}
232
233	srcb := src.Bounds()
234	dstb := dst.Bounds()
235	pixGetter := newPixelGetter(src)
236	pixSetter := newPixelSetter(dst)
237
238	parallelize(options.Parallelization, srcb.Min.Y, srcb.Max.Y, func(pmin, pmax int) {
239		for y := pmin; y < pmax; y++ {
240			for x := srcb.Min.X; x < srcb.Max.X; x++ {
241				px := pixGetter.getPixel(x, y)
242				pixSetter.setPixel(dstb.Min.X+x-srcb.Min.X, dstb.Min.Y+y-srcb.Min.Y, p.fn(px))
243			}
244		}
245	})
246}
247
248// Grayscale creates a filter that produces a grayscale version of an image.
249func Grayscale() Filter {
250	return &colorFilter{
251		fn: func(px pixel) pixel {
252			y := 0.299*px.r + 0.587*px.g + 0.114*px.b
253			return pixel{y, y, y, px.a}
254		},
255	}
256}
257
258// Sepia creates a filter that produces a sepia-toned version of an image.
259// The percentage parameter specifies how much the image should be adjusted. It must be in the range (0, 100)
260//
261// Example:
262//
263//	g := gift.New(
264//		gift.Sepia(100),
265//	)
266//	dst := image.NewRGBA(g.Bounds(src.Bounds()))
267//	g.Draw(dst, src)
268//
269func Sepia(percentage float32) Filter {
270	adjustAmount := minf32(maxf32(percentage, 0), 100) / 100
271	rr := 1 - 0.607*adjustAmount
272	rg := 0.769 * adjustAmount
273	rb := 0.189 * adjustAmount
274	gr := 0.349 * adjustAmount
275	gg := 1 - 0.314*adjustAmount
276	gb := 0.168 * adjustAmount
277	br := 0.272 * adjustAmount
278	bg := 0.534 * adjustAmount
279	bb := 1 - 0.869*adjustAmount
280	return &colorFilter{
281		fn: func(px pixel) pixel {
282			r := px.r*rr + px.g*rg + px.b*rb
283			g := px.r*gr + px.g*gg + px.b*gb
284			b := px.r*br + px.g*bg + px.b*bb
285			return pixel{r, g, b, px.a}
286		},
287	}
288}
289
290func convertHSLToRGB(h, s, l float32) (float32, float32, float32) {
291	if s == 0 {
292		return l, l, l
293	}
294
295	_v := func(p, q, t float32) float32 {
296		if t < 0 {
297			t++
298		}
299		if t > 1 {
300			t--
301		}
302		if t < 1/6.0 {
303			return p + (q-p)*6*t
304		}
305		if t < 1/2.0 {
306			return q
307		}
308		if t < 2/3.0 {
309			return p + (q-p)*(2/3.0-t)*6
310		}
311		return p
312	}
313
314	var p, q float32
315	if l < 0.5 {
316		q = l * (1 + s)
317	} else {
318		q = l + s - l*s
319	}
320	p = 2*l - q
321
322	r := _v(p, q, h+1/3.0)
323	g := _v(p, q, h)
324	b := _v(p, q, h-1/3.0)
325
326	return r, g, b
327}
328
329func convertRGBToHSL(r, g, b float32) (float32, float32, float32) {
330	max := maxf32(r, maxf32(g, b))
331	min := minf32(r, minf32(g, b))
332
333	l := (max + min) / 2
334
335	if max == min {
336		return 0, 0, l
337	}
338
339	var h, s float32
340	d := max - min
341	if l > 0.5 {
342		s = d / (2 - max - min)
343	} else {
344		s = d / (max + min)
345	}
346
347	if r == max {
348		h = (g - b) / d
349		if g < b {
350			h += 6
351		}
352	} else if g == max {
353		h = (b-r)/d + 2
354	} else {
355		h = (r-g)/d + 4
356	}
357	h /= 6
358
359	return h, s, l
360}
361
362func normalizeHue(hue float32) float32 {
363	hue = hue - float32(int(hue))
364	if hue < 0 {
365		hue++
366	}
367	return hue
368}
369
370// Hue creates a filter that rotates the hue of an image.
371// The shift parameter is the hue angle shift, typically in range (-180, 180).
372// The shift = 0 gives the original image.
373func Hue(shift float32) Filter {
374	p := normalizeHue(shift / 360)
375	if p == 0 {
376		return &copyimageFilter{}
377	}
378
379	return &colorFilter{
380		fn: func(px pixel) pixel {
381			h, s, l := convertRGBToHSL(px.r, px.g, px.b)
382			h = normalizeHue(h + p)
383			r, g, b := convertHSLToRGB(h, s, l)
384			return pixel{r, g, b, px.a}
385		},
386	}
387}
388
389// Saturation creates a filter that changes the saturation of an image.
390// The percentage parameter must be in range (-100, 500). The percentage = 0 gives the original image.
391func Saturation(percentage float32) Filter {
392	p := 1 + minf32(maxf32(percentage, -100), 500)/100
393	if p == 1 {
394		return &copyimageFilter{}
395	}
396
397	return &colorFilter{
398		fn: func(px pixel) pixel {
399			h, s, l := convertRGBToHSL(px.r, px.g, px.b)
400			s *= p
401			if s > 1 {
402				s = 1
403			}
404			r, g, b := convertHSLToRGB(h, s, l)
405			return pixel{r, g, b, px.a}
406		},
407	}
408}
409
410// Colorize creates a filter that produces a colorized version of an image.
411// The hue parameter is the angle on the color wheel, typically in range (0, 360).
412// The saturation parameter must be in range (0, 100).
413// The percentage parameter specifies the strength of the effect, it must be in range (0, 100).
414//
415// Example:
416//
417//	g := gift.New(
418//		gift.Colorize(240, 50, 100), // blue colorization, 50% saturation
419//	)
420//	dst := image.NewRGBA(g.Bounds(src.Bounds()))
421//	g.Draw(dst, src)
422//
423func Colorize(hue, saturation, percentage float32) Filter {
424	h := normalizeHue(hue / 360)
425	s := minf32(maxf32(saturation, 0), 100) / 100
426	p := minf32(maxf32(percentage, 0), 100) / 100
427	if p == 0 {
428		return &copyimageFilter{}
429	}
430
431	return &colorFilter{
432		fn: func(px pixel) pixel {
433			_, _, l := convertRGBToHSL(px.r, px.g, px.b)
434			r, g, b := convertHSLToRGB(h, s, l)
435			px.r += (r - px.r) * p
436			px.g += (g - px.g) * p
437			px.b += (b - px.b) * p
438			return px
439		},
440	}
441}
442
443// ColorBalance creates a filter that changes the color balance of an image.
444// The percentage parameters for each color channel (red, green, blue) must be in range (-100, 500).
445//
446// Example:
447//
448//	g := gift.New(
449//		gift.ColorBalance(20, -20, 0), // +20% red, -20% green
450//	)
451//	dst := image.NewRGBA(g.Bounds(src.Bounds()))
452//	g.Draw(dst, src)
453//
454func ColorBalance(percentageRed, percentageGreen, percentageBlue float32) Filter {
455	pr := 1 + minf32(maxf32(percentageRed, -100), 500)/100
456	pg := 1 + minf32(maxf32(percentageGreen, -100), 500)/100
457	pb := 1 + minf32(maxf32(percentageBlue, -100), 500)/100
458
459	return &colorFilter{
460		fn: func(px pixel) pixel {
461			px.r *= pr
462			px.g *= pg
463			px.b *= pb
464			return px
465		},
466	}
467}
468
469// Threshold creates a filter that applies black/white thresholding to the image.
470// The percentage parameter must be in range (0, 100).
471func Threshold(percentage float32) Filter {
472	p := minf32(maxf32(percentage, 0), 100) / 100
473	return &colorFilter{
474		fn: func(px pixel) pixel {
475			y := 0.299*px.r + 0.587*px.g + 0.114*px.b
476			if y > p {
477				return pixel{1, 1, 1, px.a}
478			}
479			return pixel{0, 0, 0, px.a}
480		},
481	}
482}
483
484// ColorFunc creates a filter that changes the colors of an image using custom function.
485// The fn parameter specifies a function that takes red, green, blue and alpha channels of a pixel
486// as float32 values in range (0, 1) and returns the modified channel values.
487//
488// Example:
489//
490//	g := gift.New(
491//		gift.ColorFunc(
492//			func(r0, g0, b0, a0 float32) (r, g, b, a float32) {
493//				r = 1 - r0   // invert the red channel
494//				g = g0 + 0.1 // shift the green channel by 0.1
495//				b = 0        // set the blue channel to 0
496//				a = a0       // preserve the alpha channel
497//				return
498//			},
499//		),
500//	)
501//	dst := image.NewRGBA(g.Bounds(src.Bounds()))
502//	g.Draw(dst, src)
503//
504func ColorFunc(fn func(r0, g0, b0, a0 float32) (r, g, b, a float32)) Filter {
505	return &colorFilter{
506		fn: func(px pixel) pixel {
507			r, g, b, a := fn(px.r, px.g, px.b, px.a)
508			return pixel{r, g, b, a}
509		},
510	}
511}