На 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 стало понятно, что есть баги. Отличная задача.

