Язык Си: наложить фильтр на изображение


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


Запись опубликована в рубрике С (Си). Добавьте в закладки постоянную ссылку.

Добавить комментарий

🇬🇧 Attention! Comments with URLs/email are not allowed.
🇷🇺 Комментарии со ссылками/email удаляются автоматически.