5

I am developing a project on Android, this requires that I filter up to 5 different colors - including the background color (approximate white). I can't use advanced maths algorithms because I don't know too much maths (and I will get lost completely), but with some simple logics algorithms which I derived partly by trial and error, partly logical, I have been able to a result that works up to say 70%. But I need suggestions on how to make it work 100%.

To test the algorithm, I scribbled random words/letters with 4 different colored pens on a plain white (5th color) paper, and wrote some code to decode the colors. So in the code the algorithm ...

  • if red pen ink pixel is decoded, pixel is set to digital red
  • if blue pen ink pixel is decoded, pixel is set to digital blue
  • if green pen ink pixel is decoded, pixel is set to digital green
  • if black pen ink pixel is decoded, pixel is set to digital yellow

The black ink pixel is set to digital yellow (not black, because black was hard to distinguish from ink black visually).

My code is below, but first here is one of the best results I had. As you can see there is an overlap at the fringes between black (represented by digital yellow) and red (represented by digital red). Like I said my code was a partly logic partly trial and error.

My first question is how can I improve this algorithm so as to decode/filter the colors perfectly (I chose this method because I get lost very quickly with very complex maths)?

Secondly how can I make it work for different shades of light (white light, yellow light...)?

EDIT: upon further comments and discussions I've realised that the help i need is palette color quantization Adroid java code example , thanks

Undecoded Original image

Decoded image, almost but not good enough

if(  (Blue > Green) &&(Red > Blue) && (Red - Green) > 25 )
    copyOfBM.setPixel(x,  y, Color.RED); //red
else if(  (Blue > Red) && ( (Blue > Green)) )
    copyOfBM.setPixel(x,  y, Color.BLUE);
else if( (Green >= 82) && ((Green - Blue) >= 12)  && 
  ((DecodeLuminaceColor( x, y, copyOfBM )>= 82 ) && 
   (DecodeLuminaceColor( x, y, copyOfBM )< 124))  )
    copyOfBM.setPixel(x,  y, Color.GREEN);
else if( ((Red - Green) > 6) &&((Red - Green) < 17) &&((Green - Blue) < 8) 
  && (DecodeLuminaceColor( x, y, copyOfBM )< 118 ) )
    copyOfBM.setPixel(x,  y, Color.YELLOW);



void DecodeLuminaceColor( int _x, int _y, Bitmap cBM  ){
    float avrLum = 0;
    Red = 0; Green = 0; Blue = 0;  //Color.green(green_encoded_color);

    red_encoded_color = cBM.getPixel(_x, _y);
    green_encoded_color = cBM.getPixel(_x, _y);
    blue_encoded_color  = cBM.getPixel(_x, _y);

    Red   = ( (red_encoded_color >> 16) & 0xff );
    Green = ( (green_encoded_color >> 8) & 0xff);
    Blue  = ( (blue_encoded_color >> 0) & 0xff );
}
user4666
  • 73
  • 7
  • "I chose this method because I get lost very quickly with very complex maths." My math is less than I'd like too, but there are some tasks where integer arithmetic will only ever yield approximate answers; this is probably one of them. – msw Nov 01 '15 at 11:58
  • I wonder what a '100% perfect' solution looks like. Surely the inherent fuzziness of the image means that for some pixels the color cannot be determined exactly? With that, the result so far is not bad at all. – Jongware Nov 01 '15 at 12:00
  • There is so much hard-coding in the above code in addition to the result of overlapping decodes (red colored pixels get a touch black mis-decodes) which is why i think there should be a better solution , do you know any complex method that would yield more accurate solution? If so pls share , thanks – user4666 Nov 01 '15 at 12:27
  • 2
    I see your point - you hardcoded the exact color values to look for. Perhaps you can build a histogram first, and then check the color ranges that appear most. – Jongware Nov 01 '15 at 12:39

1 Answers1

4

I would use HSV color space

it is better to detect colors (more human perception like) that should help a lot. You can also use HSV Histogram to detect how many distinct colors you have.

HSV histogram

If you still want RGB than compare differently

You have pen color0=(r0,g0,b0) and pixel color=(r,g,b) so compute distance between them:

d=((r-r0)*(r-r0))+((g-g0)*(g-g0))+((b-b0)*(b-b0))

No need for sqrt. Now you just compute d for every color you have (pens) and choose the smallest d ... You can also use less precise:

d=abs(r-r0)+abs(g-g0)+abs(b-b0)

