579 lines
14 KiB
Go
579 lines
14 KiB
Go
package gift
|
|
|
|
import (
|
|
"image"
|
|
"image/draw"
|
|
"math"
|
|
)
|
|
|
|
type uweight struct {
|
|
u int
|
|
weight float32
|
|
}
|
|
|
|
type uvweight struct {
|
|
u int
|
|
v int
|
|
weight float32
|
|
}
|
|
|
|
func prepareConvolutionWeights(kernel []float32, normalize bool) (int, []uvweight) {
|
|
size := int(math.Sqrt(float64(len(kernel))))
|
|
if size%2 == 0 {
|
|
size--
|
|
}
|
|
if size < 1 {
|
|
return 0, []uvweight{}
|
|
}
|
|
center := size / 2
|
|
|
|
weights := []uvweight{}
|
|
for i := 0; i < size; i++ {
|
|
for j := 0; j < size; j++ {
|
|
k := j*size + i
|
|
w := float32(0)
|
|
if k < len(kernel) {
|
|
w = kernel[k]
|
|
}
|
|
if w != 0 {
|
|
weights = append(weights, uvweight{u: i - center, v: j - center, weight: w})
|
|
}
|
|
}
|
|
}
|
|
|
|
if !normalize {
|
|
return size, weights
|
|
}
|
|
|
|
var sum, sumpositive float32
|
|
for _, w := range weights {
|
|
sum += w.weight
|
|
if w.weight > 0 {
|
|
sumpositive += w.weight
|
|
}
|
|
}
|
|
|
|
var div float32
|
|
if sum != 0 {
|
|
div = sum
|
|
} else if sumpositive != 0 {
|
|
div = sumpositive
|
|
} else {
|
|
return size, weights
|
|
}
|
|
|
|
for i := 0; i < len(weights); i++ {
|
|
weights[i].weight /= div
|
|
}
|
|
|
|
return size, weights
|
|
}
|
|
|
|
type convolutionFilter struct {
|
|
kernel []float32
|
|
normalize bool
|
|
alpha bool
|
|
abs bool
|
|
delta float32
|
|
}
|
|
|
|
func (p *convolutionFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) {
|
|
dstBounds = image.Rect(0, 0, srcBounds.Dx(), srcBounds.Dy())
|
|
return
|
|
}
|
|
|
|
func (p *convolutionFilter) Draw(dst draw.Image, src image.Image, options *Options) {
|
|
if options == nil {
|
|
options = &defaultOptions
|
|
}
|
|
|
|
srcb := src.Bounds()
|
|
dstb := dst.Bounds()
|
|
|
|
if srcb.Dx() <= 0 || srcb.Dy() <= 0 {
|
|
return
|
|
}
|
|
|
|
ksize, weights := prepareConvolutionWeights(p.kernel, p.normalize)
|
|
kcenter := ksize / 2
|
|
|
|
if ksize < 1 {
|
|
copyimage(dst, src, options)
|
|
return
|
|
}
|
|
|
|
pixGetter := newPixelGetter(src)
|
|
pixSetter := newPixelSetter(dst)
|
|
|
|
parallelize(options.Parallelization, srcb.Min.Y, srcb.Max.Y, func(start, stop int) {
|
|
// Init temporary rows.
|
|
starty := start
|
|
rows := make([][]pixel, ksize)
|
|
for i := 0; i < ksize; i++ {
|
|
rowy := starty + i - kcenter
|
|
if rowy < srcb.Min.Y {
|
|
rowy = srcb.Min.Y
|
|
} else if rowy > srcb.Max.Y-1 {
|
|
rowy = srcb.Max.Y - 1
|
|
}
|
|
row := make([]pixel, srcb.Dx())
|
|
pixGetter.getPixelRow(rowy, &row)
|
|
rows[i] = row
|
|
}
|
|
|
|
for y := start; y < stop; y++ {
|
|
// Calculate dst row.
|
|
for x := srcb.Min.X; x < srcb.Max.X; x++ {
|
|
var r, g, b, a float32
|
|
for _, w := range weights {
|
|
wx := x + w.u
|
|
if wx < srcb.Min.X {
|
|
wx = srcb.Min.X
|
|
} else if wx > srcb.Max.X-1 {
|
|
wx = srcb.Max.X - 1
|
|
}
|
|
rowsx := wx - srcb.Min.X
|
|
rowsy := kcenter + w.v
|
|
|
|
px := rows[rowsy][rowsx]
|
|
r += px.r * w.weight
|
|
g += px.g * w.weight
|
|
b += px.b * w.weight
|
|
if p.alpha {
|
|
a += px.a * w.weight
|
|
}
|
|
}
|
|
if p.abs {
|
|
r = absf32(r)
|
|
g = absf32(g)
|
|
b = absf32(b)
|
|
if p.alpha {
|
|
a = absf32(a)
|
|
}
|
|
}
|
|
if p.delta != 0 {
|
|
r += p.delta
|
|
g += p.delta
|
|
b += p.delta
|
|
if p.alpha {
|
|
a += p.delta
|
|
}
|
|
}
|
|
if !p.alpha {
|
|
a = rows[kcenter][x-srcb.Min.X].a
|
|
}
|
|
pixSetter.setPixel(dstb.Min.X+x-srcb.Min.X, dstb.Min.Y+y-srcb.Min.Y, pixel{r, g, b, a})
|
|
}
|
|
|
|
// Rotate temporary rows.
|
|
if y < stop-1 {
|
|
tmprow := rows[0]
|
|
for i := 0; i < ksize-1; i++ {
|
|
rows[i] = rows[i+1]
|
|
}
|
|
nextrowy := y + ksize/2 + 1
|
|
if nextrowy > srcb.Max.Y-1 {
|
|
nextrowy = srcb.Max.Y - 1
|
|
}
|
|
pixGetter.getPixelRow(nextrowy, &tmprow)
|
|
rows[ksize-1] = tmprow
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// Convolution creates a filter that applies a square convolution kernel to an image.
|
|
// The length of the kernel slice must be the square of an odd kernel size (e.g. 9 for 3x3 kernel, 25 for 5x5 kernel).
|
|
// Excessive slice members will be ignored.
|
|
// If normalize parameter is true, the kernel will be normalized before applying the filter.
|
|
// If alpha parameter is true, the alpha component of color will be filtered too.
|
|
// If abs parameter is true, absolute values of color components will be taken after doing calculations.
|
|
// If delta parameter is not zero, this value will be added to the filtered pixels.
|
|
//
|
|
// Example:
|
|
//
|
|
// // Apply the emboss filter to an image.
|
|
// g := gift.New(
|
|
// gift.Convolution(
|
|
// []float32{
|
|
// -1, -1, 0,
|
|
// -1, 1, 1,
|
|
// 0, 1, 1,
|
|
// },
|
|
// false, false, false, 0,
|
|
// ),
|
|
// )
|
|
// dst := image.NewRGBA(g.Bounds(src.Bounds()))
|
|
// g.Draw(dst, src)
|
|
//
|
|
func Convolution(kernel []float32, normalize, alpha, abs bool, delta float32) Filter {
|
|
return &convolutionFilter{
|
|
kernel: kernel,
|
|
normalize: normalize,
|
|
alpha: alpha,
|
|
abs: abs,
|
|
delta: delta,
|
|
}
|
|
}
|
|
|
|
// prepareConvolutionWeights1d prepares pixel weights using a convolution kernel.
|
|
// Weights equal to 0 are excluded.
|
|
func prepareConvolutionWeights1d(kernel []float32) (int, []uweight) {
|
|
size := len(kernel)
|
|
if size%2 == 0 {
|
|
size--
|
|
}
|
|
if size < 1 {
|
|
return 0, []uweight{}
|
|
}
|
|
center := size / 2
|
|
weights := []uweight{}
|
|
for i := 0; i < size; i++ {
|
|
w := float32(0)
|
|
if i < len(kernel) {
|
|
w = kernel[i]
|
|
}
|
|
if w != 0 {
|
|
weights = append(weights, uweight{i - center, w})
|
|
}
|
|
}
|
|
return size, weights
|
|
}
|
|
|
|
// convolveLine convolves a single line of pixels according to the given weights.
|
|
func convolveLine(dstBuf []pixel, srcBuf []pixel, weights []uweight) {
|
|
max := len(srcBuf) - 1
|
|
if max < 0 {
|
|
return
|
|
}
|
|
for dstu := 0; dstu < len(srcBuf); dstu++ {
|
|
var r, g, b, a float32
|
|
for _, w := range weights {
|
|
k := dstu + w.u
|
|
if k < 0 {
|
|
k = 0
|
|
} else if k > max {
|
|
k = max
|
|
}
|
|
c := srcBuf[k]
|
|
wa := c.a * w.weight
|
|
r += c.r * wa
|
|
g += c.g * wa
|
|
b += c.b * wa
|
|
a += wa
|
|
}
|
|
if a != 0 {
|
|
r /= a
|
|
g /= a
|
|
b /= a
|
|
}
|
|
dstBuf[dstu] = pixel{r, g, b, a}
|
|
}
|
|
}
|
|
|
|
// convolve1dv performs a fast vertical 1d convolution.
|
|
func convolve1dv(dst draw.Image, src image.Image, kernel []float32, options *Options) {
|
|
srcb := src.Bounds()
|
|
dstb := dst.Bounds()
|
|
if srcb.Dx() <= 0 || srcb.Dy() <= 0 {
|
|
return
|
|
}
|
|
if kernel == nil || len(kernel) < 1 {
|
|
copyimage(dst, src, options)
|
|
return
|
|
}
|
|
_, weights := prepareConvolutionWeights1d(kernel)
|
|
pixGetter := newPixelGetter(src)
|
|
pixSetter := newPixelSetter(dst)
|
|
parallelize(options.Parallelization, srcb.Min.X, srcb.Max.X, func(start, stop int) {
|
|
srcBuf := make([]pixel, srcb.Dy())
|
|
dstBuf := make([]pixel, srcb.Dy())
|
|
for x := start; x < stop; x++ {
|
|
pixGetter.getPixelColumn(x, &srcBuf)
|
|
convolveLine(dstBuf, srcBuf, weights)
|
|
pixSetter.setPixelColumn(dstb.Min.X+x-srcb.Min.X, dstBuf)
|
|
}
|
|
})
|
|
}
|
|
|
|
// convolve1dh performs afast horizontal 1d convolution.
|
|
func convolve1dh(dst draw.Image, src image.Image, kernel []float32, options *Options) {
|
|
srcb := src.Bounds()
|
|
dstb := dst.Bounds()
|
|
if srcb.Dx() <= 0 || srcb.Dy() <= 0 {
|
|
return
|
|
}
|
|
if kernel == nil || len(kernel) < 1 {
|
|
copyimage(dst, src, options)
|
|
return
|
|
}
|
|
_, weights := prepareConvolutionWeights1d(kernel)
|
|
pixGetter := newPixelGetter(src)
|
|
pixSetter := newPixelSetter(dst)
|
|
parallelize(options.Parallelization, srcb.Min.Y, srcb.Max.Y, func(start, stop int) {
|
|
srcBuf := make([]pixel, srcb.Dx())
|
|
dstBuf := make([]pixel, srcb.Dx())
|
|
for y := start; y < stop; y++ {
|
|
pixGetter.getPixelRow(y, &srcBuf)
|
|
convolveLine(dstBuf, srcBuf, weights)
|
|
pixSetter.setPixelRow(dstb.Min.Y+y-srcb.Min.Y, dstBuf)
|
|
}
|
|
})
|
|
}
|
|
|
|
func gaussianBlurKernel(x, sigma float32) float32 {
|
|
return float32(math.Exp(-float64(x*x)/float64(2*sigma*sigma)) / (float64(sigma) * math.Sqrt(2*math.Pi)))
|
|
}
|
|
|
|
type gausssianBlurFilter struct {
|
|
sigma float32
|
|
}
|
|
|
|
func (p *gausssianBlurFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) {
|
|
dstBounds = image.Rect(0, 0, srcBounds.Dx(), srcBounds.Dy())
|
|
return
|
|
}
|
|
|
|
func (p *gausssianBlurFilter) Draw(dst draw.Image, src image.Image, options *Options) {
|
|
if options == nil {
|
|
options = &defaultOptions
|
|
}
|
|
|
|
srcb := src.Bounds()
|
|
if srcb.Dx() <= 0 || srcb.Dy() <= 0 {
|
|
return
|
|
}
|
|
|
|
if p.sigma <= 0 {
|
|
copyimage(dst, src, options)
|
|
return
|
|
}
|
|
|
|
radius := int(math.Ceil(float64(p.sigma * 3)))
|
|
size := 2*radius + 1
|
|
center := radius
|
|
kernel := make([]float32, size)
|
|
|
|
kernel[center] = gaussianBlurKernel(0, p.sigma)
|
|
sum := kernel[center]
|
|
|
|
for i := 1; i <= radius; i++ {
|
|
f := gaussianBlurKernel(float32(i), p.sigma)
|
|
kernel[center-i] = f
|
|
kernel[center+i] = f
|
|
sum += 2 * f
|
|
}
|
|
|
|
for i := 0; i < len(kernel); i++ {
|
|
kernel[i] /= sum
|
|
}
|
|
|
|
tmp := createTempImage(srcb)
|
|
convolve1dh(tmp, src, kernel, options)
|
|
convolve1dv(dst, tmp, kernel, options)
|
|
}
|
|
|
|
// GaussianBlur creates a filter that applies a gaussian blur to an image.
|
|
// The sigma parameter must be positive and indicates how much the image will be blurred.
|
|
// Blur affected radius roughly equals 3 * sigma.
|
|
//
|
|
// Example:
|
|
//
|
|
// g := gift.New(
|
|
// gift.GaussianBlur(1.5),
|
|
// )
|
|
// dst := image.NewRGBA(g.Bounds(src.Bounds()))
|
|
// g.Draw(dst, src)
|
|
//
|
|
func GaussianBlur(sigma float32) Filter {
|
|
return &gausssianBlurFilter{
|
|
sigma: sigma,
|
|
}
|
|
}
|
|
|
|
type unsharpMaskFilter struct {
|
|
sigma float32
|
|
amount float32
|
|
threshold float32
|
|
}
|
|
|
|
func (p *unsharpMaskFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) {
|
|
dstBounds = image.Rect(0, 0, srcBounds.Dx(), srcBounds.Dy())
|
|
return
|
|
}
|
|
|
|
func unsharp(orig, blurred, amount, threshold float32) float32 {
|
|
dif := (orig - blurred) * amount
|
|
if absf32(dif) > absf32(threshold) {
|
|
return orig + dif
|
|
}
|
|
return orig
|
|
}
|
|
|
|
func (p *unsharpMaskFilter) Draw(dst draw.Image, src image.Image, options *Options) {
|
|
if options == nil {
|
|
options = &defaultOptions
|
|
}
|
|
|
|
srcb := src.Bounds()
|
|
dstb := dst.Bounds()
|
|
|
|
if srcb.Dx() <= 0 || srcb.Dy() <= 0 {
|
|
return
|
|
}
|
|
|
|
blurred := createTempImage(srcb)
|
|
blur := GaussianBlur(p.sigma)
|
|
blur.Draw(blurred, src, options)
|
|
|
|
pixGetterOrig := newPixelGetter(src)
|
|
pixGetterBlur := newPixelGetter(blurred)
|
|
pixelSetter := newPixelSetter(dst)
|
|
|
|
parallelize(options.Parallelization, srcb.Min.Y, srcb.Max.Y, func(start, stop int) {
|
|
for y := start; y < stop; y++ {
|
|
for x := srcb.Min.X; x < srcb.Max.X; x++ {
|
|
pxOrig := pixGetterOrig.getPixel(x, y)
|
|
pxBlur := pixGetterBlur.getPixel(x, y)
|
|
|
|
r := unsharp(pxOrig.r, pxBlur.r, p.amount, p.threshold)
|
|
g := unsharp(pxOrig.g, pxBlur.g, p.amount, p.threshold)
|
|
b := unsharp(pxOrig.b, pxBlur.b, p.amount, p.threshold)
|
|
a := unsharp(pxOrig.a, pxBlur.a, p.amount, p.threshold)
|
|
|
|
pixelSetter.setPixel(dstb.Min.X+x-srcb.Min.X, dstb.Min.Y+y-srcb.Min.Y, pixel{r, g, b, a})
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// UnsharpMask creates a filter that sharpens an image.
|
|
// The sigma parameter is used in a gaussian function and affects the radius of effect.
|
|
// Sigma must be positive. Sharpen radius roughly equals 3 * sigma.
|
|
// The amount parameter controls how much darker and how much lighter the edge borders become. Typically between 0.5 and 1.5.
|
|
// The threshold parameter controls the minimum brightness change that will be sharpened. Typically between 0 and 0.05.
|
|
//
|
|
// Example:
|
|
//
|
|
// g := gift.New(
|
|
// gift.UnsharpMask(1, 1, 0),
|
|
// )
|
|
// dst := image.NewRGBA(g.Bounds(src.Bounds()))
|
|
// g.Draw(dst, src)
|
|
//
|
|
func UnsharpMask(sigma, amount, threshold float32) Filter {
|
|
return &unsharpMaskFilter{
|
|
sigma: sigma,
|
|
amount: amount,
|
|
threshold: threshold,
|
|
}
|
|
}
|
|
|
|
type meanFilter struct {
|
|
ksize int
|
|
disk bool
|
|
}
|
|
|
|
func (p *meanFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) {
|
|
dstBounds = image.Rect(0, 0, srcBounds.Dx(), srcBounds.Dy())
|
|
return
|
|
}
|
|
|
|
func (p *meanFilter) Draw(dst draw.Image, src image.Image, options *Options) {
|
|
if options == nil {
|
|
options = &defaultOptions
|
|
}
|
|
|
|
srcb := src.Bounds()
|
|
if srcb.Dx() <= 0 || srcb.Dy() <= 0 {
|
|
return
|
|
}
|
|
|
|
ksize := p.ksize
|
|
if ksize%2 == 0 {
|
|
ksize--
|
|
}
|
|
|
|
if ksize <= 1 {
|
|
copyimage(dst, src, options)
|
|
return
|
|
}
|
|
|
|
if p.disk {
|
|
diskKernel := genDisk(p.ksize)
|
|
f := Convolution(diskKernel, true, true, false, 0)
|
|
f.Draw(dst, src, options)
|
|
} else {
|
|
kernel := make([]float32, ksize*ksize)
|
|
for i := range kernel {
|
|
kernel[i] = 1
|
|
}
|
|
f := Convolution(kernel, true, true, false, 0)
|
|
f.Draw(dst, src, options)
|
|
}
|
|
}
|
|
|
|
// Mean creates a local mean image filter.
|
|
// Takes an average across a neighborhood for each pixel.
|
|
// The ksize parameter is the kernel size. It must be an odd positive integer (for example: 3, 5, 7).
|
|
// If the disk parameter is true, a disk-shaped neighborhood will be used instead of a square neighborhood.
|
|
func Mean(ksize int, disk bool) Filter {
|
|
return &meanFilter{
|
|
ksize: ksize,
|
|
disk: disk,
|
|
}
|
|
}
|
|
|
|
type hvConvolutionFilter struct {
|
|
hkernel, vkernel []float32
|
|
}
|
|
|
|
func (p *hvConvolutionFilter) Bounds(srcBounds image.Rectangle) (dstBounds image.Rectangle) {
|
|
dstBounds = image.Rect(0, 0, srcBounds.Dx(), srcBounds.Dy())
|
|
return
|
|
}
|
|
|
|
func (p *hvConvolutionFilter) Draw(dst draw.Image, src image.Image, options *Options) {
|
|
if options == nil {
|
|
options = &defaultOptions
|
|
}
|
|
|
|
srcb := src.Bounds()
|
|
dstb := dst.Bounds()
|
|
|
|
if srcb.Dx() <= 0 || srcb.Dy() <= 0 {
|
|
return
|
|
}
|
|
|
|
tmph := createTempImage(srcb)
|
|
Convolution(p.hkernel, false, false, true, 0).Draw(tmph, src, options)
|
|
pixGetterH := newPixelGetter(tmph)
|
|
|
|
tmpv := createTempImage(srcb)
|
|
Convolution(p.vkernel, false, false, true, 0).Draw(tmpv, src, options)
|
|
pixGetterV := newPixelGetter(tmpv)
|
|
|
|
pixSetter := newPixelSetter(dst)
|
|
|
|
parallelize(options.Parallelization, srcb.Min.Y, srcb.Max.Y, func(start, stop int) {
|
|
for y := start; y < stop; y++ {
|
|
for x := srcb.Min.X; x < srcb.Max.X; x++ {
|
|
pxh := pixGetterH.getPixel(x, y)
|
|
pxv := pixGetterV.getPixel(x, y)
|
|
r := sqrtf32(pxh.r*pxh.r + pxv.r*pxv.r)
|
|
g := sqrtf32(pxh.g*pxh.g + pxv.g*pxv.g)
|
|
b := sqrtf32(pxh.b*pxh.b + pxv.b*pxv.b)
|
|
pixSetter.setPixel(dstb.Min.X+x-srcb.Min.X, dstb.Min.Y+y-srcb.Min.Y, pixel{r, g, b, pxh.a})
|
|
}
|
|
}
|
|
})
|
|
|
|
}
|
|
|
|
// Sobel creates a filter that applies a sobel operator to an image.
|
|
func Sobel() Filter {
|
|
return &hvConvolutionFilter{
|
|
hkernel: []float32{-1, 0, 1, -2, 0, 2, -1, 0, 1},
|
|
vkernel: []float32{-1, -2, -1, 0, 0, 0, 1, 2, 1},
|
|
}
|
|
}
|