Jump to content
How to Install Plugins ×

Average Object Color


MJW

Recommended Posts

  • 2 weeks later...

At Maximilian's request, I made a CodeLab version. If requested, I'll post the plugin DLL, but I'm not sure there's a good reason to do so. I think the code, itself, is fairly interesting in its use of the floodfill routine. I'm always a little nervous when I add complex one-time pre-rendering code to CodeLab plugins, but it seemed to work correctly. No guarantees, though!

 

The icon: AverageObjectColor.png

 

The code:

Spoiler

// Name: Average Object Color (CodeLab)
// Submenu: Object
// Author: MJW
// Title: Average Object Color (CodeLab)
// Version: 1.0.*
// Desc: Replace each object's color with the average color of the object.
// Keywords: average object color
// URL: http://forums.getpaint.net/index.php?/topic/110896-average-object-color/
// Help:
#region UICode
CheckboxControl Amount1 = false; // [0,1] Use Gamma Adjustment
#endregion

void Render(Surface dst, Surface src, Rectangle rect)
{
    GammaAdjust = Amount1;
    if ((floodFill == null) || (prevGammaAdjust != GammaAdjust) || !completed)
        SetupRendering(dst, src);
    
    int left = rect.Left, right = rect.Right, top = rect.Top, bottom = rect.Bottom;

    for (int y = top; y < bottom; y++)
    {
        for (int x = left; x < right; x++)
        {
            // If the destination pixel wasn't visted, it wasn't part of an object, so use the
            // source transparent pixel. Otherwise, ues the color in the destination pixel with
            // the alpha value in the source pixel.
            ColorBgra dstPix = dst[x, y];
            ColorBgra srcPix = src[x, y];
            dst[x, y] = (dstPix == Unvisited) ? src[x, y] : dstPix.NewAlpha(srcPix.A);
        }
    }
}

//-----------
// Constants.
//-----------
ColorBgra Unvisited = ColorBgra.FromBgra(0, 0, 0, 0);
ColorBgra VisitedForAverage = ColorBgra.FromBgra(255, 255, 255, 0);

//------------------
// Global variables.
//------------------
Surface Src, Dst;
FloodFill floodFill;
Rectangle selection;
int selLeft, selTop, selRight, selBottom;
bool GammaAdjust;
bool prevGammaAdjust;
bool completed;
Rectangle[] selectionRectangles;

//*****************************************************************************************
//----------------------------------------------------------------------------
// Setup routine to initalize variable based on the control values.
// Called from OnSetRenderInfo after ProcessControlValues has been called.
//
// Because this is done before the render threads run, the "global" variables
// can be freely changed, unlike when the setup is done in the render threads.
//-----------------------------------------------------------------------------
protected void SetupRendering(Surface dst, Surface src)
{
    Dst = dst; Src = src;
    if (floodFill == null)
    {
        PdnRegion selectionRegion = EnvironmentParameters.GetSelection(src.Bounds);
        selection = selectionRegion.GetBoundsInt();
        selLeft = selection.Left;
        selTop = selection.Top;
        selRight = selection.Right;
        selBottom = selection.Bottom;
        floodFill = new FloodFill(selLeft, selTop, selRight - 1, selBottom - 1);
        selectionRectangles = EnvironmentParameters.GetSelection(src.Bounds).GetRegionScansInt();
        ComputeAverages();
    }
    else if (!completed || (prevGammaAdjust != GammaAdjust))
    {                  
        ComputeAverages();
    }

    prevGammaAdjust = GammaAdjust;
}

// Mark all selected pixels as unvisited.
void MarkSelection()
{
    foreach (Rectangle r in selectionRectangles)
    {
        Dst.Clear(r, Unvisited);
        if (IsCancelRequested)
            return;
    }
}

