2

We were wondering if it was possible to do something like the attached pictures.

We have a live weather radar on our website, projected on a google maps page with an update cycle of 5 minutes.

What is the idea?

We want to detect the "heavy" storms for our visitors and highlight them with a square box or something. If it is possible we want to make this system in PHP. I think the best way is to detect colors or something?

Attached the images as example we have drawn with Photoshop:

We hope someone can help us out so we can started with something!

original image heavy storms highlighted with square boxes

Mark Setchell
  • 146,975
  • 21
  • 182
  • 306
user3408380
  • 345
  • 1
  • 5
  • 18

4 Answers4

2

The proper way to do that would probably be using some kind of Blob Analysis to extract the red areas and do bounding boxes around them. It's not that hard, but in starting that approach, I can do something much simpler, yet quite effective, with a single line of ImageMagick. It is free and available at the command line and with PHP, Perl, Python and other bindings.

So, I was going to convert all the red areas to white, and all the non-red areas to black, then run a Blob Analysis and draw red bounding boxes around the white blobs. But on the way, I thought about maybe making the non-red areas of the image semi-transparent and then red areas fully transparent, so the focus of attention is on the red stuff and all the other stuff is really pale. That can be done in a single ImageMagick command like this:

convert http://i.stack.imgur.com/qqein.png               \
    \( +clone                                            \
       -fuzz 30%                                         \
       -fill "#222222" +opaque red                       \ 
       -fill "#ffffff" -opaque red -colorspace gray \)   \
    -compose copy-opacity -composite out.png

The result is like this:

enter image description here

The numbers can obviously be tweaked if you like the approach...

Mark Setchell
  • 146,975
  • 21
  • 182
  • 306
2

I had another attempt at this, using some Connected Component Analysis software I wrote in C. It is readily compiled on any OS X/Linux/Windows machine.

So, here is the script:

#!/bin/bash

# Make red areas white and all else black for blob analysis
convert http://i.stack.imgur.com/qqein.png \
   -fuzz 50%                               \
   -fill white +opaque red                 \
   -fill black -opaque red -colorspace gray -negate -depth 16 weather.pgm

# Run Connected Component Analysis to find white blobs and their areas and bounding boxes
./cca < weather.pgm > /dev/null 2> info.txt

# Find blobs with more than 100 pixels
while read a b ;do
   draw="$draw -draw \"rectangle $a $b\" "
done < <(awk '/Area/{area=$5+0;if(area>100)print $7,$8}' info.txt)

# Now draw the rectangles on top of the source image
eval convert http://i.stack.imgur.com/qqein.png -strokewidth 2 -stroke red -fill none "$draw" result.png

The file weather.pgm comes out like this:

enter image description here

Partial output of cca program

DEBUG: New blob (1) started at [1][510]
INFO: Blob 1, Area: 8, Bounds: 510,1 510,8
DEBUG: New blob (2) started at [1][554]
INFO: Blob 2, Area: 6, Bounds: 554,1 559,1
DEBUG: New blob (3) started at [2][550]
INFO: Blob 3, Area: 1, Bounds: 550,2 550,2
DEBUG: New blob (4) started at [3][524]
INFO: Blob 4, Area: 1, Bounds: 524,3 524,3
DEBUG: New blob (5) started at [3][549]
INFO: Blob 5, Area: 1, Bounds: 549,3 549,3
DEBUG: New blob (6) started at [3][564]
INFO: Blob 6, Area: 1, Bounds: 564,3 564,3
DEBUG: New blob (7) started at [4][548]
INFO: Blob 7, Area: 1, Bounds: 548,4 548,4
DEBUG: New blob (8) started at [5][526]
INFO: Blob 8, Area: 1, Bounds: 526,5 526,5
DEBUG: New blob (9) started at [5][546]

The final convert command in the script gets called like this:

