How to get XY coordinate of selection line crossing a ROI

line-profile
line-roi
selection
thresholding
imagej
fiji
Tags: #<Tag:0x00007fd6a0913150> #<Tag:0x00007fd6a0912c78> #<Tag:0x00007fd6a09127f0> #<Tag:0x00007fd6a09122c8> #<Tag:0x00007fd6a0911c88> #<Tag:0x00007fd6a0911828>

#1

I have binary images with a light background and a single dark object.

What I want to be able to do is draw a selection line across an image, and then export the XY coordinates of the first and last dark pixel along that line.

Essentially I feel like I want to be able to draw a selection line manually that includes the dark object (ROI) and the background, and then ‘trim’ that line by thresholding (the selection rather than the image) for the dark object of interest. I feel this should be a fairly straightforward task, but I haven’t been able to find anything that achieves what I want.

Many thanks!


#2

Good day Michael Wallace,

these are quite some feelings …

Could you please post a representative image with such an object and such a line?

This would help.

Regards

Herbie


#3

Sure!.

In the image I would like to retrieve the XY coordinates of the first and last black pixel along this line (the approximate locations of which are indicated by the added red circles).


#4

I should clarify that I understand that I could just manually mark these points with the Multi-point tool and Save as XY Coordinates. However, manually drawing the line is an important and necessary step, and so automatically generating the two sets of coordinates would save much time (I have several hundred images to process!).


#5

Michael Wallace,

don’t worry, I shall soon post a solution.

Best

Herbie


#6

Well Michael Wallace,

here is an ImageJ-macro that does what you’ve descibed:

 // START
if ( selectionType() < 0 ) exit( "Line selection required!" );
getSelectionCoordinates( x, y );
slp = slope( x[0], y[0], x[1], y[1] );
len = lengt( x[0], y[0], x[1], y[1] );
profile = getProfile();
extrema( profile );
profile = Array.findMaxima( profile, 0, 1 );
profile[1] = len - profile[1];
if ( profile.length > 2 )  exit( "Image must be binary!" );
intersection( x, y, slp, profile );
print( "1. Intersection:\tx = " + round(x[0]) + ";\ty = " + round(y[0]) + ";" );
print( "2. Intersection:\tx = " + round(x[1]) + ";\ty = " + round(y[1]) + ";" );
exit();
//-----------------------------------------------------
function slope( x_0, y_0, x_1, y_1 ) {
	return -( y_0 - y_1 ) / ( x_0 - x_1 );
}
function lengt( x_0, y_0, x_1, y_1 ) {
	return sqrt( pow( x_0 - x_1, 2 ) + pow( y_0 - y_1, 2 ) );
}
function extrema( p ) {
	for ( i = 1; i < profile.length; i++ ) {
		p[i-1] = abs( p[i] - p[i-1] );
	}
}
function intersection( xx, yy, s, p ) {
	for ( i = 0; i < 2; i++ ) {
		if ( i > 0 ) { sign = -1; } else { sign = 1; }
		dx = sign * sqrt( pow( p[i], 2 ) / ( 1 + pow( s, 2 ) ) );
		xx[i] += dx;
		if ( slp != 1/0 ) { yy[i] -= s * dx; } else { yy[i] += sign * p[i]; }
	}
}
// END

Paste the code to an empty macro window (Plugins >> New >> Macro) and run it.

Make sure your image is binary (Process >> Binary >> Make Binary)!
Line selections must be made from left to right (or from top to bottom).

The resulting coordinates are ±1 pixel correct.

HTH

Herbie


#7

Herbie you’re a star. Thank you so much for this - its just what I needed.

Just two minor (hopefully) follow-up questions relating to exporting the results. I would like the XY coordinates to be saved as a TXT file in the same way as you’d get from Save as XY Coordinates, e.g.:

X \tab Y
X \tab Y

(1) I’ve adjusted lines 12 and 13 of your code accordingly, but I find \t doesn’t produce a tab in the Log window. I could fix this outside ImageJ, but it would be nice if the results were already formatted exactly as needed.

(2) I would like the TXT file containing the result coordinates to have the same filename as the image (e.g. xx1234.jpg and xx1234.txt). When adding to the macro a save log command is it possible to extract the image’s filename to use in the txt file path?

Thanks once again for getting me this far. I hope I’m nopt pushing my luck with the additional questions!


#8

Good day Michael Wallace,

nice to hear that the macro is functional so far …

Here is the version that saves the coordinates as tab-delimited text-file to the same directory in which the open image resides:

