пятница, 1 июня 2012 г.

Часть 6. Многопоточность на примере матричного фильтра - размытие

Итак, для примера многопоточного приложения воспользуемся применением фильтра размытие. Хорошая статья про матричные фильтры есть на хабре. Здесь на алгоритме фильтра я останавливаться не буду, кто захочет, почитает по ссылке или разберется сам по коду. Мы же сегодня посмотрим, как количество потоков влияет на производительность приложения.

Итак, для размытия будем применять вот такую матрицу с нормальным распределением Гаусса:
static double[,] filterMatrix = {
                            { 0.000789, 0.006581, 0.013347, 0.006581, 0.000789},
                            { 0.006581, 0.054901, 0.111345, 0.054901, 0.006581},
                            { 0.013347, 0.111345, 0.225821, 0.111345, 0.013347},
                            { 0.006581, 0.054901, 0.111345, 0.054901, 0.006581},
                            { 0.000789, 0.006581, 0.013347, 0.006581, 0.000789}
                        };
Собственно алгоритм состоит из нескольких методов. Метод умножения матриц для получения цвета в заданной точке:

private static Color MulMatrix(Color[,] imageMatrix)
{
    double red = 0;
    for (int col = 0; col < 5; col++)
    {
        for (int row = 0; row < 5; row++)
        {
            red += imageMatrix[col, row].R * filterMatrix[row, col];
        }
    }
    double green = 0;
    for (int col = 0; col < 5; col++)
    {
        for (int row = 0; row < 5; row++)
        {
            green += imageMatrix[col, row].G * filterMatrix[row, col];
        }
    }
    double blue = 0;
    for (int col = 0; col < 5; col++)
    {
        for (int row = 0; row < 5; row++)
        {
            blue += imageMatrix[col, row].B * filterMatrix[row, col];
        }
    }
    Color distColor = Color.FromArgb((int)red, (int)green, (int)blue);
    return distColor;
}Метод извлечения из исходной картинки матрицы цветов 5х5 с центром в заданных координатах:

private static Color[,] GetMatrixByCenter(Bitmap source, int i, int j)
{
    Color[,] imageMatrix = new Color[5, 5];
    for (int x = -2; x < 3; x++)
    {
        for (int y = -2; y < 3; y++)
        {
            imageMatrix[x + 2, y + 2] = source.GetPixel(i + x, j + y);
        }
    }
    return imageMatrix;
}
Метод пробегающий по столбцу картинки и применяющий ко всем точкам размытие:

private static void ColumnFilter(Bitmap source, Bitmap dist, int i)
{
    for (int j = 2; j < source.Height - 2; j++)
    {
        Color[,] imageMatrix = GetMatrixByCenter(source, i, j);
        Color distColor = MulMatrix(imageMatrix);
        dist.SetPixel(i, j, distColor);
    }
}
Ну и основная программа, применяющая последний метод для всех столбцов:

static void Main(string[] args)
{
    Bitmap source = new Bitmap("source.jpg");
    Bitmap dist = new Bitmap(source.Width, source.Height);
    DateTime begin = DateTime.Now;
    for (int i = 2; i < source.Width - 2; i++)
    {
        ColumnFilter(source, dist, i);               
    }
    DateTime end = DateTime.Now;
    dist.Save("dist.jpg");
    Console.WriteLine(end.Subtract(begin).TotalSeconds);
    Console.ReadKey();
}На моем тестовом примере, программа работала 17,3 секунды, ну и выдала вот такой результат (надо смотреть крупно, так как картинка достаточно большая, а размер матрицы достаточно маленький):
Собственно в данном примере мест для распараллеливания достаточно много. Мы, например, можем параллельно вычислять значения цветов в методе MulMatrix (3-мя потоками), мы можем реализовать копирование матриц (да и само умножение) хоть 25 потоками, но распараллеливать мы будем на уровне столбцов изображения. То есть в отдельных потоках будем запускать метод ColumnFilter. В связи с тем, что одиночный метод выполняется очень быстро, и в этом случае потери на переключение будут больше чем сама работа, я буду в потоке запускать расчет для полосы в 30 пикселей.Для многопоточного запуска, мы введем дополнительную константу, которая будет задавать количество одновременно запущенных потоков. Каждый вызов набора из 30 методов ColumnFilter будет запускаться в новом потоке, но мы будем следить за тем, чтобы количество одновременно запущенных потоков не превышало нашу константу. Также, в связи с тем, что Bitmap является потоко-безопасным (блокирует одновременное обращение из нескольких потоков), то мы наши картинки заменим массивами в которые скопируем цвета пикселей. Здесь же, необходимо понимать, что из первого массива мы будем только читать, а во второй (результирующий массив) хотя потоки и будут писать, они будут писать в разные его ячейки, т.е. гонок не будет. Таким образом, метод Main примет вид:

static void Main(string[] args)
{
    Bitmap source = new Bitmap("source.jpg");
    Color[,] sourceColors = new Color[source.Width, source.Height];
    for (int col = 0; col < source.Width; col++)
    {
        for (int row = 0; row < source.Height; row++)
        {
            sourceColors[col, row] = source.GetPixel(col, row);
        }
    }
    Color[,] distColors = new Color[source.Width, source.Height];
    DateTime begin = DateTime.Now;
    int maxThread = 3; // Максимальное количество потоков
    Console.WriteLine("Количество потоков: {0}", maxThread);
    // Массив запущенных потоков
    Thread[] threads = new Thread[maxThread];
    int i = 2;
    while (i < sourceColors.GetLength(0) - 32)
    {
        // Пробегаем по массиву потоков и заменяем закончившие новыми
        for (int threadIndex = 0; threadIndex < maxThread && i < sourceColors.GetLength(0) - 32; threadIndex++)
        {
            if (threads[threadIndex] == null || threads[threadIndex].ThreadState == ThreadState.Stopped)
            {
                int index = i;
                threads[threadIndex] = new Thread(new ThreadStart(
                    () =>
                    {   
                        // Это код запускаемого потока
                        for (int col = index; col < index + 30; col++)
                        {
                            ColumnFilter(sourceColors, distColors, col);
                        }
                    }
                ));  
                threads[threadIndex].Start();
                i = i + 30;
            }
        }
    }
    DateTime end = DateTime.Now;
    Bitmap dist = new Bitmap(source.Width, source.Height);
    for (int col = 0; col < source.Width; col++)
    {
        for (int row = 0; row < source.Height; row++)
        {
            dist.SetPixel(col, row, distColors[col, row]);
        }
    }
    dist.Save("dist.jpg");
    Console.WriteLine(end.Subtract(begin).TotalSeconds);
    Console.ReadKey();
}Я запускал приложение несколько раз, для разного количества потоков. Перед тем, как перейти к графику зависимости скорости работы от количества потоков, небольшой комментарий. Приложение работало для файла размером 4,5 МБ, разрешение 4288х2848, т.е. у нас матрицы каждого цвета умножались 12.212.224 раз. И все равно, время копирования из картинки в массив и из массива в результирующую картинку было сопоставимо со временем работы. Обратите внимание, что после запуска приложения нагружено одно ядро (идет копирование картинки), а потом все (применение фильтра):
Ну, и собственно зависимости (по оси абсцисс количество потоков, по оси ординат время работы в секундах):
Ладно, в следующий раз обсудим, еще один пример, но посмотрим еще один пример, но уже такой, где потребуется синхронизация потоков между собой.

Комментариев нет:

Отправить комментарий