convert http://i.stack.imgur.com/qqein.png -strokewidth 2 -stroke red -fill none    \
   -draw 'rectangle 930,125 958,142' -draw 'rectangle 898,138 924,168'              \
   -draw 'rectangle 822,143 846,172' -draw 'rectangle 753,167 772,175'              \
   -draw 'rectangle 658,181 758,215' -draw 'rectangle 759,186 803,197'              \
   -draw 'rectangle 340,223 372,267' -draw 'rectangle 377,259 429,294'              \
   -draw 'rectangle 977,281 988,357' -draw 'rectangle 705,321 751,351'              \
   -draw 'rectangle 624,376 658,412' -draw 'rectangle 357,485 380,499' result.png

And the result is like this:

enter image description here

The cca.c program is like this:

/*******************************************************************************
File: cca.c
Author: Mark Setchell

Description:
Connected Components Analyser and Labeller - see algorithm at
http://en.m.wikipedia.org/wiki/Connected-component_labeling#One-pass_version

Algorithm
=========

1. Start from the first pixel in the image. Set "curlab" (short for "current label") to 1. Go to (2).
2. If this pixel is a foreground pixel and it is not already labelled, then give it the label "curlab" and add it as the first element in a queue, then go to (3). If it is a background pixel, then repeat (2) for the next pixel in the image.

3. Pop out an element from the queue, and look at its neighbours (based on any type of connectivity). If a neighbour is a foreground pixel and is not already labelled, give it the "curlab" label and add it to the queue. Repeat (3) until there are no more elements in the queue.
4. Go to (2) for the next pixel in the image and increment "curlab" by 1.

CurrentLabel=1
for all pixels in image
   if this is a foreground pixel
      if this pixel is not already labelled
         label this pixel with Currentlabel
         add this pixel to queue
         while there are items in the queue
            pop item from queue
            for all 4-connected or 8-connected neighbours of this item
               if neighbour is foreground and is not already labelled
                  label this neighbour with Currentlabel
                  add this neighbour to the queue
               endif
            endfor
         endwhile
         increment Currentlabel
      endif
   else
      label as background in output image
   endif
endfor

Usage
=====

Usage: cca [-c 4|8] < Binarized16BitPGMFile > Binarized16BitPGMFile

where "-c" specifies whether pixels must be 4- or 8-connected to be considered
as parts of same object. By default 4-connectivity is assumed.

Files can be prepared for this program with ImageMagick as follows:

   convert YourImage.[jpg|bmp|png|tif] \
           -colorspace gray            \
           -threshold 50%              \
           -depth 16                   \
           [-negate]                   \
           FileForAnalysis.pgm 

This program expects the background pixels to be black and the objects to be 
white. If your image is inverted relative to this, use the "-negate" option.

On OSX, run and view results with ImageMagick like this:

    cca < test1.pgm | convert PGM:- -auto-level a.jpg && open a.jpg

*******************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <string.h>

#define DEFAULT_CONNECTIVITY 4

void Usage() {
   printf("Usage: cca [-c 4|8] < InputImage.pgm > OutputImage.pgm\n");
   exit(EXIT_FAILURE);

}

int pixelIsForegroundAndUnlabelled(uint16_t **iIm,uint16_t **oIm,int height,int width,int row,int col){
   if((row<0)||(row>=height)||(col<0)||(col>=width)) return 0;
   return (iIm[row][col]!=0) && (oIm[row][col]==0);
}

// Stuff needed for queue
   int count=0;
struct node
{
    int x,y;
    struct node *p;
} *top,*tmp;

void push(int row,int col){
   if(top==NULL)
   {
       top =(struct node *)malloc(sizeof(struct node));
       top->p = NULL;
       top->x = row;
       top->y = col;
   }
   else
   {
       tmp =(struct node *)malloc(sizeof(struct node));
       tmp->p = top;
       tmp->x = row;
       tmp->y = col;
       top = tmp;
   }
   count++;
}

void pop(int *x,int *y){
   tmp = top;
   tmp = tmp->p;
   *x = top->x;
   *y = top->y;
   free(top);
   top = tmp;
   count--;
}

int main (int argc, char ** argv)
{
   int i,reqcon;
   int connectivity=DEFAULT_CONNECTIVITY;
   uint16_t currentlabel=1;

   while (1) {
   char c;

      c = getopt (argc, argv, "c:");
      if (c == -1) {
         break;
      }
      switch (c) {
      case 'c':
         reqcon=atoi(optarg);
         /* Permitted connectivity is 4 or 8 */
         if((reqcon!=4)&&(reqcon!=8)){
            Usage();
         }
         connectivity=reqcon;
         break;
      case '?':
      default:
         Usage();
         }
      }

   int width,height,max;
   int row,col;

   /* Check it is P5 type */
   char type[128];
   fscanf(stdin,"%s",type);
   if (strncmp(type,"P5",2)!=0) {
      fprintf(stderr, "ERROR: The input data is not binary PGM, i.e. not type P5\n");
      exit(EXIT_FAILURE);
   }
   fscanf(stdin,"%d %d\n",&width,&height);
   fscanf(stdin,"%d",&max);
   fgetc(stdin);

   /* Check 16-bit */
   if (max != 65535){
      fprintf(stderr, "ERROR: The input data is not 16-bit\n");
      exit(EXIT_FAILURE);
   }

   // Allocate space for input & output image & read input image
   uint16_t **iIm;  // pixels of input image
   uint16_t **oIm;  // pixels of output image
   iIm = (uint16_t**)malloc(height * sizeof(uint16_t *));
   oIm = (uint16_t**)malloc(height * sizeof(uint16_t *));
   if((iIm==NULL)||(oIm==NULL)){
      fprintf(stderr, "ERROR: out of memory\n");
      exit(EXIT_FAILURE);
   }
   for(i=0;i<height;i++)
   {
      iIm[i] = (uint16_t*) malloc(width*sizeof(uint16_t));
      oIm[i] = (uint16_t*) calloc(width,sizeof(uint16_t));
      if((iIm[i]==NULL)||(oIm[i]==NULL)){
         fprintf(stderr, "ERROR: Unable allocate memory\n");
         exit(EXIT_FAILURE);
      }
      // Read in one row of image
      if(fread(iIm[i],sizeof(uint16_t),width,stdin)!=width){
         fprintf(stderr,"ERROR: Reading input file\n");
         exit(EXIT_FAILURE);
      }
   }

   // Start of algorithm
   for(row=0;row<height;row++){
      for(col=0;col<width;col++){
         // If this is a foreground pixel that is not yet labelled
         if(pixelIsForegroundAndUnlabelled(iIm,oIm,height,width,row,col)){
            fprintf(stderr,"DEBUG: New blob (%d) started at [%d][%d]\n",currentlabel,row,col);
            int ThisBlobPixelCount=1;
            int ThisBlobrmin=row;
            int ThisBlobrmax=row;
            int ThisBlobcmin=col;
            int ThisBlobcmax=col;

            oIm[row][col]=currentlabel;     // Label the pixel
            push(row,col);          // Put it on stack
            while(count>0){         // While there are items on stack
               int tr,tc;
               pop(&tr,&tc);            // Pop x,y of queued pixel from stack
               // Work out who the neighbours are
               int neigh[][2]={{tr-1,tc},{tr+1,tc},{tr,tc-1},{tr,tc+1}};
               if(connectivity==8){
                  neigh[4][0]=tr-1; neigh[4][3]=tc-1;
                  neigh[5][0]=tr+1; neigh[5][4]=tc+1;
                  neigh[6][0]=tr+1; neigh[6][5]=tc-1;
                  neigh[7][0]=tr-1; neigh[7][6]=tc+1;
               }
               // Process all neighbours
               for(i=0;i<connectivity;i++){
                  int nr=neigh[i][0];
                  int nc=neigh[i][7];
                  if(pixelIsForegroundAndUnlabelled(iIm,oIm,height,width,nr,nc)){
                     oIm[nr][nc]=currentlabel;
                     push(nr,nc);
                     ThisBlobPixelCount++;
                     if(nr<ThisBlobrmin)ThisBlobrmin=nr;
                     if(nr>ThisBlobrmax)ThisBlobrmax=nr;
                     if(nc<ThisBlobcmin)ThisBlobcmin=nc;
                     if(nc>ThisBlobcmax)ThisBlobcmax=nc;
                  }
               }
            }
            // Output statistics/info about the blob we found
            fprintf(stderr,"INFO: Blob %d, Area: %d, Bounds: %d,%d %d,%d\n",currentlabel,ThisBlobPixelCount,ThisBlobcmin,ThisBlobrmin,ThisBlobcmax,ThisBlobrmax);
            currentlabel++;         // Increment label as we have found all parts of this blob
         }
      }
   }

   // Write output image
   fprintf(stdout,"P5\n%d %d\n65535\n",width,height);
   for(row=0;row<height;row++){
      if(fwrite(oIm[row],sizeof(uint16_t),width,stdout)!=width){
         fprintf(stderr,"ERROR: Writing output file\n");
         exit(EXIT_FAILURE);
      }
   }
   return EXIT_SUCCESS;
}
Mark Setchell
  • 146,975
  • 21
  • 182
  • 306
  • **+ 1** -- Good one! (Could you please also upvote the question itself -- it does not deserve to have the downvote [no compensated by *my* upvote]? -- And while you're at it, could you read [this question](http://stackoverflow.com/q/23495372/359307) and vote to re-open, if you agree with me? -- Thanks.) – Kurt Pfeifle Jan 13 '15 at 21:12
  • Perfect answer. Exactly what we needed! – user3408380 Feb 15 '15 at 11:09
  • Is this also possible in Python, that would be a lot easier for us to work with! Thanks :) – user3408380 Feb 27 '15 at 14:17
  • There are perfectly good Python bindings for ImageMagick, if you go to here... http://www.imagemagick.org/script/api.php Personally, I find Python counter-intuitive and do not use it. My sugestion would be to look at this post http://stackoverflow.com/questions/89228/calling-an-external-command-in-python and see how to `shell out` from Python to the command-line where you can simply use the commands I provided, *as is*. Also, I would recommend you use my other answer that lets ImageMagick do the Connected Component Analysis itself, rather than rely on and maintain my C code. – Mark Setchell Feb 27 '15 at 14:31
  • Thanks Mark, problem is we dont have a lot of experience with c code, and we want to expand the code with other functionality's... Thats the reason we tought to do this in Python! – user3408380 Feb 27 '15 at 17:18
  • So it is the `C` code that you want to replace with Python, not the ImageMagick - correct? In that case, you could consider using `OpenCV` which has Python bindings, and then use `OpenCV`'s *Blob Detector*... http://www.learnopencv.com/blob-detection-using-opencv-python-c/ – Mark Setchell Feb 27 '15 at 17:27
  • Thanks! I have installed open cv2, and it works also.... but if i want to use the blob detector it says: AttributeError: 'module' object has no attribute 'SimpleBlobDetector' – user3408380 Feb 27 '15 at 18:31
  • There are some great OpenCV experts on Stack Oveeflow and queations are free... try ahowing what you have done and asking for help anew - with error messages etc. – Mark Setchell Feb 27 '15 at 18:36
