从另一个图像替换位图图像的彩色像素

Replace colored pixels of bitmap image from another image

我遇到了性能问题。

对于鞋垫模型配置器,我们有一块要上传,还有许多 material 图像要与该块图像融合。

我应该用 material 图像上的相应像素替换图片上的每个白色像素。

由于 material 图像不是单色,我不能简单地将所有白色替换为另一种单色。

图片大小相同。因此,如果图像的颜色不透明并且 material 图像上的 X 和 Z 坐标相同,我只取一个像素,我取一个像素并设置该图像的像素。

但是material人多,今天需要5分钟。

有没有更优化的方法来做到这一点?

这是我的方法:

            //For every material image, calls the fusion method below.
            foreach (string material in System.IO.Directory.GetFiles(materialsPath))
            {
               var result = FillWhiteImages(whiteImagesFolder, whiteImagesFolder + "\" + System.IO.Path.GetFileName(whiteFilePath), material);

            }


        private static void FusionWhiteImagesWithMaterials(string whiteImageFolder, string file, string materialImageFile)
        {
        if (file.ToLower().EndsWith(".db") || materialImageFile.ToLower().EndsWith(".db"))
            return;


        List<CustomPixel> lstColoredPixels = new List<CustomPixel>();


        try
        {
            Bitmap image = new Bitmap(file);
            for (int y = 0; y < image.Height; ++y)
            {
                for (int x = 0; x < image.Width; ++x)
                {
                    if (image.GetPixel(x, y).A > 0)
                    {
                        lstColoredPixels.Add(new CustomPixel(x, y));
                    }
                }
            }

            Bitmap bmpTemp = new Bitmap(materialImageFile);
            Bitmap target = new Bitmap(bmpTemp, new Size(image.Size.Width, image.Size.Height));

            for (int y = 0; y < target.Height; y++)
            {
                for (int x = 0; x < target.Width; x++)
                {
                    Color clr = image.GetPixel(x, y);
                    if (clr.A > 0)
                    {
                        if (clr.R > 200 && clr.G > 200 && clr.B > 200)
                            image.SetPixel(x, y, target.GetPixel(x, y));
                        else
                            image.SetPixel(x, y, Color.Gray);
                    }
                }
            }

         ... 
         image.Save(...);  
        }
        catch (Exception ex)
        {

        }
    }

//我缩小了图像尺寸以保持在屏幕上。实际图像尺寸为 500x1240 像素。

GetPixel/SetPixel 由于锁定和访问像素的其他开销而出了名的慢。要提高性能,您需要使用一些非托管编码来直接访问数据。

This answer should shows an example on how to improve speed when working with bitmaps.

这里是一些(未经测试!)改编自该答案的代码:

    public static unsafe Image MergeBitmaps(Bitmap mask, Bitmap background)
    {
        Debug.Assert(mask.PixelFormat == PixelFormat.Format32bppArgb);
        BitmapData maskData = mask.LockBits(new Rectangle(0, 0, mask.Width, mask.Height),
            ImageLockMode.ReadWrite, mask.PixelFormat);
        BitmapData backgroundData = background.LockBits(new Rectangle(0, 0, background.Width, background.Height),
            ImageLockMode.ReadWrite, background.PixelFormat);
        try
        {
            byte bytesPerPixel = 4;

            /*This time we convert the IntPtr to a ptr*/
            byte* maskScan0 = (byte*)maskData.Scan0.ToPointer();
            byte* backgroundScan0 = (byte*)backgroundData.Scan0.ToPointer();
            for (int i = 0; i < maskData.Height; ++i)
            {
                for (int j = 0; j < maskData.Width; ++j)
                {
                    byte* maskPtr = maskScan0 + i * maskData.Stride + j * bytesPerPixel;
                    byte* backPtr = backgroundScan0 + i * backgroundData.Stride + j * bytesPerPixel;

                    //maskPtr is a pointer to the first byte of the 4-byte color data
                    //maskPtr[0] = blueComponent;
                    //maskPtr[1] = greenComponent;
                    //maskPtr[2] = redComponent;
                    //maskPtr[3] = alphaComponent;
                    if (maskPtr[3] > 0 )
                    {
                        if (maskPtr[2] > 200 &&
                            maskPtr[1] > 200 &&
                            maskPtr[0] > 200)
                        {
                            maskPtr[3] = 255;
                            maskPtr[2]  = backPtr[2];
                            maskPtr[1]  = backPtr[1];
                            maskPtr[0]  = backPtr[0];
                        }
                        else
                        {
                            maskPtr[3] = 255;
                            maskPtr[2] = 128;
                            maskPtr[1] = 128;
                            maskPtr[0] = 128;
                        }
                    }
                }
            }
            return mask;
        }
        finally
        {
            mask.UnlockBits(maskData);
            background.UnlockBits(backgroundData);
        }
    }
}