void ComputeAverages()
{
    completed = false;
    MarkSelection();
    for (int y = selTop; y < selBottom; y++)
    {
        for (int x = selLeft; x < selRight; x++)
        {
            if (IsCancelRequested)
                return;

            if ((Src[x, y].A != 0) && (Dst[x, y] == Unvisited))
            {
                // Get the sums of the color components.
                // Then compute the average color.
                rSum = gSum = bSum = aSum = 0.0;
                if (GammaAdjust)
                {
                    floodFill.Fill4(x, y, ComputeGammaAverage);
                    double aRecip = 1.0 / aSum;
                    rSum = Math.Sqrt(aRecip * rSum);
                    gSum = Math.Sqrt(aRecip * gSum);
                    bSum = Math.Sqrt(aRecip * bSum);
                }
                else
                {
                    floodFill.Fill4(x, y, ComputeAverage);
                    double aRecip = 1.0 / aSum;
                    rSum = aRecip * rSum;
                    gSum = aRecip * gSum;
                    bSum = aRecip * bSum;
                }                     
                averagePix = ColorBgra.FromBgr((byte)(bSum + 0.5), (byte)(gSum + 0.5), (byte)(rSum + 0.5));

                // Fill the object's pixels with the average color.
                floodFill.Fill4(x, y, FillAverage);
            }
        }
    }
    completed = true;
}

double rSum, gSum, bSum, aSum;
ColorBgra averagePix;
bool ComputeGammaAverage(int x, int y)
{
    ColorBgra srcPix = Src[x, y];
    if ((srcPix.A == 0) || (Dst[x, y] != Unvisited))
        return false;

    double alpha = srcPix.A;
    rSum += alpha * Sq(srcPix.R);
    gSum += alpha * Sq(srcPix.G);
    bSum += alpha * Sq(srcPix.B);
    aSum += alpha;

    Dst[x, y] = VisitedForAverage;
    return true;
}

bool ComputeAverage(int x, int y)
{
    ColorBgra srcPix = Src[x, y];

    // See if the pixel is part of an object and hasn't been visited.
    if ((srcPix.A == 0) || (Dst[x, y] != Unvisited))
        return false;

    double alpha = srcPix.A;
    rSum += alpha * srcPix.R;
    gSum += alpha * srcPix.G;
    bSum += alpha * srcPix.B;
    aSum += alpha;

    Dst[x, y] = VisitedForAverage;  // Mark the pixel as visited.
    return true;
}

double Sq(double x)
{
    return x * x;
}

bool FillAverage(int x, int y)
{
    if ((Dst[x, y ] != VisitedForAverage) || (Src[x, y] == VisitedForAverage))
        return false;

    Dst[x, y] = averagePix;
    return true;
}


// Flood fill routine based on "A Seed Fill Algorithm" by Paul Heckbert
// from "Graphics Gems", Academic Press, 1990
// Some details improved by MJW, including better handling of the initial span, and
// elimination of the unnecessary retesting of the pixels to the left and right of the
// parent span.
//
// I removed the 8-connected fill code because I wasn't certain it was correct. I hope
// to replace it soon.
//
// To use, first create a FllodFill class with:
//     FloodFill floodFill = new FloodFill();
// -- or --
//     FloodFill floodFill = new FloodFill(xMin, yMin, xMax, yMax);
// if the first version is used, then before doing a fill, the size of the region
// must be set with:
//     floodFill.SetRange(xMin, yMin, xMax, yMax);
// To perform fills, call:
//     floodFill.Fill4(x, y, testAndModifyPixel);   // 4-connected fill.
// -- or --
//     floodFill.Fill8(x, y, testAndModifyPixel);   // 8-connected fill.
// bool testAndModifyPixel(int x, int y) is a delegate that's passed the x and y
// coordinates of a pixel. If the pixel matches whatever criteria is required for
// filling, testAndModifyPixel modifies the pixel to the "filled" condition, and
// returns true; otherwise, it returns false. For example, to replace the Primary
// Color with the Secondary Color, testAndModifyPixel would test to see if the
// pixel's color equaled the Primary Color, and if so, set it to the Secondary
// Color and return true.
class FloodFill
{
    private SpanStack stack;

    public FloodFill()
    {
        stack = new SpanStack();
    }

    public FloodFill(int xMin, int yMin, int xMax, int yMax)
    {
        stack = new SpanStack();
        SetRange(xMin, yMin, xMax, yMax);
    }

