【问题标题】:Split resize algorithm into two passes将调整大小算法拆分为两遍
【发布时间】:2016-04-19 07:32:14
【问题描述】:

我编写了以下调整大小的算法,可以正确地放大或缩小图像。由于每个循环上的权重数组的内部迭代,它太慢了。

我相当肯定我应该能够将算法分成两遍,就像使用两遍高斯模糊一样,这将大大降低操作复杂性并提高性能。不幸的是,我无法让它工作。有人可以帮忙吗?

Parallel.For(
    startY,
    endY,
    y =>
    {
        if (y >= targetY && y < targetBottom)
        {
            Weight[] verticalValues = this.verticalWeights[y].Values;

            for (int x = startX; x < endX; x++)
            {
                Weight[] horizontalValues = this.horizontalWeights[x].Values;

                // Destination color components
                Color destination = new Color();

                // This is where there is too much operation complexity.
                foreach (Weight yw in verticalValues)
                {
                    int originY = yw.Index;

                    foreach (Weight xw in horizontalValues)
                    {
                        int originX = xw.Index;
                        Color sourceColor = Color.Expand(source[originX, originY]);
                        float weight = yw.Value * xw.Value;
                        destination += sourceColor * weight;
                    }
                }

                destination = Color.Compress(destination);
                target[x, y] = destination;
            }
        }
    });

权重和指数计算如下。每个维度一个:

/// <summary>
/// Computes the weights to apply at each pixel when resizing.
/// </summary>
/// <param name="destinationSize">The destination section size.</param>
/// <param name="sourceSize">The source section size.</param>
/// <returns>
/// The <see cref="T:Weights[]"/>.
/// </returns>
private Weights[] PrecomputeWeights(int destinationSize, int sourceSize)
{
    IResampler sampler = this.Sampler;
    float ratio = sourceSize / (float)destinationSize;
    float scale = ratio;

    // When shrinking, broaden the effective kernel support so that we still
    // visit every source pixel.
    if (scale < 1)
    {
        scale = 1;
    }

    float scaledRadius = (float)Math.Ceiling(scale * sampler.Radius);
    Weights[] result = new Weights[destinationSize];

    // Make the weights slices, one source for each column or row.
    Parallel.For(
        0,
        destinationSize,
        i =>
            {
                float center = ((i + .5f) * ratio) - 0.5f;
                int start = (int)Math.Ceiling(center - scaledRadius);

                if (start < 0)
                {
                    start = 0;
                }

                int end = (int)Math.Floor(center + scaledRadius);

                if (end > sourceSize)
                {
                    end = sourceSize;

                    if (end < start)
                    {
                        end = start;
                    }
                }

                float sum = 0;
                result[i] = new Weights();

                List<Weight> builder = new List<Weight>();
                for (int a = start; a < end; a++)
                {
                    float w = sampler.GetValue((a - center) / scale);

                    if (w < 0 || w > 0)
                    {
                        sum += w;
                        builder.Add(new Weight(a, w));
                    }
                }

                // Normalise the values
                if (sum > 0 || sum < 0)
                {
                    builder.ForEach(w => w.Value /= sum);
                }

                result[i].Values = builder.ToArray();
                result[i].Sum = sum;
            });

    return result;
}

/// <summary>
/// Represents the weight to be added to a scaled pixel.
/// </summary>
protected class Weight
{
    /// <summary>
    /// The pixel index.
    /// </summary>
    public readonly int Index;

    /// <summary>
    /// Initializes a new instance of the <see cref="Weight"/> class.
    /// </summary>
    /// <param name="index">The index.</param>
    /// <param name="value">The value.</param>
    public Weight(int index, float value)
    {
        this.Index = index;
        this.Value = value;
    }

    /// <summary>
    /// Gets or sets the result of the interpolation algorithm.
    /// </summary>
    public float Value { get; set; }
}

/// <summary>
/// Represents a collection of weights and their sum.
/// </summary>
protected class Weights
{
    /// <summary>
    /// Gets or sets the values.
    /// </summary>
    public Weight[] Values { get; set; }

    /// <summary>
    /// Gets or sets the sum.
    /// </summary>
    public float Sum { get; set; }
}

每个 IResampler 都会根据给定的索引提供适当的权重系列。双三次重采样器的工作原理如下。

/// <summary>
/// The function implements the bicubic kernel algorithm W(x) as described on
/// <see href="https://en.wikipedia.org/wiki/Bicubic_interpolation#Bicubic_convolution_algorithm">Wikipedia</see>
/// A commonly used algorithm within imageprocessing that preserves sharpness better than triangle interpolation.
/// </summary>
public class BicubicResampler : IResampler
{
    /// <inheritdoc/>
    public float Radius => 2;

    /// <inheritdoc/>
    public float GetValue(float x)
    {
        // The coefficient.
        float a = -0.5f;

        if (x < 0)
        {
            x = -x;
        }

        float result = 0;

        if (x <= 1)
        {
            result = (((1.5f * x) - 2.5f) * x * x) + 1;
        }
        else if (x < 2)
        {
            result = (((((a * x) + 2.5f) * x) - 4) * x) + 2;
        }

        return result;
    }
}

这是一个通过现有算法调整大小的图像示例。输出是正确的(注意保留了银色光泽)。

