/halftone-Basic-image-manipulation-in-OpenCV-under-C--

Laboratory Projects forThis is actually from Laboratory Projects for Digital Image Processing 3/e

Primary LanguageC++

Basic Image Manipulation - Convert Images to Halftone

Tim Feirg, SYSU, 11311030

March, 11, 2014

This is a report for my homework assignment in digital image processing course, also the first attempt to write something using OpenCV in C++ for myself - a program that converts any image to 2 color halftone, each pixel of the source image will be represented as different 3x3 dot patterns according to its intensity:

Dot Pattern

Quote from the project manual:

Each gray level is represented by a 3 x 3 pattern of black and white dots. A 3 x 3 area full of black dots is the approximation to gray-level black, or 0. Similarly, a 3 x 3 area of white dots represents gray level 9, or white. The other dot patterns are approximations to gray levels in between these two extremes. A gray-level printing scheme based on dots patterns such as these is called "halftoning."

Goal

  • Learning basic image manipulation in OpenCV
  • Understanding halftoning printing
  • Reduce image gray level (from 2-255)
  • Four arithmetic operations between two images
  • Further improve C++ skill

The knowledge behind

I'm very new to both C++ and OpenCV so there're certainly many things that I wanted to share, considering the nature of this report I'll just cover the my most surprising findings during the work. You'll have to learn the OpenCV basics to understand what I'm talking about.

Loading the source image

OpenCV provides cv::imread() for image reading, since I'm trying to convert image to black & white, it's necessary to load image in grayscale, study the code to see how exactly to do this.

Creating custom Mat object

One of the challenge that I've encountered is to create black and white dot patterns that's going to comprise the ultimate target picture. OpenCV allows direct pixel manipulation via Mat::at method, but it'll be fairly troublesome to set values for each object. So I choose to create these dot pattern Mat objects via array, for example:

uchar halftone_array[3][3] = 
{
    /* this is the array based on which the 4th gray level dot 
    pattern in halftoning will be generated */
    {255, 255, 0},
    {0,   0,   0},
    {0,   0,   255}
}

// creating Mat object via array
halftone_matrix[3] = Mat(3, 3, CV_8UC1, halftone_array);

Printing the image in halftoning

I called Mat::at and Mat::copyTo methods in a loop to do the job, notice that in grayscale, the intensity of each pixel determine which 3x3 dot pattern will be "copied" to the target image. So I extract the pixel intensity as uchar and divided by 28.3, the result represents the gray level in halftoning, of that particular pixel.

Flow chart

Arithmetic operations between images

With the help of all the methods under Mat object, basic operations like image addition, multiplication and so on, can mostly be done in a line. Study the arith() function within my code for detail.

The result

Printing image in halftone

./exp_1 --halftone [image directory]

I have my source image like so:

Skull

This image is used as example throughout the project.

