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

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

Rough Edges With Lanczos Resampling in Golang

问题

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

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

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

以下是我的代码:

  1. func resize(original image.Image, edgeSize int, filterSize int) image.Image {
  2. oldBounds := original.Bounds()
  3. if oldBounds.Dx() < edgeSize && oldBounds.Dy() < edgeSize {
  4. // 不需要调整大小
  5. return original
  6. }
  7. threshold := edgeSize * 4 / 3
  8. if oldBounds.Dx() > threshold || oldBounds.Dy() > threshold {
  9. fmt.Println("Upstream")
  10. original = customResizeImageToFitBounds(original, threshold, filterSize)
  11. oldBounds = original.Bounds()
  12. }
  13. newBounds := getNewBounds(oldBounds, edgeSize)
  14. resized := image.NewRGBA(newBounds)
  15. var ratioX = float64(oldBounds.Dx()) / float64(newBounds.Dx())
  16. var ratioY = float64(oldBounds.Dy()) / float64(newBounds.Dy())
  17. for x := 0; x < newBounds.Dx(); x++ {
  18. for y := 0; y < newBounds.Dy(); y++ {
  19. sourceX := ratioX * float64(x)
  20. minX := int(math.Floor(sourceX))
  21. sourceY := ratioY * float64(y)
  22. minY := int(math.Floor(sourceY))
  23. sampleSize := filterSize<<1 + 1
  24. var xCoeffs = make([]float64, sampleSize)
  25. var yCoeffs = make([]float64, sampleSize)
  26. var sumX = 0.0
  27. var sumY = 0.0
  28. for i := 0; i < sampleSize; i++ {
  29. xCoeffs[i] = lanczos(filterSize, sourceX-float64(minX+i-filterSize))
  30. yCoeffs[i] = lanczos(filterSize, sourceY-float64(minY+i-filterSize))
  31. sumX += xCoeffs[i]
  32. sumY += yCoeffs[i]
  33. }
  34. for i := 0; i < sampleSize; i++ {
  35. xCoeffs[i] /= sumX
  36. yCoeffs[i] /= sumY
  37. }
  38. rgba := make([]float64, 4)
  39. for i := 0; i < sampleSize; i++ {
  40. if yCoeffs[i] == 0.0 {
  41. continue
  42. }
  43. currY := minY + i - filterSize
  44. rgbaRow := make([]float64, 4)
  45. for j := 0; j < sampleSize; j++ {
  46. if xCoeffs[j] == 0.0 {
  47. continue
  48. }
  49. currX := minX + i - filterSize
  50. rij, gij, bij, aij := original.At(clamp(currX, currY, oldBounds)).RGBA()
  51. rgbaRow[0] += float64(rij) * xCoeffs[j]
  52. rgbaRow[1] += float64(gij) * xCoeffs[j]
  53. rgbaRow[2] += float64(bij) * xCoeffs[j]
  54. rgbaRow[3] += float64(aij) * xCoeffs[j]
  55. }
  56. rgba[0] += float64(rgbaRow[0]) * yCoeffs[i]
  57. rgba[1] += float64(rgbaRow[1]) * yCoeffs[i]
  58. rgba[2] += float64(rgbaRow[2]) * yCoeffs[i]
  59. rgba[3] += float64(rgbaRow[3]) * yCoeffs[i]
  60. }
  61. rgba[0] = clampRangeFloat(0, rgba[0], 0xFFFF)
  62. rgba[1] = clampRangeFloat(0, rgba[1], 0xFFFF)
  63. rgba[2] = clampRangeFloat(0, rgba[2], 0xFFFF)
  64. rgba[3] = clampRangeFloat(0, rgba[3], 0xFFFF)
  65. var rgbaF [4]uint64
  66. rgbaF[0] = (uint64(math.Floor(rgba[0]+0.5)) * 0xFF) / 0xFFFF
  67. rgbaF[1] = (uint64(math.Floor(rgba[1]+0.5)) * 0xFF) / 0xFFFF
  68. rgbaF[2] = (uint64(math.Floor(rgba[2]+0.5)) * 0xFF) / 0xFFFF
  69. rgbaF[3] = (uint64(math.Floor(rgba[3]+0.5)) * 0xFF) / 0xFFFF
  70. rf := uint8(clampRangeUint(0, uint32(rgbaF[0]), 255))
  71. gf := uint8(clampRangeUint(0, uint32(rgbaF[1]), 255))
  72. bf := uint8(clampRangeUint(0, uint32(rgbaF[2]), 255))
  73. af := uint8(clampRangeUint(0, uint32(rgbaF[3]), 255))
  74. resized.Set(x, y, color.RGBA{R: rf, G: gf, B: bf, A: af})
  75. }
  76. }
  77. return resized
  78. }
  79. // 机器精度
  80. var epsilon = math.Nextafter(1.0, 2.0) - 1
  81. func lanczos(filterSize int, x float64) float64 {
  82. x = math.Abs(x)
  83. fs := float64(filterSize)
  84. if x < epsilon {
  85. return 1.0
  86. }
  87. if x > fs {
  88. return 0
  89. }
  90. piX := math.Pi * x
  91. piXOverFS := piX / fs
  92. return (math.Sin(piX) / piX) * (math.Sin(piXOverFS) / (piXOverFS))
  93. }

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

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

