Paint a SingleChannel Image


#1

Hi,

I thought a SingleChannel image behaves like an image with only an alpha channel, or am I understanding this wrong? I’m having the following issues, on Windows 10:

  • Using a ColourGradient doesn’t seem to work, the commented out block draws something, but it isn’t a circular gradient.

  • And painting a resized version of the image with smoothing produces artefacts. See the paint method and resulting image below.

void paint(Graphics &g) override
{
    g.fillAll(Colours::yellow);

    // image with simple diagonal gradient
    int w = 10, h = 10;
    Image monochrome(Image::SingleChannel, w, h, false);
    
    {
        Image::BitmapData bm(monochrome, Image::BitmapData::writeOnly);
        for (int y = 0; y < h; ++y)
        {
            uint8 *line = bm.getLinePointer(y);
            for (int x = 0; x < w; ++x)
            {
                line[x] = (uint8)jlimit(0, 255, (x+y*3) * 50 - 900);
            }
        }
    }
        
    // because this doesn't draw a circular gradient...
    //{
    //    Graphics ig(monochrome);
    //    ColourGradient gradient(Colours::transparentBlack, 5.f, 5.f, Colours::black, 11.f, 5.f, true);
    //    ig.setGradientFill(gradient);
    //    ig.fillAll();
    //}

    // use black fill:
    g.setColour(Colours::black);

    // actual size
    g.drawImageWithin(monochrome, 0, 0, w, h, RectanglePlacement::doNotResize, true);

    // with low quality resizing
    g.saveState();
    g.setImageResamplingQuality(Graphics::lowResamplingQuality);
    g.drawImageWithin(monochrome, 15, 0, 200, 200, RectanglePlacement::stretchToFit, true);
    g.restoreState();

    // with default quality resizing
    g.drawImageWithin(monochrome, 220, 0, 200, 200, RectanglePlacement::stretchToFit, true);
}


#2

I think the coordinates that you are passing to ColourGradient are wrong. The following works fine for me:

void MainContentComponent::paint (Graphics& g)
{
    const Rectangle<int>& r = getLocalBounds();
    g.fillAll(Colours::yellow);
    ColourGradient gradient(Colours::transparentBlack, r.getCentreX(), r.getCentreY(), Colours::black,
                            r.getCentreX(), r.getY(), true);
    g.setGradientFill(gradient);
    g.fillAll();
}

#3

OK some more tests using gradients:

I’m painting on an image, not directly to the component.

I noticed that the problem is the worst when running either a debug build or a release build in the Visual Studio debugger. Without the debugger there are only a few artefacts.

Here’s another paint routine, comparing what happens if you use:

  • a single channel image
  • ARGB with transparent gradient
  • ARGB with fully opaque gradient

Code:

    g.fillAll(Colours::yellow);

    g.setColour(Colours::black);
    int w = 50, h = 50;
    int x = 10;

    Image monochrome(juce::Image::SingleChannel, w, h, false);
    {
        Graphics ig(monochrome);
        ColourGradient gradient(Colours::transparentBlack, w/2.f, h/2.f, Colours::black, w * 1.05f, h/2.f, true);
        ig.setGradientFill(gradient);
        ig.fillAll();
    }
    g.drawImageAt(monochrome, x, 10, true);
    x += w+5;

    Image rgbimg1(juce::Image::ARGB, w, h, false);
    {
        Graphics ig(rgbimg1);
        ColourGradient gradient(Colours::black, w/2.f, h/2.f, Colours::transparentBlack, w * 1.05f, h/2.f, true);
        ig.setGradientFill(gradient);
        ig.fillAll();
    }
    g.drawImageAt(rgbimg1, x, 10, false);
    x += w+5;

    Image rgbimg2(juce::Image::ARGB, w, h, false);
    {
        Graphics ig(rgbimg2);
        ColourGradient gradient(Colours::blue, w/2.f, h/2.f, Colours::white, w * 1.05f, h/2.f, true);
        ig.setGradientFill(gradient);
        ig.fillAll();
    }
    g.drawImageAt(rgbimg2, x, 10, false);
    x += w+5;


    g.drawImageWithin(monochrome, 10, 20+h, 5*w, 5*h, RectanglePlacement::stretchToFit, true);

