In the 4th week of the CS50 (Harvard) course, we studied the principles of working with memory and data arrays. We were faced with the task of writing a program that will apply filters to the original image (filter-less). You need to make 4 filters: black and white, sepia, specular and blur. The original picture is a 24-bit .BMP file, which means that each pixel of this picture contains 24-bit information that encodes an RGB color (8 bits are allocated for each individual color – red, green, blue).
The color encoding can be considered in hexadecimal as, for example, 0xff – which in decimal corresponds to 255 (if red is encoded this way, then 255 means “the most red”). Each image contains in its code not only the colors themselves, but also metadata in the header (14 bytes BITMAPFILEHEADER and 40 bytes BITMAPINFOHEADER). Immediately after the headers, color encoding in the bitmap begins. Given that a hexadecimal number encodes 4 bits, it would take two hexadecimal numbers to encode one color (eg red).
The result is an array of numbers with data on the colors of the pixels, by changing which we can achieve various effects on the photo.
To make a b/w filter, we take three colors of one pixel (RGB) and calculate the average, so that each source color can then be assigned a new value. As a result, in each pixel, three colors will have the same value from 0 to 255, thereby the image will go from a color image to a black and white image with shades of gray.
The sepia filter is applied using a special formula:
sepiaRed = .393 * originalRed + .769 * originalGreen + .189 * originalBlue sepiaGreen = .349 * originalRed + .686 * originalGreen + .168 * originalBlue sepiaBlue = .272 * originalRed + .534 * originalGreen + .131 * originalBlue
That is, for each original RGB value, a new value is calculated, which, when replacing all the pixels, creates an image with the effect of an “old photo”.
A mirror filter can be created using different approaches. But the general idea is this: you need to overwrite the RBG values from the last pixel in place of the first pixel, and thus swap the values in the array so that the photo is reflected as in a mirror. That is, in this filter, the values themselves remain original, but their order changes.
The “blur” filter is achieved by taking a square of 8 pixels that “surround” the source pixel, and from these 9 pixels (including the source itself) we calculate the average, which needs to be overwritten in place of the original RBG value of the taken pixel. And so it continues throughout the array of values. I drew a diagram for myself that helped me figure out this task:
True, in the end it turned out that I mixed up x and y a little, that is, height and width. Because of this error, I didn’t get this program for a long time, and I also had the problem that I initiated int where it should have been float, and as a result, my values \u200b\u200bare rounded a little wrong.
We only needed to write one helpers.c file since the rest of the function was already implemented.
Program code for filters (Shtukensia):
#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; }
*Igroglaz at the microphone* Great, Shtukens! Interestingly, we solve all problems in different ways. Igroglaz’s solution:
#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; }
I mostly suffered when debugging with bugs that arose when copy-pasting one line to another 🙂 Although in general I stepped on all the traps and rakes. If it were not for debugging in cs50, I would not have noticed many jambs, because Outwardly, with the naked eye, all the pictures were ok, and only when checking through check50 it became clear that there were bugs. Great task.