// START
path = getDirectory("image") + File.nameWithoutExtension + ".txt";
if ( selectionType() < 0 ) exit( "Line selection required!" );
getSelectionCoordinates( x, y );
slp = slope( x[0], y[0], x[1], y[1] );
len = lengt( x[0], y[0], x[1], y[1] );
profile = getProfile();
extrema( profile );
profile = Array.findMaxima( profile, 0, 1 );
profile[1] = len - profile[1];
if ( profile.length > 2 )  exit( "Image must be binary!" );
intersection( x, y, slp, profile );
makeLine( x[0], y[0], x[1], y[1] );
txtData = "" + round(x[0]) + "\t" + round(y[0]) + "\n" + round(x[1]) + "\t" + round(y[1]);
File.saveString( txtData, path );
//print( "1. Intersection:\tx = " + round(x[0]) + ";\ty = " + round(y[0]) + ";" );
//print( "2. Intersection:\tx = " + round(x[1]) + ";\ty = " + round(y[1]) + ";" );
exit();
//-----------------------------------------------------
function slope( x_0, y_0, x_1, y_1 ) {
	return -( y_0 - y_1 ) / ( x_0 - x_1 );
}
function lengt( x_0, y_0, x_1, y_1 ) {
	return sqrt( pow( x_0 - x_1, 2 ) + pow( y_0 - y_1, 2 ) );
}
function extrema( p ) {
	for ( i = 1; i < profile.length; i++ ) {
		p[i-1] = abs( p[i] - p[i-1] );
	}
}
function intersection( xx, yy, s, p ) {
	for ( i = 0; i < 2; i++ ) {
		if ( i > 0 ) { sign = -1; } else { sign = 1; }
		dx = sign * sqrt( pow( p[i], 2 ) / ( 1 + pow( s, 2 ) ) );
		xx[i] += dx;
		if ( slp != 1/0 ) { yy[i] -= s * dx; } else { yy[i] += sign * p[i]; }
	}
}
// END

Regards

Herbie


#9

Thanks Herbie for your continued help with this.

Seems something has gone awry.

I noticed that the generated TXT files have the two sets of coordinates on the same line. It seems "\n" in line 14 isn’t work. Copy and pasting elsewhere seems to respect the line break though - so can’t think why not visible in Notepad.

Whilst trying to fix that, I noticed that re-running the macro produced different results despite the selection line having not been changed. For example, below are the three sets of results by hitting Run three consecutive times - as you can see they vary quite a bit:

2356 938
483 983

2430 936
307 987

2578 933
-45 996

Manually placed points (i.e. close to correct) are:

2274.0000 933.0000
660.0000 975.0000

As you can some of the automatically generated coordinates are way off what they are meant to be, and the lack of repeatable suggests something is not quite right.

Any ideas what’s going on? Am I missing something obvious?


#10

Well Michael Wallace,

evidently you are on a Windows machine and I can’t care for problems with that. Perhaps you need a “Carriage Return” in addition to “New Line” but this is up to you.

Concerning the second issue: It isn’t an issue!
Please look at the selection after you’ve run the macro: A new selection is set according to what you wrote earlier. If this is no longer desired, then please commment out the line:
makeLine( x[0], y[0], x[1], y[1] );

HTH

Herbie


#11

Michael Wallace,

and finally, are you sure you’ve respected the correct way of drawing the line selection?

And don’t forget that you need a perfectly binary image. In general, JEPG-compressed images are unsuited.

Greetings

Herbie


#12

Hi Michael,

You might want to consult this thread on the forum about this issue. I still don’t fully understand why the “/n” delimiter doesn’t work as expected - something to do with Notepad?

It is possible to add text to a new line of an open text file using the print(); function, and each new addition appears to go on a new line (I think, haven’t tried this).

So an edit to the code would be:

file = File.open(path);
txtData1 = "" + round(x[0]) + "\t" + round(y[0]);
txtData2 = "" + round(x[1]) + "\t" + round(y[1]);
print(file, txtData1);
print(file, txtData2);
File.close(file);

EDIT: On second thoughts… It doesn’t work, will attempt to fix, the thread I linked to has some info on how to get it to work properly.

EDIT2: Fixed.


#13

Michael,
Use plot line, all the information you seem to be asking for will be included in the plot.
Also if you press the shift key while defining the plot the line will be straight.
Hope this helps.
Bob


#14

Good day Bob,

could you please explain in greater detail how to get the desired result and are you sure about the task?

Regards

Herbie


#15

Hey Herbie!
When you use “plot line” use the line marker, this will draw a “plot Chart” . On that chart you will see a rapid upturn and then a rapid downturn. These are the stop and start points of the image. Then just hover the curser over these points for the coordinates, or print the results table for a listing from start to finish.

Hope this helps.
Bob


#16

Well Bob,

the plot will give you a start and an end value (1D) with respect to the line length but it won’t give you the 2D-coordinates (i.e. x_1, y_1, x_2, y_2) of the start and end with respect to the image plane.

My approach indeed uses the line plot and computes the desired 2D-coordinates with respect to the line beginning and the line ending …

Regards

Herbie


#17

Herbie and 7rebor thank you.

My ‘issue 2’

Concerning the second issue: It isn’t an issue!