    // Set the allowable ranges for X and Y.
    // Must be called before the first fill.
    protected int xMin, yMin, xMax, yMax;
    public void SetRange(int xMin, int yMin, int xMax, int yMax)
    {
        this.xMin = xMin; this.yMin = yMin;
        this.xMax = xMax; this.yMax = yMax;
        stack.SetYRange(yMin, yMax);
    }

    public void DeallocateMemory()
    {
        stack.DeallocateShadows();
    }

    public int GetAllocationCount()
    {
        return stack.ShadowCount;
    }

    private class SpanStack
    {
        private int yMin, yMax;
        private int shadowCount = 0;

        public SpanStack()
        {
            InitShadows();
        }

        public void SetYRange(int yMin, int yMax)
        {
            this.yMin = yMin; this.yMax = yMax;
        }


        private struct Shadow
        {
            public int y, yParent, xLeft, xRight;

            public Shadow(int y, int yParent, int xLeft, int xRight)
            {
                this.y = y;
                this.yParent = yParent;
                this.xLeft = xLeft;
                this.xRight = xRight;
            }

            public void GetValues(out int y, out int yParent, out int xLeft, out int xRight)
            {
                y = this.y;
                yParent = this.yParent;
                xLeft = this.xLeft;
                xRight = this.xRight;
            }

            public void SetValues(int y, int yParent, int xLeft, int xRight)
            {
                this.y = y;
                this.yParent = yParent;
                this.xLeft = xLeft;
                this.xRight = xRight;
            }

            public void ExtendRight(int newRight)
            {
                xRight = newRight;
            }
        }

        // Theres's no automatic way to keep the count of the maximum allocated shadows.
        // When the flag is true, the value is maintained; otherwise it won't.
        public const bool MaintainShadowCount = false;

        const int InitialShadowAllocation = 1000;
        Stack<Shadow> shadow;

        public void DeallocateShadows()
        {
            shadow = null;
        }

        // Return the maximum number of shadows allocated.
        public int ShadowCount
        {
            get { return shadowCount; }
        }

        private void InitShadows(int initialShadowAllocation)
        {
            shadow = new Stack<Shadow>(initialShadowAllocation);
            shadowCount = 0;
        }

        private void InitShadows()
        {
            InitShadows(InitialShadowAllocation);
        }

        public void Push(int y, int yParent, int xLeft, int xRight)
        {
            if ((y >= yMin) && (y <= yMax))
            {
                shadow.Push(new Shadow(y, yParent, xLeft, xRight));

                if (MaintainShadowCount && (shadowCount < shadow.Count))
                    shadowCount = shadow.Count;
            }
        }

        // Same, except no need to test for inside Y range.
        // The parent Y will always be in range.
        public void PushParent(int y, int yParent, int xLeft, int xRight)
        {
            shadow.Push(new Shadow(y, yParent, xLeft, xRight));

            if (MaintainShadowCount && (shadowCount < shadow.Count))
                    shadowCount = shadow.Count;
        }

        public void Pop(out int y, out int yParent,
                        out int xLeft, out int xRight)
        {
            shadow.Pop().GetValues(out y, out yParent, out xLeft, out xRight);
        }

        // Replace the right X value so the shadow is extended.
        public void Extend(int newRight)
        {
            shadow.Peek().ExtendRight(newRight);
        }

        public bool Empty
        {
            get { return shadow.Count == 0; }
        }
    }

    // The delegate must test the pixel and modify it if it fits the match criteria.
    // Return true if the pixel matched (and was therefore modified) and false otherwise.
    public delegate bool TestAndModifyPixel(int x, int y);

