convolution.go 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. /*Package convolution provides the functionality to create and apply a kernel to an image.*/
  2. package convolution
  3. import (
  4. "image"
  5. "math"
  6. "github.com/anthonynsimon/bild/clone"
  7. "github.com/anthonynsimon/bild/parallel"
  8. )
  9. // Options are the Convolve function parameters.
  10. // Bias is added to each RGB channel after convoluting. Range is -255 to 255.
  11. // Wrap sets if indices outside of image dimensions should be taken from the opposite side.
  12. // KeepAlpha sets if alpha should be convolved or kept from the source image.
  13. type Options struct {
  14. Bias float64
  15. Wrap bool
  16. KeepAlpha bool
  17. }
  18. // Convolve applies a convolution matrix (kernel) to an image with the supplied options.
  19. //
  20. // Usage example:
  21. //
  22. // result := Convolve(img, kernel, &Options{Bias: 0, Wrap: false})
  23. func Convolve(img image.Image, k Matrix, o *Options) *image.RGBA {
  24. // Config the convolution
  25. bias := 0.0
  26. wrap := false
  27. keepAlpha := false
  28. if o != nil {
  29. wrap = o.Wrap
  30. bias = o.Bias
  31. keepAlpha = o.KeepAlpha
  32. }
  33. return execute(img, k, bias, wrap, keepAlpha)
  34. }
  35. func execute(img image.Image, k Matrix, bias float64, wrap, keepAlpha bool) *image.RGBA {
  36. // Kernel attributes
  37. lenX := k.MaxX()
  38. lenY := k.MaxY()
  39. radiusX := lenX / 2
  40. radiusY := lenY / 2
  41. // Pad the source image, basically pre-computing the pixels outside of image bounds
  42. var src *image.RGBA
  43. if wrap {
  44. src = clone.Pad(img, radiusX, radiusY, clone.EdgeWrap)
  45. } else {
  46. src = clone.Pad(img, radiusX, radiusY, clone.EdgeExtend)
  47. }
  48. // src bounds now includes padded pixels
  49. srcBounds := src.Bounds()
  50. srcW, srcH := srcBounds.Dx(), srcBounds.Dy()
  51. dst := image.NewRGBA(img.Bounds())
  52. // To keep alpha we simply don't convolve it
  53. if keepAlpha {
  54. // Notice we can't use lenY since it will be larger than the actual padding pixels
  55. // as it includes the identity element
  56. parallel.Line(srcH-(radiusY*2), func(start, end int) {
  57. // Correct range so we don't iterate over the padded pixels on the main loop
  58. for y := start + radiusY; y < end+radiusY; y++ {
  59. for x := radiusX; x < srcW-radiusX; x++ {
  60. var r, g, b float64
  61. // Kernel has access to the padded pixels
  62. for ky := 0; ky < lenY; ky++ {
  63. iy := y - radiusY + ky
  64. for kx := 0; kx < lenX; kx++ {
  65. ix := x - radiusX + kx
  66. kvalue := k.At(kx, ky)
  67. ipos := iy*src.Stride + ix*4
  68. r += float64(src.Pix[ipos+0]) * kvalue
  69. g += float64(src.Pix[ipos+1]) * kvalue
  70. b += float64(src.Pix[ipos+2]) * kvalue
  71. }
  72. }
  73. // Map x and y indices to non-padded range
  74. pos := (y-radiusY)*dst.Stride + (x-radiusX)*4
  75. dst.Pix[pos+0] = uint8(math.Max(math.Min(r+bias, 255), 0))
  76. dst.Pix[pos+1] = uint8(math.Max(math.Min(g+bias, 255), 0))
  77. dst.Pix[pos+2] = uint8(math.Max(math.Min(b+bias, 255), 0))
  78. dst.Pix[pos+3] = src.Pix[y*src.Stride+x*4+3]
  79. }
  80. }
  81. })
  82. } else {
  83. // Notice we can't use lenY since it will be larger than the actual padding pixels
  84. // as it includes the identity element
  85. parallel.Line(srcH-(radiusY*2), func(start, end int) {
  86. // Correct range so we don't iterate over the padded pixels on the main loop
  87. for y := start + radiusY; y < end+radiusY; y++ {
  88. for x := radiusX; x < srcW-radiusX; x++ {
  89. var r, g, b, a float64
  90. // Kernel has access to the padded pixels
  91. for ky := 0; ky < lenY; ky++ {
  92. iy := y - radiusY + ky
  93. for kx := 0; kx < lenX; kx++ {
  94. ix := x - radiusX + kx
  95. kvalue := k.At(kx, ky)
  96. ipos := iy*src.Stride + ix*4
  97. r += float64(src.Pix[ipos+0]) * kvalue
  98. g += float64(src.Pix[ipos+1]) * kvalue
  99. b += float64(src.Pix[ipos+2]) * kvalue
  100. a += float64(src.Pix[ipos+3]) * kvalue
  101. }
  102. }
  103. // Map x and y indices to non-padded range
  104. pos := (y-radiusY)*dst.Stride + (x-radiusX)*4
  105. dst.Pix[pos+0] = uint8(math.Max(math.Min(r+bias, 255), 0))
  106. dst.Pix[pos+1] = uint8(math.Max(math.Min(g+bias, 255), 0))
  107. dst.Pix[pos+2] = uint8(math.Max(math.Min(b+bias, 255), 0))
  108. dst.Pix[pos+3] = uint8(math.Max(math.Min(a, 255), 0))
  109. }
  110. }
  111. })
  112. }
  113. return dst
  114. }