git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/imaging/adjust.go (about) 1 package imaging 2 3 import ( 4 "image" 5 "image/color" 6 "math" 7 ) 8 9 // Grayscale produces a grayscale version of the image. 10 func 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. 35 func 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 // 62 // dstImage = imaging.AdjustSaturation(srcImage, 25) // Increase image saturation by 25%. 63 // dstImage = imaging.AdjustSaturation(srcImage, -10) // Decrease image saturation by 10%. 64 func AdjustSaturation(img image.Image, percentage float64) *image.NRGBA { 65 if percentage == 0 { 66 return Clone(img) 67 } 68 69 percentage = math.Min(math.Max(percentage, -100), 100) 70 multiplier := 1 + percentage/100 71 72 return AdjustFunc(img, func(c color.NRGBA) color.NRGBA { 73 h, s, l := rgbToHSL(c.R, c.G, c.B) 74 s *= multiplier 75 if s > 1 { 76 s = 1 77 } 78 r, g, b := hslToRGB(h, s, l) 79 return color.NRGBA{r, g, b, c.A} 80 }) 81 } 82 83 // AdjustHue changes the hue of the image using the shift parameter (measured in degrees) and returns the adjusted image. 84 // The shift = 0 (or 360 / -360 / etc.) gives the original image. 85 // The shift = 180 (or -180) corresponds to a 180° degree rotation of the color wheel and thus gives the image with its hue inverted for each pixel. 86 // 87 // Examples: 88 // 89 // dstImage = imaging.AdjustHue(srcImage, 90) // Shift Hue by 90°. 90 // dstImage = imaging.AdjustHue(srcImage, -30) // Shift Hue by -30°. 91 func AdjustHue(img image.Image, shift float64) *image.NRGBA { 92 if math.Mod(shift, 360) == 0 { 93 return Clone(img) 94 } 95 96 summand := shift / 360 97 98 return AdjustFunc(img, func(c color.NRGBA) color.NRGBA { 99 h, s, l := rgbToHSL(c.R, c.G, c.B) 100 h += summand 101 h = math.Mod(h, 1) 102 //Adding 1 because Golang's Modulo function behaves differently to similar operators in most other languages. 103 if h < 0 { 104 h++ 105 } 106 r, g, b := hslToRGB(h, s, l) 107 return color.NRGBA{r, g, b, c.A} 108 }) 109 } 110 111 // AdjustContrast changes the contrast of the image using the percentage parameter and returns the adjusted image. 112 // The percentage must be in range (-100, 100). The percentage = 0 gives the original image. 113 // The percentage = -100 gives solid gray image. 114 // 115 // Examples: 116 // 117 // dstImage = imaging.AdjustContrast(srcImage, -10) // Decrease image contrast by 10%. 118 // dstImage = imaging.AdjustContrast(srcImage, 20) // Increase image contrast by 20%. 119 func AdjustContrast(img image.Image, percentage float64) *image.NRGBA { 120 if percentage == 0 { 121 return Clone(img) 122 } 123 124 percentage = math.Min(math.Max(percentage, -100.0), 100.0) 125 lut := make([]uint8, 256) 126 127 v := (100.0 + percentage) / 100.0 128 for i := 0; i < 256; i++ { 129 switch { 130 case 0 <= v && v <= 1: 131 lut[i] = clamp((0.5 + (float64(i)/255.0-0.5)*v) * 255.0) 132 case 1 < v && v < 2: 133 lut[i] = clamp((0.5 + (float64(i)/255.0-0.5)*(1/(2.0-v))) * 255.0) 134 default: 135 lut[i] = uint8(float64(i)/255.0+0.5) * 255 136 } 137 } 138 139 return adjustLUT(img, lut) 140 } 141 142 // AdjustBrightness changes the brightness of the image using the percentage parameter and returns the adjusted image. 143 // The percentage must be in range (-100, 100). The percentage = 0 gives the original image. 144 // The percentage = -100 gives solid black image. The percentage = 100 gives solid white image. 145 // 146 // Examples: 147 // 148 // dstImage = imaging.AdjustBrightness(srcImage, -15) // Decrease image brightness by 15%. 149 // dstImage = imaging.AdjustBrightness(srcImage, 10) // Increase image brightness by 10%. 150 func AdjustBrightness(img image.Image, percentage float64) *image.NRGBA { 151 if percentage == 0 { 152 return Clone(img) 153 } 154 155 percentage = math.Min(math.Max(percentage, -100.0), 100.0) 156 lut := make([]uint8, 256) 157 158 shift := 255.0 * percentage / 100.0 159 for i := 0; i < 256; i++ { 160 lut[i] = clamp(float64(i) + shift) 161 } 162 163 return adjustLUT(img, lut) 164 } 165 166 // AdjustGamma performs a gamma correction on the image and returns the adjusted image. 167 // Gamma parameter must be positive. Gamma = 1.0 gives the original image. 168 // Gamma less than 1.0 darkens the image and gamma greater than 1.0 lightens it. 169 // 170 // Example: 171 // 172 // dstImage = imaging.AdjustGamma(srcImage, 0.7) 173 func AdjustGamma(img image.Image, gamma float64) *image.NRGBA { 174 if gamma == 1 { 175 return Clone(img) 176 } 177 178 e := 1.0 / math.Max(gamma, 0.0001) 179 lut := make([]uint8, 256) 180 181 for i := 0; i < 256; i++ { 182 lut[i] = clamp(math.Pow(float64(i)/255.0, e) * 255.0) 183 } 184 185 return adjustLUT(img, lut) 186 } 187 188 // AdjustSigmoid changes the contrast of the image using a sigmoidal function and returns the adjusted image. 189 // It's a non-linear contrast change useful for photo adjustments as it preserves highlight and shadow detail. 190 // The midpoint parameter is the midpoint of contrast that must be between 0 and 1, typically 0.5. 191 // The factor parameter indicates how much to increase or decrease the contrast, typically in range (-10, 10). 192 // If the factor parameter is positive the image contrast is increased otherwise the contrast is decreased. 193 // 194 // Examples: 195 // 196 // dstImage = imaging.AdjustSigmoid(srcImage, 0.5, 3.0) // Increase the contrast. 197 // dstImage = imaging.AdjustSigmoid(srcImage, 0.5, -3.0) // Decrease the contrast. 198 func AdjustSigmoid(img image.Image, midpoint, factor float64) *image.NRGBA { 199 if factor == 0 { 200 return Clone(img) 201 } 202 203 lut := make([]uint8, 256) 204 a := math.Min(math.Max(midpoint, 0.0), 1.0) 205 b := math.Abs(factor) 206 sig0 := sigmoid(a, b, 0) 207 sig1 := sigmoid(a, b, 1) 208 e := 1.0e-6 209 210 if factor > 0 { 211 for i := 0; i < 256; i++ { 212 x := float64(i) / 255.0 213 sigX := sigmoid(a, b, x) 214 f := (sigX - sig0) / (sig1 - sig0) 215 lut[i] = clamp(f * 255.0) 216 } 217 } else { 218 for i := 0; i < 256; i++ { 219 x := float64(i) / 255.0 220 arg := math.Min(math.Max((sig1-sig0)*x+sig0, e), 1.0-e) 221 f := a - math.Log(1.0/arg-1.0)/b 222 lut[i] = clamp(f * 255.0) 223 } 224 } 225 226 return adjustLUT(img, lut) 227 } 228 229 func sigmoid(a, b, x float64) float64 { 230 return 1 / (1 + math.Exp(b*(a-x))) 231 } 232 233 // adjustLUT applies the given lookup table to the colors of the image. 234 func adjustLUT(img image.Image, lut []uint8) *image.NRGBA { 235 src := newScanner(img) 236 dst := image.NewNRGBA(image.Rect(0, 0, src.w, src.h)) 237 lut = lut[0:256] 238 parallel(0, src.h, func(ys <-chan int) { 239 for y := range ys { 240 i := y * dst.Stride 241 src.scan(0, y, src.w, y+1, dst.Pix[i:i+src.w*4]) 242 for x := 0; x < src.w; x++ { 243 d := dst.Pix[i : i+3 : i+3] 244 d[0] = lut[d[0]] 245 d[1] = lut[d[1]] 246 d[2] = lut[d[2]] 247 i += 4 248 } 249 } 250 }) 251 return dst 252 } 253 254 // AdjustFunc applies the fn function to each pixel of the img image and returns the adjusted image. 255 // 256 // Example: 257 // 258 // dstImage = imaging.AdjustFunc( 259 // srcImage, 260 // func(c color.NRGBA) color.NRGBA { 261 // // Shift the red channel by 16. 262 // r := int(c.R) + 16 263 // if r > 255 { 264 // r = 255 265 // } 266 // return color.NRGBA{uint8(r), c.G, c.B, c.A} 267 // } 268 // ) 269 func AdjustFunc(img image.Image, fn func(c color.NRGBA) color.NRGBA) *image.NRGBA { 270 src := newScanner(img) 271 dst := image.NewNRGBA(image.Rect(0, 0, src.w, src.h)) 272 parallel(0, src.h, func(ys <-chan int) { 273 for y := range ys { 274 i := y * dst.Stride 275 src.scan(0, y, src.w, y+1, dst.Pix[i:i+src.w*4]) 276 for x := 0; x < src.w; x++ { 277 d := dst.Pix[i : i+4 : i+4] 278 r := d[0] 279 g := d[1] 280 b := d[2] 281 a := d[3] 282 c := fn(color.NRGBA{r, g, b, a}) 283 d[0] = c.R 284 d[1] = c.G 285 d[2] = c.B 286 d[3] = c.A 287 i += 4 288 } 289 } 290 }) 291 return dst 292 }