    // Fill4: If the pixel at (x, y) fits the match criteria, modify it. Then check all its 4-connected
    // neighbors. Modify any than match, and check their 4-connected neighbors, etc. Continue until all the
    // matching connected pixels have been modified. 
    // A 4-connected neighbor is a pixel above, below, left, or right of a pixel.
    public void Fill4(int x, int y, TestAndModifyPixel testAndModifyPixel)
    {
        int left, right, parentLeft, parentRight, parentY;

        // Exit if the position is out of range.
        if ((x < xMin) || (y < yMin) ||
            (x > xMax) || (y > yMax))
            return;

        // Scan the first span as a special case.  The first span is unique,
        // since it has no parent span.

        // Scan left starting at the initial pixel.
        left = x;
        while ((left >= xMin) && testAndModifyPixel(left, y))
            left--;

        // If no pixels were modified, just return.
        if (left == x)
            return;

        // Scan right for the end of the seed span, starting at the pixel just right
        // of the initial pixel.
        do
        {
            x++;
        }
        while ((x <= xMax) && testAndModifyPixel(x, y));

        // Push the span for the lines above and below.
        left++; // Adjust left so it contains the starting X coordinate of the span.
        right = x - 1;
        stack.Push(y - 1, y, left, right);
        stack.Push(y + 1, y, left, right);

        // Scan the remaining spans, adding new 'shadow' spans of pixels to test to
        // adjacent lines as we go. Above and below each span is a 'parent' line that
        // generated the span, and an 'other' line that lies on the opposite side.
        // For each group of set pixels in the current line, we must add a corresponding
        // span in the 'other' line.  Spans for the parent line only need to be added
        // for the regions that extend to the left and right of the parent span, since
        // the parent span has already been tested.  The pixels immediately left and
        // right of the parent span were tested while processing the parent span, so
        // they don't need to be retested.
        // 
        // Parent:        aa ppppppppppppppppp aaaa
        // Current:       cccc cccccc  cccc ccccccc
        // Other:         aaaa aaaaaa  aaaa aaaaaaa
        while (!stack.Empty)
        {
            stack.Pop(out y, out parentY, out parentLeft, out parentRight);
            int otherY = y + (y - parentY);

            // Scan left, starting at the leftmost pixel of the parent span, for
            // pixels that need replacing.
            x = right = parentLeft;
            while ((x >= xMin) && testAndModifyPixel(x, y))
                x--;

            // See if the first span starts at or before the parent span.
            // If it does, scan for the end of the span, and output a non-parent
            // span, and a parent span if necessary.
            if (x != parentLeft)
            {
                left = x + 1;    // Save X of leftmost modified pixel.
                // The pixel with the same X as the parent's leftmost pixel
                // was replaced.  We know the parent's leftmost pixel was modified
                // and that the pixel to its left was tested but not modified,
                // so there's no there's no need to retest them.  Only add a new
                // parent span if we need to test the pixels at least two pixels
                // to the left of the parent's leftmost pixel.
                if (left <= parentLeft - 2)
                    stack.PushParent(parentY, y, left, parentLeft - 2);
                x = parentLeft + 1;

                // Scan rightward for the end of the first span. When
                // the end of the span is reached, push the span for the
                // non-parent neighboring line.
                while ((x <= xMax) && testAndModifyPixel(x, y))
                    x++;
                right = x - 1;
                stack.Push(otherY, y, left, right);
            }

            // Output all the non-parent spans, except for the first span when it
            // begins at or before the parent span (it has already been output).
            // When entering this loop, we are outside a span, and x contains the
            // X coordinate of the last pixel tested (which wasn't filled).
            // Scan for the beginning next span. Once found, scan for the end.
            while (x < parentRight)
            {
                if (testAndModifyPixel(++x, y))
                {
                    left = x++;
                    // Scan rightward for the end of the current span. When
                    // the end of the span is reached, push the span for the
                    // non-parent neighboring line.
                    while ((x <= xMax) && testAndModifyPixel(x, y))
                        x++;
                    right = x - 1;
                    stack.Push(otherY, y, left, right);
                }
            }

            // Either we've reached right boundary, or the current pixel doesn't need
            // filling and we're at or past the end of the parent span. In either
            // case, we're done with the current shadow.
            // See if we need to add a shadow to the parent.
            // We know the parent's rightmost pixel was modified and that the pixel
            // to its right was tested but not modified, so there's no there's no
            // need to retest them. Therefore, if we're less than two pixel's past the
            // parent span, we don't need to add a span. 
            if (right >= parentRight + 2)
                stack.PushParent(parentY, y, parentRight + 2, right);
        }
    }
}

 

 

  • Upvote 5
Link to comment
Share on other sites

