amulllb
amulllb

Reputation: 3166

imagemagick get a co-ordinate for pixel with most common color


In short, I want to find out co-ordinate of a pixel with predominant color.

To be specific, I want to achieve the following:

  1. Find predominant color. By predominant, I mean a color that majority of the pixels in an image have (I have achieved it using histogram)

  2. After getting this color (in my case its black), I want to find a pixel that is black and its surrounded by only black pixels. Basically, the center of most concentrated area of black.

Till now, I could only get the predominant color.

convert src.png -format %c histogram:info: > x.txt
cat x.txt | awk '{print $1}' | sed 's/://g' > x1.txt
h=$(sort -n x1.txt | tail -1)
cat x.txt | grep "$h"
rm -rf x.txt

Result:

    169211: (  0,  0,  0,255) #000000 black

Now, I can also get all the co-ordinates for black

convert src.png txt: | grep black
469,799: (  0,  0,  0,255)  #000000  black
470,799: (  0,  0,  0,255)  #000000  black
471,799: (  0,  0,  0,255)  #000000  black
472,799: (  0,  0,  0,255)  #000000  black
473,799: (  0,  0,  0,255)  #000000  black
474,799: (  0,  0,  0,255)  #000000  black
475,799: (  0,  0,  0,255)  #000000  black
476,799: (  0,  0,  0,255)  #000000  black
477,799: (  0,  0,  0,255)  #000000  black
478,799: (  0,  0,  0,255)  #000000  black
...

But I need a random co-ordinate of a black pixel that is located at a place where there is a only black pixels around it...

I am using Linux and Imagemagick version 6.6.5

Upvotes: 2

Views: 2847

Answers (4)

Mark Setchell
Mark Setchell

Reputation: 207375

I like this approach using Euclidean distance morphology - it makes the wizard look rather demonic (!) and runs in under a second!

convert logo: -virtual-pixel none             \
              -morphology Distance Euclidean  \
              -auto-level -channel GB         \
              -threshold 85% 85.png

enter image description here

and thresholding at 95% and 99% gets you these:

enter image description here

enter image description here

And, if you want the text coordinates:

convert logo: -virtual-pixel none -morphology Distance Euclidean -auto-level -threshold 99% txt:- | grep white
151,328: (255,255,255,1)  #FFFFFF  white
152,328: (255,255,255,1)  #FFFFFF  white

Upvotes: 1

Mark Setchell
Mark Setchell

Reputation: 207375

Here is a different idea that runs in a fraction of a second...

If you are looking for large black areas, repeatedly tile the image into smaller and smaller tiles until you get a tile that is fully black, then choose the centre of that tile. Here is a little video showing the convergence. The process is looking at the semi-transparent red areas till one is found that only contains black.

enter image description here

Here is the code:

#!/bin/bash
# Set interesting pixels to black, others to white
convert logo: -fill black +opaque white -negate start.png

# Now tile, into 4,9,16,25 till we get a tile that is fully black
for i in $(seq 2 8); do
   rm tmp*.png 2> /dev/null
   convert -crop ${i}x${i}@ start.png tmp%d.png
   for f in tmp*png; do
     mean=$(identify -format "%[mean]" "$f")
     if [ "$mean" = "0" ]; then
        j=$((i*i))
        echo "$f" of $j tiles
        exit
     fi
   done
done

And the output is this:

tmp6.png of 9 tiles

Upvotes: 0

Kurt Pfeifle
Kurt Pfeifle

Reputation: 90203

(Updated to eliminate some weaknesses)

First, find your predominant color. You already seem to know how to do this, so I skip this step. (I also didn't check or verify your code for this...)

Your code has some weaknesses:

  1. You only clean up the x.txt, but not your x1.txt.

  2. You should drop the | sed 's/://g' part from your second command. It eliminates the : colon from your variable, but this can lead to h=21 (instead of h=21:) which leads to your grep "$h" finding all such lines:

     1: (221, 86, 77) #DD564D srgb(221,86,77)
     1: (221,196,192) #DDC4C0 srgb(221,196,192)
     1: (221,203,197) #DDCBC5 srgb(221,203,197)
    [...] 
    21: (255,255,255) #FFFFFF white
    

    If you keep it at h=21: you'll find the one line you are looking for! (Example to verify: use the built-in rose: image in place of your src.png to see what I mean.)

Second, apply a very small amount of blur on the image, by averaging each pixel with its 8 surrounding pixels for each location: -blur 1x65535 (this operation uses a 3x3 square kernel). After that step, in the resulting image only those pixels will remain purely black, which were surrounded by only black pixels in the original image.

Third, make all non-black pixels white: by applying a -fill white +opaque black -fill white -opaque black-operation on the image. (See also "Morphology", esp. "Erosion".) This junks all other colors from the image by making all non-black colors white and simplifies your search for pure black pixels. (Note: this doesn't work for such src.png files which do not contain at least one 3x3 pixels area with pure black pixels...)

Fourth: We have to account for pixels which are on the border of the image (these don't have 8 neighbours!), and hence we assign the color 'none' to these with -virtual-pixel none.

I'll use the ImageMagick built-in special picture named 'logo:' to demonstrate my approach:

convert logo: logo.png

As you can easily see, this image has white as its pre-dominant color. (So I switch the code for this example to make all white pixels black...)

Commandline so far:

convert                                                \
   logo:                                               \
  -virtual-pixel none                                  \
   $(for i in {1..2}; do echo " -blur 1x65535 "; done) \
  -fill black                                          \
  -opaque white                                        \
   2_blur-logo-virtpixnone.png

Here are the 2 images side by side:

  • logo.png on the left
  • 2_blur-log-virtpixnone.png on the right

 

Fifth: Leather, rinse, repeat.

Now let's apply a few more iterations of this algorithm, like 100, 500, 1000 and 1300, and lets also apply an annotation to the result so we know which image is which:

for j in 100 500 1000 1300; do
   convert                                                       \
      logo:                                                      \
     -virtual-pixel none                                         \
      $(for i in $(seq 1 ${j}); do echo " -blur 1x65535 "; done) \
     -fill black                                                 \
     -opaque white                                               \
     -gravity NorthEast -annotate +10+10 "$j iterations"         \
      ${j}_blur-logo-virtpixnone.png
done

As you can clearly see, my algorithm makes the black area converge towards that spot which you'd have intuitively guessed as being the 'center' of the white colored areas when looking at the original logo.png:

 

 

Once you arrive at an output image with no black spot left, you've iterated once too often. Go back one iteration. :-)

There should now be only a very limited number of candidate pixels which match your criteria.

Upvotes: 4

BehemothTheCat
BehemothTheCat

Reputation: 315

This is just a hunch and not a complete answer, but you could preprocess the image with a blur filter first. Then the pixels that are black in the blurred image must have had black neighborhoods in the original image.

This won't work for an arbitrary color, though.

Upvotes: 0

Related Questions