在 Golang 中使用 Lanczos 重采样的粗糙边缘
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 {
// No resize necessary
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
}
// 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 < 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))
}
它的性能不是特别好,因为我想在考虑优化之前获得高质量的结果。
有图像重采样经验的人是否发现任何潜在问题?
作为参考,这是我的源图像:
这是我的结果:
如果我删除递归调用,这是我的结果:
这是使用 RMagick/ImageMagick 到 Ruby 的结果(我正在拍摄):
有没有人对我如何获得更平滑的缩小结果有建议?
这个特殊的例子是一个非常剧烈的缩小,但是 Rmagick 能够非常快速地缩小它并且质量很好,所以它一定是可能的。
我听说 Lanczos3 重采样产生了很好的结果,这就是我在这里尝试使用的 - 不过我不确定我的实现是否正确。
此外,作为旁注:0xFF / 0xFFFF 转换是因为 golang 的 "At" 函数 returns rgba 值在 [0, 0xFFFF] ([0, 65535]) 范围内,但是 "Set" 采用一种颜色,它的初始化范围为 [0, 0xFF] ([0, 255])
目前,我更关心质量而不是性能。
好的,我想我找到了解决别名问题的方法。我没有使用 lanczos3,而是使用双线性插值以略高于我想要的尺寸 (edgeSize = 1080) 对源图像进行重新采样,对图像进行高斯模糊处理,然后将图像缩小到目标尺寸 (edgeSize = 600) ,这次是双三次插值。这给我的结果与 RMagick 给我的结果几乎相同。
我一直在写一些在 Golang 中调整图像大小的基本方法。我已经看过几篇关于调整图像大小的帖子,但对于我的生活,我无法弄清楚我错过了什么......
基本上,我的问题是在 Golang 中调整图像大小时,我的结果似乎有很多锯齿。
我已经尝试对图像进行迭代下采样,但这并没有产生太大的改进。
这是我的代码:
func resize(original image.Image,
edgeSize int, filterSize int) image.Image {
oldBounds := original.Bounds()
if oldBounds.Dx() < edgeSize && oldBounds.Dy() < edgeSize {
// No resize necessary
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
}
// 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 < 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))
}
它的性能不是特别好,因为我想在考虑优化之前获得高质量的结果。
有图像重采样经验的人是否发现任何潜在问题?
作为参考,这是我的源图像:
这是我的结果:
如果我删除递归调用,这是我的结果:
这是使用 RMagick/ImageMagick 到 Ruby 的结果(我正在拍摄):
有没有人对我如何获得更平滑的缩小结果有建议? 这个特殊的例子是一个非常剧烈的缩小,但是 Rmagick 能够非常快速地缩小它并且质量很好,所以它一定是可能的。
我听说 Lanczos3 重采样产生了很好的结果,这就是我在这里尝试使用的 - 不过我不确定我的实现是否正确。
此外,作为旁注:0xFF / 0xFFFF 转换是因为 golang 的 "At" 函数 returns rgba 值在 [0, 0xFFFF] ([0, 65535]) 范围内,但是 "Set" 采用一种颜色,它的初始化范围为 [0, 0xFF] ([0, 255])
目前,我更关心质量而不是性能。
好的,我想我找到了解决别名问题的方法。我没有使用 lanczos3,而是使用双线性插值以略高于我想要的尺寸 (edgeSize = 1080) 对源图像进行重新采样,对图像进行高斯模糊处理,然后将图像缩小到目标尺寸 (edgeSize = 600) ,这次是双三次插值。这给我的结果与 RMagick 给我的结果几乎相同。