I’m not sure what it was I said that implied I wanted the line to move (nor does it matter). In any case commenting(/removing) makeLine( x[0], y[0], x[1], y[1] ); has (as your suggested resolved the ‘problem’.

New lines

I still don’t fully understand why the “/n” delimiter doesn’t work as expected - something to do with Notepad?

I’m not sure whether it is exclusively a Windows Notepad issue as the TXT files were not being read correctly when imported into R. Needless to say though your suggested fix worked a charm. For the record, and to help any others, I replaced the following four lines from Herbie’s second code block with the six from 7rebor.
txtData = "" + round(x[0]) + "\t" + round(y[0]) + "\n" + round(x[1]) + "\t" + round(y[1]); File.saveString( txtData, path ); //print( "1. Intersection:\tx = " + round(x[0]) + ";\ty = " + round(y[0]) + ";" ); //print( "2. Intersection:\tx = " + round(x[1]) + ";\ty = " + round(y[1]) + ";" );

Plot line

smith_roberj, I was aware of the plot line tool and that it could be used to manually identify the binary transition. What I was after was an automated method that only required the line to be drawn. Herbie’s macro with the /n fix by 7rebor does this.

In summary

Thank you for your help. The macro now does exactly what I needed it to do. What a great community.

Final thought

respected the correct way of drawing the line selection?

I was just wondering if someone could explain to me why the direction the line is drawn matters? Does it determine the order the landmarks are returned?

The reason I ask is that it just so happens to be (and there’s no way anyone could know this) but the direction of left-to-right and top-to-bottom to be the counter-intuitive for me personally (I tend to draw from the other end, i.e. from the first landmark to the second). Certainly something I can live with / get used to, but was curious about the reason and whether it could be changed.

Here’s the final macro

// START
path = getDirectory("image") + File.nameWithoutExtension + ".txt";
if ( selectionType() < 0 ) exit( "Line selection required!" );
getSelectionCoordinates( x, y );
slp = slope( x[0], y[0], x[1], y[1] );
len = lengt( x[0], y[0], x[1], y[1] );
profile = getProfile();
extrema( profile );
profile = Array.findMaxima( profile, 0, 1 );
profile[1] = len - profile[1];
if ( profile.length > 2 )  exit( "Image must be binary!" );
intersection( x, y, slp, profile );
file = File.open(path);
txtData1 = "" + round(x[0]) + "\t" + round(y[0]);
txtData2 = "" + round(x[1]) + "\t" + round(y[1]);
print(file, txtData1);
print(file, txtData2);
File.close(file);
exit();
//-----------------------------------------------------
function slope( x_0, y_0, x_1, y_1 ) {
	return -( y_0 - y_1 ) / ( x_0 - x_1 );
}
function lengt( x_0, y_0, x_1, y_1 ) {
	return sqrt( pow( x_0 - x_1, 2 ) + pow( y_0 - y_1, 2 ) );
}
function extrema( p ) {
	for ( i = 1; i < profile.length; i++ ) {
		p[i-1] = abs( p[i] - p[i-1] );
	}
}
function intersection( xx, yy, s, p ) {
	for ( i = 0; i < 2; i++ ) {
		if ( i > 0 ) { sign = -1; } else { sign = 1; }
		dx = sign * sqrt( pow( p[i], 2 ) / ( 1 + pow( s, 2 ) ) );
		xx[i] += dx;
		if ( slp != 1/0 ) { yy[i] -= s * dx; } else { yy[i] += sign * p[i]; }
	}
}
// END


#18

Good day Michael Wallace,

nice to hear that you are happy with the solution.

Just some remarks:

  1. makeLine( x[0], y[0], x[1], y[1] );

I’m not sure what it was I said […]

I referred to:

and then ‘trim’ that line

So my “makeLine()”-command was meant to shorten the initial and long line selection to the line selection that exists solely along the object.

  1. The “\n”-issue is a windows problem.
    The text file is correctly written on my Mac and I suspect on every UNIX-based system.

  2. Direction of making line selections
    You can test it easily: Make the same lines selection, e.g. one time from left to right and then from right to left. And let the overhang be unsymmetrical. Do profile plots after each selection. You shall see that the plots follow the drawing direction.
    Of course one could change the macro code for other drawing directions and maybe one may even compensate for the drawing direction, but this is left to you!

Regards

Herbie


#19

Thanks Herbie, those comments make a lot of sense. Thanks for the clarifications. And in regard to

Of course one could change the macro code for other drawing directions and maybe one may even compensate for the drawing direction, but this is left to you!

True, but I think I’ll just get used to drawing left-to-right.


#20

I have had a further thought.

I’m using JPG's and so upon opening they are not binary. I would like to remove the manual step of making the image binary, making it part of the macro. The problem I’m having is that makeBinary() clears the current selection.

I came up with the idea of the macro (1) saving the roi, (2) making binary, (3) opening the .roi and (4) running your macro. This doesn’t seem to work - I don’t get any messages or errors, just the .txt file isn’t generated.

Any suggestions how I can get around this little binary issue?
(Herbie, I appreciate you might be bored by this thread at this stage)