adjust.go 8.4 KB

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