github.com/aloncn/graphics-go@v0.0.1/graphics/convolve/convolve.go (about)

     1  // Copyright 2011 The Graphics-Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package convolve
     6  
     7  import (
     8  	"errors"
     9  	"fmt"
    10  	"image"
    11  	"image/draw"
    12  	"math"
    13  )
    14  
    15  // clamp clamps x to the range [x0, x1].
    16  func clamp(x, x0, x1 float64) float64 {
    17  	if x < x0 {
    18  		return x0
    19  	}
    20  	if x > x1 {
    21  		return x1
    22  	}
    23  	return x
    24  }
    25  
    26  // Kernel is a square matrix that defines a convolution.
    27  type Kernel interface {
    28  	// Weights returns the square matrix of weights in row major order.
    29  	Weights() []float64
    30  }
    31  
    32  // SeparableKernel is a linearly separable, square convolution kernel.
    33  // X and Y are the per-axis weights. Each slice must be the same length, and
    34  // have an odd length. The middle element of each slice is the weight for the
    35  // central pixel. For example, the horizontal Sobel kernel is:
    36  //	sobelX := &SeparableKernel{
    37  //		X: []float64{-1, 0, +1},
    38  //		Y: []float64{1, 2, 1},
    39  //	}
    40  type SeparableKernel struct {
    41  	X, Y []float64
    42  }
    43  
    44  func (k *SeparableKernel) Weights() []float64 {
    45  	n := len(k.X)
    46  	w := make([]float64, n*n)
    47  	for y := 0; y < n; y++ {
    48  		for x := 0; x < n; x++ {
    49  			w[y*n+x] = k.X[x] * k.Y[y]
    50  		}
    51  	}
    52  	return w
    53  }
    54  
    55  // fullKernel is a square convolution kernel.
    56  type fullKernel []float64
    57  
    58  func (k fullKernel) Weights() []float64 { return k }
    59  
    60  func kernelSize(w []float64) (size int, err error) {
    61  	size = int(math.Sqrt(float64(len(w))))
    62  	if size*size != len(w) {
    63  		return 0, errors.New("graphics: kernel is not square")
    64  	}
    65  	if size%2 != 1 {
    66  		return 0, errors.New("graphics: kernel size is not odd")
    67  	}
    68  	return size, nil
    69  }
    70  
    71  // NewKernel returns a square convolution kernel.
    72  func NewKernel(w []float64) (Kernel, error) {
    73  	if _, err := kernelSize(w); err != nil {
    74  		return nil, err
    75  	}
    76  	return fullKernel(w), nil
    77  }
    78  
    79  func convolveRGBASep(dst *image.RGBA, src image.Image, k *SeparableKernel) error {
    80  	if len(k.X) != len(k.Y) {
    81  		return fmt.Errorf("graphics: kernel not square (x %d, y %d)", len(k.X), len(k.Y))
    82  	}
    83  	if len(k.X)%2 != 1 {
    84  		return fmt.Errorf("graphics: kernel length (%d) not odd", len(k.X))
    85  	}
    86  	radius := (len(k.X) - 1) / 2
    87  
    88  	// buf holds the result of vertically blurring src.
    89  	bounds := dst.Bounds()
    90  	width, height := bounds.Dx(), bounds.Dy()
    91  	buf := make([]float64, width*height*4)
    92  	for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
    93  		for x := bounds.Min.X; x < bounds.Max.X; x++ {
    94  			var r, g, b, a float64
    95  			// k0 is the kernel weight for the center pixel. This may be greater
    96  			// than kernel[0], near the boundary of the source image, to avoid
    97  			// vignetting.
    98  			k0 := k.X[radius]
    99  
   100  			// Add the pixels from above.
   101  			for i := 1; i <= radius; i++ {
   102  				f := k.Y[radius-i]
   103  				if y-i < bounds.Min.Y {
   104  					k0 += f
   105  				} else {
   106  					or, og, ob, oa := src.At(x, y-i).RGBA()
   107  					r += float64(or>>8) * f
   108  					g += float64(og>>8) * f
   109  					b += float64(ob>>8) * f
   110  					a += float64(oa>>8) * f
   111  				}
   112  			}
   113  
   114  			// Add the pixels from below.
   115  			for i := 1; i <= radius; i++ {
   116  				f := k.Y[radius+i]
   117  				if y+i >= bounds.Max.Y {
   118  					k0 += f
   119  				} else {
   120  					or, og, ob, oa := src.At(x, y+i).RGBA()
   121  					r += float64(or>>8) * f
   122  					g += float64(og>>8) * f
   123  					b += float64(ob>>8) * f
   124  					a += float64(oa>>8) * f
   125  				}
   126  			}
   127  
   128  			// Add the central pixel.
   129  			or, og, ob, oa := src.At(x, y).RGBA()
   130  			r += float64(or>>8) * k0
   131  			g += float64(og>>8) * k0
   132  			b += float64(ob>>8) * k0
   133  			a += float64(oa>>8) * k0
   134  
   135  			// Write to buf.
   136  			o := (y-bounds.Min.Y)*width*4 + (x-bounds.Min.X)*4
   137  			buf[o+0] = r
   138  			buf[o+1] = g
   139  			buf[o+2] = b
   140  			buf[o+3] = a
   141  		}
   142  	}
   143  
   144  	// dst holds the result of horizontally blurring buf.
   145  	for y := 0; y < height; y++ {
   146  		for x := 0; x < width; x++ {
   147  			var r, g, b, a float64
   148  			k0, off := k.X[radius], y*width*4+x*4
   149  
   150  			// Add the pixels from the left.
   151  			for i := 1; i <= radius; i++ {
   152  				f := k.X[radius-i]
   153  				if x-i < 0 {
   154  					k0 += f
   155  				} else {
   156  					o := off - i*4
   157  					r += buf[o+0] * f
   158  					g += buf[o+1] * f
   159  					b += buf[o+2] * f
   160  					a += buf[o+3] * f
   161  				}
   162  			}
   163  
   164  			// Add the pixels from the right.
   165  			for i := 1; i <= radius; i++ {
   166  				f := k.X[radius+i]
   167  				if x+i >= width {
   168  					k0 += f
   169  				} else {
   170  					o := off + i*4
   171  					r += buf[o+0] * f
   172  					g += buf[o+1] * f
   173  					b += buf[o+2] * f
   174  					a += buf[o+3] * f
   175  				}
   176  			}
   177  
   178  			// Add the central pixel.
   179  			r += buf[off+0] * k0
   180  			g += buf[off+1] * k0
   181  			b += buf[off+2] * k0
   182  			a += buf[off+3] * k0
   183  
   184  			// Write to dst, clamping to the range [0, 255].
   185  			dstOff := (y-dst.Rect.Min.Y)*dst.Stride + (x-dst.Rect.Min.X)*4
   186  			dst.Pix[dstOff+0] = uint8(clamp(r+0.5, 0, 255))
   187  			dst.Pix[dstOff+1] = uint8(clamp(g+0.5, 0, 255))
   188  			dst.Pix[dstOff+2] = uint8(clamp(b+0.5, 0, 255))
   189  			dst.Pix[dstOff+3] = uint8(clamp(a+0.5, 0, 255))
   190  		}
   191  	}
   192  
   193  	return nil
   194  }
   195  
   196  func convolveRGBA(dst *image.RGBA, src image.Image, k Kernel) error {
   197  	b := dst.Bounds()
   198  	bs := src.Bounds()
   199  	w := k.Weights()
   200  	size, err := kernelSize(w)
   201  	if err != nil {
   202  		return err
   203  	}
   204  	radius := (size - 1) / 2
   205  
   206  	for y := b.Min.Y; y < b.Max.Y; y++ {
   207  		for x := b.Min.X; x < b.Max.X; x++ {
   208  			if !image.Pt(x, y).In(bs) {
   209  				continue
   210  			}
   211  
   212  			var r, g, b, a, adj float64
   213  			for cy := y - radius; cy <= y+radius; cy++ {
   214  				for cx := x - radius; cx <= x+radius; cx++ {
   215  					factor := w[(cy-y+radius)*size+cx-x+radius]
   216  					if !image.Pt(cx, cy).In(bs) {
   217  						adj += factor
   218  					} else {
   219  						sr, sg, sb, sa := src.At(cx, cy).RGBA()
   220  						r += float64(sr>>8) * factor
   221  						g += float64(sg>>8) * factor
   222  						b += float64(sb>>8) * factor
   223  						a += float64(sa>>8) * factor
   224  					}
   225  				}
   226  			}
   227  
   228  			if adj != 0 {
   229  				sr, sg, sb, sa := src.At(x, y).RGBA()
   230  				r += float64(sr>>8) * adj
   231  				g += float64(sg>>8) * adj
   232  				b += float64(sb>>8) * adj
   233  				a += float64(sa>>8) * adj
   234  			}
   235  
   236  			off := (y-dst.Rect.Min.Y)*dst.Stride + (x-dst.Rect.Min.X)*4
   237  			dst.Pix[off+0] = uint8(clamp(r+0.5, 0, 0xff))
   238  			dst.Pix[off+1] = uint8(clamp(g+0.5, 0, 0xff))
   239  			dst.Pix[off+2] = uint8(clamp(b+0.5, 0, 0xff))
   240  			dst.Pix[off+3] = uint8(clamp(a+0.5, 0, 0xff))
   241  		}
   242  	}
   243  
   244  	return nil
   245  }
   246  
   247  // Convolve produces dst by applying the convolution kernel k to src.
   248  func Convolve(dst draw.Image, src image.Image, k Kernel) (err error) {
   249  	if dst == nil || src == nil || k == nil {
   250  		return nil
   251  	}
   252  
   253  	b := dst.Bounds()
   254  	dstRgba, ok := dst.(*image.RGBA)
   255  	if !ok {
   256  		dstRgba = image.NewRGBA(b)
   257  	}
   258  
   259  	switch k := k.(type) {
   260  	case *SeparableKernel:
   261  		err = convolveRGBASep(dstRgba, src, k)
   262  	default:
   263  		err = convolveRGBA(dstRgba, src, k)
   264  	}
   265  
   266  	if err != nil {
   267  		return err
   268  	}
   269  
   270  	if !ok {
   271  		draw.Draw(dst, b, dstRgba, b.Min, draw.Src)
   272  	}
   273  	return nil
   274  }