Lab 3 An Image Class

Aims

The aim of this lab is to continue our exploration of TDD in C++ by developing an Image Class

  1. Using GitHub
  2. Developing a Simple Image Class
  3. Writing Images with OpenImageIO
  4. Some simple Image Algorithms.

Getting Started

We are going to use GitHub for this work and I have sent you a link to create the repository in advance.

In my case the repo is https://github.com/NCCA/labcode-jmacey so I am going to clone it to my working directory.

git clone https://github.com/NCCA/labcode-jmacey

We can now change into the directory and create an empty README.md file using either touch or New-Item.

# Jon Macey's Lab Repository

This repo will contain the labs we do in ASE

We then use the following git commands.


git add README.md
git commit -am "added readme file"
git push -u origin master

This should now upload the new README.md file to GitHub in your new repo. We will use this repo for all our lab session to make it easier for code review by staff. To make this easier we are now going to add a .gitignore file to ensure certain files and folders are not uploaded by mistake.

This link has some good starter .gitignore files for various languages we will modify the C++ and CMake ones.

Default .gitignore [click to expand]
# Prerequisites
*.d

# Compiled Object files
*.slo
*.lo
*.o
*.obj

# Precompiled Headers
*.gch
*.pch

# Compiled Dynamic libraries
*.so
*.dylib
*.dll

# Compiled Static libraries
*.lai
*.la
*.a
*.lib

# Executables
*.exe
*.out
*.app
# CMake
CMakeLists.txt.user
CMakeCache.txt
CMakeFiles
CMakeScripts
Testing
Makefile
cmake_install.cmake
install_manifest.txt
compile_commands.json
CTestTestfile.cmake
_deps
build/

First we will create a .gitignore at the root of our repository the copy the data in.

Starting a New Project

We are going to start a new project but this time it will be in the new repository we created so we can upload to GitHub later.

mkdir Image
cd Image
mkdir src,include,tests
New-Item CMakeLists.txt
New-Item src/main.cpp
touch tests/ImageTests.cpp
mkdir Image
cd Image
mkdir src include tests
touch CMakeLists.txt
touch src/main.cpp
touch tests/ImageTests.cpp

We will now open the CMakeLists.txt file using QtCreator and add in the basic CMakeLists.txt file we used for the project in the last lab.

# We will always try to use a version > 3.1 if avaliable
cmake_minimum_required(VERSION 3.2)

if(NOT DEFINED CMAKE_TOOLCHAIN_FILE AND DEFINED ENV{CMAKE_TOOLCHAIN_FILE})
   set(CMAKE_TOOLCHAIN_FILE $ENV{CMAKE_TOOLCHAIN_FILE})
endif()

# name of the project It is best to use something different from the exe name
project(Image_build)
# Here we set the C++ standard to use
set(CMAKE_CXX_STANDARD 17)
# add include paths
include_directories(include)
# Now we add our target executable and the file it is built from.
add_executable(Image)
target_sources(Image PRIVATE src/main.cpp src/Image.cpp include/Image.h)

#################################################################################
# Testing code
#################################################################################

find_package(GTest CONFIG REQUIRED)
include(GoogleTest)
enable_testing()
add_executable(ImageTests)
target_sources(ImageTests PRIVATE tests/ImageTests.cpp src/Image.cpp include/Image.h)
target_link_libraries(ImageTests PRIVATE GTest::gtest GTest::gtest_main )
gtest_discover_tests(ImageTests)

An Image Class

We are going to develop an image class using the RGBA struct we developed in the last lab.

classDiagram

class Image {
 -m_width : size_t
 -m_height : size_t
 -m_pixels : std::unique_ptr<RGBA []>
 +Image()
 +Image(_w : size_t, _h : size_t)
 +Image(_w : size_t, _h : size_t, _r : unsigned char,_g : unsigned char,_b : unsigned char,_a : unsigned char)
 +width()  size_t
 +height()  size_t
 +getPixel(_x : size_t, _y : size_t)  RGBA
 +setPixel(_x : size_t, _y : size_t, _r : unsigned char,_g : unsigned char,_b : unsigned char,_a : unsigned char)
 +setPixel(_x : size_t, _y : size_t, _r : p : RGBA)
 +clear(_r : unsigned char, unsigned char,_g : unsigned char,_b : unsigned char,_a : unsigned char)
 +save(_fname : const std::string &)  bool
}

