Working with raster image wrappers

Andrew Markovich

March 30, 2017

Raster image wrappers provide a way to modify raster image data without modifying the original raster image. Each raster image wrapper stores a smart pointer to the original image and can get any data from the original image and return it with or without modifications. Actually, by using raster image wrappers we can construct a conveyor for multi-pass image processing.

Simplifying raster image creation using a raster image wrapper

We can use a raster image wrapper to simplify the GeneratedRasterImage class described in Part 2 of this series, “Creating a raster image from scratch.” Since we use some data from the input image, we can simply inherit our GeneratedRasterImage class from the OdGiRasterImageWrapper class instead of OdGiRasterImage and set the input raster image which we load from file as an original image for this raster image wrapper:

class GeneratedRasterImage : public OdGiRasterImageWrapper
{
    protected:
    OdUInt32 m_pixW, m_pixH;
    ODCOLORREF m_cornerColors[4];
    void computePixel(OdUInt32 x, OdUInt32 y, OdUInt8 *pOutput) const
    {
        // Interpolate colors by X-axis
        const double interpolateX = double(x) / (m_pixW - 1);
        const ODCOLORREF bottom = ODRGB((1.0 - interpolateX) * ODGETRED(m_cornerColors[0])   + interpolateX * ODGETRED(m_cornerColors[1]),
        (1.0 - interpolateX) * ODGETGREEN(m_cornerColors[0]) + interpolateX * ODGETGREEN(m_cornerColors[1]),
        (1.0 - interpolateX) * ODGETBLUE(m_cornerColors[0])  + interpolateX * ODGETBLUE(m_cornerColors[1]));
        const ODCOLORREF top = ODRGB((1.0 - interpolateX) * ODGETRED(m_cornerColors[2])   + interpolateX * ODGETRED(m_cornerColors[3]),
        (1.0 - interpolateX) * ODGETGREEN(m_cornerColors[2]) + interpolateX * ODGETGREEN(m_cornerColors[3]),
        (1.0 - interpolateX) * ODGETBLUE(m_cornerColors[2])  + interpolateX * ODGETBLUE(m_cornerColors[3]));
        // Interpolate colors in Y-axis
        const double interpolateY = double(y) / (m_pixH - 1);
        const ODCOLORREF color = ODRGB((1.0 - interpolateY) * ODGETRED(bottom)   + interpolateY * ODGETRED(top),
        (1.0 - interpolateY) * ODGETGREEN(bottom) + interpolateY * ODGETGREEN(top),
        (1.0 - interpolateY) * ODGETBLUE(bottom)  + interpolateY * ODGETBLUE(top));
        pOutput[0] = ODGETRED(color); pOutput[1] = ODGETGREEN(color); pOutput[2] = ODGETBLUE(color);
    }
    public:
    virtual OdUInt32 pixelWidth() const { return m_pixW; }
    virtual OdUInt32 pixelHeight() const { return m_pixH; }

    virtual const OdUInt8* scanLines() const { return NULL; }
    virtual void scanLines(OdUInt8* scnLines, OdUInt32 firstScanline, OdUInt32 numLines = 1) const
    {
        OdUInt32 scanLen = scanLineSize();
        for (OdUInt32 i = firstScanline; i < firstScanline + numLines; i++)
        {
            OdUInt8 *pScanLine = scnLines + ((i - firstScanline) * scanLen);
            for (OdUInt32 j = 0; j < m_pixW; j++, pScanLine += 3)
            computePixel(j, i, pScanLine);
        }
    }

    GeneratedRasterImage() : m_pixW(1), m_pixH(1) {}
    void configureImage(OdUInt32 nWidth, OdUInt32 nHeight, const OdGiRasterImage *pOriginal, const ODCOLORREF *pColors)
    {
        setOriginal(pOriginal);
        m_pixW = nWidth; m_pixH = nHeight;
        for (int i = 0; i < 4; i++)
        m_cornerColors[i] = pColors[i];
    }
};
OdSmartPtr<GeneratedRasterImage> pImage = OdRxObjectImpl<GeneratedRasterImage>::createObject();
pImage->configureImage(256, 256, pInputImage, inputColors);
pOutputImage = pImage;

In a new version of code, we don’t require implementation of any pure-virtual methods from the OdGiRasterImage interface, addition of stubs and so on. By default the OdGiRasterImageWrapper class implements all raster image interface methods to redirect raster image data from the original raster image. In our example, override only the pixelWidth/pixelHeight methods to return dimensions of the generated raster image and the scanLines methods to return pixels of the generated image instead of pixels from the original raster image. The pixel format and other image properties will be returned from the original raster image.

Creating an image processing wrapper

We can use raster image wrappers to create any kind of image modifiers, effects, and so on with minimal implementation of OdGiRasterImage interface functionality. To show this kind of Teigha functionality, below is an image modifier which changes pixel color intensity of the original raster image to create the visual effect of circles on the water:

class RasterImageWrapperEx : public OdGiRasterImageWrapper
{
    protected:
    void increaseIntensity(OdUInt8 &color, double intensity) const
    {
        intensity = intensity * color;
        if (intensity > 255.0) intensity = 255.0;
        if (intensity < 0.0) intensity = 0.0;
        color = (OdUInt8)intensity;
    }
    void processPixel(OdUInt32 x, OdUInt32 y, OdUInt8 *pOutput) const
    {
        const OdGeVector2d imageCenter(original()->pixelWidth() / 2, original()->pixelHeight() / 2);
        const double maxLength = imageCenter.length();
        const double lengthFromCenter = (OdGeVector2d(x, y) - imageCenter).length();
        // Define noise level parameters
        const double frequency = 50.0 / OdaPI;
        const double intensity = 0.5;
        // Compute noise level
        const double noiseLevel = (cos((lengthFromCenter / maxLength) * frequency) + 1.0) * intensity + intensity;
        // Apply noise level to pixel color
        increaseIntensity(pOutput[0], noiseLevel);
        increaseIntensity(pOutput[1], noiseLevel);
        increaseIntensity(pOutput[2], noiseLevel);
    }
    public:
    virtual const OdUInt8* scanLines() const { return NULL; }
    virtual void scanLines(OdUInt8* scnLines, OdUInt32 firstScanline, OdUInt32 numLines = 1) const
    {
        OdUInt32 scanLen = scanLineSize();
        for (OdUInt32 i = firstScanline; i < firstScanline + numLines; i++)
        {
            OdUInt8 *pScanLine = scnLines + ((i - firstScanline) * scanLen);
            original()->scanLines(pScanLine, i);
            for (OdUInt32 j = 0; j < original()->pixelWidth(); j++, pScanLine += 3)
                processPixel(j, i, pScanLine);
        }
    }
    RasterImageWrapperEx() {}
};

This raster image wrapper simply implements the scanLines method to get pixels from the original image and calls the processPixel method for each image pixel to apply cosine noise to its color. This implementation doesn’t have any members, and all cosine noise parameters were hardcoded to simplify the example.

Now we can create a new instance of raster image modifiers, set our generated raster image as the original and keep the raster image modifier smart pointer for further processing:

OdSmartPtr<RasterImageWrapperEx> pImage = OdRxObjectImpl<RasterImageWrapperEx>::createObject();
pImage->setOriginal(pOutputImage);
pOutputImage = pImage;

Finally we can store our generated raster image using code from “Saving raster images in file” in Part 1 of this series, and look at the resulting file:

Predefined raster image wrappers

The GiRasterWrappers.h header file contains a set of predefined raster image wrappers that can be used anywhere in any Teigha-based project.

One of the simplest predefined raster image wrappers is the upside-down raster image transformer. It can be attached to an image processing conveyor using a single line of code:

pOutputImage = OdGiUpsideDownRasterTransformer::createObject(pOutputImage);

It simply returns image scanlines in reversed order; as a result we can see the image mirrored from top to bottom:

Similarly, we can use a left-to-right image transformer (the OdGiLeftToRightRasterTransformer class) to mirror pixels of the image scanlines in reverse order.

OdGiInversionRasterTransformer can be used to invert pixel colors in the original raster image:

pOutputImage = OdGiInversionRasterTransformer::createObject(pOutputImage);

If the original raster image contains a palette with inverted colors inside it, elsewhere it will have inverted image pixel colors:

OdGiGrayscaleRasterTransformer converts colors inside the raster image to grayscale:

pOutputImage = OdGiGrayscaleRasterTransformer::createObject(pOutputImage);

The final image after using this wrapper will contain only shades of gray:

OdGiMonochromaticRasterTransformer can be used to convert raster image colors to monochrome colors (black and white colors only):

pOutputImage = OdGiMonochromaticRasterTransformer::createObject(pOutputImage, 127);

This raster image modifier contains an additional parameter, “threshold,” which can be used to set the pixel color intensity limit between dark and light colors. The final image generated with a default (middle of range) threshold:

Conclusion

By following the examples, we’ve used five image modifiers to generate a final monochrome raster image. Raster services during the final file saving, by calling the scanLines method for the last raster image wrapper, initiates a chain of scanLines method calls between raster image wrappers. First, the GeneratedRasterImage class returns initial gradient pixels; then RasterImageWrapperEx adds the effect of water circles onto the gradient image; next the scanLines method calls a chain of mirroring the image scanline, inverting image colors, converting image colors into shades of gray, and finally applying monochrome color effect. Memory usage in this image processing chain is minimal, since the raster image wrappers don’t store copies of pixels. None of the parameters of the initial raster image are changed while applying the image modifiers. The final raster image is still seen by any API as the OdGiRasterImage interface, so we can use the modified raster image anywhere we want just as an original raster image read from a file.