Starting from 2021-07-01, all LRZ GitLab users will be required to explicitly accept the GitLab Terms of Service. Please see the detailed information at https://doku.lrz.de/display/PUBLIC/GitLab and make sure that your projects conform to the requirements.

Commit c2d7af29 authored by David Frank's avatar David Frank Committed by Tobias Lasser
Browse files

#97 Add PGM image writer

Write 2D containers to the PGM file format. This can be used to quickly
output and debug images.
parent 6c147734
Pipeline #413347 passed with stages
in 33 minutes and 15 seconds
......@@ -30,6 +30,7 @@
// IO headers
#include "EDFHandler.h"
#include "MHDHandler.h"
#include "PGMHandler.h"
// Logging headers
#include "Logger.h"
......
......@@ -8,12 +8,14 @@ set(ELSA_MODULE_EXPORT_TARGET elsa_${ELSA_MODULE_NAME}Targets)
set(MODULE_HEADERS
EDFHandler.h
MHDHandler.h
PGMHandler.h
ioUtils.h)
# list all the code files of the module
set(MODULE_SOURCES
EDFHandler.cpp
MHDHandler.cpp
PGMHandler.cpp
ioUtils.cpp)
......
#include "PGMHandler.h"
#include <ostream>
#include <iostream>
#include <exception>
namespace elsa
{
template <typename data_t>
void PGM::write(const DataContainer<data_t>& data, const std::string& filename)
{
std::ofstream ofs(filename, std::ios_base::out);
PGM::write(data, ofs);
}
template <typename data_t>
void PGM::write(const DataContainer<data_t>& data, std::ostream& stream)
{
// If `data_t` is float or double use that to cast values to, if it's
// `index_t` then use real_t for multiplications
using CastType = std::conditional_t<std::is_floating_point_v<data_t>, data_t, real_t>;
const auto dim = data.getDataDescriptor().getNumberOfDimensions();
if (dim != 2) {
throw std::invalid_argument("PGM:: Can only handle 2D data");
}
const auto dims = data.getDataDescriptor().getNumberOfCoefficientsPerDimension();
// Get maximum value, TODO: this should be inside DataContainer....
const auto maxValue = [&] {
auto tmp = std::numeric_limits<data_t>::min();
for (int i = 0; i < data.getSize(); ++i) {
if (data[i] > tmp)
tmp = data[i];
}
return tmp;
}();
// Scale all values from DataContainer to a range from [0, 255]
const auto scaleFactor = 255.f / static_cast<CastType>(std::ceil(maxValue));
// P2: Magic number specifying grey scale, then the two dimensions in the next line
// Then the maximum value of the image in our case always 255
stream << "P2\n" << dims[0] << " " << dims[1] << "\n" << 255 << "\n";
// Write data, ugly casts to silence warnings
for (int i = 0; i < data.getSize(); ++i) {
stream << static_cast<int>(static_cast<CastType>(data[i]) * scaleFactor) << "\n";
}
}
template void PGM::write(const DataContainer<float>&, const std::string&);
template void PGM::write(const DataContainer<double>&, const std::string&);
template void PGM::write(const DataContainer<index_t>&, const std::string&);
template void PGM::write(const DataContainer<float>&, std::ostream&);
template void PGM::write(const DataContainer<double>&, std::ostream&);
template void PGM::write(const DataContainer<index_t>&, std::ostream&);
} // namespace elsa
#pragma once
#include "elsaDefines.h"
#include "DataContainer.h"
#include "DataDescriptor.h"
#include "ioUtils.h"
namespace elsa
{
/**
* \brief class to write PGM images
*
* \author David Frank - initial code
*
* Class to handle writing PGM image files from DataContainers.
*
* The fileformat is split into the header and the body (with image data).
* The header starts with a magic number, then width and height (in ASCII) are specified and the
* last part of the header is the maximum value of the colour component. The Magic number in our
* case is "P2" as we want to write a grey scale image. The width and height are taken from the
* `DataContainer` and the maximum value is 255 (values are scaled accordingly)
*
* An example of a header would be:
* ```
* P2
* 100 50 # width height
* 255 # Max value, 1 byte
* ```
*
* Then the body prints one value in each line
*
* Reference: http://paulbourke.net/dataformats/ppm/
*/
class PGM
{
public:
/// write the DataContainer to the file named filename. Currently we only handle 2D images
template <typename data_t = real_t>
static void write(const DataContainer<data_t>& data, const std::string& filename);
/// write the DataContainer to ostream with the specified format. Only 2D buffers are
/// supported
template <typename data_t = real_t>
static void write(const DataContainer<data_t>& data, std::ostream& stream);
};
} // namespace elsa
......@@ -8,3 +8,4 @@ include(Catch)
ELSA_TEST(ioUtils)
ELSA_TEST(EDFHandler)
ELSA_TEST(MHDHandler)
ELSA_TEST(PGMHandler)
/**
* \file test_PGMHandler.cpp
*
* \brief Tests for the PGMHandler class
*
* \author David Frank - initial code
*/
#include <catch2/catch.hpp>
#include "PGMHandler.h"
#include "VolumeDescriptor.h"
#include <sstream>
#include <string_view>
using namespace elsa;
inline bool file_exists(std::string name)
{
std::ifstream f(name.c_str());
return f.good();
}
SCENARIO("Write PGM file")
{
GIVEN("A 2D DataContainer")
{
IndexVector_t numCoeff{{10, 10}};
VolumeDescriptor dd(numCoeff);
DataContainer dc(dd);
for (int i = 0; i < dc.getSize(); ++i) {
dc[i] = static_cast<real_t>(i);
}
// Also compute the max value and the scale factor
const auto maxVal = dc.getSize() - 1;
const auto scaleFactor = 255.f / static_cast<real_t>(std::ceil(maxVal));
THEN("PGM is written")
{
std::stringstream buf;
PGM::write(dc, buf);
// Get string from buffer
auto str = buf.str();
// Lambda to get the next token from `str` until `delimiter`
// remove the part from `str` and return it
auto nextToken = [](std::string& str, std::string_view delimiter) {
auto pos = str.find(delimiter);
auto token = str.substr(0, pos);
str.erase(0, pos + delimiter.length());
return token;
};
// Check header
REQUIRE(nextToken(str, "\n") == "P2");
REQUIRE(nextToken(str, "\n") == "10 10");
REQUIRE(nextToken(str, "\n") == "255");
// Now check the content
int counter = 0;
while (!str.empty()) {
auto val = static_cast<int>(static_cast<real_t>(counter) * scaleFactor);
REQUIRE(std::to_string(val) == nextToken(str, "\n"));
counter++;
}
}
}
GIVEN("A 1D DataContainer")
{
IndexVector_t numCoeff1d{{10}};
VolumeDescriptor dd1d(numCoeff1d);
DataContainer dc1d(dd1d);
THEN("A exception is raised")
{
REQUIRE_THROWS_AS(PGM::write(dc1d, "test.pgm"), std::invalid_argument);
}
}
GIVEN("A 3D DataContainer")
{
IndexVector_t numCoeff3d{{10, 8, 17}};
VolumeDescriptor dd(numCoeff3d);
DataContainer dc(dd);
THEN("A exception is raised")
{
REQUIRE_THROWS_AS(PGM::write(dc, "test.pgm"), std::invalid_argument);
}
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment