在Golang中使用Lanczos重采样处理粗糙边缘

huangapple go评论72阅读模式
英文:

Rough Edges With Lanczos Resampling in Golang

问题

我一直在使用Golang编写一些基本的图像调整方法。我看过几篇关于调整图像大小的帖子,但是我始终无法弄清楚我漏掉了什么...

实际上,我的问题是在使用Golang调整图像大小时,结果似乎存在很多锯齿状的边缘。

我尝试了迭代地对图像进行降采样,但效果不太好。

以下是我的代码:

func resize(original image.Image, edgeSize int, filterSize int) image.Image {
    oldBounds := original.Bounds()
    if oldBounds.Dx() < edgeSize && oldBounds.Dy() < edgeSize {
        // 不需要调整大小
        return original
    }

    threshold := edgeSize * 4 / 3
    if oldBounds.Dx() > threshold || oldBounds.Dy() > threshold {
        fmt.Println("Upstream")
        original = customResizeImageToFitBounds(original, threshold, filterSize)
        oldBounds = original.Bounds()
    }

    newBounds := getNewBounds(oldBounds, edgeSize)

    resized := image.NewRGBA(newBounds)

    var ratioX = float64(oldBounds.Dx()) / float64(newBounds.Dx())
    var ratioY = float64(oldBounds.Dy()) / float64(newBounds.Dy())

    for x := 0; x < newBounds.Dx(); x++ {
        for y := 0; y < newBounds.Dy(); y++ {
            sourceX := ratioX * float64(x)
            minX := int(math.Floor(sourceX))

            sourceY := ratioY * float64(y)
            minY := int(math.Floor(sourceY))

            sampleSize := filterSize<<1 + 1
            var xCoeffs = make([]float64, sampleSize)
            var yCoeffs = make([]float64, sampleSize)

            var sumX = 0.0
            var sumY = 0.0
            for i := 0; i < sampleSize; i++ {
                xCoeffs[i] = lanczos(filterSize, sourceX-float64(minX+i-filterSize))
                yCoeffs[i] = lanczos(filterSize, sourceY-float64(minY+i-filterSize))

                sumX += xCoeffs[i]
                sumY += yCoeffs[i]
            }

            for i := 0; i < sampleSize; i++ {
                xCoeffs[i] /= sumX
                yCoeffs[i] /= sumY
            }

            rgba := make([]float64, 4)

            for i := 0; i < sampleSize; i++ {
                if yCoeffs[i] == 0.0 {
                    continue
                }
                currY := minY + i - filterSize

                rgbaRow := make([]float64, 4)
                for j := 0; j < sampleSize; j++ {
                    if xCoeffs[j] == 0.0 {
                        continue
                    }
                    currX := minX + i - filterSize

                    rij, gij, bij, aij := original.At(clamp(currX, currY, oldBounds)).RGBA()

                    rgbaRow[0] += float64(rij) * xCoeffs[j]
                    rgbaRow[1] += float64(gij) * xCoeffs[j]
                    rgbaRow[2] += float64(bij) * xCoeffs[j]
                    rgbaRow[3] += float64(aij) * xCoeffs[j]
                }
                rgba[0] += float64(rgbaRow[0]) * yCoeffs[i]
                rgba[1] += float64(rgbaRow[1]) * yCoeffs[i]
                rgba[2] += float64(rgbaRow[2]) * yCoeffs[i]
                rgba[3] += float64(rgbaRow[3]) * yCoeffs[i]
            }

            rgba[0] = clampRangeFloat(0, rgba[0], 0xFFFF)
            rgba[1] = clampRangeFloat(0, rgba[1], 0xFFFF)
            rgba[2] = clampRangeFloat(0, rgba[2], 0xFFFF)
            rgba[3] = clampRangeFloat(0, rgba[3], 0xFFFF)

            var rgbaF [4]uint64
            rgbaF[0] = (uint64(math.Floor(rgba[0]+0.5)) * 0xFF) / 0xFFFF
            rgbaF[1] = (uint64(math.Floor(rgba[1]+0.5)) * 0xFF) / 0xFFFF
            rgbaF[2] = (uint64(math.Floor(rgba[2]+0.5)) * 0xFF) / 0xFFFF
            rgbaF[3] = (uint64(math.Floor(rgba[3]+0.5)) * 0xFF) / 0xFFFF

            rf := uint8(clampRangeUint(0, uint32(rgbaF[0]), 255))
            gf := uint8(clampRangeUint(0, uint32(rgbaF[1]), 255))
            bf := uint8(clampRangeUint(0, uint32(rgbaF[2]), 255))
            af := uint8(clampRangeUint(0, uint32(rgbaF[3]), 255))

            resized.Set(x, y, color.RGBA{R: rf, G: gf, B: bf, A: af})
        }
    }
    return resized
}

// 机器精度
var epsilon = math.Nextafter(1.0, 2.0) - 1

func lanczos(filterSize int, x float64) float64 {
    x = math.Abs(x)
    fs := float64(filterSize)
    if x < epsilon {
        return 1.0
    }

    if x > fs {
        return 0
    }

    piX := math.Pi * x
    piXOverFS := piX / fs
    return (math.Sin(piX) / piX) * (math.Sin(piXOverFS) / (piXOverFS))
}

这段代码的性能不是特别好,因为我想在优化之前获得良好的质量结果。

有没有对图像重采样有经验的人看到可能有问题的地方?

供参考,这是我的源图像:
在Golang中使用Lanczos重采样处理粗糙边缘

这是我的结果:
在Golang中使用Lanczos重采样处理粗糙边缘

如果我删除递归调用,这是我的结果:
在Golang中使用Lanczos重采样处理粗糙边缘

这是使用Ruby的RMagick/ImageMagick的结果(我想要的效果):
在Golang中使用Lanczos重采样处理粗糙边缘

有没有人有建议如何获得更平滑的缩小结果?
这个例子是一个相当大的缩小,但是Rmagick能够以很快的速度和很好的质量进行缩小,所以肯定是有可能的。

我听说Lanczos3重采样可以得到很好的结果,这就是我在这里尝试使用的方法,但我不确定我的实现是否正确。

另外,顺便说一下:0xFF / 0xFFFF 的转换是因为Golang的 "At" 函数返回的RGBA值范围是 [0, 0xFFFF]([0, 65535]),但 "Set" 函数接受的颜色范围是 [0, 0xFF]([0, 255])。

目前,我更关注质量而不是性能。

英文:

I've been writing some basic methods to resize images in Golang. I've seen several posts about resizing images, but for the life of me I can't figure out what I'm missing...

Essentially, my issue is that when resizing an image in Golang, my results seem to have a lot of aliasing.

I've tried iteratively downsampling the image, but that didn't yield much of an improvement.

Here's my code:

func resize(original image.Image,
edgeSize int, filterSize int) image.Image {
oldBounds := original.Bounds()
if oldBounds.Dx() &lt; edgeSize &amp;&amp; oldBounds.Dy() &lt; edgeSize {
// No resize necessary
return original
}
threshold := edgeSize * 4 / 3
if oldBounds.Dx() &gt; threshold || oldBounds.Dy() &gt; threshold {
fmt.Println(&quot;Upstream&quot;)
original = customResizeImageToFitBounds(original, threshold, filterSize)
oldBounds = original.Bounds()
}
newBounds := getNewBounds(oldBounds, edgeSize)
resized := image.NewRGBA(newBounds)
var ratioX = float64(oldBounds.Dx()) / float64(newBounds.Dx())
var ratioY = float64(oldBounds.Dy()) / float64(newBounds.Dy())
for x := 0; x &lt; newBounds.Dx(); x++ {
for y := 0; y &lt; newBounds.Dy(); y++ {
sourceX := ratioX * float64(x)
minX := int(math.Floor(sourceX))
sourceY := ratioY * float64(y)
minY := int(math.Floor(sourceY))
sampleSize := filterSize&lt;&lt;1 + 1
var xCoeffs = make([]float64, sampleSize)
var yCoeffs = make([]float64, sampleSize)
var sumX = 0.0
var sumY = 0.0
for i := 0; i &lt; sampleSize; i++ {
xCoeffs[i] = lanczos(filterSize, sourceX-float64(minX+i-filterSize))
yCoeffs[i] = lanczos(filterSize, sourceY-float64(minY+i-filterSize))
sumX += xCoeffs[i]
sumY += yCoeffs[i]
}
for i := 0; i &lt; sampleSize; i++ {
xCoeffs[i] /= sumX
yCoeffs[i] /= sumY
}
rgba := make([]float64, 4)
for i := 0; i &lt; sampleSize; i++ {
if yCoeffs[i] == 0.0 {
continue
}
currY := minY + i - filterSize
rgbaRow := make([]float64, 4)
for j := 0; j &lt; sampleSize; j++ {
if xCoeffs[j] == 0.0 {
continue
}
currX := minX + i - filterSize
rij, gij, bij, aij := original.At(
clamp(currX, currY, oldBounds)).RGBA()
rgbaRow[0] += float64(rij) * xCoeffs[j]
rgbaRow[1] += float64(gij) * xCoeffs[j]
rgbaRow[2] += float64(bij) * xCoeffs[j]
rgbaRow[3] += float64(aij) * xCoeffs[j]
}
rgba[0] += float64(rgbaRow[0]) * yCoeffs[i]
rgba[1] += float64(rgbaRow[1]) * yCoeffs[i]
rgba[2] += float64(rgbaRow[2]) * yCoeffs[i]
rgba[3] += float64(rgbaRow[3]) * yCoeffs[i]
}
rgba[0] = clampRangeFloat(0, rgba[0], 0xFFFF)
rgba[1] = clampRangeFloat(0, rgba[1], 0xFFFF)
rgba[2] = clampRangeFloat(0, rgba[2], 0xFFFF)
rgba[3] = clampRangeFloat(0, rgba[3], 0xFFFF)
var rgbaF [4]uint64
rgbaF[0] = (uint64(math.Floor(rgba[0]+0.5)) * 0xFF) / 0xFFFF
rgbaF[1] = (uint64(math.Floor(rgba[1]+0.5)) * 0xFF) / 0xFFFF
rgbaF[2] = (uint64(math.Floor(rgba[2]+0.5)) * 0xFF) / 0xFFFF
rgbaF[3] = (uint64(math.Floor(rgba[3]+0.5)) * 0xFF) / 0xFFFF
rf := uint8(clampRangeUint(0, uint32(rgbaF[0]), 255))
gf := uint8(clampRangeUint(0, uint32(rgbaF[1]), 255))
bf := uint8(clampRangeUint(0, uint32(rgbaF[2]), 255))
af := uint8(clampRangeUint(0, uint32(rgbaF[3]), 255))
resized.Set(x, y, color.RGBA{R: rf, G: gf, B: bf, A: af})
}
}
return resized
}
// Machine epsilon
var epsilon = math.Nextafter(1.0, 2.0) - 1
func lanczos(filterSize int, x float64) float64 {
x = math.Abs(x)
fs := float64(filterSize)
if x &lt; epsilon {
return 1.0
}
if x &gt; fs {
return 0
}
piX := math.Pi * x
piXOverFS := piX / fs
return (math.Sin(piX) / piX) * (math.Sin(piXOverFS) / (piXOverFS))
}

It isn't particularly performant, because I want to get a good quality result before I look at optimization.

Does anyone who has experience with image resampling see anything potentially problematic?

For reference, here is my source image:
在Golang中使用Lanczos重采样处理粗糙边缘

Here is my result:
在Golang中使用Lanczos重采样处理粗糙边缘

Here is my result if I remove the recursive call:
在Golang中使用Lanczos重采样处理粗糙边缘

Here is the result using RMagick/ImageMagick through Ruby (what I'm shooting for):
在Golang中使用Lanczos重采样处理粗糙边缘

Does anyone have advice for how I can get a smoother downscale result?
This particular example is a pretty drastic downscale, but Rmagick was able to downscale it very quickly with great quality, so it must be possible.

I'm told that Lanczos3 Resampling yields good results, and that's what I'm trying to use here - I'm not sure if my implementation is correct though.

Also, as a side note: the 0xFF / 0xFFFF conversion is because golang's "At" function returns rgba values in the range [0, 0xFFFF] ([0, 65535]) but "Set" takes a color which is initialized with the range [0, 0xFF] ([0, 255])

For now, I'm more concerned with quality than performance.

答案1

得分: 1

好的,我认为我找到了解决别名问题的一种方法。我使用双线性插值而不是使用lanczos3来对源图像进行重新采样,采样尺寸略大于我所需的尺寸(edgeSize = 1080),对图像进行高斯模糊处理,然后将图像缩放到目标尺寸(edgeSize = 600),这次使用双三次插值。这给我带来的结果几乎与RMagick给出的结果相同。

英文:

Alright, I think I've found one way to solve the aliasing problem. Instead of using lanczos3, I used bilinear interpolation to resample the source image at a size slightly higher than what I was going for (edgeSize = 1080), gaussian blurred the image, then scaled the image down to the target size (edgeSize = 600), this time with bicubic interpolation. This gave me results just about the same as the ones RMagick was giving me.

huangapple
  • 本文由 发表于 2016年8月21日 08:38:47
  • 转载请务必保留本文链接:https://go.coder-hub.com/39059902.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定