I started the CS50 and am at pset4. I completed both the less and more parts for the filter app that takes an image and applies a filter to it. In this pset we need to write the filters code only(greyscale, sepia, reflect, blur and edges). The edges function should apply the Sobel operator to an image so that it detects its edges.
It works by taking each pixel and modifying it based on the 3x3 grid of pixels that surrounds that pixel. Each neighbour is multiplied by the its correspondent Gx kernel value and added to the sum. The same is done for Gy. the middle pixel is also included. In this case bmp images are used and are represented by a 2d array of values for Red Green and Blue. So we need to do the process above for all three colour channels. The new pixels get the value of the square root of Gx^2 + Gy^2 to a max of 255; Pixels past the edge should be treated as solid black (0 0 0 RGB value);
Link if not very clear explanation : https://cs50.harvard.edu/x/2020/psets/4/filter/more
After 2 days I completed the task but I am sure my way is not the best way to do it, so I'm asking for some improvements that can be implement by a beginer.
Files bmp.h and filter.c were written for us and we are not allowed to modify them in any way. Code for the 2 files below.
bmp.h
// BMP-related data types based on Microsoft's own
#include <stdint.h>
/**
* Common Data Types
*
* The data types in this section are essentially aliases for C/C++
* primitive data types.
*
* Adapted from http://msdn.microsoft.com/en-us/library/cc230309.aspx.
* See http://en.wikipedia.org/wiki/Stdint.h for more on stdint.h.
*/
typedef uint8_t BYTE;
typedef uint32_t DWORD;
typedef int32_t LONG;
typedef uint16_t WORD;
/**
* BITMAPFILEHEADER
*
* The BITMAPFILEHEADER structure contains information about the type, size,
* and layout of a file that contains a DIB [device-independent bitmap].
*
* Adapted from http://msdn.microsoft.com/en-us/library/dd183374(VS.85).aspx.
*/
typedef struct
{
WORD bfType;
DWORD bfSize;
WORD bfReserved1;
WORD bfReserved2;
DWORD bfOffBits;
} __attribute__((__packed__))
BITMAPFILEHEADER;
/**
* BITMAPINFOHEADER
*
* The BITMAPINFOHEADER structure contains information about the
* dimensions and color format of a DIB [device-independent bitmap].
*
* Adapted from http://msdn.microsoft.com/en-us/library/dd183376(VS.85).aspx.
*/
typedef struct
{
DWORD biSize;
LONG biWidth;
LONG biHeight;
WORD biPlanes;
WORD biBitCount;
DWORD biCompression;
DWORD biSizeImage;
LONG biXPelsPerMeter;
LONG biYPelsPerMeter;
DWORD biClrUsed;
DWORD biClrImportant;
} __attribute__((__packed__))
BITMAPINFOHEADER;
/**
* RGBTRIPLE
*
* This structure describes a color consisting of relative intensities of
* red, green, and blue.
*
* Adapted from http://msdn.microsoft.com/en-us/library/aa922590.aspx.
*/
typedef struct
{
BYTE rgbtBlue;
BYTE rgbtGreen;
BYTE rgbtRed;
} __attribute__((__packed__))
RGBTRIPLE;
filter.c
#include <getopt.h>
#include <stdio.h>
#include <stdlib.h>
#include "helpers.h"
int main(int argc, char *argv[])
{
// Define allowable filters
char *filters = "begr";
// Get filter flag and check validity
char filter = getopt(argc, argv, filters);
if (filter == '?')
{
fprintf(stderr, "Invalid filter.\n");
return 1;
}
// Ensure only one filter
if (getopt(argc, argv, filters) != -1)
{
fprintf(stderr, "Only one filter allowed.\n");
return 2;
}
// Ensure proper usage
if (argc != optind + 2)
{
fprintf(stderr, "Usage: filter [flag] infile outfile\n");
return 3;
}
// Remember filenames
char *infile = argv[optind];
char *outfile = argv[optind + 1];
// Open input file
FILE *inptr = fopen(infile, "r");
if (inptr == NULL)
{
fprintf(stderr, "Could not open %s.\n", infile);
return 4;
}
// Open output file
FILE *outptr = fopen(outfile, "w");
if (outptr == NULL)
{
fclose(inptr);
fprintf(stderr, "Could not create %s.\n", outfile);
return 5;
}
// Read infile's BITMAPFILEHEADER
BITMAPFILEHEADER bf;
fread(&bf, sizeof(BITMAPFILEHEADER), 1, inptr);
// Read infile's BITMAPINFOHEADER
BITMAPINFOHEADER bi;
fread(&bi, sizeof(BITMAPINFOHEADER), 1, inptr);
// Ensure infile is (likely) a 24-bit uncompressed BMP 4.0
if (bf.bfType != 0x4d42 || bf.bfOffBits != 54 || bi.biSize != 40 ||
bi.biBitCount != 24 || bi.biCompression != 0)
{
fclose(outptr);
fclose(inptr);
fprintf(stderr, "Unsupported file format.\n");
return 6;
}
int height = abs(bi.biHeight);
int width = bi.biWidth;
// Allocate memory for image
RGBTRIPLE(*image)[width] = calloc(height, width * sizeof(RGBTRIPLE));
if (image == NULL)
{
fprintf(stderr, "Not enough memory to store image.\n");
fclose(outptr);
fclose(inptr);
return 7;
}
// Determine padding for scanlines
int padding = (4 - (width * sizeof(RGBTRIPLE)) % 4) % 4;
// Iterate over infile's scanlines
for (int i = 0; i < height; i++)
{
// Read row into pixel array
fread(image[i], sizeof(RGBTRIPLE), width, inptr);
// Skip over padding
fseek(inptr, padding, SEEK_CUR);
}
// Filter image
switch (filter)
{
// Blur
case 'b':
blur(height, width, image);
break;
// Edges
case 'e':
edges(height, width, image);
break;
// Grayscale
case 'g':
grayscale(height, width, image);
break;
// Reflect
case 'r':
reflect(height, width, image);
break;
}
// Write outfile's BITMAPFILEHEADER
fwrite(&bf, sizeof(BITMAPFILEHEADER), 1, outptr);
// Write outfile's BITMAPINFOHEADER
fwrite(&bi, sizeof(BITMAPINFOHEADER), 1, outptr);
// Write new pixels to outfile
for (int i = 0; i < height; i++)
{
// Write row to outfile
fwrite(image[i], sizeof(RGBTRIPLE), width, outptr);
// Write padding at end of row
for (int k = 0; k < padding; k++)
{
fputc(0x00, outptr);
}
}
// Free memory for image
free(image);
// Close infile
fclose(inptr);
// Close outfile
fclose(outptr);
return 0;
}
I created the functions for the filters in the file bellow
helpers.c
#include "helpers.h"
#include <math.h>
void grayscale(int height, int width, RGBTRIPLE image[height][width])
{
//declare a variable average to store the average colour
float average;
//loop through image rows
for (int i = 0; i < height; i++)
{
//loop through image pixels
for (int j = 0; j < width; j++)
{
//calculate average value for each pixel
average = 1.0 * (image[i][j].rgbtBlue + image[i][j].rgbtGreen + image[i][j].rgbtRed) / 3;
//assign the average value to all values of each pixel
image[i][j].rgbtBlue = round(average);
image[i][j].rgbtGreen = round(average);
image[i][j].rgbtRed = round(average);
}
}
}
// Reflect image horizontally
void reflect(int height, int width, RGBTRIPLE image[height][width])
{
//loop through image rows
for (int i = 0; i < height; i++)
{
//loop through image pixels
for (int j = 0; j < width / 2; j++)
{
int swapBlue = image[i][j].rgbtBlue;
int swapGreen = image[i][j].rgbtGreen;
int swapRed = image[i][j].rgbtRed;
image[i][j].rgbtBlue = image[i][width - j - 1].rgbtBlue;
image[i][j].rgbtGreen = image[i][width - j - 1].rgbtGreen;
image[i][j].rgbtRed = image[i][width - j - 1].rgbtRed;
image[i][width - j - 1].rgbtBlue = swapBlue;
image[i][width - j - 1].rgbtGreen = swapGreen;
image[i][width - j - 1].rgbtRed = swapRed;
}
}
}
// Blur image
void blur(int height, int width, RGBTRIPLE image[height][width])
{
//initialise a variable average for each colour, a new array to copy to and a counter
float averageGreen;
float averageRed;
float averageBlue;
RGBTRIPLE image2[height][width];
int count;
//loop through array
for (int i = 0; i < height; i++)
{
for (int j = 0; j < width; j++)
{
//set counter and averages to 0
count = 0;
averageBlue = 0;
averageGreen = 0;
averageRed = 0;
//loop to get pixels above and below height
for (int r = i - 1; r <= i + 1 && r < height; r++)
{
//if index out of bounds make it 0
if (r < 0)
{
continue;
}
//loop to get pixels to the left and right of width
for (int c = j - 1; c <= j + 1 && c < width; c++)
{
//if index out of bounds make it 0
if (c < 0)
{
continue;
}
//add values to the average and increment count
averageBlue += image[r][c].rgbtBlue;
averageGreen += image[r][c].rgbtGreen;
averageRed += image[r][c].rgbtRed;
count++;
}
}
//udpdate copy array with average values divided by counter
image2[i][j].rgbtBlue = round(averageBlue / count);
image2[i][j].rgbtGreen = round(averageGreen / count);
image2[i][j].rgbtRed = round(averageRed / count);
}
}
for (int i = 0; i < height; i++)
{
for (int j = 0; j < width; j++)
{
image[i][j] = image2[i][j];
}
}
}
// Detect edges
void edges(int height, int width, RGBTRIPLE image[height][width])
{
//delcare variables for Gx and Gy to calculate values for each colour
int GxBlue;
int GxGreen;
int GxRed;
int GyBlue;
int GyGreen;
int GyRed;
//initialise the Gx and Gy kernels
int Gx[3][3] =
{
{-1, 0, 1},
{-2, 0, 2},
{-1, 0, 1}
};
int Gy[3][3] =
{
{-1, -2, -1},
{0, 0, 0},
{1, 2, 1}
};
//delcare new array of bigger size to pad original array with 0's
RGBTRIPLE image2[height + 2][width + 2];
//delcare new array to store new values
RGBTRIPLE image3[height][width];
//loop big aray to add 0 padding and original array
for (int i = 0 ; i < height + 2; i ++)
{
for (int j = 0; j < width + 2; j++)
{
//if on edges add 0
if (i == 0 || j == 0 || i == height + 1 || j == width + 1)
{
image2[i][j].rgbtBlue = 0;
image2[i][j].rgbtGreen = 0;
image2[i][j].rgbtRed = 0;
}
//else add original array values
if (i > 0 && i < height + 1 && j > 0 && j < width + 1)
{
image2[i][j].rgbtBlue = image[i - 1][j - 1].rgbtBlue;
image2[i][j].rgbtGreen = image[i - 1][j - 1].rgbtGreen;
image2[i][j].rgbtRed = image[i - 1][j - 1].rgbtRed;
}
}
}
//loop inner array
for (int i = 1; i < height + 1; i++)
{
for (int j = 1; j < width + 1; j++)
{
//initialise variables to 0 every time we switch pixel
GxBlue = 0;
GxGreen = 0;
GxRed = 0;
GyBlue = 0;
GyGreen = 0;
GyRed = 0;
//loop all neighbours of inner array
for (int row = i - 1; row <= i + 1; row++)
{
for (int col = j - 1; col <= j + 1; col++)
{
//add values of the neighbours multiplied to the corresponded Gx and Gy values to the sum
GxBlue += image2[row][col].rgbtBlue * Gx[row - i + 1][col - j + 1];
GxGreen += image2[row][col].rgbtGreen * Gx[row - i + 1][col - j + 1];
GxRed += image2[row][col].rgbtRed * Gx[row - i + 1][col - j + 1];
GyBlue += image2[row][col].rgbtBlue * Gy[row - i + 1][col - j + 1];
GyGreen += image2[row][col].rgbtGreen * Gy[row - i + 1][col - j + 1];
GyRed += image2[row][col].rgbtRed * Gy[row - i + 1][col - j + 1];
}
}
//make sure each value does not exceed 255
//calculte the square root of the squared sums
if (sqrt(GxBlue * GxBlue + GyBlue * GyBlue) > 255)
{
image3[i - 1][j - 1].rgbtBlue = 255;
}
else
{
image3[i - 1][j - 1].rgbtBlue = round(sqrt(GxBlue * GxBlue + GyBlue * GyBlue));
}
if (sqrt(GxGreen * GxGreen + GyGreen * GyGreen) > 255)
{
image3[i - 1][j - 1].rgbtGreen = 255;
}
else
{
image3[i - 1][j - 1].rgbtGreen = round(sqrt(GxGreen * GxGreen + GyGreen * GyGreen));
}
if (sqrt(GxRed * GxRed + GyRed * GyRed) > 255)
{
image3[i - 1][j - 1].rgbtRed = 255;
}
else
{
image3[i - 1][j - 1].rgbtRed = round(sqrt(GxRed * GxRed + GyRed * GyRed));
}
}
}
//copy values in original array
for (int i = 0; i < height; i++)
{
for (int j = 0; j < width; j++)
{
image[i][j].rgbtBlue = image3[i][j].rgbtBlue;
image[i][j].rgbtGreen = image3[i][j].rgbtGreen;
image[i][j].rgbtRed = image3[i][j].rgbtRed;
}
}
}
If you want to see the effects of the edge filter:
So I have 2 questions:
Is there any way of writing the edge function code cleaner and more efficient for a beginer
In the blur function it took me waaaaaay to long to make all the for loops work and I almost gave up. Is there a good way to find write this type of loops easier if I clearly know what to do(ie: I know I need to access the i - 1 to i + 1 element but how do I write this without going out of bounds). I could probably rewrite the function using the 0 padding as well, like for the edge one but this seems cleaner.
3.For the edge function I admit the padding with 0 on edges on a bigger array idea isn't mine and I found it on stackoverflow, but it wasn't implemented there, so I just used the idea. Is it a bad practice or something that is usually done?
Thanks you and sorry if unclear.
PS: my first post here :)