If you do not know the colors prior this and do not want to use histograms

  1. form a (re)color table (set of distinct visible colors you will set to each found new pen)
  2. create empty list of found colors
  3. process all pixels of input image
  4. compute distance d to all found colors in a list
  5. if d is smaller then some treshold constant the pixels belong to that color in found colors list. Else add it as new found color.
  6. recolor pixel with color from recolor table.

This will eliminate the shading and anti-aliasing color distortions. You can also ignore the recolor table and use the color from the found colors list. This process is form of Color Quantization.

[Edit1] After using HSV color and recoloring to found color list (no histogram) I got this result:

HSV simple recolor

This shows that your image has not the same lighting conditions (not a render but real photo). So Ilumination normalization should improve this even more. Also I use 2 tresholds one for gray scales and one for colors ... to distinguish the two ... Also you can detect background color by:

  • pixel count (should be much bigger then the color of text)
  • dispersion along the image (should cover large area with relatively high density uniformly dispersed ... Text is localized)

Here C++/VCL source for this:

backbuffer bmp; // source and target image
struct _color { DWORD rgb; int h,s,v; };    // color entry in (re)color table
_color ld_rgb(DWORD rgb)                    // just RGB -> HSV conversion
    {
    const int _b=0;
    const int _g=1;
    const int _r=2;
    const int _a=3;
    union { DWORD dd; BYTE db[4]; } c;
    double r,g,b,min,max,del,h,s,v,dr,dg,db;
    c.dd=rgb;
    r=c.db[_r]; r/=255.0;
    g=c.db[_g]; g/=255.0;
    b=c.db[_b]; b/=255.0;
    min=r; if (min>g) min=g; if(min>b) min=b;
    max=r; if (max<g) max=g; if(max<b) max=b;
    del=max-min;
    v=max;
    if (del<=0.1) { h=0; s=0; } // grayscale
    else{
        s=del/max;
        dr=(((max-r)/6.0)+(del/2.0))/del;
        dg=(((max-g)/6.0)+(del/2.0))/del;
        db=(((max-b)/6.0)+(del/2.0))/del;
        if      (fabs(r-max)<1e-10) h=db-dg;
        else if (fabs(g-max)<1e-10) h=(1.0/3.0)+dr-db;
        else if (fabs(b-max)<1e-10) h=(2.0/3.0)+dg-dr;
        if (h<0.0) h+=1.0;
        if (h>1.0) h-=1.0;
        }
    _color ccc;
    ccc.rgb=rgb;
    ccc.h=255.0*h;
    ccc.s=255.0*s;
    ccc.v=255.0*v;
    return ccc;
    }
void recolor() // this is the recolor you want
    {
    // load input jpg file to bmp image
    TJPEGImage *jpg=new TJPEGImage();
    jpg->LoadFromFile("in.jpg");
    bmp.bmp->Assign(jpg);
    bmp.resize(bmp.bmp->Width,bmp.bmp->Height);
    delete jpg;

    // recolor bmp
    int i,x,y,d;
    _color c0,c1;
    List<_color> col;                   // color list
    col.num=0;                          // clear colro list
    for (y=0;y<bmp.ys;y++)              // process all pixels
     for (x=0;x<bmp.xs;x++)
        {
        c0=ld_rgb(bmp.pyx[y][x]);       // pixel color -> hsv

        if ((c0.h==0)&&(c0.s==0))       // compare it to found colors (grayscales)
         for (i=0;i<col.num;i++)
            {
//          i=-1; c1.rgb=0x00202020; break;
            c1=col[i];
            if ((c1.h!=0)||(c1.s!=0)) continue;
            d=abs(c1.v-c0.v);
            if (d<32) { i=-1; break; }  // match found ?
            }
        else                            // compare it to found colors
         for (i=0;i<col.num;i++)
            {
//          i=-1; c1.rgb=0x0000FF00; break;
            c1=col[i];
            if ((c1.h==0)&&(c1.s==0)) continue;
            d=(abs(c1.h-c0.h))+(abs(c1.s-c0.s));
            if (d<50) { i=-1; break; }  // match found ?
            }
        if (i>=0) { c1=c0; col.add(c1); }   // if not add new color
        bmp.pyx[y][x]=c1.rgb;               // recolor;
        }
    bmp.bmp->Canvas->Brush->Style=bsClear;
    bmp.bmp->Canvas->Font->Color=0x00802040;
    bmp.bmp->Canvas->TextOutA(5,0,"Found colors: "+AnsiString(col.num));
    bmp.bmp->Canvas->Brush->Style=bsSolid;
    for (d=16,i=0;i<col.num;i++)
     for (y=d;y<=d+d;y++)
      for (x=d*i+1;(x<d*i+d)&&(x<bmp.xs);x++)
       bmp.pyx[y][x]=col[i].rgb;
    }
  • List<T> l; is dynamic array like std::vector<T> ... represents T l[l.num];
  • backbuffer bmp; is mine image class ... bmp.bmp Holds GDI bitmap and bmp.xs,bmp.ys is the resolution
  • col holds found colors ...