class RGBA {
  <<struct>>
    + pixels : uint32_t # note this is a union / struct
    + r : unsigned char
    + g : unsigned char
    + b : unsigned char
    + a : unsigned char
  +RGBA()
  +RGBA(_r : unsigned char, _g : unsigned char _b : unsigned char, _a : unsigned char)
}


Image  o-- RGBA

We will use a TDD approach and write the following tests in sequence.

// Test #1
TEST(IMAGE,construct)
// Test #2
TEST(IMAGE,constructUser)
// Test #3
TEST(IMAGE,getPixelDefault)
// Test #4
TEST(Image,constructWithColour)
//# TEST 5
TEST(Image,clear)
// # Test 6
TEST(IMAGE,setPixel)
// # Test 7
TEST(IMAGE,setPixelRGBA)
// # test 8
TEST(IMAGE,save)

OpenImageIO

To save our image to file we will use the OpenImageIO library which has been installed in the labs using vcpkg.

To Use OpenImageIO we need to install it using vcpkg. The basic install takes some time and we also need to install the ecm package for cmake.

./vcpkg install openimageio
./vcpkg install ecm 

We need to add the following to the CMakeLists.txt file to use OpenImageIO, first we need to find the correct packages. This can be added to the top part of the CMakeLists file after the project definition.

find_package(OpenImageIO CONFIG REQUIRED)
find_package(IlmBase CONFIG REQUIRED)
find_package(OpenEXR CONFIG REQUIRED)

Next we need to add the libraries to each of our targets.

# for Image
target_link_libraries(Image PRIVATE OpenImageIO::OpenImageIO OpenImageIO::OpenImageIO_Util)
target_link_libraries(Image PRIVATE  IlmBase::Iex IlmBase::Half IlmBase::Imath IlmBase::IexMath)
target_link_libraries(Image PRIVATE OpenEXR::IlmImf OpenEXR::IlmImfUtil OpenEXR::IlmImfConfig)

## for ImageTests
target_link_libraries(ImageTests PRIVATE OpenImageIO::OpenImageIO OpenImageIO::OpenImageIO_Util)
target_link_libraries(ImageTests PRIVATE  IlmBase::Iex IlmBase::Half IlmBase::Imath IlmBase::IexMath)
target_link_libraries(ImageTests PRIVATE OpenEXR::IlmImf OpenEXR::IlmImfUtil OpenEXR::IlmImfConfig)

Now we can add the following code to save our image.

bool Image::save(std::string_view _fname) const
{
  bool success=true;

  using namespace OIIO;
  std::unique_ptr<ImageOutput> out = ImageOutput::create (_fname.data());
  if(!out)
  {
    return false;
  }
  ImageSpec spec (m_width,m_height,4, TypeDesc::UINT8);
  success=out->open(_fname.data(),spec);
  success=out->write_image(TypeDesc::UINT8,m_pixels.get());
  success=out->close();
  return success;
}

Exercises

Using the Image class created above add a new method called

void line(int _sx, int _sy, int _ex, int _ey, unsigned char _r, unsigned char _g, unsigned char _b, unsigned char _a  ) ;

Which uses the Bresenham line drawing algorithm outlined here to draw to the Image buffer. Note the following C++ functions may be of use std::swap and std::abs

A sample unit test for this method would be as follows, however this only tests a horizontal line. What other tests could be easily written?

TEST(IMAGE,line)
{
    Image a(200,200);

    a.line(0,100,200,100,255,0,0,255);
    RGBA p;
    for(size_t x=0; x<a.width(); ++x)
    {
        p=a.getPixel(x,100);
        ASSERT_EQ(p.r,255);
        ASSERT_EQ(p.g,0);
        ASSERT_EQ(p.b,0);
        ASSERT_EQ(p.a,255);
        
    }
    ASSERT_TRUE(a.save("line1.png"));

}

Also we can write a method called rectangle which takes in two _x,_y values for Top Left and Bottom Right and fills the area with a colour.

void rectangle(int _tx, int _ty, int _bx, int _by, unsigned char _r, unsigned char _g, unsigned char _b, unsigned char _a  ) ;

With both of these methods we should think about what happens for values out of the image range.

Previous
Next