Calculate the distances between points of different colours

Hi all,

I’m trying to calculate the distances between points of different colours, as shown in this image:

The goal is to use a macro to print the distances from the red dot to the yellow dot and then the yellow dot to the purple dot. I believe the best way to do this is to either:

  1. Use a macro to print the coordinates of the red dots, and then a macro to print the coordinates of the closest yellow dots. Then a second macro to print the coordinates of the purple dots closest to a given yellow dot. I would also like to calculate the angle of the line relative to the center point, but I believe I can do this in Excel if I’m given the XY coordinates of each point as well.

  2. Drawing lines between the red and yellow points and the yellow and purple points, printing the length and angle of those lines.

Ideally being able to do both would be good, but either would work for my purposes (calculating the distance and angle of the lines between the points from red to yellow and yellow to purple).

What is the best way to do this via macro? There are some examples (like this) but I don’t believe it 1) selects for points based on colour or 2) picks the next closest point automatically.

Hello Ophthalmologist -

I’ve outlined how you might do this in a jython (python) script,
below. One could write essentially the same script in the IJ
Macro language (or in other scripting languages), but I chose
jython as a matter of preference.

You basically need to do two high-level things: First, find the
dots and determine their colors; and second, link the “triplets”
of points together and calculate the distances and angles.

The sample image you posted is quite clean – finding the dots
and colors can be done straightforwardly in a number of different
ways. Using the machinery of Analyze Particles... is
probably overkill, but it’s built in and convenient, so that’s how I
find the dots. To assign one of the three colors to each of the dots,
I separate the RGB image into hue, saturation, and (ignored)
brightness images, and use the hue to assign colors.

Linking the dots is potentially more tricky. Again, in your case
the sample image is quite clean – simplistically looking for
minimum distances works. There is one imperfection in the sample
image – one blue (purple?) dot is missing. This could break or
make more complicated the linking algorithm, but the simple
work-around of starting the linking process with the blue dots
works. The remaining red-yellow two-dot “stub” is then linked
with a separate small piece of code.

If you want to roughly match the script to gui menu commands,
look at:
Image > Type > "HSB Stack"
Image > Stacks > Stack to Images
Analyze > Analyze Particles...
Analyze > Tools > Roi Manager...
and then in the ROI Manager, click on "Measure".

When applied to the right images, the above will give you a
Results Table with hue (“Mean”) and location (“X” and “Y”)
of the dots in it.

I’m not aware of any built-in tools that will link the dots
up into triplets for you and calculate the distances and
angles. So that part of the script is just python that
doesn’t use any ImageJ-specific facilities.

Here is the script:


import math

from java.awt import Color

from ij import IJ
from ij import ImagePlus
from ij.measure import Measurements
from ij.measure import ResultsTable
from ij.plugin.filter import Analyzer
from ij.plugin.filter import ParticleAnalyzer
from ij.plugin.frame import RoiManager
from ij.process import ImageConverter
from ij.process import ImageProcessor

impdots = IJ.openImage ('dots.png')

# get hue and saturation as separate ImagePlus's (ignore brightness)
impdup = impdots.duplicate()
ic = ImageConverter (impdup)
ims = impdup.getStack()

imphue = ImagePlus ('hue', ims.getProcessor (1))
impsat = ImagePlus ('saturation', ims.getProcessor (2))


# because the hue = 0 of the red dots matches the black background
# we use the saturation image to find the dots and the hue image to
# get their colors

# clear any old results from the Roi Manager
rm = RoiManager.getRoiManager()
# run Analyze Particles on saturation image to locate dots
pa = ParticleAnalyzer(ParticleAnalyzer.ADD_TO_MANAGER, 0, None, 0.01, 150.0)

impsat.getProcessor().threshold (63)
impsat.getProcessor().setThreshold (255, 255, ImageProcessor.NO_LUT_UPDATE)
pa.analyze (impsat)


# dots are now in the Roi Manager

# clear any old results from the Results Table
rt = ResultsTable.getResultsTable()
rt.reset() ('Results')

# run Measure on hue image to get the hue (and location) of dots
Analyzer.setMeasurements (Measurements.MEAN | Measurements.CENTROID)
rm = RoiManager.getRoiManager()
rm.runCommand (imphue, 'Measure')

# now we have the dots by color in the Results Table

# hard-wired hue numbers to identify dot colors
hueRange = 3.0
hueRed = 0.0
hueYellow = 36.0
hueBlue = 182.0

# dot indices and locations by color
# (the rXInd, etc., are for linking the triplets)
redInd = []
redX = []
redY = []
rXInd = []
yellowInd = []
yellowX = []
yellowY = []
yRInd = []
blueInd = []
blueX = []
blueY = []
bYInd = []

for  i in range (rt.size()):
    hue = rt.getValue ('Mean', i)
    if  abs (hue - hueRed) <= hueRange:
        redInd.append (i)
        redX.append (rt.getValue ('X', i))
        redY.append (rt.getValue ('Y', i))
        rXInd.append (-1)
    if  abs (hue - hueYellow) <= hueRange:
        yellowInd.append (i)
        yellowX.append (rt.getValue ('X', i))
        yellowY.append (rt.getValue ('Y', i))
        yRInd.append (-1)
    if  abs (hue - hueBlue) <= hueRange:
        blueInd.append (i)
        blueX.append (rt.getValue ('X', i))
        blueY.append (rt.getValue ('Y', i))
        bYInd.append (-1)

