283 lines
9.9 KiB
C++
Executable File
283 lines
9.9 KiB
C++
Executable File
/*
|
|
* Copyright 2016 The Cartographer Authors
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
#include "cartographer/io/xray_points_processor.h"
|
|
|
|
#include <cmath>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "Eigen/Core"
|
|
#include "absl/memory/memory.h"
|
|
#include "absl/strings/str_cat.h"
|
|
#include "cartographer/common/lua_parameter_dictionary.h"
|
|
#include "cartographer/common/math.h"
|
|
#include "cartographer/io/draw_trajectories.h"
|
|
#include "cartographer/io/image.h"
|
|
#include "cartographer/mapping/3d/hybrid_grid.h"
|
|
#include "cartographer/mapping/detect_floors.h"
|
|
#include "cartographer/transform/transform.h"
|
|
|
|
namespace cartographer {
|
|
namespace io {
|
|
namespace {
|
|
|
|
struct PixelData {
|
|
size_t num_occupied_cells_in_column = 0;
|
|
float mean_r = 0.;
|
|
float mean_g = 0.;
|
|
float mean_b = 0.;
|
|
};
|
|
|
|
class PixelDataMatrix {
|
|
public:
|
|
PixelDataMatrix(const int width, const int height)
|
|
: width_(width), data_(width * height) {}
|
|
|
|
int width() const { return width_; }
|
|
int height() const { return data_.size() / width_; }
|
|
const PixelData& operator()(const int x, const int y) const {
|
|
return data_.at(x + y * width_);
|
|
}
|
|
|
|
PixelData& operator()(const int x, const int y) {
|
|
return data_.at(x + y * width_);
|
|
}
|
|
|
|
private:
|
|
int width_;
|
|
std::vector<PixelData> data_;
|
|
};
|
|
|
|
float Mix(const float a, const float b, const float t) {
|
|
return a * (1. - t) + t * b;
|
|
}
|
|
|
|
// Convert 'matrix' into a pleasing-to-look-at image.
|
|
Image IntoImage(const PixelDataMatrix& matrix, double saturation_factor) {
|
|
Image image(matrix.width(), matrix.height());
|
|
float max = std::numeric_limits<float>::min();
|
|
for (int y = 0; y < matrix.height(); ++y) {
|
|
for (int x = 0; x < matrix.width(); ++x) {
|
|
const PixelData& cell = matrix(x, y);
|
|
if (cell.num_occupied_cells_in_column == 0.) {
|
|
continue;
|
|
}
|
|
max = std::max<float>(max, std::log(cell.num_occupied_cells_in_column));
|
|
}
|
|
}
|
|
|
|
for (int y = 0; y < matrix.height(); ++y) {
|
|
for (int x = 0; x < matrix.width(); ++x) {
|
|
const PixelData& cell = matrix(x, y);
|
|
if (cell.num_occupied_cells_in_column == 0.) {
|
|
image.SetPixel(x, y, {{255, 255, 255}});
|
|
continue;
|
|
}
|
|
|
|
// We use a logarithmic weighting for how saturated a pixel will be. The
|
|
// basic idea here was that walls (full height) are fully saturated, but
|
|
// details like chairs and tables are still well visible.
|
|
const float saturation =
|
|
std::min<float>(1.0, std::log(cell.num_occupied_cells_in_column) /
|
|
max * saturation_factor);
|
|
const FloatColor color = {{Mix(1.f, cell.mean_r, saturation),
|
|
Mix(1.f, cell.mean_g, saturation),
|
|
Mix(1.f, cell.mean_b, saturation)}};
|
|
image.SetPixel(x, y, ToUint8Color(color));
|
|
}
|
|
}
|
|
return image;
|
|
}
|
|
|
|
bool ContainedIn(const common::Time& time,
|
|
const std::vector<mapping::Timespan>& timespans) {
|
|
for (const mapping::Timespan& timespan : timespans) {
|
|
if (timespan.start <= time && time <= timespan.end) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
XRayPointsProcessor::XRayPointsProcessor(
|
|
const double voxel_size, const double saturation_factor,
|
|
const transform::Rigid3f& transform,
|
|
const std::vector<mapping::Floor>& floors,
|
|
const DrawTrajectories& draw_trajectories,
|
|
const std::string& output_filename,
|
|
const std::vector<mapping::proto::Trajectory>& trajectories,
|
|
FileWriterFactory file_writer_factory, PointsProcessor* const next)
|
|
: draw_trajectories_(draw_trajectories),
|
|
trajectories_(trajectories),
|
|
file_writer_factory_(file_writer_factory),
|
|
next_(next),
|
|
floors_(floors),
|
|
output_filename_(output_filename),
|
|
transform_(transform),
|
|
saturation_factor_(saturation_factor) {
|
|
for (size_t i = 0; i < (floors_.empty() ? 1 : floors.size()); ++i) {
|
|
aggregations_.emplace_back(
|
|
Aggregation{mapping::HybridGridBase<bool>(voxel_size), {}});
|
|
}
|
|
}
|
|
|
|
std::unique_ptr<XRayPointsProcessor> XRayPointsProcessor::FromDictionary(
|
|
const std::vector<mapping::proto::Trajectory>& trajectories,
|
|
FileWriterFactory file_writer_factory,
|
|
common::LuaParameterDictionary* const dictionary,
|
|
PointsProcessor* const next) {
|
|
std::vector<mapping::Floor> floors;
|
|
const bool separate_floor = dictionary->HasKey("separate_floors") &&
|
|
dictionary->GetBool("separate_floors");
|
|
const auto draw_trajectories = (!dictionary->HasKey("draw_trajectories") ||
|
|
dictionary->GetBool("draw_trajectories"))
|
|
? DrawTrajectories::kYes
|
|
: DrawTrajectories::kNo;
|
|
const double saturation_factor =
|
|
dictionary->HasKey("saturation_factor")
|
|
? dictionary->GetDouble("saturation_factor")
|
|
: 1.;
|
|
if (separate_floor) {
|
|
CHECK_EQ(trajectories.size(), 1)
|
|
<< "Can only detect floors with a single trajectory.";
|
|
floors = mapping::DetectFloors(trajectories.at(0));
|
|
}
|
|
|
|
return absl::make_unique<XRayPointsProcessor>(
|
|
dictionary->GetDouble("voxel_size"), saturation_factor,
|
|
transform::FromDictionary(dictionary->GetDictionary("transform").get())
|
|
.cast<float>(),
|
|
floors, draw_trajectories, dictionary->GetString("filename"),
|
|
trajectories, file_writer_factory, next);
|
|
}
|
|
|
|
void XRayPointsProcessor::WriteVoxels(const Aggregation& aggregation,
|
|
FileWriter* const file_writer) {
|
|
if (bounding_box_.isEmpty()) {
|
|
LOG(WARNING) << "Not writing output: bounding box is empty.";
|
|
return;
|
|
}
|
|
|
|
// Returns the (x, y) pixel of the given 'index'.
|
|
const auto voxel_index_to_pixel =
|
|
[this](const Eigen::Array3i& index) -> Eigen::Array2i {
|
|
// We flip the y axis, since matrices rows are counted from the top.
|
|
return Eigen::Array2i(bounding_box_.max()[1] - index[1],
|
|
bounding_box_.max()[2] - index[2]);
|
|
};
|
|
|
|
// Hybrid grid uses X: forward, Y: left, Z: up.
|
|
// For the screen we are using. X: right, Y: up
|
|
const int xsize = bounding_box_.sizes()[1] + 1;
|
|
const int ysize = bounding_box_.sizes()[2] + 1;
|
|
PixelDataMatrix pixel_data_matrix(xsize, ysize);
|
|
for (mapping::HybridGridBase<bool>::Iterator it(aggregation.voxels);
|
|
!it.Done(); it.Next()) {
|
|
const Eigen::Array3i cell_index = it.GetCellIndex();
|
|
const Eigen::Array2i pixel = voxel_index_to_pixel(cell_index);
|
|
PixelData& pixel_data = pixel_data_matrix(pixel.x(), pixel.y());
|
|
const auto& column_data = aggregation.column_data.at(
|
|
std::make_pair(cell_index[1], cell_index[2]));
|
|
pixel_data.mean_r = column_data.sum_r / column_data.count;
|
|
pixel_data.mean_g = column_data.sum_g / column_data.count;
|
|
pixel_data.mean_b = column_data.sum_b / column_data.count;
|
|
++pixel_data.num_occupied_cells_in_column;
|
|
}
|
|
|
|
Image image = IntoImage(pixel_data_matrix, saturation_factor_);
|
|
if (draw_trajectories_ == DrawTrajectories::kYes) {
|
|
for (size_t i = 0; i < trajectories_.size(); ++i) {
|
|
DrawTrajectory(
|
|
trajectories_[i], GetColor(i),
|
|
[&voxel_index_to_pixel, &aggregation,
|
|
this](const transform::Rigid3d& pose) -> Eigen::Array2i {
|
|
return voxel_index_to_pixel(aggregation.voxels.GetCellIndex(
|
|
(transform_ * pose.cast<float>()).translation()));
|
|
},
|
|
image.GetCairoSurface().get());
|
|
}
|
|
}
|
|
|
|
image.WritePng(file_writer);
|
|
CHECK(file_writer->Close());
|
|
}
|
|
|
|
void XRayPointsProcessor::Insert(const PointsBatch& batch,
|
|
Aggregation* const aggregation) {
|
|
constexpr FloatColor kDefaultColor = {{0.f, 0.f, 0.f}};
|
|
for (size_t i = 0; i < batch.points.size(); ++i) {
|
|
const sensor::RangefinderPoint camera_point = transform_ * batch.points[i];
|
|
const Eigen::Array3i cell_index =
|
|
aggregation->voxels.GetCellIndex(camera_point.position);
|
|
*aggregation->voxels.mutable_value(cell_index) = true;
|
|
bounding_box_.extend(cell_index.matrix());
|
|
ColumnData& column_data =
|
|
aggregation->column_data[std::make_pair(cell_index[1], cell_index[2])];
|
|
const auto& color =
|
|
batch.colors.empty() ? kDefaultColor : batch.colors.at(i);
|
|
column_data.sum_r += color[0];
|
|
column_data.sum_g += color[1];
|
|
column_data.sum_b += color[2];
|
|
++column_data.count;
|
|
}
|
|
}
|
|
|
|
void XRayPointsProcessor::Process(std::unique_ptr<PointsBatch> batch) {
|
|
if (floors_.empty()) {
|
|
CHECK_EQ(aggregations_.size(), 1);
|
|
Insert(*batch, &aggregations_[0]);
|
|
} else {
|
|
for (size_t i = 0; i < floors_.size(); ++i) {
|
|
if (!ContainedIn(batch->start_time, floors_[i].timespans)) {
|
|
continue;
|
|
}
|
|
Insert(*batch, &aggregations_[i]);
|
|
}
|
|
}
|
|
next_->Process(std::move(batch));
|
|
}
|
|
|
|
PointsProcessor::FlushResult XRayPointsProcessor::Flush() {
|
|
if (floors_.empty()) {
|
|
CHECK_EQ(aggregations_.size(), 1);
|
|
WriteVoxels(aggregations_[0],
|
|
file_writer_factory_(output_filename_ + ".png").get());
|
|
} else {
|
|
for (size_t i = 0; i < floors_.size(); ++i) {
|
|
WriteVoxels(
|
|
aggregations_[i],
|
|
file_writer_factory_(absl::StrCat(output_filename_, i, ".png"))
|
|
.get());
|
|
}
|
|
}
|
|
|
|
switch (next_->Flush()) {
|
|
case FlushResult::kRestartStream:
|
|
LOG(FATAL) << "X-Ray generation must be configured to occur after any "
|
|
"stages that require multiple passes.";
|
|
|
|
case FlushResult::kFinished:
|
|
return FlushResult::kFinished;
|
|
}
|
|
LOG(FATAL);
|
|
}
|
|
|
|
} // namespace io
|
|
} // namespace cartographer
|