[Edit1] bi-cubic Illumination normalization

I recently was rewriting my DIP lib upgrading mine illumination normalization so I give a shot at your input image (as one of many test images) and here the result (with forced (detected) empty space recolor):

normalized image

As you can see the middle red-ish lighting spot is gone. You can try your algo on this so you know if applying illumination normalization helps before encoding it (it is a bit complicated if done properly). This one is done like this:

  1. create grid (table) for your image

    each cell contains average color and cumulative delta (noise) of the cell area. Also single flag telling if cell is paper or ink. Cell size should be around <0.5 - 1.5> of min detail size (like letter or pen width ...)

  2. set all cells with high delta as ink the rest as paper

  3. compute average color of all paper cells combined
  4. each paper cell neighboring ink cell

    set as ink if its average color is too far from the global average paper color. Be carefull not to take those newly ink set cells as neighbor condition for this step. (use temp flag or different flag bit and restore after this is done...

  5. find 16 control points uniformly dispersed along image (use only paper cells)

    they should be around coordinates 0%,33%,66%,100% of image resolution So the bi-cubic interpolation is valid.

  6. for each pixel

    bi-cubically compute the cell color and call it c0 Then apply normalization to pixel (in RGB space!!!):

    • pixel+=global_avg_color-c0;

    This will equalize the paper color along the whole image to very close match to global_avg_color. Leaving the non paper details undistorted.

  7. optionally recolor all paper cells area with global_avg_color

    This is not necessary but it will eliminate much of the noise from background. Like paper texture ...

Community
  • 1
  • 1
Spektre
  • 49,595
  • 11
  • 110
  • 380
  • Many thanks for the reply. Its very helpful. though I am still trying to grasp the theory of what you explained. I did a bit in the morning and now studying it again now. I'm may have some questions later. In the mean time, an observation.... because i took a camera photo of images to post my camera has further distorted the original colors when posted..... – user4666 Nov 02 '15 at 21:37
  • .for instance the A4 I posted has been distorted from white to grey. But lots of thanks for your reply. I've got some questions ... would post in a bit – user4666 Nov 02 '15 at 21:52
  • Thanks. first how do I get the HSV data of the colors listed? Meaning the H value, the S value and the V value Second how do i convert to RGB and finally maybe most importantly is there a java version of your code, I am working completely in Java only. I think first and second questions are there but not yet explicit enough for me to see yet. Many thanks – user4666 Nov 02 '15 at 22:16
  • @user4666 I do not code in JAVA at all so I do not know but for HSV/RGB conversions see: http://www.javascripter.net/faq/rgb2hsv.htm http://stackoverflow.com/q/3018313/2521214 and https://en.wikipedia.org/wiki/HSL_and_HSV or translate mine conversion (`ld_rgb`) to JAVA (it should be almost the same). BTW For this task you do not need the reverse `HSV -> RGB` conversion. If you need JAVA add its TAG to the question ... Also if you google `HSV to RGB` and `JAVA` you should find some source (like my first link) – Spektre Nov 03 '15 at 07:25
  • @user4666 added illumination normalization process to answer (edit1) you can try the sample image if it actually helps you before trying to coding it (I code it and experiment with it for 2 days while knowing what I was doing so it is not that easy to implement... but the results are worth it). – Spektre Nov 13 '15 at 14:52
  • Thanks for the new update a couple days ago. I've going over the algorithm since. There are still some parts that i don't fully grasp. To save hassling you, do you know of any online resource (for beginners) on this topic - particularly the first part- detecting distinct colors (using your method)? I was trying to covert your code (in the first answer to java, but my lack of sufficient knowledge on the theory is hindering me). Many thanks – user4666 Nov 16 '15 at 05:48
  • @user4666 distinct color is any color not present in already found colors list. .... so you take a pixel compare to every color in list. if the distance is smaller then some treshold to any of them you handle it as that color otherwise add pixel color to the list as new distinct color. That is all just single for loop and single if statement ... – Spektre Nov 16 '15 at 08:17