Matt Young
Matt Young

Reputation: 73

C++ Image filtering algorithm

I'm busy trying to implement an image filtering algorithm that works as follows: A filter is a 2-dimensional array of size N (N has to be an odd number), therefore having N*N elements. An example filter of size 3 would be:

0.25 1 0.25
0.25 0 0.25
0.25 1 0.25

For every unsigned char (pixel) in the image data, place the center of the filter array at the current working pixel. Then for every pixel that the filter covers in the image, find the weighted sum of all the pixels covered by the filter (i.e each filter value multiplied by the pixel it is currently covering) and set your current working image pixel value to that weighted sum. Do this for every pixel in the image. If a filter pixel falls out of range of the image 2D array (i.e off left, right, top, bottom), then it must wrap around the appropriate edge of the image.

So I have the following code:

Image Image::operator%(const Filter & g) {
    Image filtered = *this;
    std::vector<std::vector<float>> filter = g.get_filter();
    Image::iterator beg = filtered.begin();
    Image::iterator end = filtered.end();
    unsigned char* pixel_data = filtered.data.get();
    int pixel_index = 0;

    while(beg != end) {
        // current working pixel value
        unsigned char* c_pixel = *beg;

        float weight = 0;

        // starting x and y position (top left) relative to the centre
        // of the filter at index 'pixel'
        int start_y = pixel_index - (g.get_size()-1) / 2;
        int start_x = pixel_index - (g.get_size()-1) / 2;

        for(int row = 0; row < g.get_size(); ++row) {
            std::vector<float> r = filter.at(row);
            int c_row = start_y + row;

            if(c_row >= height) {
                c_row %= height;
            } else if(c_row < 0) {
                c_row += height;
            }

            for(int col = 0; col < g.get_size(); ++col) {
                // current column of filter relative
                // to the image pixel
                int c_col = start_x + col;

                if(c_col >= width) {
                    c_col %= width;
                } else if(c_col < 0) {
                    c_col += width;
                }
                weight += pixel_data[this->index(c_col, c_row)]*r.at(col);
            }
        }
        *c_pixel = weight;
        ++beg;
        ++pixel_index;
    }
    return filtered;
}

In-case you are wondering, this->index(c_col, c_row) is treating a 1D array as a 2D array:

int Image::index(int x, int y) {
    return width*y + x;
}

... and the image data is protected by a std::unique_ptr<unsigned char[]>. This code gives me some strange output. The resulting image has vertical streaks of different pixel colors, somewhat resembling the original image color. I have no idea what I am doing wrong, because this method checks out on paper but not in code. I'll be glad to add any extra information if needed. :)

Upvotes: 3

Views: 4304

Answers (1)

James Poag
James Poag

Reputation: 2380

My first concern is the image pixel format. You say the output is std::unique_ptr<unsigned char[]>, but the weight is computed and written using floats. Your index method returns an index without taking into account the pixel data size { 1_BYTE (monochrome), 3_BYTE (RGB8), 4_Byte(RGBA8) }. pixel_data is char (Byte) so I'm not certain you are indexing the pixel data correctly, not taking into account pixel size and not ignoring alpha (if needed).

The other concern is that if you are using RGB(a) data, then conversion from INT->Float isn't going to scale correctly. Multiplying by float will scale the pixel as a solid number and not the channels individually. This will result in channels spilling into each other and generally not being correct.

Your next step is to create a filter that reads and writes the data as pixels with RGB channels (ignoring alpha) to make sure your filter is pass-thru. Then, you will write a filter that removes an RGB channel by setting it to 0 or 255. (Red Channel, Blue Channel, Green Channel)

Once you are certain that you can manipulate RGB separately and correctly, then you can start applying weights.

The first attempt will be slow. Eventually you will find out that you can use MASKs to grab R_B channel separate from G channel and you won't worry about overflow. This magic looks generally like this:

    UInt32 WeightPixel(UInt32 value, float weight)
    {
        const UInt32 MASK1 = (UInt32)0x00ff00ff; // _R_B
        const UInt32 MASK2 = (UInt32)0xff00ff00; // A_G_

        int f2 = (int)(256 * weight); // Scale weight into char range

        // >> 8 is just divide by 256
        return (UInt32)(((((value & MASK1) * f2)) >> 8) & MASK1)
               | (UInt32)(((((value & MASK2) * f2)) >> 8) & MASK2);
    }

Upvotes: 2

Related Questions