我找到了这个解决方案,它要快得多。

但是它占用了太多资源。

C# 中的并行编程对我有帮助:

         //I called my method in a parallel foreach 
         Parallel.ForEach(System.IO.Directory.GetFiles(materialsPath), filling =>
            {
               var result = FillWhiteImages(whiteImagesFolder, whiteImagesFolder + "\" + System.IO.Path.GetFileName(whiteFilePath), filling);
            });





        //Instead of a classic foreach loop like this.
        foreach (string material in System.IO.Directory.GetFiles(materialsPath))
        {
           var result = FillWhiteImages(whiteImagesFolder, whiteImagesFolder + "\" + System.IO.Path.GetFileName(whiteFilePath), material);

        }

替换白色是一种可能,但不是很漂亮。根据您那里的图像,理想的解决方案是获得应用了正确 alpha 的图案,然后在其上绘制可见的黑线。这实际上是一个包含更多步骤的过程:

  • 从脚形图像中提取 alpha
  • 从脚形图像中提取黑线
  • 将 alpha 应用于图案图像
  • 在经过 alpha 调整的图案图像上绘制黑线

我采用的方法是将两幅图像的数据提取为 ARGB 字节数组,也就是说,每个像素为四个字节,顺序为 B、G、R、A。然后,对于每个像素,我们只需将脚形图像的 alpha 字节复制到图案图像的 alpha 字节,这样您就可以得到图案图像,并应用脚形的透明度。

现在,在一个相同大小的新字节数组中,以纯00字节开始(意思是,由于A,R,G和B均为零,透明黑色),我们构造黑线。如果像素既不是白色又可见,则可以将其视为 "black"。所以理想的结果,包括平滑的淡入淡出,是将这个新图像的 alpha 调整为 alpha 的最小值和亮度的倒数。由于它是灰度,因此 R、G、B 中的任何一个都可以用于亮度。要将倒数作为字节值,我们只需要 (255 - brightness).

请注意,如果您需要将其应用于大量图像,您可能只想提前提取一次足部图案图像的字节、尺寸和步幅,并将它们保存在变量中以提供给 alpha -更换过程。事实上,由于黑线图像也不会改变,因此生成该图像的预处理步骤应该会加快速度。

public static void BakeImages(String whiteFilePath, String materialsFolder, String resultFolder)
{
    Int32 width;
    Int32 height;
    Int32 stride;
    // extract bytes of shape & alpha image
    Byte[] shapeImageBytes;
    using (Bitmap shapeImage = new Bitmap(whiteFilePath))
    {
        width = shapeImage.Width;
        height = shapeImage.Height;
        // extract bytes of shape & alpha image
        shapeImageBytes = GetImageData(shapeImage, out stride, PixelFormat.Format32bppArgb);
    }
    using (Bitmap blackImage = ExtractBlackImage(shapeImageBytes, width, height, stride))
    {
        //For every material image, calls the fusion method below.
        foreach (String materialImagePath in Directory.GetFiles(materialsFolder))
        {
            using (Bitmap patternImage = new Bitmap(materialImagePath))
            using (Bitmap result = ApplyAlphaToImage(shapeImageBytes, width, height, stride, patternImage))
            {
                if (result == null)
                    continue;
                // paint black lines image onto alpha-adjusted pattern image.
                using (Graphics g = Graphics.FromImage(result))
                    g.DrawImage(blackImage, 0, 0);
                result.Save(Path.Combine(resultFolder, Path.GetFileNameWithoutExtension(materialImagePath) + ".png"), ImageFormat.Png);
            }
        }
    }
}

黑线图像:

public static Bitmap ExtractBlackImage(Byte[] shapeImageBytes, Int32 width, Int32 height, Int32 stride)
{
    // Create black lines image.
    Byte[] imageBytesBlack = new Byte[shapeImageBytes.Length];
    // Line start offset is set to 3 to immediately get the alpha component.
    Int32 lineOffsImg = 3;
    for (Int32 y = 0; y < height; y++)
    {
        Int32 curOffs = lineOffsImg;
        for (Int32 x = 0; x < width; x++)
        {
            // copy either alpha or inverted brightness (whichever is lowest)
            // from the shape image onto black lines image as alpha, effectively
            // only retaining the visible black lines from the shape image.
            // I use curOffs - 1 (red) because it's the simplest operation.
            Byte alpha = shapeImageBytes[curOffs];
            Byte invBri = (Byte) (255 - shapeImageBytes[curOffs - 1]);
            imageBytesBlack[curOffs] = Math.Min(alpha, invBri);
            // Adjust offset to next pixel.
            curOffs += 4;
        }
        // Adjust line offset to next line.
        lineOffsImg += stride;
    }
    // Make the black lines images out of the byte array.
    return BuildImage(imageBytesBlack, width, height, stride, PixelFormat.Format32bppArgb);
}

将脚部图像的透明度应用于图案图像的处理:

public static Bitmap ApplyAlphaToImage(Byte[] alphaImageBytes, Int32 width, Int32 height, Int32 stride, Bitmap texture)
{
    Byte[] imageBytesPattern;
    if (texture.Width != width || texture.Height != height)
        return null;
    // extract bytes of pattern image. Stride should be the same.
    Int32 patternStride;
    imageBytesPattern = ImageUtils.GetImageData(texture, out patternStride, PixelFormat.Format32bppArgb);
    if (patternStride != stride)
        return null;
    // Line start offset is set to 3 to immediately get the alpha component.
    Int32 lineOffsImg = 3;
    for (Int32 y = 0; y < height; y++)
    {
        Int32 curOffs = lineOffsImg;
        for (Int32 x = 0; x < width; x++)
        {
            // copy alpha from shape image onto pattern image.
            imageBytesPattern[curOffs] = alphaImageBytes[curOffs];
            // Adjust offset to next pixel.
            curOffs += 4;
        }
        // Adjust line offset to next line.
        lineOffsImg += stride;
    }
    // Make a image out of the byte array, and return it.
    return BuildImage(imageBytesPattern, width, height, stride, PixelFormat.Format32bppArgb);
}

从图像中提取字节的辅助函数:

public static Byte[] GetImageData(Bitmap sourceImage, out Int32 stride, PixelFormat desiredPixelFormat)
{
    Int32 width = sourceImage.Width;
    Int32 height = sourceImage.Height;
    BitmapData sourceData = sourceImage.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, desiredPixelFormat);
    stride = sourceData.Stride;
    Byte[] data = new Byte[stride * height];
    Marshal.Copy(sourceData.Scan0, data, 0, data.Length);
    sourceImage.UnlockBits(sourceData);
    return data;
}

从字节数组创建新图像的辅助函数:

public static Bitmap BuildImage(Byte[] sourceData, Int32 width, Int32 height, Int32 stride, PixelFormat pixelFormat)
{
    Bitmap newImage = new Bitmap(width, height, pixelFormat);
    BitmapData targetData = newImage.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, newImage.PixelFormat);
    // Get actual data width.
    Int32 newDataWidth = ((Image.GetPixelFormatSize(pixelFormat) * width) + 7) / 8;
    Int32 targetStride = targetData.Stride;
    Int64 scan0 = targetData.Scan0.ToInt64();
    // Copy per line, copying only data and ignoring any possible padding.
    for (Int32 y = 0; y < height; ++y)
        Marshal.Copy(sourceData, y * stride, new IntPtr(scan0 + y * targetStride), newDataWidth);
    newImage.UnlockBits(targetData);
    return newImage;
}

我的测试工具的结果:

如您所见,黑色线条保留在图案顶部。