SandpointsGitHook/vendor/github.com/disintegration/gift/convolution.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},
}
}