Реализация RGB-алгоритма изменения контраста изображения



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

Так как программа была предназначена для обработки видео, то от реализации требовалась высокая производительность, в том числе способность обрабатывать видео разрешения Full HD. Код был написан на С++ с использованием библиотеки OpenMP.



Существует несколько алгоритмов изменения контраста, часть из которых рассмотрена в этой статье [1].

Рассмотрим RGB-алгоритм изменения контраста.
Вначале мы вычисляем среднее значение яркости изображения lAB. Используем директиву OpenMP для распараллеливания цикла:
#pragma omp parallel for private(valueB,valueG,valueR) reduction(+:lAB) schedule(static,1)
for (int j = 0; j < ptrImage->imageSize; j = j+3)
{
	valueB = ptrImage->imageData[j];
	valueG = ptrImage->imageData[j+1];
	valueR = ptrImage->imageData[j+2];

	lAB += (int)(valueR * 0.299 + valueG * 0.587 + valueB * 0.114);
}

//средняя яркость 
lAB /= imageRows * imageCols;   

Затем для каждого пикселя изображения для каждой из цветовых компонент R, G, и B мы должны найти отклонение от среднего значения яркости delta, и это отклонение умножить на коэффициент усиления(ослабления) контраста k. Выполнять это непосредственно над всеми пикселями изображения слишком дорогостоящая операция. Поэтому, учитывая, что мы преобразовываем все цветовые компоненты пикселя одинаково, введём массив из 256 значений, который будет представлять палитру для каждой цветовой компоненты. Индекс массива соответствует старому значению цветовой компоненты, а значение массива — преобразованному. Таким образом, применяя ранее описанные преобразования для изменения контраста к палитре, получим:
//Коэффициент коррекции
 double k = 1.0 + correction / 100.0;
	
 for (int i = 0; i < 256; i++)
{
	int delta = (int)i - lAB;
	int temp  = (int)(lAB + k *delta);

	if (temp < 0)
		temp = 0;

	if (temp >= 255)
		temp = 255;
	b[i] = (unsigned char)temp;
}


И далее, применяем новые значения для пикселей всего изображения, сопоставляя старое значение цветовой компоненты пикселя с новым, используя полученный массив палитры:
#pragma omp parallel for firstprivate(b)
for (int j = 0; j < ptrImage->imageSize; j++)
{
	unsigned char value = ptrImage->imageData[j];
	ptrImage->imageData[j] = (unsigned char)b[value];
}

Весь исходный код функции для изменения контраста:
const int L = 256;

void AddContrastFilter(IplImage* ptrImage, int _correction)
{
	// установить число потоков в 4, равным количеству процессоров в системе
	omp_set_num_threads(4); 

	int correction = _correction;

        //палитра
	int b[L];

	//Число строк и столбцов
	int imageRows = ptrImage->height;
	int imageCols = ptrImage->width;

	int lAB = 0;
	unsigned char valueB = 0;
	unsigned char valueG= 0;
	unsigned char valueR= 0;

	//Находим яркость всех пикселей
	#pragma omp parallel for private(valueB,valueG,valueR) reduction(+:lAB) schedule(static,1)
	for (int j = 0; j < ptrImage->imageSize; j = j+3)
	{
		valueB = ptrImage->imageData[j];
		valueG = ptrImage->imageData[j+1];
		valueR = ptrImage->imageData[j+2];

		lAB += (int)(valueR * 0.299 + valueG * 0.587 + valueB * 0.114);
	}

	//средняя яркость всех пикселей
	lAB /= imageRows * imageCols;   

	//Коэффициент коррекции
	 double k = 1.0 + correction / 100.0;

	//RGB алгоритм изменения контраста	
	 for (int i = 0; i < L; i++)
	{
		int delta = (int)i - lAB;
		int temp  = (int)(lAB + k *delta);

		if (temp < 0)
			temp = 0;

		if (temp >= 255)
			temp = 255;
		b[i] = (unsigned char)temp;
	}

	
	#pragma omp parallel for firstprivate(b)
	for (int j = 0; j < ptrImage->imageSize; j++)
	{
		unsigned char value = ptrImage->imageData[j];
		ptrImage->imageData[j] = (unsigned char)b[value];
	}
				
	return 0;
}