Because I think flood filling might be useful for other plugins, I'll add a comment explaining in more detail how I use it in this plugin. However, while I was looking at the Average Object Color code, I realized I might be able to implement it without creating an auxiliary surface. That would make it simpler, so I'm going to see if I can do that first.

 

EDIT: Unfortunately, the auxiliary surface seems to be necessary to avoid writing the destination surface outside non-rectangular selections.

 

EDIT 2: I figured out a better way to do it that doesn't require an auxiliary surface.

Link to comment
Share on other sites

I changed the CodeLab code so that it's simpler and doesn't require an auxiliary surface. I also modified the FloodFill code to use the C# Stack class for the stack rather than a linked list.

Link to comment
Share on other sites

Now I'll (more or less) briefly explain how it works.

 

First I create an instance of the FloodFill class with its range set to the selection. Note that FloodFill takes the actual max X and max Y, not versions with one added. The range can be changed by calling SetRange(int xMin, int yMin, int xMax, int yMax).

 

               PdnRegion selectionRegion = EnvironmentParameters.GetSelection(src.Bounds);
               selection = selectionRegion.GetBoundsInt();
               selLeft = selection.Left;
               selTop = selection.Top;
               selRight = selection.Right;
               selBottom = selection.Bottom;
               floodFill = new FloodFill(selLeft, selTop, selRight - 1, selBottom - 1);

 

In the dst surface, I fill all the selected pixels with a flag color called "Unvisited" (which is transparent black) that indicates the pixel hasn't been visited.

            foreach (Rectangle r in selectionRectangles)
            {
                Dst.Clear(r, Unvisited);
                if (IsCancelRequested)
                    return;
            }

