1package imaging
2
3import (
4 "image"
5 "image/color"
6 "math"
7)
8
9// Grayscale produces a grayscale version of the image.
10func Grayscale(img image.Image) *image.NRGBA {
11 src := newScanner(img)
12 dst := image.NewNRGBA(image.Rect(0, 0, src.w, src.h))
13 parallel(0, src.h, func(ys <-chan int) {
14 for y := range ys {
15 i := y * dst.Stride
16 src.scan(0, y, src.w, y+1, dst.Pix[i:i+src.w*4])
17 for x := 0; x < src.w; x++ {
18 d := dst.Pix[i : i+3 : i+3]
19 r := d[0]
20 g := d[1]
21 b := d[2]
22 f := 0.299*float64(r) + 0.587*float64(g) + 0.114*float64(b)
23 y := uint8(f + 0.5)
24 d[0] = y
25 d[1] = y
26 d[2] = y
27 i += 4
28 }
29 }
30 })
31 return dst
32}
33
34// Invert produces an inverted (negated) version of the image.
35func Invert(img image.Image) *image.NRGBA {
36 src := newScanner(img)
37 dst := image.NewNRGBA(image.Rect(0, 0, src.w, src.h))
38 parallel(0, src.h, func(ys <-chan int) {
39 for y := range ys {
40 i := y * dst.Stride
41 src.scan(0, y, src.w, y+1, dst.Pix[i:i+src.w*4])
42 for x := 0; x < src.w; x++ {
43 d := dst.Pix[i : i+3 : i+3]
44 d[0] = 255 - d[0]
45 d[1] = 255 - d[1]
46 d[2] = 255 - d[2]
47 i += 4
48 }
49 }
50 })
51 return dst
52}
53
54// AdjustSaturation changes the saturation of the image using the percentage parameter and returns the adjusted image.
55// The percentage must be in the range (-100, 100).
56// The percentage = 0 gives the original image.
57// The percentage = 100 gives the image with the saturation value doubled for each pixel.
58// The percentage = -100 gives the image with the saturation value zeroed for each pixel (grayscale).
59//
60// Examples:
61// dstImage = imaging.AdjustSaturation(srcImage, 25) // Increase image saturation by 25%.
62// dstImage = imaging.AdjustSaturation(srcImage, -10) // Decrease image saturation by 10%.
63//
64func AdjustSaturation(img image.Image, percentage float64) *image.NRGBA {
65 percentage = math.Min(math.Max(percentage, -100), 100)
66 multiplier := 1 + percentage/100
67
68 return AdjustFunc(img, func(c color.NRGBA) color.NRGBA {
69 h, s, l := rgbToHSL(c.R, c.G, c.B)
70 s *= multiplier
71 if s > 1 {
72 s = 1
73 }
74 r, g, b := hslToRGB(h, s, l)
75 return color.NRGBA{r, g, b, c.A}
76 })
77}
78
79// AdjustContrast changes the contrast of the image using the percentage parameter and returns the adjusted image.
80// The percentage must be in range (-100, 100). The percentage = 0 gives the original image.
81// The percentage = -100 gives solid gray image.
82//
83// Examples:
84//
85// dstImage = imaging.AdjustContrast(srcImage, -10) // Decrease image contrast by 10%.
86// dstImage = imaging.AdjustContrast(srcImage, 20) // Increase image contrast by 20%.
87//
88func AdjustContrast(img image.Image, percentage float64) *image.NRGBA {
89 percentage = math.Min(math.Max(percentage, -100.0), 100.0)
90 lut := make([]uint8, 256)
91
92 v := (100.0 + percentage) / 100.0
93 for i := 0; i < 256; i++ {
94 switch {
95 case 0 <= v && v <= 1:
96 lut[i] = clamp((0.5 + (float64(i)/255.0-0.5)*v) * 255.0)
97 case 1 < v && v < 2:
98 lut[i] = clamp((0.5 + (float64(i)/255.0-0.5)*(1/(2.0-v))) * 255.0)
99 default:
100 lut[i] = uint8(float64(i)/255.0+0.5) * 255
101 }
102 }
103
104 return adjustLUT(img, lut)
105}
106
107// AdjustBrightness changes the brightness of the image using the percentage parameter and returns the adjusted image.
108// The percentage must be in range (-100, 100). The percentage = 0 gives the original image.
109// The percentage = -100 gives solid black image. The percentage = 100 gives solid white image.
110//
111// Examples:
112//
113// dstImage = imaging.AdjustBrightness(srcImage, -15) // Decrease image brightness by 15%.
114// dstImage = imaging.AdjustBrightness(srcImage, 10) // Increase image brightness by 10%.
115//
116func AdjustBrightness(img image.Image, percentage float64) *image.NRGBA {
117 percentage = math.Min(math.Max(percentage, -100.0), 100.0)
118 lut := make([]uint8, 256)
119
120 shift := 255.0 * percentage / 100.0
121 for i := 0; i < 256; i++ {
122 lut[i] = clamp(float64(i) + shift)
123 }
124
125 return adjustLUT(img, lut)
126}
127
128// AdjustGamma performs a gamma correction on the image and returns the adjusted image.
129// Gamma parameter must be positive. Gamma = 1.0 gives the original image.
130// Gamma less than 1.0 darkens the image and gamma greater than 1.0 lightens it.
131//
132// Example:
133//
134// dstImage = imaging.AdjustGamma(srcImage, 0.7)
135//
136func AdjustGamma(img image.Image, gamma float64) *image.NRGBA {
137 e := 1.0 / math.Max(gamma, 0.0001)
138 lut := make([]uint8, 256)
139
140 for i := 0; i < 256; i++ {
141 lut[i] = clamp(math.Pow(float64(i)/255.0, e) * 255.0)
142 }
143
144 return adjustLUT(img, lut)
145}
146
147// AdjustSigmoid changes the contrast of the image using a sigmoidal function and returns the adjusted image.
148// It's a non-linear contrast change useful for photo adjustments as it preserves highlight and shadow detail.
149// The midpoint parameter is the midpoint of contrast that must be between 0 and 1, typically 0.5.
150// The factor parameter indicates how much to increase or decrease the contrast, typically in range (-10, 10).
151// If the factor parameter is positive the image contrast is increased otherwise the contrast is decreased.
152//
153// Examples:
154//
155// dstImage = imaging.AdjustSigmoid(srcImage, 0.5, 3.0) // Increase the contrast.
156// dstImage = imaging.AdjustSigmoid(srcImage, 0.5, -3.0) // Decrease the contrast.
157//
158func AdjustSigmoid(img image.Image, midpoint, factor float64) *image.NRGBA {
159 if factor == 0 {
160 return Clone(img)
161 }
162
163 lut := make([]uint8, 256)
164 a := math.Min(math.Max(midpoint, 0.0), 1.0)
165 b := math.Abs(factor)
166 sig0 := sigmoid(a, b, 0)
167 sig1 := sigmoid(a, b, 1)
168 e := 1.0e-6
169
170 if factor > 0 {
171 for i := 0; i < 256; i++ {
172 x := float64(i) / 255.0
173 sigX := sigmoid(a, b, x)
174 f := (sigX - sig0) / (sig1 - sig0)
175 lut[i] = clamp(f * 255.0)
176 }
177 } else {
178 for i := 0; i < 256; i++ {
179 x := float64(i) / 255.0
180 arg := math.Min(math.Max((sig1-sig0)*x+sig0, e), 1.0-e)
181 f := a - math.Log(1.0/arg-1.0)/b
182 lut[i] = clamp(f * 255.0)
183 }
184 }
185
186 return adjustLUT(img, lut)
187}
188
189func sigmoid(a, b, x float64) float64 {
190 return 1 / (1 + math.Exp(b*(a-x)))
191}
192
193// adjustLUT applies the given lookup table to the colors of the image.
194func adjustLUT(img image.Image, lut []uint8) *image.NRGBA {
195 src := newScanner(img)
196 dst := image.NewNRGBA(image.Rect(0, 0, src.w, src.h))
197 lut = lut[0:256]
198 parallel(0, src.h, func(ys <-chan int) {
199 for y := range ys {
200 i := y * dst.Stride
201 src.scan(0, y, src.w, y+1, dst.Pix[i:i+src.w*4])
202 for x := 0; x < src.w; x++ {
203 d := dst.Pix[i : i+3 : i+3]
204 d[0] = lut[d[0]]
205 d[1] = lut[d[1]]
206 d[2] = lut[d[2]]
207 i += 4
208 }
209 }
210 })
211 return dst
212}
213
214// AdjustFunc applies the fn function to each pixel of the img image and returns the adjusted image.
215//
216// Example:
217//
218// dstImage = imaging.AdjustFunc(
219// srcImage,
220// func(c color.NRGBA) color.NRGBA {
221// // Shift the red channel by 16.
222// r := int(c.R) + 16
223// if r > 255 {
224// r = 255
225// }
226// return color.NRGBA{uint8(r), c.G, c.B, c.A}
227// }
228// )
229//
230func AdjustFunc(img image.Image, fn func(c color.NRGBA) color.NRGBA) *image.NRGBA {
231 src := newScanner(img)
232 dst := image.NewNRGBA(image.Rect(0, 0, src.w, src.h))
233 parallel(0, src.h, func(ys <-chan int) {
234 for y := range ys {
235 i := y * dst.Stride
236 src.scan(0, y, src.w, y+1, dst.Pix[i:i+src.w*4])
237 for x := 0; x < src.w; x++ {
238 d := dst.Pix[i : i+4 : i+4]
239 r := d[0]
240 g := d[1]
241 b := d[2]
242 a := d[3]
243 c := fn(color.NRGBA{r, g, b, a})
244 d[0] = c.R
245 d[1] = c.G
246 d[2] = c.B
247 d[3] = c.A
248 i += 4
249 }
250 }
251 })
252 return dst
253}