供参考,这是我的源图像:
在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:

  1. func resize(original image.Image,
  2. edgeSize int, filterSize int) image.Image {
  3. oldBounds := original.Bounds()
  4. if oldBounds.Dx() &lt; edgeSize &amp;&amp; oldBounds.Dy() &lt; edgeSize {
  5. // No resize necessary
  6. return original
  7. }
  8. threshold := edgeSize * 4 / 3
  9. if oldBounds.Dx() &gt; threshold || oldBounds.Dy() &gt; threshold {
  10. fmt.Println(&quot;Upstream&quot;)
  11. original = customResizeImageToFitBounds(original, threshold, filterSize)
  12. oldBounds = original.Bounds()
  13. }
  14. newBounds := getNewBounds(oldBounds, edgeSize)
  15. resized := image.NewRGBA(newBounds)
  16. var ratioX = float64(oldBounds.Dx()) / float64(newBounds.Dx())
  17. var ratioY = float64(oldBounds.Dy()) / float64(newBounds.Dy())
  18. for x := 0; x &lt; newBounds.Dx(); x++ {
  19. for y := 0; y &lt; newBounds.Dy(); y++ {
  20. sourceX := ratioX * float64(x)
  21. minX := int(math.Floor(sourceX))
  22. sourceY := ratioY * float64(y)
  23. minY := int(math.Floor(sourceY))
  24. sampleSize := filterSize&lt;&lt;1 + 1
  25. var xCoeffs = make([]float64, sampleSize)
  26. var yCoeffs = make([]float64, sampleSize)
  27. var sumX = 0.0
  28. var sumY = 0.0
  29. for i := 0; i &lt; sampleSize; i++ {
  30. xCoeffs[i] = lanczos(filterSize, sourceX-float64(minX+i-filterSize))
  31. yCoeffs[i] = lanczos(filterSize, sourceY-float64(minY+i-filterSize))
  32. sumX += xCoeffs[i]
  33. sumY += yCoeffs[i]
  34. }
  35. for i := 0; i &lt; sampleSize; i++ {
  36. xCoeffs[i] /= sumX
  37. yCoeffs[i] /= sumY
  38. }
  39. rgba := make([]float64, 4)
  40. for i := 0; i &lt; sampleSize; i++ {
  41. if yCoeffs[i] == 0.0 {
  42. continue
  43. }
  44. currY := minY + i - filterSize
  45. rgbaRow := make([]float64, 4)
  46. for j := 0; j &lt; sampleSize; j++ {
  47. if xCoeffs[j] == 0.0 {
  48. continue
  49. }
  50. currX := minX + i - filterSize
  51. rij, gij, bij, aij := original.At(
  52. clamp(currX, currY, oldBounds)).RGBA()
  53. rgbaRow[0] += float64(rij) * xCoeffs[j]
  54. rgbaRow[1] += float64(gij) * xCoeffs[j]
  55. rgbaRow[2] += float64(bij) * xCoeffs[j]
  56. rgbaRow[3] += float64(aij) * xCoeffs[j]
  57. }
  58. rgba[0] += float64(rgbaRow[0]) * yCoeffs[i]
  59. rgba[1] += float64(rgbaRow[1]) * yCoeffs[i]
  60. rgba[2] += float64(rgbaRow[2]) * yCoeffs[i]
  61. rgba[3] += float64(rgbaRow[3]) * yCoeffs[i]
  62. }
  63. rgba[0] = clampRangeFloat(0, rgba[0], 0xFFFF)
  64. rgba[1] = clampRangeFloat(0, rgba[1], 0xFFFF)
  65. rgba[2] = clampRangeFloat(0, rgba[2], 0xFFFF)
  66. rgba[3] = clampRangeFloat(0, rgba[3], 0xFFFF)
  67. var rgbaF [4]uint64
  68. rgbaF[0] = (uint64(math.Floor(rgba[0]+0.5)) * 0xFF) / 0xFFFF
  69. rgbaF[1] = (uint64(math.Floor(rgba[1]+0.5)) * 0xFF) / 0xFFFF
  70. rgbaF[2] = (uint64(math.Floor(rgba[2]+0.5)) * 0xFF) / 0xFFFF
  71. rgbaF[3] = (uint64(math.Floor(rgba[3]+0.5)) * 0xFF) / 0xFFFF
  72. rf := uint8(clampRangeUint(0, uint32(rgbaF[0]), 255))
  73. gf := uint8(clampRangeUint(0, uint32(rgbaF[1]), 255))
  74. bf := uint8(clampRangeUint(0, uint32(rgbaF[2]), 255))
  75. af := uint8(clampRangeUint(0, uint32(rgbaF[3]), 255))
  76. resized.Set(x, y, color.RGBA{R: rf, G: gf, B: bf, A: af})
  77. }
  78. }
  79. return resized
  80. }
  81. // Machine epsilon
  82. var epsilon = math.Nextafter(1.0, 2.0) - 1
  83. func lanczos(filterSize int, x float64) float64 {
  84. x = math.Abs(x)
  85. fs := float64(filterSize)
  86. if x &lt; epsilon {
  87. return 1.0
  88. }
  89. if x &gt; fs {
  90. return 0
  91. }
  92. piX := math.Pi * x
  93. piXOverFS := piX / fs
  94. return (math.Sin(piX) / piX) * (math.Sin(piXOverFS) / (piXOverFS))
  95. }

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:

确定