Coskun Ozogul
Coskun Ozogul

Reputation: 2469

Replace colored pixels of bitmap image from another image

I have a performance problem.

For a insole model configurator, we have a piece to upload and many material images to fusion with the piece image.

I should replace every white pixel on the piece image by the corresponding pixel on the material image.

As the material image is not a mono color, I cant replace simply all white by another mono color.

Image sizes are the same. So I simply take a pixel if the color is not transparent from the piece image and with the same X and Z coordinates on the material images, I take a pixel and I set the pixel of the piece image.

But as there are many materials, it takes 5 minutes today.

Is there a mor optimised way to do this ?

Here is my method :

            //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)
        {

        }
    }

//I reduced image sizes to keep on the screen. Real image sizes are 500x1240 px.

Sample piece image

Sample material image

Upvotes: 1

Views: 516

Answers (3)

Nyerguds
Nyerguds

Reputation: 5629

Replacing the white is one possibility, but it's not a very pretty one. Based on the images you have there, the ideal solution for this is to get the pattern with the correct alpha applied, and then paint the visible black lines over it. This is actually a process with some more steps:

  • Extract the alpha from the foot shape image
  • Extract the black lines from the foot shape image
  • Apply the alpha to the pattern image
  • Paint the black lines over the alpha-adjusted pattern image

The way I'd approach this is to extract the data of both images as ARGB byte arrays, meaning, each pixel is four bytes, in the order B, G, R, A. Then, for each pixel, we simply copy the alpha byte from the foot shape image into the alpha byte of the pattern image, so you end up with the pattern image, with the transparency of the foot shape applied to it.

Now, in a new byte array of the same size, which starts with pure 00 bytes (meaning, since A,R,G and B are all zero, transparent black), we construct the black line. Pixels can be considered "black" if they're both not white, and visible. So the ideal result, including smooth fades, is to adjust the alpha of this new image to the minimum value of the alpha and the inverse of the brightness. Since it's grayscale, any of the R, G, B will do for brightness. To get the inverse as byte value, we just take (255 - brightness).

Note, if you need to apply this to a load of images, you probably want to extract the bytes, dimensions and stride of the foot pattern image only once in advance, and keep them in variables to give to the alpha-replacing process. In fact, since the black lines image won't change either, a preprocessing step to generate that should speed things up even more.

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);
            }
        }
    }
}

The black lines image:

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);
}

The processing to apply the foot image's transparency to the pattern image:

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);
}

The helper function to extract the bytes from an image:

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;
}

The helper function to make a new image from a byte array:

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;
}

The result in my test tool:

Combined image

As you see, the black lines are preserved on top of the pattern.

Upvotes: 1

Coskun Ozogul
Coskun Ozogul

Reputation: 2469

I found this solution, it is much more faster.

But it uses too much resources.

Parallel programing in C# came to my help :

         //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);

        }

Upvotes: 0

JonasH
JonasH

Reputation: 36341

GetPixel/SetPixel are notoriously slow due to locking and other overhead accessing the pixels. To improve performance you will need to use some unmanaged coding to access the data directly.

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

Here is some (untested!) code adapted from that anwer:

    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);
        }
    }
}

Upvotes: 1

Related Questions