原图

使用双三次重采样器将图像大小减半。

代码是我正在编写的更大的library 的一部分,用于向 corefx 添加图像处理。

【问题讨论】:

  • 指标和权重值是多少?没有它,就没有办法计算递归细分......除了蛮力......而且只有在满足某些属性的情况下......还有一个好主意是在过滤之前和之后共享样本图像,所以有一些东西可以比较...
  • @Spektre 我在问题中添加了更多信息。如果您还有其他需要,请告诉我。
  • 以及这是双三次(不是我从您的代码中假设的任意卷积,不是太深的外观),那么这不能通过递归细分来解决。相反,这可以按照您的意图分为 2x1D 问题,但我不确定(在当今的计算机上)它是否会比直接 2D 重新采样更快。必须对此进行一些测试,但要到星期一才有时间。
  • 是的,双三次是我使用的众多算法之一。如果你能做一些测试,那就太好了。当我的高斯模糊算法切换到 2x1D 时,它产生了更好的性能,所以手指交叉也应该如此。周一绝对没问题,周末不上班:)
  • 高斯从 2D 分离到 2x1D 更好,因为它是 2D 积分。双立方过滤不是一个整体,所以不要指望如此高的速度。此外,通过类似于 FFT 算法的递归细分,有时可以将高斯一维从 O(n) 加速到 O(log(n)) ... n x 在更小的区域上积分更快...

标签: c# algorithm image-processing imageprocessor


【解决方案1】:

好的,这就是我的做法。

诀窍是首先只调整图像的宽度,保持高度与原始图像相同。我们将生成的像素存储在临时图像中。

然后就是将图像的大小调整为我们的最终输出。

如您所见,我们不再遍历每个像素上的两个权重集合。尽管必须对外部像素循环进行两次迭代,但算法的运行速度要快得多,在我的测试图像上平均快 25% 左右。

// Interpolate the image using the calculated weights.
// First process the columns.
Parallel.For(
    0,
    sourceBottom,
    y =>
    {
        for (int x = startX; x < endX; x++)
        {
            Weight[] horizontalValues = this.HorizontalWeights[x].Values;

            // Destination color components
            Color destination = new Color();

            foreach (Weight xw in horizontalValues)
            {
                int originX = xw.Index;
                Color sourceColor = Color.Expand(source[originX, y]);
                destination += sourceColor * xw.Value;
            }

            destination = Color.Compress(destination);
            this.firstPass[x, y] = destination;
        }
    });

// Now process the rows.
Parallel.For(
    startY,
    endY,
    y =>
    {
        if (y >= targetY && y < targetBottom)
        {
            Weight[] verticalValues = this.VerticalWeights[y].Values;

            for (int x = startX; x < endX; x++)
            {
                // Destination color components
                Color destination = new Color();

                foreach (Weight yw in verticalValues)
                {
                    int originY = yw.Index;
                    int originX = x;
                    Color sourceColor = Color.Expand(this.firstPass[originX, originY]);
                    destination += sourceColor * yw.Value;
                }

                destination = Color.Compress(destination);
                target[x, y] = destination;
            }
        }
    });

【讨论】:

  • 最大的速度提升是当您专门读取连续的内存区域时。为此,您必须转置矩阵两次 - 一次在缩放 X 之后,然后在缩放 Y 之后。好处是您可以在写入操作期间进行转置,如果该内存区域没有读取依赖关系,CPU 可以异步执行此操作.
  • 这应该可以简化您的代码——您现在只知道如何在一个方向上缩放,并且在编写时总是转置。您调用 Scale1D 两次,每次都传入不同的宽度/高度和加权结构。现在我只在 C 中对此进行了测试,因此数组边界检查可能会减慢速度,但我认为底层缓存一致性的好处也应该在 CLR JIT 中起作用。这种在写入时转置、连续读取的方法似乎可以提供接近一个数量级的性能提升。
【解决方案2】:

您可以尝试加权 voronoi 图。随机尝试一组点并计算 voronoi 图。使用 Lloyd 算法平滑多边形并插入多边形的颜色。使用权重计算加权 voronoi 图。例如 voronoi 点画和马赛克:http://www.mrl.nyu.edu/~ajsecord/stipples.html 和 http://www.evilmadscientist.com/2012/stipplegen-weighted-voronoi-stippling-and-tsp-paths-in-processing/。

【讨论】:

  • 谢谢,我会看看这个。
【解决方案3】:

从抽象的角度来看(对图像处理了解不多),我认为您似乎正在多次计算 weight 和 sourcecolor 的值(在最里面的 foreach 循环中)(只要同一对索引作物)再次上升);简单地预先计算它们是否可行? 您需要为您的 Horizo​​ntalWeight 和 VerticalWeight 矩阵计算一个“直接乘积”矩阵(只需将每对索引 (x, y) 的值相乘),并且还可以提前将 Color.Expand 应用于源。 这些任务可以并行完成,并且“直接产品”(抱歉,我不知道该野兽的正确名称)应该可以在很多库中使用。

【讨论】:

  • 我想我明白你所说的谢谢。我将尝试这种方法。
猜你喜欢
  • 1970-01-01
  • 2016-11-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-01-06
  • 1970-01-01
  • 2011-03-20
相关资源
最近更新 更多