print  'number of dots found:'
print  '   red:    ', len (redInd)
print  '   yellow: ', len (yellowInd)
print  '   blue:   ', len (blueInd)

# link up the triplets
# we start from the blue end because we know we are missing a blue dot

for  iB in range (len (blueInd)):   # link blue to yellow
    minDist = float ('inf')
    minInd = -1
    for  iY in range (len (yellowInd)):
        if  yRInd[iY] != -1:  continue
        dist = ((yellowX[iY] - blueX[iB])**2 + (yellowY[iY] - blueY[iB])**2)**0.5
        if  dist < minDist:
            minInd = iY
            minDist = dist
    if  minInd == -1:
        msg = 'yellow dot not found for iB = ' + str (iB) + ', blueInd = ' + str (blueInd (iB))
        raise Exception (msg)
    iY = minInd
    bYInd[iB] = iY
    minDist = float ('inf')
    minInd = -1
    for  iR in range (len (redInd)):   # link yellow to red
        if  rXInd[iR] != -1:  continue
        dist = ((redX[iR] - yellowX[iY])**2 + (redY[iR] - yellowY[iY])**2)**0.5
        if  dist < minDist:
            minInd = iR
            minDist = dist
    if  minInd == -1:
        msg = 'red dot not found for iY = ' + str (iY) + ', yellowInd = ' + str (yellowInd (iY))        
        raise Exception (msg)
    iR = minInd
    yRInd[iY] = iR
    rXInd[iR] = 0   # flag this red dot as being linked

# calculate and print out lengths and angles and draw connections

ipdots = impdots.getProcessor()
ipdots.setColor (

print 'triplet-# dist-B-Y dist-Y-R angle'
for  iB in range (len (blueInd)):
    # bI = blueInd[iB]
    iY = bYInd[iB]
    iR = yRInd[iY]
    xB = blueX[iB]
    yB = blueY[iB]
    xY = yellowX[iY]
    yY = yellowY[iY]
    xR = redX[iR]
    yR = redY[iR]
    dx1 = xY - xB
    dy1 = yY - yB
    dx2 = xR - xY
    dy2 = yR - yY
    d1 = (dx1**2 + dy1**2)**0.5
    d2 = (dx2**2 + dy2**2)**0.5
    th = math.degrees (math.atan2 (dx2, dy2) - math.atan2 (-dx1, -dy1))
    if  th < -180.0:  th += 360.0
    if  th >  180.0:  th -= 360.0
    print iB, d1, d2, th
    # draw connections
    xBi =  int (round (xB))
    yBi =  int (round (yB))
    xYi =  int (round (xY))
    yYi =  int (round (yY))
    xRi =  int (round (xR))
    yRi =  int (round (yR))
    ipdots.drawLine (xBi, yBi, xYi, yYi)
    ipdots.drawLine (xYi, yYi, xRi, yRi)

# find the stub for missing blue dot -- verify only one
# find unlinked yellow dot
cntY = 0
iYS = -1
for  iY in range (len (yellowInd)):
    if  yRInd[iY] == -1:
        iYS = iY
        cntY += 1
if  cntY != 1:
    raise Exception ('stub yellow-dot miscount')
# find unlinked red dot
cntR = 0
iRS = -1
for  iR in range (len (redInd)):
    if  rXInd[iR] == -1:
        iRS = iR
        cntR += 1
if  cntR != 1:
    raise Exception ('stub red-dot miscount')

# print stub length and draw stub line
xY = yellowX[iYS]
yY = yellowY[iYS]
xR = redX[iRS]
yR = redY[iRS]
d = ((xR - xY)**2 + (yR - yY)**2)**0.5
print 'stub distance =', d
xYi =  int (round (xY))
yYi =  int (round (yY))
xRi =  int (round (xR))
yRi =  int (round (yR))
ipdots.setColor (
ipdots.drawLine (xYi, yYi, xRi, yRi)
impdots.setTitle ('connect-the-dots')
IJ.saveAs (impdots, 'PNG', 'connect-the-dots.png')

(You can run the script by opening the Script Editor:
File > New > Script..., and in the Script Editor, running:
File > Open... (or paste the script into an empty Script
Editor window and select Language > "Python"), and then
click the “Run” button. You will need to put a copy of
dots.png into the appropriate directory or edit the path
to dots.png in the script.)

Here are the results printed out by the script:

number of dots found:
   red:     12
   yellow:  12
   blue:    11
triplet-# dist-B-Y dist-Y-R angle
0 130.383662254 69.2408087931 -155.970490171
1 109.250031698 50.6277016988 -143.788030476
2 44.3636319975 90.4149755462 175.01435899
3 95.1722392274 43.6422750343 -126.210432418
4 144.517157768 46.6055586342 173.492995617
5 77.6209655486 54.8054956823 -147.134726801
6 76.3758682074 30.1225814329 176.376328381
7 58.2358591356 105.042393697 155.780886085
8 68.7213357568 61.661278901 -165.549150906
9 137.410420261 118.925502738 131.517072162
10 128.450918742 59.6041728272 -139.049983839
stub distance = 50.6482341776

And here is your sample image with added lines that link the
triplets together:

Thanks, mm


Excellent, thank you so much! Going to try this out.

Loved the explanation as well. I learned a lot!