And here’s the result:

That difference usually happens when code uses uninitialized values. The debugger changes the behaviour of such code because it initializes any newly allocated memory (both on the stack and the heap) with a fixed bit pattern (eg. on the stack, that will be 0xCC).


#4

Well you are not clearing your images. There will be random data in the image and you draw over this with a semi-transparent gradient.


#5

Allright I see. But am I right to assume that’s not necessary when filling an image with a BitmapData?

Any idea what the problem is with painting that image? What is the usual way to paint a single channel image? As shown in the OP this gives artefacts if the image is scaled.


#6

No it is necessay. Calling drawImageWithin with fillAlphaChannelWithCurrentBrush = true will clip your drawing to the channel of your image - so if your pixel is white, the context will be filled with the current brush - if it is black, it will do nothing to the graphics context’s pixel - i.e. the original pixel will remain unchanged.

Therefore you still must clear your graphics context.


#7

OK, to recap:

First, sorry about all the gradient stuff — that was my bad.

Second, suppose we allocate a single channel image and initialize it with a BitmapData:

int w = 10, h = 10;
Image monochrome(Image::SingleChannel, w, h, false); 
Image::BitmapData bm(monochrome, Image::BitmapData::writeOnly);
for (int y = 0; y < h; ++y)
{
    uint8 *line = bm.getLinePointer(y);
    for (int x = 0; x < w; ++x)
    {
        line[x] = (uint8)jlimit(0, 255, (x+y*3) * 50 - 900);
    }
}

Now let’s paint our component

void paint(Graphics &g) override
{
    g.fillAll(Colours::yellow);
    g.drawImageWithin(monochrome, 10, 10, 200, 200, RectanglePlacement::stretchToFit, true);
}

That works fine, except for the smoothing of that image. That results in the discontinuities shown in the first image in the OP.


#8

You can’t assume that the pixels on each line are packed as contiguous bytes - try using BitmapData::setPixelColour to make sure you’re actually writing it correctly.

And like Fabian suggested, you should clear the image first, unless there’s a really genuine performance reason not to.


#9

Yes I can do this the usual way:

    g.fillAll(Colours::yellow);
    Image monochrome(juce::Image::SingleChannel, w, h, true);
    {
        Graphics ig(monochrome);
        ColourGradient gradient(
            Colours::transparentBlack, 0.f, 0.f, 
            Colours::black, (float)w, 0.f,
            false);
        ig.setGradientFill(gradient);
        ig.fillAll();
    }
    g.drawImageWithin(monochrome, 10, 10, 5*w, 5*h, RectanglePlacement::stretchToFit, true);

But it gives the same result — the smooth interpolation is not really smooth.


#10

Anyway I figured out the problem. In juce_RenderingHelpers.h line1255:

void render4PixelAverage (PixelAlpha* const dest, const uint8* src, const uint32 subPixelX, const uint32 subPixelY) noexcept
{
    uint32 c = 256 * 128;
    c += src[0] * ((256 - subPixelX) * (256 - subPixelY));
    src += this->srcData.pixelStride;
    c += src[1] * (subPixelX * (256 - subPixelY));
    src += this->srcData.lineStride;
    c += src[1] * (subPixelX * subPixelY);
    src -= this->srcData.pixelStride;

    c += src[0] * ((256 - subPixelX) * subPixelY);

    *((uint8*) dest) = (uint8) (c >> 16);
}

Replace the two instances of src[1] with src[0], the line src += this->srcData.pixelStride; takes care of that offset.


#11

Hi roeland,

I’ve made that change to the render4PixelAverage method and pushed it to the develop branch. Good find!

Ed