3D-images. It started sometime in the late nineties. One of the most
cited papers1 in this area shows how to use eigenvalues of the
local second order derivatives to identify tubular structures
heuristically. It also shows how to combine the results on multiple scales.
Raw Data |
After Pre-processing |
assume any global structure. Therefore, when the data does not have
evidence for tubular structures, you still do not see them, and
furthermore, you have to have a method that allows you to say "points
x and y lie on the same tube". This, and determining properties of the
tubes, are the problems which most of the followupr research strives
to solve.
I wanted to have a quick-and-dirty approach to the problem. For a
given seed point, I would like to know the connectivity of the tubular
structure to this seed point. I pre-processed the raw image using the
method of Sato et al.1, determined the seed point, the graph is
defined implicitly over the 3D-image using voxel-neighborhoods, so we
could in theory just run Dijkstra on it. I was lazy, however and did
not want to implement it all.
BGL Grid Graph
A quick google search turned up
boost::grid_graph
, a relatively newmember of the boost graph library (BGL). It defines a graph on a grid
without representing everything. Neat. The documentation is still a
bit shortish and sadly, only a 6-neighborhood (in 3D) is supported,
but who'd expect a free lunch?
Usability is (at first) easy as pie:
typedef grid_graph<3> graph_t; typedef graph_traits<graph_t> Traits; typedef Traits::edge_descriptor edge_descriptor; typedef Traits::vertex_descriptor vertex_descriptor; // ... boost::array<vidx_t, 3> lengths = { { 256, 256, 256 } }; graph_t graph(lengths, false); // do not wrap any dimensions
Defining properties of edges in a grid graph
Contrary to graphs with internal properties, we have to define
external properties for
graph_t
. This is currently not documentedwell, and it took me some time to figure out:
- In most BGL examples, nodes are indexed using an integer type. The
grid_graph
, however, uses n-tuples of typeboost::array<n>
.
This means, most examples cannot by applied togrid_graph
in a
straight-forward way: You cannot index an array by a tuple without
doing some tricks first.
- We would like to have an arbitrary function of the nodes as the
weight. Most BGL examples simply use an array for whichever
property they put in their graph.
The first problem can be solved using shared array property maps
(which do not seem to be well-documented either):
shared_array_property_map<vertex_descriptor, property_map<graph_t, vertex_index_t>::const_type> p_map(num_vertices(graph), get(vertex_index, graph)); shared_array_property_map<double, property_map<graph_t, vertex_index_t>::const_type> d_map(num_vertices(graph), get(vertex_index, graph));We will use
p_map
as our predecessor map, which maps a vertex index (integer)to a vertex descriptor (n-tuple).
The second map
d_map
is a mapping from vertex index to double, whichwill be our distance map.
Finally, we need to create a
edge_weight
for graph_t
, and itshould be a function, not an array. For this, I peaked W.P. McNeills
Boost-Implicit-Graph Example on GitHub.
struct edge_weight_map; namespace boost { template<> struct property_map< graph_t, edge_weight_t > { typedef edge_weight_map type; typedef edge_weight_map const_type; }; } /* Map from edges to weight values */ struct edge_weight_map { typedef double value_type; typedef value_type reference; typedef edge_descriptor key_type; typedef boost::readable_property_map_tag category; const graph_t& m_graph; edge_weight_map(const graph_t& g) :m_graph(g) { } reference operator[](key_type e) const; // implemented below }; typedef boost::property_map<graph_t, boost::edge_weight_t>::const_type const_edge_weight_map; typedef boost::property_traits<const_edge_weight_map>::reference edge_weight_map_value_type; typedef boost::property_traits<const_edge_weight_map>::key_type edge_weight_map_key; namespace boost{ // PropertyMap valid expressions edge_weight_map_value_type get(const_edge_weight_map pmap, edge_weight_map_key e) { return pmap[e]; } // ReadablePropertyGraph valid expressions const_edge_weight_map get(boost::edge_weight_t, const graph_t&g) { return const_edge_weight_map(g); } edge_weight_map_value_type get(boost::edge_weight_t tag, const graph_t& g, edge_weight_map_key e) { return get(tag, g)[e]; } } // whoa, lots of typedefs, but now we can write the function that we // wanted to write: Map edges to weights! edge_weight_map::reference edge_weight_map::operator[](key_type e) const { vertex_descriptor t = target(e,m_graph); vertex_descriptor s = source(e,m_graph); // f = f(t,s) return f; }That was more nasty than I expected, but it is still not that much
code to write.
Representing the data in a Boost Multi-Array
Finally, we need to represent 3D-data. I hate dealing with raw memory,
so I always wrap it in some convenient abstracting structure first
chance I get. 3D-arrays are definitely a case for Boost Multi-Array.
Its not a big deal using it, but I want to advertise its simplicity here:
typedef boost::multi_array_ref<unsigned char, 3> array_type; std::ifstream dat("raw.dat", std::ios::in | std::ios::binary); unsigned char* data = new unsigned char[256*256*256]; dat.read((char*)data,256*256*256); array_type A(data,boost::extents[256][256][256]); dat.close();By the way, you can also change the base of this array as well as the
memory order, so if you need to port Matlab (TM) code to a programming
language (no adjective here on purpose), you can't get much simpler.
Finish line: Running Dijkstra
This is trivial now and straight from the book:
boost::array<vidx_t, 3> start = { { 109, 129, 24 } }; dijkstra_shortest_paths(graph, start ,predecessor_map(p_map) .distance_map(d_map) );Note the "." before
distance_map
, this is a named argument calleddistance_map
, which is set to d_map
. The same goes for p_map
.Thresholded Distances |
the raw image:
Traced paths starting at thresholded raw data |
No comments:
Post a Comment