Lab 4 (Having fun with images)

Programming Workshop 2 (CSCI 1061U)

Winter 2021

Faculty of Science

Ontario Tech University


Introduction

The goal of this lab is to load, manipulate, and save jpg image files. Instead of writing our own jpeg routines, we will use the single-file public domain libraries for C/C++ that provide basic image read/write functionality. These libraries are available at https://github.com/nothings/stb. You can download these from Github as follows

$ git clone https://github.com/nothings/stb.git

or you can also get it from here.

The following file showcases how you will be able to use these files to read/write a jpeg file.

#include <iostream>
#include <fstream>

#define STB_IMAGE_IMPLEMENTATION
#define STB_IMAGE_WRITE_IMPLEMENTATION

#include "stb/stb_image.h"
#include "stb/stb_image_resize.h"
#include "stb/stb_image_write.h"

using namespace std;

int main()
{
    int width, height, channels;
    unsigned char* img = stbi_load("apple.jpg", &width, &height, &channels, 0);
    if (img == 0) {
        cout << "Error loading image file" << endl;
        return -1;
    }

    cout << "Loading image\n";
    cout << "\twidth = " << width << "\n";
    cout << "\theight = " << height << "\n";
    cout << "\tchannels = " << channels << "\n";

    stbi_write_jpg("apple-copy.jpg", width, height, channels, img, 100);    

    stbi_image_free(img);
    return 0;
}

We can compile this file using

$ g++ main.cpp -o foo

It assumes that STB header files are available in the stb folder as seen below.

$ tree -L 1 .
.
├── apple.jpg
├── bambi.jpg
├── main.cpp
└── stb
    ├── LICENSE
    ├── README.md
    ├── data
    ├── deprecated
    ├── docs
    ├── stb.h
    ├── stb_c_lexer.h
    ├── stb_connected_components.h
    ├── stb_divide.h
    ├── stb_ds.h
    ├── stb_dxt.h
    ├── stb_easy_font.h
    ├── stb_herringbone_wang_tile.h
    ├── stb_image.h
    ├── stb_image_resize.h
    ├── stb_image_write.h
    ├── stb_include.h
    ├── stb_leakcheck.h
    ├── stb_perlin.h
    ├── stb_rect_pack.h
    ├── stb_sprintf.h
    ├── stb_textedit.h
    ├── stb_tilemap_editor.h
    ├── stb_truetype.h
    ├── stb_vorbis.c
    ├── stb_voxel_render.h
    ├── stretchy_buffer.h
    ├── tests
    └── tools

Loading jpeg files

The following lines

int width, height, channels;
unsigned char* img = stbi_load("apple.jpg", &width, &height, &channels, 0 /* desired number of channels = 0 */);
if (img == 0) {
    cout << "Error loading image file" << endl;
    return -1;
}

load image data int an unsigned char* buffer. The function also returns the width, height, and the number of channels of an image. Color images are typically stored as RedGreenBlue, so these have 3 channels. The total size of data is width*height*channels. The function returns a 0 if it is unable to read in the file.

Writing jpeg files

The following lines

stbi_write_jpg("apple-copy.jpg", width, height, channels, img, 100 /* quality = 100 */);

Save img data to a jpeg file. It requires the width, height, number of channels, plus the buffer containing image data.

Image buffer

Function stbi_load returns a pointer to an unsigned char* buffer. The size of this buffer is width * height * channels. The image data is stored in the buffer in row order, i.e., the first width * channels bytes belong to the first row of image. The following code sets the first 10 rows of the input image to black.

// Lets set the first 10 rows to black
for (unsigned char* p = img; p != img + 10*width*channels; p += channels) {
    *p = (uint8_t) 0;
    *(p+1) = (uint8_t) 0;
    *(p+2) = (uint8_t) 0;
}

Image layout is illustrated below.

Tasks

Goal 1 [50%]

Complete the following code that creates a noisy image. Specifically, it sets 20% of image pixels to black. The pixels locations are randomly generated.

#include <iostream>
#include <fstream>

#define STB_IMAGE_IMPLEMENTATION
#define STB_IMAGE_WRITE_IMPLEMENTATION

#include "stb/stb_image.h"
#include "stb/stb_image_resize.h"
#include "stb/stb_image_write.h"

using namespace std;

void create_a_random_pixel_location(int width, int height, int& r, int &c)
{
    // TO DO
}

//
// Image structure
//
struct MyImg {
    unsigned char* data;
    int width;
    int channels;
    int height;
    std::string filename;
};

void print_img_info(MyImg* img)
{
  cout << "width = " << img->width << "\n"
       << "height = " << img->height << "\n"
       << "channels = " << img->channels << endl;
}

void delete_img(MyImg** img)
{
    // Deletes img and sets it to NULL
    //
    // TO DO
}

//
// Image read/write
//
MyImg* load_jpeg_file(const string& filename)
{
    // Returns a MyImg pointer containing image data.
    // If the file loading is unsuccessful, it returns a 0 
    // 
    // TO DO
    return 0;
}

void save_to_jpeg_file(const string& filename, MyImg* img)
{
    // Saves to a jpeg file
    //
    // TO DO
}

//
// Setters
//
void set_pixel_red(MyImg* img, int r, int c, uint8_t val)
{
    // TO DO
}

void set_pixel_green(MyImg* img, int r, int c, uint8_t val)
{
    // TO DO
}

void set_pixel_blue(MyImg* img, int r, int c, uint8_t val)
{
    // TO DO
}

//
// Getters
//
uint8_t get_pixel_red(MyImg* img, int r, int c)
{
    // TO DO
}

uint8_t get_pixel_green(MyImg* img, int r, int c)
{
    // TO DO
}

uint8_t get_pixel_blue(MyImg* img, int r, int c)
{
    // TO DO
}

//
// The main function
//
int main()
{
    MyImg* img = load_jpeg_file("apple.jpg");

    int total_pixels = img->width * img->height;
    
    // Set 20% of the total_pixels to black
    
    // Save results to apple-noisy.jpg
     
    // Memory cleanup
 
    return 0;
}

The following images show the input image and the noisy image.

Goal 2 [35%]

Write a function that picks a subregion from the image. The function will have the following signature.

MyImg* get_subregion(MyImg* src, int top, int left, int bottom, int right)

When we use this function as follows

MyImg* img2 = get_subregion(img, 50, 5, 250, 250);
save_to_jpeg_file("apple-resized.jpg", img2);

we get the following

You can allocate a new image of width w and height h using malloc as seen below

unsigned char* data = malloc(w*h*3);

You can still use stbi_image_free(data) to release the allocated image data.

Goal 3 [15%]

Write a function that flips an image in place (i.e., the image passed to it is flipped).

void flip(MyImg* img, int dir);

If dir is 1, the image is flipped horizontally (left below), and if it is 0, the image is flipped vertically (right below).

main2.cpp file, for example, flips the image vertically

...
MyImg* img = load_jpeg_file("apple.jpg");
flip(img, 0);
save_to_jpeg_file("apple-up-down.jpg", img);
...

Similarly, main3.cpp file will flip the image horizontally

...
MyImg* img = load_jpeg_file("apple.jpg");
flip(img, 1);
save_to_jpeg_file("apple-left-right.jpg", img);
...

Hint

Think how you would reverse a series of numbers? The following figure illustrates how one might reverse (flip) a series of numbers.

Recall that flipping an image is simply reversing rows (vertical/up-down flip) or reversing columns (horizontal/left-right flip).

You can reuse the get and set pixel methods that you have already developed to complete this task.

Submission

Please submit main1.cpp, main2.cpp, and main3.cpp via Canvas.