Search code examples
c++image-processingboost-graphgeotiff

How to load a TIFF image like a graph in C++ BOOST


I want do load a tiff image (GEOTIFF with pixels with float values) like graph in boost C++ (i'm a newbie in C++). My goal is use the bidirectional Dijkstra from source A to target B to get more performance.

Boost:GIL load tiif images:
std::string filename( "raster_clip.tif" );
rgb8_image_t img;
read_image( filename, img, tiff_tag() ); 

But how convert to Boost graph? I am reading the documentation and looking for examples but I have not yet been able to implement it.

Similar questions and examples that i found:

Shortest path graph algorithm help Boost;

http://www.geeksforgeeks.org/shortest-path-for-directed-acyclic-graphs/

I am currently using the scikit-image library and use skimage.graph.route_through_array function to load graph with array in python. I use GDAL to get an array by load image as suggested by @ustroetz in this example Here:

    raster = gdal.Open("raster.tiff")
    band = raster.GetRasterBand(1)
    array = band.ReadAsArray()

Example of TIFF (was converted to PNG after upload) is: This is a example of image


Solution

  • Ok, so read the PNG:

    I've cropped the whitespace border since it wasn't consistent anyways

    Reading And Sampling The Image

    using Img = boost::gil::rgb8_image_t; // gray8_image_t;
    using Px  = Img::value_type;
    
    Img img;
    //boost::gil::png_read_image("graph.png", img);
    boost::gil::png_read_and_convert_image("graph.png", img);
    auto vw = view(img);
    

    Next up, make sure we know the dimensions and how to address the center pixels for each cell:

    double constexpr cell_w = 30.409;
    double constexpr cell_h = 30.375;
    
    auto pixel_sample = [=](boost::array<size_t, 2> xy) -> auto& {
        return vw((xy[0]+.5)*cell_w, (xy[1]+.5)*cell_h);
    };
    
    auto const w= static_cast<size_t>(img.dimensions()[0] / cell_w);
    auto const h= static_cast<size_t>(img.dimensions()[1] / cell_h);
    

    Constructing The Graph

    Now let's make the graph. For this task a grid-graph seems in order. It should be w×h and not wrap around at the edges (if it should, change false to true):

    using Graph = boost::grid_graph<2>;
    Graph graph({{w,h}}, false);
    

    We want to attach weights at each edge. We can either use an old-fashioned external property map that's sized up-front:

    std::vector<double> weight_v(num_edges(graph));
    auto weights = boost::make_safe_iterator_property_map(weight_v.begin(), weight_v.size(), get(boost::edge_index, graph));
    

    Alternatively, we can use a dynamically allocating and growing property-map:

    auto weights = boost::make_vector_property_map<float>(get(boost::edge_index, graph));
    

    As a bonus, here's the equivalent approach using an associative property-map:

    std::map<Graph::edge_descriptor, double> weight_m;
    auto weights = boost::make_assoc_property_map(weight_m);
    

    Each of these are drop-in compatible and the choice is yours.

    Filling The Graph

    We simply iterate all edges, setting the cost from the colour difference:

    BGL_FORALL_EDGES(e, graph, Graph) {
        auto& from = pixel_sample(e.first);
        auto& to   = pixel_sample(e.second);
    
        // compare RED channels only
        auto cost = std::abs(from[0] - to[0]);
        put(weights, e, cost);
    }
    

    Note Consider normalizing weight to e.g. [0.0, 1.0) using the actual bit-depth of the source image

    Let's create a verification TIF so we can actually see where the samples were taken in the image:

    {
        BGL_FORALL_VERTICES(v, graph, Graph) {
            pixel_sample(v) = Px(255, 0, 123); // mark the center pixels so we can verify the sampling
        }
    
        boost::gil::tiff_write_view("/tmp/verification.tif", const_view(img));
    }
    

    The verification.tif ends up like (note the center pixel for each cell):

    enter image description here

    Bonus: Visualize The Grid Graph

    Let's write it to a Graphviz file:

    {
        auto calc_color = [&](size_t v) {
            std::ostringstream oss;
            oss << std::hex << std::noshowbase << std::setfill('0');
    
            auto const& from = pixel_sample(vertex(v, graph));
            oss << "#" << std::setw(2) << static_cast<int>(from[0])
                << std::setw(2) << static_cast<int>(from[1])
                << std::setw(2) << static_cast<int>(from[2]);
    
            return oss.str();
        };
    
        write_dot_file(graph, weights, calc_color);
    }
    

    This calculates the color from the same sample pixel and uses some Graphviz-specific magic to write to a file:

    template <typename Graph, typename Weights, typename ColorFunction>
    void write_dot_file(Graph const& graph, Weights const& weights, ColorFunction calc_color) {
        boost::dynamic_properties dp;
        dp.property("node_id",   get(boost::vertex_index, graph));
        dp.property("fillcolor", boost::make_transform_value_property_map(calc_color, get(boost::vertex_index, graph)));
        dp.property("style", boost::make_static_property_map<typename Graph::vertex_descriptor>(std::string("filled")));
        std::ofstream ofs("grid.dot");
    
        auto vpw = boost::dynamic_vertex_properties_writer { dp, "node_id" };
        auto epw = boost::make_label_writer(weights);
        auto gpw = boost::make_graph_attributes_writer(
                std::map<std::string, std::string> { },
                std::map<std::string, std::string> { {"shape", "rect"} },
                std::map<std::string, std::string> { }
            );
    
        boost::write_graphviz(ofs, graph, vpw, epw, gpw);
    }
    

    Which results in a grid.dot file like this.

    Next, let's layout using neato:

    neato -T png grid.dot -o grid.png
    

    And the result is:enter image description here

    FULL CODE LISTING

    #include <boost/gil/extension/io/png_dynamic_io.hpp>
    #include <boost/gil/extension/io/tiff_dynamic_io.hpp>
    #include <boost/graph/grid_graph.hpp>
    #include <boost/graph/iteration_macros.hpp>
    #include <boost/graph/graphviz.hpp>
    #include <iostream>
    
    template <typename Graph, typename Weights, typename ColorFunction>
    void write_dot_file(Graph const& graph, Weights const& weights, ColorFunction);
    
    int main() try {
        using Img = boost::gil::rgb8_image_t; // gray8_image_t;
        using Px  = Img::value_type;
    
        Img img;
        //boost::gil::png_read_image("/home/sehe/graph.png", img);
        boost::gil::png_read_and_convert_image("/home/sehe/graph.png", img);
        auto vw = view(img);
    
        double constexpr cell_w = 30.409;
        double constexpr cell_h = 30.375;
    
        auto pixel_sample = [=](boost::array<size_t, 2> xy) -> auto& {
            return vw((xy[0]+.5)*cell_w, (xy[1]+.5)*cell_h);
        };
    
        auto const w= static_cast<size_t>(img.dimensions()[0] / cell_w);
        auto const h= static_cast<size_t>(img.dimensions()[1] / cell_h);
    
        using Graph = boost::grid_graph<2>;
        Graph graph({{w,h}}, false);
    
    #if 0 // dynamic weight map
        auto weights = boost::make_vector_property_map<float>(get(boost::edge_index, graph));
        std::cout << "Edges: " << (weights.storage_end() - weights.storage_begin()) << "\n";
    
    #elif 1 // fixed vector weight map
        std::vector<double> weight_v(num_edges(graph));
        auto weights = boost::make_safe_iterator_property_map(weight_v.begin(), weight_v.size(), get(boost::edge_index, graph));
    
    #else // associative weight map
        std::map<Graph::edge_descriptor, double> weight_m;
        auto weights = boost::make_assoc_property_map(weight_m);
    #endif
    
        auto debug_vertex = [] (auto& v) -> auto& { return std::cout << "{" << v[0] << "," << v[1] << "}"; };
        auto debug_edge   = [&](auto& e) -> auto& { debug_vertex(e.first) << " -> "; return debug_vertex(e.second); };
    
        BGL_FORALL_EDGES(e, graph, Graph) {
            //debug_edge(e) << "\n";
            auto& from = pixel_sample(e.first);
            auto& to   = pixel_sample(e.second);
    
            // compare RED channels only
            auto cost = std::abs(from[0] - to[0]);
            put(weights, e, cost);
        }
    
        {
            auto calc_color = [&](size_t v) {
                std::ostringstream oss;
                oss << std::hex << std::noshowbase << std::setfill('0');
    
                auto const& from = pixel_sample(vertex(v, graph));
                oss << "#" << std::setw(2) << static_cast<int>(from[0])
                    << std::setw(2) << static_cast<int>(from[1])
                    << std::setw(2) << static_cast<int>(from[2]);
    
                return oss.str();
            };
    
            write_dot_file(graph, weights, calc_color);
        }
    
        {
            BGL_FORALL_VERTICES(v, graph, Graph) {
                pixel_sample(v) = Px(255, 0, 123); // mark the center pixels so we can verify the sampling
            }
    
            boost::gil::tiff_write_view("/tmp/verification.tif", const_view(img));
        }
    
    } catch(std::exception const& e) {
        std::cout << "Exception occured: " << e.what() << "\n";
    }
    
    template <typename Graph, typename Weights, typename ColorFunction>
    void write_dot_file(Graph const& graph, Weights const& weights, ColorFunction calc_color) {
        boost::dynamic_properties dp;
        dp.property("node_id",   get(boost::vertex_index, graph));
        dp.property("fillcolor", boost::make_transform_value_property_map(calc_color, get(boost::vertex_index, graph)));
        dp.property("style", boost::make_static_property_map<typename Graph::vertex_descriptor>(std::string("filled")));
        std::ofstream ofs("grid.dot");
    
        auto vpw = boost::dynamic_vertex_properties_writer { dp, "node_id" };
        auto epw = boost::make_label_writer(weights);
        auto gpw = boost::make_graph_attributes_writer(
                std::map<std::string, std::string> { },
                std::map<std::string, std::string> { {"shape", "rect"} },
                std::map<std::string, std::string> { }
            );
    
        boost::write_graphviz(ofs, graph, vpw, epw, gpw);
    }