На 4 неделе курса CS50 (Harvard) мы изучили принципы работы с памятью и массивы данных. Перед нами встала задача — написать программу, которая будет накладывать фильтры на исходное изображение (filter-less). Нужно сделать 4 фильтра: черно-белый, сепия, зеркальный и размытие. Исходная картинка представляет собой 24-битный .BMP файл, это означает, что каждый пиксель этой картинки содержит информацию в 24 бита, кодирующую RGB цвет (на каждый отдельный цвет — красный, зеленый, синий — выделяется по 8 бит).
Кодировка цвета может рассматриваться в шестнадцатеричной системе как, например, 0xff — что в десятеричной системе соответствует 255 (если так кодируется красный цвет, то 255 — это означает «максимально много красного»). Каждое изображение содержит в своем коде не только сами цвета, но и метаданные в заголовке (14 байт BITMAPFILEHEADER и 40 байт BITMAPINFOHEADER). Сразу после заголовков начинается кодирование цветов в bitmap. Учитывая, что шестнадцатеричное число кодирует 4 бита, для кодирования одного цвета (например, красного) потребуется два шестнадцатеричных числа.
В итоге получается массив чисел с данными о цветах пикселей, изменяя который мы можем добиваться разнообразных эффектов на фото.
Чтобы сделать фильтр ч/б, мы возьмем три цвета одного пикселя (RGB) и посчитаем среднее, чтобы затем каждому исходному цвету присвоить новое значение. В итоге в каждом пикселе три цвета будут иметь одинаковое значение от 0 до 255, тем самым изображение перейдет из цветной картинки в черно-белую с оттенками серого.
Фильтр сепия накладывается при помощь специальной формулы:
sepiaRed = .393 * originalRed + .769 * originalGreen + .189 * originalBlue sepiaGreen = .349 * originalRed + .686 * originalGreen + .168 * originalBlue sepiaBlue = .272 * originalRed + .534 * originalGreen + .131 * originalBlue
То есть каждого исходного значения RGB высчитывается новое значение, которое при замене всех пикселей создает изображение с эффектом «старой фотографии».
Зеркальный фильтр можно создать при помощи разных подходов. Но общая идея такая: нужно перезаписать на место первого пикселя значения RBG из последнего, и таким образом поменять значения в массиве местами, чтобы фотография отразилась как в зеркале. То есть в этом фильтре сами значения остаются исходными, но меняется их порядок.
Фильтр «размытие» достигается тем, что мы берем квадрат из 8 пикселей, которые «окружают» исходный пиксель, и из этих всех 9 пикселей (включая сам исходный) высчитываем среднее, которое и нужно перезаписать на место исходного значения RBG взятого пикселя. И так продолжается по всему массиву значений. Я для себя нарисовала схему, которая помогла мне разобраться в этом задании:
Правда в итоге оказалось, что я немного перепутала x и y, то есть высоту и ширину. Из-за этой ошибки у меня долго не получалась эта программа, а также у меня возникла проблема с тем, что я инициировала int
, там где надо было инициировать float
, и в итоге мои значения округлялись немного неверно.
Нам нужно было написать только один файл helpers.c, так как остальная функция была имплементирована заранее.
Код программы для фильтров (Штукенция):
#include "helpers.h" #include <math.h> // Convert image to grayscale void grayscale(int height, int width, RGBTRIPLE image[height][width]) { for (int i = 0; i <= (height - 1); i++) { for (int j = 0; j <= (width - 1); j++) { int sum = image[i][j].rgbtBlue + image[i][j].rgbtGreen + image[i][j].rgbtRed; float average = sum / 3.0; float aver_fl = roundf(average); int aver = (aver_fl); // reassign new RGB values image[i][j].rgbtBlue = aver; image[i][j].rgbtGreen = aver; image[i][j].rgbtRed = aver; } } return; } // Convert image to sepia void sepia(int height, int width, RGBTRIPLE image[height][width]) { for (int i = 0; i <= (height - 1); i++) { for (int j = 0; j <= (width - 1); j++) { float sepia_red = 0.393 * image[i][j].rgbtRed + 0.769 * image[i][j].rgbtGreen + 0.189 * image[i][j].rgbtBlue; float sepia_Green = 0.349 * image[i][j].rgbtRed + 0.686 * image[i][j].rgbtGreen + 0.168 * image[i][j].rgbtBlue; float sepia_Blue = 0.272 * image[i][j].rgbtRed + 0.534 * image[i][j].rgbtGreen + 0.131 * image[i][j].rgbtBlue; sepia_red = roundf(sepia_red); sepia_Green = roundf(sepia_Green); sepia_Blue = roundf(sepia_Blue); // Assign 255 to each value that is greater than 255 if (sepia_red > 255) { sepia_red = 255; } if (sepia_Green > 255) { sepia_Green = 255; } if (sepia_Blue > 255) { sepia_Blue = 255; } // reassign new RGB values image[i][j].rgbtRed = sepia_red; image[i][j].rgbtGreen = sepia_Green; image[i][j].rgbtBlue = sepia_Blue; } } return; } // Reflect image horizontally void reflect(int height, int width, RGBTRIPLE image[height][width]) { for (int i = 0; i <= (height - 1); i++) { for (int j = 0; j < (roundf(width / 2)); j++) { // Make variable to store reflected value int refl_Blue = image[i][width - 1 - j].rgbtBlue; int refl_Green = image[i][width - 1 - j].rgbtGreen; int refl_Red = image[i][width - 1 - j].rgbtRed; // Make variable to store copy of initial value int temp_blue = image[i][j].rgbtBlue; int temp_Green = image[i][j].rgbtGreen; int temp_Red = image[i][j].rgbtRed; image[i][j].rgbtBlue = refl_Blue; image[i][width - 1 - j].rgbtBlue = temp_blue; image[i][j].rgbtGreen = refl_Green; image[i][width - 1 - j].rgbtGreen = temp_Green; image[i][j].rgbtRed = refl_Red; image[i][width - 1 - j].rgbtRed = temp_Red; } } return; } // Blur image void blur(int height, int width, RGBTRIPLE image[height][width]) { RGBTRIPLE copy[height][width]; for (int i = 0; i <= (height - 1); i++) { for (int j = 0; j <= (width - 1); j++) { copy[i][j] = image[i][j]; // make copy of initail array of colors } } for (int k = 0; k <= (height - 1); k++) { for (int m = 0; m <= (width - 1); m++) { float amount = 1.0; // to divide at 1.0 and not just 1 to get float // Initial sum of RGB is equal to values of point int sum_Blue = copy[k][m].rgbtBlue; int sum_Green = copy[k][m].rgbtGreen; int sum_Red = copy[k][m].rgbtRed; // Check if the point has suitable neighbors and calculate sum RGB if (0 <= (m - 1) && 0 <= (k - 1)) { sum_Blue += copy[k - 1][m - 1].rgbtBlue; sum_Green += copy[k - 1][m - 1].rgbtGreen; sum_Red += copy[k - 1][m - 1].rgbtRed; amount += 1; } if (0 <= (m - 1)) { sum_Blue += copy[k][m - 1].rgbtBlue; sum_Green += copy[k][m - 1].rgbtGreen; sum_Red += copy[k][m - 1].rgbtRed; amount += 1; } if (0 <= (m - 1) && (k + 1) <= (height - 1)) { sum_Blue += copy[k + 1][m - 1].rgbtBlue; sum_Green += copy[k + 1][m - 1].rgbtGreen; sum_Red += copy[k + 1][m - 1].rgbtRed; amount += 1; } if (0 <= (k - 1)) { sum_Blue += copy[k - 1][m].rgbtBlue; sum_Green += copy[k - 1][m].rgbtGreen; sum_Red += copy[k - 1][m].rgbtRed; amount += 1; } if ((k + 1) <= (height - 1)) { sum_Blue += copy[k + 1][m].rgbtBlue; sum_Green += copy[k + 1][m].rgbtGreen; sum_Red += copy[k + 1][m].rgbtRed; amount += 1; } if ((m + 1) <= (width - 1) && 0 <= (k - 1)) { sum_Blue += copy[k - 1][m + 1].rgbtBlue; sum_Green += copy[k - 1][m + 1].rgbtGreen; sum_Red += copy[k - 1][m + 1].rgbtRed; amount += 1; } if ((m + 1) <= (width - 1)) { sum_Blue += copy[k][m + 1].rgbtBlue; sum_Green += copy[k][m + 1].rgbtGreen; sum_Red += copy[k][m + 1].rgbtRed; amount += 1; } if ((m + 1) <= (width - 1) && (k + 1) <= (height - 1)) { sum_Blue += copy[k + 1][m + 1].rgbtBlue; sum_Green += copy[k + 1][m + 1].rgbtGreen; sum_Red += copy[k + 1][m + 1].rgbtRed; amount += 1; } // Calculate the average for RGB float averageBlue = sum_Blue / amount; // We divide here on float! int averBlue = (roundf(averageBlue)); float averageGreen = sum_Green / amount; // We divide here on float! int averGreen = (roundf(averageGreen)); float averageRed = sum_Red / amount; // We divide here on float! int averRed = (roundf(averageRed)); // reassign new RGB values image[k][m].rgbtBlue = averBlue; image[k][m].rgbtGreen = averGreen; image[k][m].rgbtRed = averRed; } } return; }
*Игроглаз у микрофона* Отлично, Штукенция! Интересно, что мы все задачи решаем по-разному. Решение Игроглаза:
#include "helpers.h" #include <math.h> // Convert image to grayscale void grayscale(int height, int width, RGBTRIPLE image[height][width]) { // 1) loop through image, row by row // 2) round up RGB and make them equal to one number for (int h = 0; h < height; h++) { for (int w = 0; w < width; w++) { image[h][w].rgbtRed = round((image[h][w].rgbtRed + image[h][w].rgbtGreen + image[h][w].rgbtBlue) / 3.0); image[h][w].rgbtGreen = image[h][w].rgbtRed; image[h][w].rgbtBlue = image[h][w].rgbtRed; } } return; } // Convert image to sepia void sepia(int height, int width, RGBTRIPLE image[height][width]) { for (int h = 0; h < height; h++) { for (int w = 0; w < width; w++) { int r, g, b; // formulas r = round(.393 * image[h][w].rgbtRed + .769 * image[h][w].rgbtGreen + .189 * image[h][w].rgbtBlue); g = round(.349 * image[h][w].rgbtRed + .686 * image[h][w].rgbtGreen + .168 * image[h][w].rgbtBlue); b = round(.272 * image[h][w].rgbtRed + .534 * image[h][w].rgbtGreen + .131 * image[h][w].rgbtBlue); image[h][w].rgbtRed = r; // in case of type overflow if (r > 255) image[h][w].rgbtRed = 255; image[h][w].rgbtGreen = g; if (g > 255) image[h][w].rgbtGreen = 255; image[h][w].rgbtBlue = b; if (b > 255) image[h][w].rgbtBlue = 255; } } return; } // Reflect image horizontally void reflect(int height, int width, RGBTRIPLE image[height][width]) { // to reflect we should use buffer to exchange opposite pixels // we will need also to find center of picture.. width / 2.. so: // 1) find center of picture and split array into two parts // 2) create buffer array // 3) take array element from 1st part ([1]) and store it in a [buffer] // 4) take array element from 2nd part ([2]) and store it to ([1]) // 5) put buffer to [2] // but wait.. there is better way. we can just swap stuff using buffer array // without dividing it in halfs // create buffer array RGBTRIPLE buffer[height][width]; for (int h = 0; h < height; h++) { for (int w = 0; w < width; w++) { // fill buffer array buffer[h][w].rgbtRed = image[h][w].rgbtRed; buffer[h][w].rgbtGreen = image[h][w].rgbtGreen; buffer[h][w].rgbtBlue = image[h][w].rgbtBlue; } } // hack.. -1 cause width/height starts from 1; while we count from 0 height -= 1; width -= 1; // put pixels from buffer array back to the picture for (int h = 0; h <= height; h++) { for (int w = 0; w <= width; w++) { // copy from buffer image[h][w].rgbtRed = buffer[h][width - w].rgbtRed; image[h][w].rgbtGreen = buffer[h][width - w].rgbtGreen; image[h][w].rgbtBlue = buffer[h][width - w].rgbtBlue; } } return; } // Blur image void blur(int height, int width, RGBTRIPLE image[height][width]) { // to blur we need to take pixel in the center and equalize to it neighboring pixels // create buffer array RGBTRIPLE buffer[height][width]; // hack.. -1 cause width/height starts from 1; while we count from 0 height -= 1; width -= 1; for (int h = 0; h <= height; h++) { for (int w = 0; w <= width; w++) { // upper left corner if (h == 0 && w == 0) { buffer[h][w].rgbtRed = round((image[h][w].rgbtRed + image[h][w + 1].rgbtRed + image[h + 1][w].rgbtRed + image[h + 1][w + 1].rgbtRed) / 4.0); buffer[h][w].rgbtGreen = round((image[h][w].rgbtGreen + image[h][w + 1].rgbtGreen + image[h + 1][w].rgbtGreen + image[h + 1][w + 1].rgbtGreen) / 4.0); buffer[h][w].rgbtBlue = round((image[h][w].rgbtBlue + image[h][w + 1].rgbtBlue + image[h + 1][w].rgbtBlue + image[h + 1][w + 1].rgbtBlue) / 4.0); } // upper right corner else if (h == 0 && w == width) { buffer[h][w].rgbtRed = round((image[h][w].rgbtRed + image[h][w - 1].rgbtRed + image[h + 1][w].rgbtRed + image[h + 1][w - 1].rgbtRed) / 4.0); buffer[h][w].rgbtGreen = round((image[h][w].rgbtGreen + image[h][w - 1].rgbtGreen + image[h + 1][w].rgbtGreen + image[h + 1][w - 1].rgbtGreen) / 4.0); buffer[h][w].rgbtBlue = round((image[h][w].rgbtBlue + image[h][w - 1].rgbtBlue + image[h + 1][w].rgbtBlue + image[h + 1][w - 1].rgbtBlue) / 4.0); } // bottom left corner else if (h == height && w == 0) { buffer[h][w].rgbtRed = round((image[h][w].rgbtRed + image[h][w + 1].rgbtRed + image[h - 1][w].rgbtRed + image[h - 1][w + 1].rgbtRed) / 4.0); buffer[h][w].rgbtGreen = round((image[h][w].rgbtGreen + image[h][w + 1].rgbtGreen + image[h - 1][w].rgbtGreen + image[h - 1][w + 1].rgbtGreen) / 4.0); buffer[h][w].rgbtBlue = round((image[h][w].rgbtBlue + image[h][w + 1].rgbtBlue + image[h - 1][w].rgbtBlue + image[h - 1][w + 1].rgbtBlue) / 4.0); } // bottom right corner else if (h == height && w == width) { buffer[h][w].rgbtRed = round((image[h][w].rgbtRed + image[h][w - 1].rgbtRed + image[h - 1][w].rgbtRed + image[h - 1][w - 1].rgbtRed) / 4.0); buffer[h][w].rgbtGreen = round((image[h][w].rgbtGreen + image[h][w - 1].rgbtGreen + image[h - 1][w].rgbtGreen + image[h - 1][w - 1].rgbtGreen) / 4.0); buffer[h][w].rgbtBlue = round((image[h][w].rgbtBlue + image[h][w - 1].rgbtBlue + image[h - 1][w].rgbtBlue + image[h - 1][w - 1].rgbtBlue) / 4.0); } // upper border else if (h == 0) { buffer[h][w].rgbtRed = round((image[h][w].rgbtRed + image[h][w - 1].rgbtRed + image[h][w + 1].rgbtRed + image[h + 1][w - 1].rgbtRed + image[h + 1][w].rgbtRed + image[h + 1][w + 1].rgbtRed) / 6.0); buffer[h][w].rgbtGreen = round((image[h][w].rgbtGreen + image[h][w - 1].rgbtGreen + image[h][w + 1].rgbtGreen + image[h + 1][w - 1].rgbtGreen + image[h + 1][w].rgbtGreen + image[h + 1][w + 1].rgbtGreen) / 6.0); buffer[h][w].rgbtBlue = round((image[h][w].rgbtBlue + image[h][w - 1].rgbtBlue + image[h][w + 1].rgbtBlue + image[h + 1][w - 1].rgbtBlue + image[h + 1][w].rgbtBlue + image[h + 1][w + 1].rgbtBlue) / 6.0); } // left side else if (w == 0) { buffer[h][w].rgbtRed = round((image[h][w].rgbtRed + image[h - 1][w].rgbtRed + image[h - 1][w + 1].rgbtRed + image[h][w + 1].rgbtRed + image[h + 1][w].rgbtRed + image[h + 1][w + 1].rgbtRed) / 6.0); buffer[h][w].rgbtGreen = round((image[h][w].rgbtGreen + image[h - 1][w].rgbtGreen + image[h - 1][w + 1].rgbtGreen + image[h][w + 1].rgbtGreen + image[h + 1][w].rgbtGreen + image[h + 1][w + 1].rgbtGreen) / 6.0); buffer[h][w].rgbtBlue = round((image[h][w].rgbtBlue + image[h - 1][w].rgbtBlue + image[h - 1][w + 1].rgbtBlue + image[h][w + 1].rgbtBlue + image[h + 1][w].rgbtBlue + image[h + 1][w + 1].rgbtBlue) / 6.0); } //right side else if (w == width) { buffer[h][w].rgbtRed = round((image[h][w].rgbtRed + image[h - 1][w].rgbtRed + image[h - 1][w - 1].rgbtRed + image[h][w - 1].rgbtRed + image[h + 1][w].rgbtRed + image[h + 1][w - 1].rgbtRed) / 6.0); buffer[h][w].rgbtGreen = round((image[h][w].rgbtGreen + image[h - 1][w].rgbtGreen + image[h - 1][w - 1].rgbtGreen + image[h][w - 1].rgbtGreen + image[h + 1][w].rgbtGreen + image[h + 1][w - 1].rgbtGreen) / 6.0); buffer[h][w].rgbtBlue = round((image[h][w].rgbtBlue + image[h - 1][w].rgbtBlue + image[h - 1][w - 1].rgbtBlue + image[h][w - 1].rgbtBlue + image[h + 1][w].rgbtBlue + image[h + 1][w - 1].rgbtBlue) / 6.0); } // bottom border else if (h == height) { buffer[h][w].rgbtRed = round((image[h][w].rgbtRed + image[h][w - 1].rgbtRed + image[h - 1][w - 1].rgbtRed + image[h - 1][w].rgbtRed + image[h][w + 1].rgbtRed + image[h - 1][w + 1].rgbtRed) / 6.0); buffer[h][w].rgbtGreen = round((image[h][w].rgbtGreen + image[h][w - 1].rgbtGreen + image[h - 1][w - 1].rgbtGreen + image[h - 1][w].rgbtGreen + image[h][w + 1].rgbtGreen + image[h - 1][w + 1].rgbtGreen) / 6.0); buffer[h][w].rgbtBlue = round((image[h][w].rgbtBlue + image[h][w - 1].rgbtBlue + image[h - 1][w - 1].rgbtBlue + image[h - 1][w].rgbtBlue + image[h][w + 1].rgbtBlue + image[h - 1][w + 1].rgbtBlue) / 6.0); } // general case else { buffer[h][w].rgbtRed = round((image[h][w].rgbtRed + image[h - 1][w - 1].rgbtRed + image[h - 1][w].rgbtRed + image[h - 1][w + 1].rgbtRed + image[h][w - 1].rgbtRed + image[h][w + 1].rgbtRed + image[h + 1][w - 1].rgbtRed + image[h + 1][w].rgbtRed + image[h + 1][w + 1].rgbtRed) / 9.0); buffer[h][w].rgbtGreen = round((image[h][w].rgbtGreen + image[h - 1][w - 1].rgbtGreen + image[h - 1][w].rgbtGreen + image[h - 1][w + 1].rgbtGreen + image[h][w - 1].rgbtGreen + image[h][w + 1].rgbtGreen + image[h + 1][w - 1].rgbtGreen + image[h + 1][w].rgbtGreen + image[h + 1][w + 1].rgbtGreen) / 9.0); buffer[h][w].rgbtBlue = round((image[h][w].rgbtBlue + image[h - 1][w - 1].rgbtBlue + image[h - 1][w].rgbtBlue + image[h - 1][w + 1].rgbtBlue + image[h][w - 1].rgbtBlue + image[h][w + 1].rgbtBlue + image[h + 1][w - 1].rgbtBlue + image[h + 1][w].rgbtBlue + image[h + 1][w + 1].rgbtBlue) / 9.0); } } } // put pixels from buffer array back to the picture for (int h = 0; h <= height; h++) { for (int w = 0; w <= width; w++) { image[h][w].rgbtRed = buffer[h][w].rgbtRed; image[h][w].rgbtGreen = buffer[h][w].rgbtGreen; image[h][w].rgbtBlue = buffer[h][w].rgbtBlue; } } return; }
Я в основном мучился при отладке с багами, которые возникли при копипасте одной строки в другую 🙂 Хотя вообще я на все капканы и грабли понаступал. Если бы не отладка в cs50 — я бы и не заметил многие косяки, т.к. внешне невооруженным глазом все картинки были ок и только при проверке через check50 стало понятно, что есть баги. Отличная задача.