У меня получилось, что изменение контраста Full HD кадра на четырёх-ядерном процессоре Intel Xeon занимает по времени порядка 8 мс.

Ссылки


1. Преобразование контраста в программном обеспечении: http://www.kweii.com/site/color_theory/Contrast100_ru.pdf
Реклама
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее

Комментарии 27

    –6
    image
    Простите, не удержался.
      –4
      8 сек. это слишком много. Мне кажется, у вас потоки борятся за ресурсы. Во время работы алгоритма, какая нагрузка на процессоры, случайно не 100%?
      • НЛО прилетело и опубликовало эту надпись здесь
          –1
          Упс, действительно (:
            0
            125 кадров в секунду, т.е. не более 5 потоков осилит. GPU бы к такому привлечь.
            • НЛО прилетело и опубликовало эту надпись здесь
                +2
                8 мс — это очень долго.
                мы за это время успеваем FullHD кадр прогнать через несколько намного более тяжелых алгоритмов. (GPU)
                • НЛО прилетело и опубликовало эту надпись здесь
          +16
          Эта статья — отличный пример неправильного использования многопоточности.
          Вычислений почти нет, в основном работа с памтятью. Вот код, который на на 15% быстрее и выполняется в одном потоке:
          void contrastFilter(unsigned char * imageData, size_t dataSize, int contrast) // contrast (256 - normal)
          {
            unsigned char buf[256];
            unsigned int midBright = 0;
            for (size_t i = 0; i < dataSize; i += 3)
              midBright += imageData[i] * 77 + imageData[i + 1] * 150 + imageData[i + 2] * 29;
            midBright /= (256 * dataSize / 3);
            for (size_t i = 0; i < 256; i++)
            {
              int a = (((i - midBright) * contrast) >> 8) + midBright;
              if (a < 0) buf[i] = 0;
                else if (a > 255) buf[i] = 255;
                  else buf[i] = a;
            }
            for (size_t i = 0; i < dataSize; i++)
              imageData[i] = buf[imageData[i]];
          }
          
            0
            Ничего себе вы уместили. А можете показать ошибки автора? У меня недостаточно опыта, чтобы самому их найти, но очень интересно. И ещё вопрос — почему и у вас и у автора коэффициенты
            midBright += imageData[i] * 77 + imageData[i + 1] * 150 + imageData[i + 2] * 29;
            разные? Потому что глаз воспринимает по-разному? И последнее — в одном случае вы умножаете на 256
            256 * dataSize
            , а в другом делаете так
            >> 8
            . Не ухудшает ли это читаемость кода?
              +2
              Основная ошибка автора — использование распараллеливания там, где это не нужно. Вычислений почти нет, обращений к памяти много. Основное время процессор будет ждать передачи дынных, на сколько потоков это не раскидывай — лучше не будет.

              Вместо 1.0 максимальная яркость принята за 256, поэтому константы почти такие же как у автора.
              77 / 256 ~= 0.3
              150 / 256 ~= 0.586
              29 / 256 ~= 0.113

              По поводу умножения и сдвига на 8 (деления на 256): да, константы для операций с фиксированной точкой могут показаться странными.
              • НЛО прилетело и опубликовало эту надпись здесь
                  0
                  Да, так и есть. Я обычно пишу x / 4, а не x >> 2, т.к. всё же деление воспринимается более интуитивно, а компилятор сам решит как поступить. Просто о том, что значит / 4, мой мозг узнал в 1 классе, а про >> 2 — лет 6 назад, так что ему всё же быстрее понять / 4 — не выходит из кэша, а за >> 2 надо лезть в оперативку :)
                +1
                ещё по поводу ошибок:

                int b[L];
                палитру лучше было сделать из (unsigned char)

                lAB += (int)(valueR * 0.299 + valueG * 0.587 + valueB * 0.114);
                при округлении в этом месте будет теряться много информации, например, если весь буфер будет заполнен синим цветом с яркостью 6, то переменная lAB, которая средняя яркость всех пикселей, будет равна 0.

                ну и ещё много мелочей, вроде «return 0;» в функции которая возвращает void, кучи лишних приведений типов и ни о чём не говорящих названий переменных.

                (у поста уже 17 плюсов, кто все эти люди? :) )
                  0
                  меня еще смущает вот эта строка:
                  int correction = _correction;
                  зачем?
                  –1
                  Алгоритм распаралелен неправильно, но проблема здесь не с забиванием шины памяти. Даже на Sandy Bridge в идеально оптимизированном коде на одном потоке можно получить максимум 75% пропускной способности. А невекторизованным кодом забить шину памяти ещё сложнее. Проблема с кодом у автора поста в том, что разные потоки пишут в одни и те же строки кэша. Когда один из потоков хочет записать что-то в память, он проверяет, не закэширована ли эта строка в кэше другого процессора. Т.к. все потоки пишут в одни и те же строки кэша, скорее всего обнаружится, что эту строку кэша уже изменил другой поток. В результате будет отправлен запрос RFO (Request for ownership) к другому ядру процессора, чтобы оно прислало изменённую строку кэша. Попутно контроллер памяти отследит RFO запрос, и запишет изменённую строку кэша обратно в память (это нужно чтобы правильно работал MESI алгоритм). Всё это, естественно, очень не быстро, и главное, превращает код в по сути однопоточный.
                    +1
                    я правильный вывод сделал, что если параллелить, то не каждый третий пиксель в другой поток, а лучше разбить кадр горизонтальными линиями на n частей/потоков?
                      +2
                      Правильный. Только линии должны быть кратны размеру кэш-строки (можно считать, что 128 байт)
                  +1
                  еще одна очевидная оптимизация первого цикла. +5% буст

                  	unsigned int midBright = 0, midBright1 = 0, midBright2 = 0;
                  	for (size_t i = 0; i < dataSize; )
                  	{
                  		midBright += imageData[i++];
                  		midBright1 += imageData[i++];
                  		midBright2 += imageData[i++];
                  	}
                  		midBright = (midBright * 77 + midBright1 * 150 + midBright2 * 29) / (256 * dataSize / 3);
                  
                    0
                    кстати можно даже вернуть вычисления с двойной точностью без потерь производительности
                      0
                      Только сначала лучше померить, действительно ли это скомпилируется в более быстрый код. У меня было несколько идей по поводу второго цикла: пробовал производить вычисления вместо обращения к палитре, пробовал считать по 4 байта и писать сразу dword, пробовал разносить чтение и запись на 1024 байта, пробовал вручную разворачивать цикл, в итоге оставил самый простой вариант, как самый быстрый.
                        +1
                        кстати в имеющемся виде 255 максимальное значение субпикселя. в кадре fullHD их 2млн штук каждого цвета. итого после финального умножения 32хбитный инт переполняется.

                        то бишь надо либо сначала делить, а потом умножать, но так погрешность выше.
                        либо long int надо брать, ну либо возвращаться к double, как у автора.
                          0
                          Не переполнится, я посчитал, перед тем как делать.
                          1920 * 1080 * 256 = 0x 1FA4 0000

                          Размер long int сейчас обычно такой же как и int, возможно, вы имели в виду long long int.
                            0
                            это число потом еще умножается на 150!

                            ЗЫ я имел ввиду int64
                              0
                              а да, точно, спасибо :)
                              тогда uint64_t
                        0
                        А да, не заметил сразу, что умножения вынесены за цикл. Да, так будет быстрее.
                      0
                      Данный алгоритм не учитывает нелинейность исходного цветового пространства. Так делать нельзя.

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

                      Самое читаемое