Замена цветных пикселей растрового изображения из другого изображения - PullRequest
1 голос
/ 03 апреля 2020

У меня проблема с производительностью.

Для конфигуратора модели стельки у нас есть кусок для загрузки и множество изображений материалов для слияния с изображением.

Я должен заменить каждый белый пиксель на изображении куска соответствующим пикселем на изображении материала.

Поскольку изображение материала не является одноцветным, я не могу просто заменить весь белый другим одноцветным.

Размеры изображения одинаковы. Поэтому я просто беру пиксель, если цвет не прозрачен на изображении фрагмента и с такими же координатами X и Z на изображениях материала, я беру пиксель и устанавливаю пиксель изображения фрагмента.

Но поскольку материалов много, сегодня это занимает 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 пикселей.

Sample piece image

Sample material image

Ответы [ 3 ]

1 голос
/ 07 апреля 2020

Замена белого - одна из возможностей, но она не очень хороша. Основываясь на изображениях, которые у вас есть, идеальное решение для этого - получить шаблон с правильной альфа-версией, а затем закрасить видимые черные линии поверх него. Это на самом деле процесс с еще несколькими шагами:

  • Извлечение альфа из изображения формы ноги
  • Извлечение черных линий из изображения формы ноги
  • Применение альфа к изображению шаблона
  • закрасьте черные линии поверх изображения шаблона с альфа-коррекцией

Для этого я бы извлек данные обоих изображений в виде байтовых массивов ARGB Это означает, что каждый пиксель составляет четыре байта в порядке B, G, R, A. Затем для каждого пикселя мы просто копируем альфа-байт из изображения формы ноги в альфа-байт изображения шаблона, так что вы в конечном итоге с изображением шаблона, с примененной к нему прозрачностью формы лапки.

Теперь в новом байтовом массиве того же размера, который начинается с чистых 00 байтов (то есть, поскольку A, R, G и B - все ноль, прозрачный черный), мы строим черную линию. Пиксели можно считать «черными», если они не белые, а видимые. Таким образом, идеальный результат, в том числе плавное затухание, состоит в том, чтобы настроить альфа этого нового изображения на минимальное значение альфа и инверсию яркости. Так как это оттенки серого, любой из R, G, B подойдет для яркости. Чтобы получить обратное значение в виде байта, мы просто берем (255 - brightness).

Обратите внимание: если вам нужно применить это к загрузке изображений, вы, вероятно, захотите извлечь байты, размеры и шаг шаблона ноги. image только один раз заранее, и держите их в переменных, чтобы дать процессу альфа-замены. Фактически, поскольку изображение черных линий также не изменится, шаг предварительной обработки для генерации должен ускорить процесс.

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

Результат в моем тестовом инструменте:

Combined image

Как видите, черные линии сохраняются поверх шаблона .

1 голос
/ 03 апреля 2020

GetPixel / SetPixel общеизвестно медленны из-за блокировки и других затрат доступа к пикселям. Для повышения производительности вам нужно будет использовать некоторое неуправляемое кодирование для прямого доступа к данным.

В этом ответе должен быть показан пример того, как повысить скорость при работе с растровыми изображениями.

Вот некоторый (непроверенный!) Код, адаптированный из этого ответа:

    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);
        }
    }
}
0 голосов
/ 06 апреля 2020

Я нашел это решение, оно намного быстрее.

Но он использует слишком много ресурсов.

Параллельное программирование в 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);

        }
...