1

In order to detect inselbergs from a geographic region, I've downloaded topographic imagery (relief and declivity) from that region. The declivity images seems best suited for the task.

Declivity image

After applying a gaussian blur (or a much faster common blur, with ImageMagick), the image seems ready for automatic detection.

Declivity image after gaussian blur

Now I'm wondering the best/fastest way to detect these white stains on the black background. My first idea is to use a simple function (no external library) that works like the "bucket paint" function of drawing programs, calculating the area of an object lighter than a predefined threshold. Problem is: since the images are quite large, 5400x3600 pixels, the usual recursion function will probably cause a stack overflow, specially on massive mountain ranges like below.

Declivity image of a mountain range

So, any suggestions? I think the ideal language may be C++ (perhaps JavaScript). I'm not used to Python. Maybe it gets easier using a library like OpenCV, but maybe the problem is too trivial to ask for an external library (including the implied learning curve).


The TIFF images came from here (I can convert them to other format for processing). The example image is at the 17S42 quadricule ("Declividade" option), close to the coordinates 18°S 41°W. In the middle image (above), each "white ball" is an inselberg. The precision will depend on the chosen threshold of gray.

Cris Luengo
  • 55,762
  • 10
  • 62
  • 120
Rodrigo
  • 4,706
  • 6
  • 51
  • 94
  • Maybe you could add an image showing your expected output for one of the inputs? – Mark Setchell Jan 27 '20 at 20:47
  • @MarkSetchell The expected output is just the calculation of the area (in pixels), latitude and longitude (of the centroid) of each individual stain. A table, actually, not an image. – Rodrigo Jan 27 '20 at 20:52
  • Are your images actually geo-rectified TIFFs then? Can you share one if so? Also, can you mark up a copy using Photoshop or somesuch with what you consider to be a good detection of an inselberg? – Mark Setchell Jan 27 '20 at 20:58
  • @MarkSetchell The TIFF images came from [here](http://www.webmapit.com.br/inpe/topodata/) (I can convert them to other format for processing). The example image is at the 17S42 quadricule ("Declividade" option), close to the coordinates 18°S 41°W. In the middle image (above), each "white ball" is an inselberg. The precision will depend on the chosen threshold of gray. I hope this solves your questions? – Rodrigo Jan 27 '20 at 22:16
  • So, for the middle image you expect around 23 lines of output, one for each inselberg and each line will give the lat/lon of the centroid, and its area in pixels? – Mark Setchell Jan 27 '20 at 22:23
  • @MarkSetchell Exactly. – Rodrigo Jan 27 '20 at 22:33

2 Answers2

4

In short, you'll get a very poor estimate of size using your method. The edges of the blurred regions, no matter what threshold you pick, do not correspond to the edges of the inselbergs you want to measure. Instead, I suggest you follow the recipe below. I'm using DIPlib in Python for this (disclaimer: I'm an author). The Python bindings are a thin layer on the C++ library, it's fairly simple to translate the Python code below to C++ (it's just easier for me to develop it interactively in Python). Install with pip install diplib.

I downloaded the original height data from the link you provided (rather than the declivity). DIPlib can directly read floating-point valued TIFF files, so there is no need for any special conversion. I cropped a region similar to the one used by OP for this demo, but there is no reason to not apply the method to the whole tile.

import diplib as dip

height = dip.ImageRead('17S42_ZN.tif')
height.SetPixelSize(0.000278, 'rad')  # not really radian, but we don't have degrees
height = height[3049:3684, 2895:3513];

The code also sets the pixel size according to data in the TIFF file (using units of radian, because DIPlib doesn't do degrees).

image height

Next, I apply the top-hat filter with a specific diameter (25 pixels). This isolates all peaks with a diameter of 25 pixels or less. Adjust this size according to what you think the maximum width an inselberg should be.

local_height = dip.Tophat(height, 25)

In effect, the result is a local height, the height above some baseline determined by the size of the filter.

image local_height

Next, I apply a hysteresis threshold (double threshold). This yields a binary image, thresholded at 100m above the baseline, where the terrain goes above 200m above that baseline. That is, I decided that an inselberg should be at least 200m above the baseline, but cut each of them off at 100m. At this height we'll be measuring the size (area). Again, adjust the thresholds as you see fit.

inselbergs = dip.HysteresisThreshold(local_height, 100, 200)

image inselbergs

Now all that is left is measuring the regions we found:

labels = dip.Label(inselbergs)
result = dip.MeasurementTool.Measure(labels, features=['Size', 'Center'])
print(result)

This outputs:

   |       Size |                  Center | 
-- | ---------- | ----------------------- | 
   |            |       dim0 |       dim1 | 
   |     (rad²) |      (rad) |      (rad) | 
-- | ---------- | ---------- | ---------- | 
 1 |  1.863e-05 |     0.1514 |    0.01798 | 
 2 |  4.220e-05 |     0.1376 |    0.02080 | 
 3 |  6.214e-05 |    0.09849 |    0.04429 | 
 4 |  6.492e-06 |     0.1282 |    0.04710 | 
 5 |  3.022e-05 |     0.1354 |    0.04925 | 
 6 |  4.274e-05 |     0.1510 |    0.05420 | 
 7 |  2.218e-05 |     0.1228 |    0.05802 | 
 8 |  1.932e-05 |     0.1420 |    0.05689 | 
 9 |  7.690e-05 |     0.1493 |    0.06960 | 
10 |  3.285e-05 |     0.1120 |    0.07089 | 
11 |  5.248e-05 |     0.1389 |    0.07851 | 
12 |  4.637e-05 |     0.1096 |    0.09016 | 
13 |  3.787e-05 |    0.07146 |     0.1012 | 
14 |  2.133e-05 |    0.09046 |    0.09908 | 
15 |  3.895e-05 |    0.08553 |     0.1064 | 
16 |  3.308e-05 |    0.09972 |     0.1143 | 
17 |  3.277e-05 |    0.05312 |     0.1174 | 
18 |  2.581e-05 |    0.07298 |     0.1167 | 
19 |  1.955e-05 |    0.04038 |     0.1304 | 
20 |  4.846e-05 |    0.03657 |     0.1448 | 

(Remember, where it says 'rad' it is really degrees.) An area in square degrees is a bit weird, but you can convert this to square meters since you know the location on the globe. It might in fact be easier to translate the pixel sizes to meters before the computations.

The values given here for 'Center' are with respect to the top-left pixel, if we hadn't cropped the tile to begin with, we could have added the coordinates for the tile (as can be obtained from the corresponding tag in the TIFF file): (-42.0, -17.0).


In C++ the code should look like this:

#include <diplib/simple_file_io.h>
#include <diplib/morphology.h>
#include <diplib/segmentation.h>
#include <diplib/regions.h>
#include <diplib/measurement.h>

//...

dip::Image height = dip::ImageRead("17S42_ZN.tif");
height.SetPixelSize(0.000278 * dip::Units::Radian());
height = height.At(dip::Range(3049, 3684), dip::Range(2895, 3513));

dip::Image local_height = dip::Tophat(height, 25);

dip::Image inselbergs = dip::HysteresisThreshold(local_height, 100, 200);

dip::Image labels = dip::Label(inselbergs);
dip::MeasurementTool measurementTool;
dip::Measurement result = measurementTool.Measure(labels, {}, {"Size", "Center"});
std::cout << result;
Cris Luengo
  • 55,762
  • 10
  • 62
  • 120
  • Simply awesome! I'm gonna need a few days to dig both answers, so nevermind the delay. – Rodrigo Jan 28 '20 at 00:59
  • Excellent answer! Thank you. – Mark Setchell Jan 28 '20 at 11:45
  • I'm trying to follow your solution, but I'm having problems installing the PyDIP package. Can you please take a look at this [other question](https://stackoverflow.com/q/59952057/1086511)? – Rodrigo Jan 28 '20 at 16:26
  • @Rodrigo -- [Wouter gave the right answer](https://stackoverflow.com/a/59953038/7328782), you'll have to install from source. There's a binary distribution for Windows, for some reason we haven't been able to produce one for Linux yet. But it's pretty straight-forward to build from source. Just make sure that the version of Python found by CMake matches the one that you intend to use PyDIP with. – Cris Luengo Jan 28 '20 at 16:38
  • @Rodrigo: Also, if you end up writing a C++ program using DIPlib, you'll want to build it from sources anyway. – Cris Luengo Jan 28 '20 at 16:46
  • I built it from sources, but can't compile a test case. I've downloaded `display.cpp`, commented out everything but the first two lines (`ImageReadICS` and `DIP_STACK_TRACE_THIS`) and the last (`Spin`), leaving the try/catch structure intact. My command line is `g++ display.cpp -I/home/user/Downloads/diplib-master/include`. I should link to the compiled binaries too, but can't find where they've gone. `sudo find / -iname 'diplib*'` only return makefiles and the like. After a few warnings, I got the error message: `display.cpp:(.text+0x14d): undefined reference to 'dip::viewer::ShowSimple(...` – Rodrigo Feb 04 '20 at 20:22
  • @Rodrigo: The `cmake` command output should tell you where the library will be installed when you do `make install`. By default this is under `/usr/local/`, which means you might need root privileges to install. You can change this location to somewhere else if you prefer. When compiling your executable, you'll need to add `-lDIP` to your command line (the library is `libDIP.so`). Furthermore, the `display.cpp` example also links to the viewer, which is a separate library: `-lDIPviewer`. If in the default locations, use `-I/usr/local/include` and `-L/usr/local/lib`. – Cris Luengo Feb 04 '20 at 20:39
  • Thank you. I had the privileges, so installed to the default location (couldn't find where the library was installed from the CMakeOutput.log, however). The main library is at `/usr/local/lib/libDIP.so`, but the second can't be found. Is it called `libDIPviewer.so`? If I use `-I/usr/local/include` and `-L/usr/local/lib`, I get a `fatal error: dipviewer.h: No such file or directory`. If I include the previous include directory (`.../diplib-master/include`), I get the first error again. – Rodrigo Feb 04 '20 at 20:57
  • @Rodrigo: Remove the reference to `dipviewer.h` from the source file, and all calls to functions in the `dip::viewer::` namespace. You don't really need the viewer to do image processing. It looks like you didn't compile the viewer, likely because of a missing dependency. If you do want to build the viewer, you need to build the library following the instructions here: https://github.com/DIPlib/diplib/blob/master/INSTALL_Linux.md -- this shows which packages to install if you use Ubuntu. – Cris Luengo Feb 04 '20 at 21:04
  • All that's left is `#include "diplib.h"`, `#include "diplib/file_io.h"` and, inside main(), `auto grey = dip::ImageReadICS( "/home/usuario/Downloads/diplib-master/examples/cermet.ics" );`. Still got errors when trying `g++ display.cpp -I/usr/local/include -L/usr/local/lib`. Something like: `/usr/bin/ld: /tmp/ccgwMrXS.o: in function 'dip::ImageReadICS(...)': display.cpp:(...): undefined reference to 'dip::ImageReadICS(...)' collect2: error: ld returned 1 exit status` – Rodrigo Feb 04 '20 at 21:30
  • @Rodrigo: Don't forget to also add `-lDIP` to your build command. – Cris Luengo Feb 04 '20 at 21:37
  • Right, it worked. Thank you! In the end, all that was needed was the `-lDIP` option. – Rodrigo Feb 06 '20 at 12:10
  • It would be useful if you could post your code in C++, since the functions are different from the Python version. I managed to compile most of them, except for the last one, `MeasurementTool.Measure`. And that's just for compiling, I'm not even sure if I interpreted and modified them correctly. – Rodrigo Feb 06 '20 at 13:07
  • Found [here](https://diplib.github.io/diplib-docs/classdip_1_1MeasurementTool.html) how to use that function. However, why does the Python version only ask for 2 parameters, but the C++ asks for 5? Why do I have to send an image, beyond the labels? And which image should it be: the original, the one after Tophat, or the one after the HysteresisThreshold? Apparently the first two work, while the latter not even compile... Is there any difference between sending the original or the Tophat'ed images? – Rodrigo Feb 06 '20 at 14:36
  • @Rodrigo: The 2nd parameter to `dip::Measurement::Measure` is a gray-value image that you'd need if the requested features need that. You don't need it. In Python it is possible to skip parameters using the `name=value` syntax, which is not possible in C++. Hence in C++ it is mandatory to pass this gray-value image. A default-constructed image is sufficient (`{}`). The last 2 parameters can be left out, the default values are good. I have added the C++ version of each Python statement. I haven't tried compiling it, I might have made a mistake, but it should look more or less like that. – Cris Luengo Feb 06 '20 at 15:56
3

One way is to use ImageMagick connected components and extract the centroids from each white region after an appropriate threshold.

Here is the command line for that in ImageMagick 6. But you can use the C+ API, if you want.

Burred Image:

enter image description here

convert blur.png -auto-level -threshold 65% -type bilevel \
-define connected-components:verbose=true \
-define connected-components:mean-color=true \
-connected-components 4 binary.png |\
grep "gray(255)" | awk '{print $3}'


Centroids (in pixel coordinates):

915.9,367.4
873.4,76.8
936.4,269.5
664.8,212.0
598.2,554.3
954.0,59.3
732.6,446.0
353.5,696.4
69.7,848.0
750.4,357.6
537.5,609.9
832.0,230.9
517.9,539.1


Thresholded image:

enter image description here

fmw42
  • 46,825
  • 10
  • 62
  • 80
  • I didn't know one could do that using only ImageMagick! Awesome! – Rodrigo Jan 27 '20 at 23:46
  • 1
    You can do something similar in OpenCV by thresholding and then get the centroids of contours (or even using their connected components processing). See https://docs.opencv.org/4.1.1/d3/dc0/group__imgproc__shape.html#gadf1ad6a0b82947fa1fe3c3d497f260e0 and https://docs.opencv.org/4.1.1/d3/dc0/group__imgproc__shape.html#ga107a78bf7cd25dec05fb4dfc5c9e765f and https://www.pyimagesearch.com/2016/02/01/opencv-center-of-contour/ – fmw42 Jan 28 '20 at 01:09
  • I wonder how to apply Cris' answer using ImageMagick. The first step (Tophat) is simple: `convert in.png -morphology TopHat Disk:25 out.png`. What about the HysteresisThreshold? – Rodrigo Jan 31 '20 at 14:25