Apparently the output image will be 9 itmes as large as the source image (see figure . I didn't scale it down in my report so it'd be easier to observe.

Halftone image

You can save the image and zoom to see the actual dot pattern.

Reduce image gray level

./exp_1 --reduce [image directory]

You'll then have to specify gray level before runing the program, say, 3, the target image will contain only 3 gray level, so after reduced to 3 colors the result turned out to be like this:

Reduce gray level: 3 colors

Arithmetic operations

./exp_1 --arithmetic [image 1] [image 2]

You'll then have to specify what kind of operation you want on these two images, one of them is the x-ray skull image that I used in halftoning, another is a rose image in figure which is 1024x1024.

image 2 used in arithmetic operations

The arithmetic part has been coded to perform as generic as possible, so the smaller image is going to be resized to fit the larger one. In my example the skull image is resized to 1024x1024.

Image addition is done through cv::add() function, result is converted to CV_16UC1.

Addition

The result image sees a brighter area in rose shape, which is quite reasonable.

Multiplication is also done through a one-liner using cv::multiply(), the result is stored in CV_16UC1 format in order to eliminate the effect of overflow.

Multiplication

There're lots of 0 value pixel in the rose image, so the output was very dark generally, nothing wrong with the result.

The code by default divide the first image specified by the second, using cv::divide(). See the official documents for detail.

Division

Division cause image to become darker.

To maintain consistency the result of subtraction is also in 16 bit, even though subtraction between 2 images doesn't produce pixel intensity larger than 255. The OpenCV function is cv::subtact().

Subtraction

If you'd like to do some more, build the executable file and try out for yourself.

C++ program

If you were to read the source code please check out main.cpp in the repository, I post the code beneath because it's a homework report for my laboratory project.

The actual C++ code is quite big, mostly declaring a huge array.

#include <opencv2/opencv.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <stdio.h>

using namespace cv;
using namespace std;

int help(void)
{
    cout<<"Usage: "<<endl
    <<"./exp_1 [mode] [image directory]"<<endl;
    return 0;
}

int main(int argc, char *argv[])
{
    
    Mat source_image = imread( argv[argc - 1], 0 );
    Mat target_image;
    // function declaration
    Mat halftoning(Mat source_image);
    Mat reduceIntensity(Mat source_image, int grayLevel);
    Mat arith(Mat img1, Mat img2);
    
    if (strcmp(argv[1], "--halftone") == 0) {
        target_image = halftoning(source_image);
    }
    else if (strcmp(argv[1], "--reduce") == 0) {
        int grayLevel;
        cout<<"Please specify the gray level you want"<<endl;
        cin>>grayLevel;
        target_image = reduceIntensity(source_image, grayLevel);
    }
    else if (strcmp(argv[1], "--arithmetic") == 0) {
        Mat img1 = imread(argv[2],0);
        target_image = arith(img1, source_image);
    }
    else return help();
    
    imshow("target image", target_image);
    waitKey();
    return 0;
}

Mat halftoning(Mat source_image)
{
    Mat target_image(source_image.rows * 3, source_image.cols * 3, CV_8UC1);
    // arrays used to generate halftone pattern
    uchar halftone_source[10][3][3] =
    {   //0
        {
            {0,0,0},
            {0,0,0},
            {0,0,0}
        },
        //1
        {
            {0,255,0},
            {0,0,0},
            {0,0,0}
        },
        //2
        {
            {0,255,0},
            {0,0,0},
            {0,0,255}
        },
        //3
        {
            {255,255,0},
            {0,0,0},
            {0,0,255}
        },
        //4
        {
            {255,255,0},
            {0,0,0},
            {255,0,255}
        },
        //5
        {
            {255,255,255},
            {0,0,0},
            {255,0,255}
        },
        //6
        {
            {255,255,255},
            {0,0,255},
            {255,0,255}
        },
        //7
        {
            {255,255,255},
            {0,0,255},
            {255,255,255}
        },
        //8
        {
            {255,255,255},
            {255,0,255},
            {255,255,255}
        },
        //9
        {
            {255,255,255},
            {255,255,255},
            {255,255,255}
        },
        
    };
    
    
    // generating halftone dots pattern
    Mat halftone_matrix[10];
    for (int i = 0; i<=9; i++) {
        halftone_matrix[i] = Mat(3, 3, CV_8UC1, halftone_source[i]);
    }
    
    // speed up the process by moving function call outside the loop
    uint source_rows = source_image.rows,
    source_cols = source_image.cols;
    // creating halftoning image
    for ( uint i = 0; i < source_rows - 1; i++) {
        for ( uint j = 0; j < source_cols - 1; j++) {
            
            int pixel_intensity = source_image.at<uchar>(i, j) / 28.3;
            halftone_matrix[pixel_intensity].copyTo(
                target_image(Rect_<uint>(j*3, i*3, 3, 3)));
            
        }
    }
    return target_image;
}

Mat reduceIntensity(Mat source_image, int grayLevel)
{
    uchar divider = ceil( 255 / (double)grayLevel );
    
    // calculate and redistribute indensity for each pixel
    for ( uint i = 0; i < source_image.rows; i++ ) {
        for ( uint j = 0; j < source_image.cols; j++) {
            
            source_image.at<uchar>(i, j) = (
                source_image.at<uchar>(i, j) / divider) * (255 / (grayLevel - 1 ));
            
        }
    }
    return source_image;
}

Mat arith(Mat img1, Mat img2) {
    
    // convert to 16 bit to prevent overflow
    img1.convertTo(img1, CV_16UC1);
    img2.convertTo(img2, CV_16UC1);
    int user_choice;
    
    // resize the smaller image to fit the larger one
    if ( img1.rows * img1.cols >= img2.rows * img2.cols ) {
        resize(img2, img2, img1.size());
    }
    else {
        resize(img1, img1, img2.size());
    }
    
    cout<<"What do you want: "<<endl
    <<"1. add two images"<<endl
    <<"2, multiply"<<endl
    <<"3. divide"<<endl
    <<"4. minus"<<endl;
    
    cin>>user_choice;
    
    switch (user_choice) {
        case 1:
            add(img1, img2, img1);
            img1 *= 257;
            break;
        case 2:
            multiply(img1, img2, img1);
            break;
        case 3:
            divide(img1, img2, img1);
            img1 *= 256;
            break;
        case 4:
            subtract(img1, img2, img1);
            img1 *= 257;
            break;
        default:
            cout<<"bad input";
            break;
    }
    return img1;
}