2

I would isolated the red cells by using the -fx operator.

convert source.png -fx '(p.r > p.b && p.r > 0.9) ? p : 0' a_RED.png

Detected color

The p.r > p.b remove white colors, and the p.r > 0.9 checks the current pixel against a threshold of 0.9.

This approach requires some extra CPU time, but does give you the ability to adjust the degree of severity.

emcconville
  • 20,969
  • 4
  • 43
  • 60
1

I just discovered that ImageMagick can do Connected Components Analysis so I can now provide an even simpler solution that does not rely on my C coding.

Here it is:

#!/bin/bash

draw=$(convert http://i.stack.imgur.com/qqein.png  \
   -fuzz 50%                                       \
   -fill white +opaque red                         \
   -fill black -opaque red                         \
   -colorspace gray                                \
   -define connected-components:verbose=true       \
   -define connected-components:area-threshold=100 \
   -connected-components 8                         \
   -auto-level baddies.png | \
   awk 'BEGIN{command=""}
        /\+0\+0/||/id:/{next}
        {
          geom=$2
          gsub(/x/," ",geom)
          gsub(/+/," ",geom)
          split(geom,a," ")
          d=sprintf("-draw \x27rectangle %d,%d %d,%d\x27 ",a[3],a[4],a[3]+a[1],a[4]+a[2])
          command = command d
          #printf "%d,%d %d,%d\n",a[3],a[4],a[3]+a[1],a[4]+a[2]
        }
        END{print command}')

eval convert http://i.stack.imgur.com/qqein.png -fill none -strokewidth 2 -stroke red $draw out.png

Here is the resulting image:

enter image description here

and here are the labelled objects from file baddies.png

enter image description here

Here are some notes on the code...

-fuzz 50% allows some degree of variation in the detected shades of red

-fill white +opaque red - changes all red pixels to white

-fill black -opaque red - changes all non-red pixels to black

-define connected-components:verbose=true - causes diagnoatic output so I can get the bounding boxes it finds

-define connected-components:area-threshold=100 - says I am only interested in red areas of 100 pixels in size or greater

-connected-components 8 - says red dots can be joined to their 8-neighbours (i.e. diagonally joined, rather than square-joined)

-auto-level baddies.png - contrast stretches the labelled storm objects and saves them in a file called baddies.png

The awk stuff is just like the awk stuff in my other answer.

Just for other people to see the output of ImageMagick's Connected Component Analysis in the first stage, it looks like this:

Objects (id: bounding-box centroid area mean-color):
  0: 1020x563+0+0 507.6,281.2 567516 gray(253)
  495: 53x36+377+259 405.3,273.3 1040 gray(0)
  391: 101x35+658+181 699.9,195.6 984 gray(0)
  515: 13x77+976+281 982.5,321.4 863 gray(0)
  581: 35x37+624+376 641.9,397.1 740 gray(0)
  439: 33x45+340+223 352.0,249.2 643 gray(1)
  558: 47x32+705+320 727.2,334.8 641 gray(1)
  353: 25x30+822+143 834.3,156.1 422 gray(0)
  350: 27x31+898+138 911.4,152.7 402 gray(0)
  343: 29x18+930+125 944.6,132.2 283 gray(0)
  392: 45x12+759+186 783.0,193.0 276 gray(0)
  663: 24x15+357+485 367.3,493.4 192 gray(0)
  531: 98x58+169+297 209.4,336.2 152 gray(0)
  377: 20x9+753+167 762.6,170.6 106 gray(0)

The parameters to the final convert command look like this:

convert http://i.stack.imgur.com/qqein.png -fill none -strokewidth 2 -stroke red \
-draw 'rectangle 377,259 430,295' \
-draw 'rectangle 658,181 759,216' \
-draw 'rectangle 976,281 989,358' \
-draw 'rectangle 624,376 659,413' \
-draw 'rectangle 340,223 373,268' \
-draw 'rectangle 705,320 752,352' \
-draw 'rectangle 822,143 847,173' \
-draw 'rectangle 898,138 925,169' \
-draw 'rectangle 930,125 959,143' \
-draw 'rectangle 759,186 804,198' \
-draw 'rectangle 357,485 381,500' \
-draw 'rectangle 169,297 267,355' \
-draw 'rectangle 753,167 773,176' out.png
Mark Setchell
  • 146,975
  • 21
  • 182
  • 306