For each pixel in the selection rectangle, I call the FloodFill routine twice if the src pixel isn't transparent and the dst pixel is marked as Unvisited. (For clarity, I omit the gamma-correction stuff.)

 

                for (int x = selLeft; x < selRight; x++)
                {
                    if (IsCancelRequested)
                        return;

                    if ((Src[x, y].A != 0) && (Dst[x, y] == Unvisited))
                    {
                        // Get the sums of the color components.
                        // Then compute the average color.
                        rSum = gSum = bSum = aSum = 0.0;
 
                        floodFill.Fill4(x, y, ComputeAverage);
                        double aRecip = 1.0 / aSum;
                        rSum = aRecip * rSum;
                        gSum = aRecip * gSum;
                        bSum = aRecip * bSum;                    
                        averagePix = ColorBgra.FromBgr((byte)(bSum + 0.5), (byte)(gSum + 0.5), (byte)(rSum + 0.5));

                        // Fill the object's pixels with the average color.
                        floodFill.Fill4(x, y, FillAverage);
                    }

 

Fiil4 takes three arguments: the int x,y  coordinates and a delegate. The delegate takes two arguments -- the int x,y coordinates -- and returns a bool. 

 

The delegate tests the specified pixel to see if it needs to be modified. If not, it returns false; otherwise, it modifies the pixel and returns true. (Modifying the pixel should set the pixel to a condition that no longer fits the modification criteria.) The delegates are never passed coordinates outside the range set for the instance of the FloodFill class.

 

The first call to Fill4 passes the ComputeAverage delegate:

        double rSum, gSum, bSum, aSum;
        ColorBgra averagePix;
        bool ComputeAverage(int x, int y)
        {
            ColorBgra srcPix = Src[x, y];
            if ((srcPix.A == 0) || (Dst[x, y] != Unvisited))
                return false;

            double alpha = srcPix.A;
            rSum += alpha * srcPix.R;
            gSum += alpha * srcPix.G;
            bSum += alpha * srcPix.B;
            aSum += alpha;

            Dst[x, y] = VisitedForAverage;
            return true;
        }

This delegate returns false if either the src pixel is transparent or the dst pixel isn't Unvisited. Otherwise, it sums the color components into "global" variables and sets the dst pixel to VisitedForAverage (which is a somewhat random transparent color) and returns true to indicate it modified the pixel.

 

When this fill is completed, all the pixels in the object containing the original pixel will be marked as VisitedForAverage, and the sums will be the sum of the color components for the entire object.

 

The calling loop computes the average color, stores it in a "global" variable and calls Fill4 with the FillAverage delegate.

        bool FillAverage(int x, int y)
        {
            if ((Dst[x, y] != VisitedForAverage) || (Src[x, y] == VisitedForAverage))
                return false;

            Dst[x, y] = averagePix;
            return true;
        }

FillAverage tests to see if the dst pixel is set to VisitedForAverage and the src pixel is not. If not, it returns false, otherwise it sets the dst pixel to the average color and returns true.

 

All this occurs before the main render loops are called.

 

The main render loop looks like this:

 

        protected override void Render(Surface dst, Surface src, Rectangle rect)
        {
            int left = rect.Left, right = rect.Right, top = rect.Top, bottom = rect.Bottom;

            for (int y = top; y < bottom; y++)
            {
                for (int x = left; x < right; x++)
                {
                    // If the destination pixel wasn't visited, it wasn't part of an object, so use the
                    // source transparent pixel. Otherwise, ues the color in the destination pixel with
                    // the alpha value from the source pixel.
                    ColorBgra dstPix = dst[x, y];
                    ColorBgra srcPix = src[x, y];
                    dst[x, y] = (dstPix == Unvisited) ? src[x, y] : dstPix.NewAlpha(srcPix.A);
                }
            }
        }

If a pixel in the selection was originally transparent, the fill routines won't be called for it, so the dst pixel will still be Unvisited. In that case, the src pixel value is stored into the dst pixel. If the dst pixel was visited, it will contain the average color of the object it's in, but with an alpha of 255. So the alpha value of the src pixel will replace it.

 

Note: The name Fill4 refers to the fact that it fills 4-connected regions. Pixels are 4-connected if they touch on the sides. Pixels are 8-connected if they touch on the sides or the corners. 4-connected fills are much more commonly used, but 8-connected fills can also be useful. They're also trickier to write, and I don't yet have one I'm satisfied with.

 

Note 2: The fills would be more efficient if the test-and-modify routines were hardcoded rather than delegates, but would be much less flexible.

 

Note 3: Instead of processing the selection rectangle, I could just process the selected pixels using the same approach used to fill them with Unvisited. I just didn't think of that when I wrote it, and it shouldn't make much difference in performance, since the fill routines will never be called for those pixels.

 

Note 4: Pixel that are modified by a floodfill are called "interior pixels"; pixels that aren't modified are called "exterior pixels."  The floodfill must, of course, test all the interior pixels at least once to see that they need to be filled. It also has to test the exterior pixels connected to interior pixels at least once to see that they don't. It may retest an interior pixel to find that it's already filled. It may also retest exterior pixels. I haven't yet figured out how to prove it, but from experimentation I know that my floodfill routine never retests interior pixels unless the interior region has a hole. Exterior pixels are quite often retested. For example, a line of singe exterior pixels separating interior pixels will be tested twice -- once when the interior pixels on one side are filled, and again when the pixels on the other side are filled.

 

EDIT: To my considerable chagrin, I realized I have to test the src pixel in FillAverage to see if it equals VisitedForAverage. I hoped to avoid testing the src pixel, but I don't see how I can. There's no flag value for VisitedForAverage that couldn't occur in a unselected pixel within the selection rectangle. If such a pixel were next to a filled pixel, the FillAverage routine would modify it when it shouldn't. I could avoid the problem by using a auxillary buffer, since the pixels outside the selection could be set to any value, rather than being required to equal the src values.

 

EDIT 2: I changed VisitedForAverage from TransparentWhite to a more random transparent color. This is to make it less likely that an unselected pixel will match, saving an extra comparison to the source pixel. The advantage is at best marginal, but the change costs nothing.

  • Upvote 5
Link to comment
Share on other sites

Thank you very much for the updated code and elaborated explanations, MJW. I compiled without any problem and made a little first test:

 

Average_Object_Color_Test_1.jpg

Edited by Maximilian
fixed broken link
  • Upvote 3
Link to comment
Share on other sites

Thank you, @MJW.  This could be very useful!  :D

 

averageobjectcolor_01.png

 

 

Edited by lynxster4
re-hosted image
  • Upvote 2
Link to comment
Share on other sites

<3 Dear MJW! 

 

Which of these three pictures is the right one? Thanks in advance for your reply!  :cake: :coffee:

 

13_02_2017_h.png

 

13_02_2017_hs.png

 

13_02_2017.png

 

*Without hexagonal grid ..

 

  • Upvote 4

Live as if you were to die tomorrow. Learn as if you were to live forever.

Gandhi

 

mae3426x.png

Link to comment
Share on other sites

3 hours ago, Seerose said:

Which of these three pictures is the right one?

 

I'm not sure what you mean by "the right one." I think I prefer the third one, because the colors are quite striking, but that doesn't make the others wrong.

Link to comment
Share on other sites

A very interesting Plugin @MJW and thank you.  I'm not sure, however, that I am using it correctly?  I can only get it to work if I duplicate the image and change the mode of tht layer after using the plugin.  However, am getting some lovely colors with it and I played around to get these:

 

aEzB6pp.png    

 

3nEgHwq.png        

     

 

 

 

  • Upvote 3

30b8T8B.gif

How I made Jennifer & Halle in Paint.net

My Gallery | My Deviant Art

"Rescuing one animal may not change the world, but for that animal their world is changed forever!" anon.

 
Link to comment
Share on other sites

I'm a little confused what you mean, Pixey. As long as you have areas of non-transparency separated by areas of transparency, it should work, independent of any blending modes. Whether the result is useful or not depends on what you're trying to achieve. You might try the simple flower example I show in the original comment to get a feeling for what the plugin does.

 

I like that blue bird picture.

Link to comment
Share on other sites

I think it's important to highlight the relevance of an assisting plugin, such as the hexagonal grid, in order to be able to determine areas of different colors in an image that can be averaged by this plugin. What I did in my example above is select inside the cells of the grid with the magic wand in global mode, which causes all cells to be selected and of course separated by transparent areas which correspond with the framing of the grid. Then I duplicate the layer containing the flowers image, make it the active layer, and run Average Object Color (which will actually run on all cells selected with the aid of the hexagonal grid by averaging the colors constrained within each cell-shaped selection). In this way the flowers acquire a mosaic-like appearance. After that I played with the colors with the Pastel effect, and also played around with blend modes and with the hexagonal grid itself.

 

I hope I'm not talking nonsense. Just trying to help those in confusion.

  • Upvote 2
Link to comment
Share on other sites

Thanks for the pointers @Maximilian which I will try.  I have been mystified as to why I could not get the Plugin to work on a single layer.  I tried it with @MJW's flowers and this is what I get:

 

th_Huh_zps6hpoj4dz.png

 

Back to the drawing board :P.

     

 

 

 

30b8T8B.gif

How I made Jennifer & Halle in Paint.net

My Gallery | My Deviant Art

"Rescuing one animal may not change the world, but for that animal their world is changed forever!" anon.

 
Link to comment
Share on other sites

12 minutes ago, Pixey said:

I have been mystified as to why I could not get the Plugin to work on a single layer.

 

It works on a single layer but you MUST "cut" it before in pieces of "color islands" in a sea of transparency ;). Therefore you need a second layer like the hexagon grid to select the grid, switch back to the real image and delete the selection.

 

BTW: After using the Average Object Color plugin you can use Fill Gaps to merge together all the color islands.

Edited by IRON67
  • Upvote 1
Link to comment
Share on other sites

Many thanks to @Max for his steps which then made everything clear.  Happy to say I now know how to use it :D .

 

colorful_zpstkfq3u5q.png

 

 

     

 

 

 

  • Upvote 2

30b8T8B.gif

How I made Jennifer & Halle in Paint.net

My Gallery | My Deviant Art

"Rescuing one animal may not change the world, but for that animal their world is changed forever!" anon.

 
Link to comment
Share on other sites

Thanks to Maximilian and Iron67 for their explanations. I'll add a bit more detail to my original example.

 

EDIT: I've now added an explanation of how the transparent grid was added to the flower picture. I'm sorry I didn't to that in the first place.

  • Upvote 1
Link to comment
Share on other sites

Glad I've been of help. Understanding the proper use of a plugin may sometimes be a bit tricky, but that's part of the beauty of creation :)

  • Like 1
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...