Skip to content

Commit 165a634

Browse files
ukoethewolfv
authored andcommitted
Allow image IO for value_types other than uint8_t (#26)
fixed IO for images with value_type != uint8
1 parent 0ad01bf commit 165a634

File tree

5 files changed

+123
-36
lines changed

5 files changed

+123
-36
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ install:
4141
export CXX=clang++ CC=clang;
4242
conda install libcxx clangdev -c QuantStack -c conda-forge
4343
fi
44-
- conda install cmake openimageio libsndfile boost zlib xtensor=0.15.5 -c QuantStack -c conda-forge
44+
- conda install cmake openimageio libsndfile boost zlib xtensor=0.15.6 -c QuantStack -c conda-forge
4545
- export LDFLAGS="${LDFLAGS} -Wl,-rpath,$CONDA_PREFIX/lib"
4646
- export LINKFLAGS="${LDFLAGS}"
4747
- mkdir build

CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ message(STATUS "Building xtensor-io v${${PROJECT_NAME}_VERSION}")
4444
find_package(xtensor REQUIRED)
4545
message(STATUS "Found xtensor: ${xtensor_INCLUDE_DIRS}/xtensor")
4646

47+
find_package(xtl REQUIRED)
48+
message(STATUS "Found xtl: ${xtl_INCLUDE_DIRS}/xtl")
49+
4750
# Find OpenImageIO
4851
set(CMAKE_MODULE_PATH "${CMAKE_MODULE_PATH}" "${CMAKE_SOURCE_DIR}/modules")
4952

include/xtensor-io/ximage.hpp

Lines changed: 87 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
#include <cstddef>
1313
#include <stdexcept>
1414
#include <string>
15+
#include <memory>
1516

1617
#include <xtensor/xarray.hpp>
18+
#include <xtensor/xmath.hpp>
1719
#include <xtensor/xeval.hpp>
1820

1921
#include "xtensor_io_config.hpp"
@@ -46,73 +48,128 @@ namespace xt
4648
template <class T = unsigned char>
4749
xarray<T> load_image(std::string filename)
4850
{
49-
// TODO handle types better
50-
OIIO::ImageInput* in = OIIO::ImageInput::open(filename);
51+
auto close_file = [](OIIO::ImageInput * file)
52+
{
53+
file->close();
54+
OIIO::ImageInput::destroy(file);
55+
};
56+
57+
std::unique_ptr<OIIO::ImageInput, decltype(close_file)> in(OIIO::ImageInput::open(filename), close_file);
5158
if (!in)
5259
{
53-
// something went wrong
54-
throw std::runtime_error("Error reading image.");
60+
throw std::runtime_error("load_image(): Error reading image '" + filename + "'.");
5561
}
62+
5663
const OIIO::ImageSpec& spec = in->spec();
57-
int xres = spec.width;
58-
int yres = spec.height;
59-
int channels = spec.nchannels;
6064

6165
// allocate memory
6266
auto image = xarray<T>::from_shape({static_cast<std::size_t>(spec.height),
6367
static_cast<std::size_t>(spec.width),
6468
static_cast<std::size_t>(spec.nchannels)});
6569

66-
in->read_image(OIIO::TypeDesc::UINT8, image.raw_data());
67-
in->close();
68-
OIIO::ImageInput::destroy(in);
70+
in->read_image(OIIO::BaseTypeFromC<T>::value, image.raw_data());
6971

7072
return image;
7173
}
7274

75+
/** \brief Pass options to dump_image().
76+
*/
77+
struct dump_image_options
78+
{
79+
/** \brief Initialize options to default values.
80+
*/
81+
dump_image_options()
82+
: spec(0,0,0)
83+
, autoconvert(true)
84+
{
85+
spec.attribute("CompressionQuality", 90);
86+
}
87+
88+
/** \brief Forward an attribute to an OpenImageIO ImageSpec.
89+
90+
See the documentation of OIIO::ImageSpec::attribute() for a list
91+
of supported attributes.
92+
93+
Default: "CompressionQuality" = 90
94+
*/
95+
template <class T>
96+
dump_image_options & attribute(OIIO::string_view name, T const & v)
97+
{
98+
spec.attribute(name, v);
99+
return *this;
100+
}
101+
102+
OIIO::ImageSpec spec;
103+
bool autoconvert;
104+
};
73105

74106
/**
75107
* Save image to disk.
76108
* The desired image format is deduced from ``filename``.
77109
* Supported formats are those supported by OpenImageIO.
78110
* Most common formats are supported (jpg, png, gif, bmp, tiff).
111+
* The shape of the array must be ``HEIGHT x WIDTH`` or ``HEIGHT x WIDTH x CHANNELS``.
79112
*
80113
* @param filename The path to the desired file
81114
* @param data Image data
82-
* @param quality If saving in a compressed format such as JPEG, setting the quality
83-
* will select how strong the compression will reduce the image quality.
84-
* Quality is an integer from 100 (best image quality, less compression)
85-
* to 0 (worst image quality, low filesize).
115+
* @param options Pass a dump_image_options object to fine-tune image export
86116
*/
87117
template <class E>
88-
void dump_image(std::string filename, const xexpression<E>& data, int quality = 90)
118+
void dump_image(std::string filename, const xexpression<E>& data,
119+
dump_image_options const & options = dump_image_options())
89120
{
90-
auto&& ex = eval(data.derived_cast());
121+
using value_type = typename std::decay_t<decltype(data.derived_cast())>::value_type;
91122

92-
XTENSOR_PRECONDITION(ex.dimension() == 2 || ex.dimension() == 3,
123+
auto shape = data.derived_cast().shape();
124+
XTENSOR_PRECONDITION(shape.size() == 2 || shape.size() == 3,
93125
"dump_image(): data must have 2 or 3 dimensions (channels must be last).");
94126

95-
OIIO::ImageOutput* out = OIIO::ImageOutput::create(filename);
127+
auto close_file = [](OIIO::ImageOutput * file)
128+
{
129+
file->close();
130+
OIIO::ImageOutput::destroy(file);
131+
};
132+
133+
std::unique_ptr<OIIO::ImageOutput, decltype(close_file)> out(OIIO::ImageOutput::create(filename), close_file);
96134
if (!out)
97135
{
98-
// something went wrong
99136
throw std::runtime_error("dump_image(): Error opening file '" + filename + "' to write image.");
100137
}
101138

102-
int channels = ex.dimension() == 2
103-
? 1
104-
: static_cast<int>(ex.shape()[2]);
139+
OIIO::ImageSpec spec = options.spec;
105140

106-
OIIO::ImageSpec spec(static_cast<int>(ex.shape()[1]),
107-
static_cast<int>(ex.shape()[0]),
108-
channels, OIIO::TypeDesc::UINT8);
109-
110-
spec.attribute("CompressionQuality", quality);
141+
spec.width = static_cast<int>(shape[1]);
142+
spec.height = static_cast<int>(shape[0]);
143+
spec.nchannels = (shape.size() == 2)
144+
? 1
145+
: static_cast<int>(shape[2]);
146+
spec.format = OIIO::BaseTypeFromC<value_type>::value;
111147

112148
out->open(filename, spec);
113-
out->write_image(OIIO::TypeDesc::UINT8, ex.raw_data());
114-
out->close();
115-
OIIO::ImageOutput::destroy(out);
149+
150+
auto && ex = eval(data.derived_cast());
151+
if(out->spec().format != OIIO::BaseTypeFromC<value_type>::value)
152+
{
153+
// OpenImageIO changed the target type because the file format doesn't support value_type.
154+
// It will do automatic conversion, but the data should be in the range 0...1
155+
// for good results.
156+
auto mM = minmax(ex)();
157+
158+
if(mM[0] != mM[1])
159+
{
160+
using real_t = real_promote_type_t<value_type>;
161+
auto && normalized = eval((real_t(1.0) / (mM[1] - mM[0])) * (ex - mM[0]));
162+
out->write_image(OIIO::BaseTypeFromC<real_t>::value, normalized.raw_data());
163+
}
164+
else
165+
{
166+
out->write_image(OIIO::BaseTypeFromC<value_type>::value, ex.raw_data());
167+
}
168+
}
169+
else
170+
{
171+
out->write_image(OIIO::BaseTypeFromC<value_type>::value, ex.raw_data());
172+
}
116173
}
117174
}
118175

test/test_ximage.cpp

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include "gtest/gtest.h"
1010
#include "xtensor/xoperation.hpp"
1111
#include "xtensor/xview.hpp"
12+
#include "xtensor/xio.hpp"
1213

1314
#include "xtensor-io/ximage.hpp"
1415

@@ -62,9 +63,18 @@ namespace xt
6263

6364
TEST(ximage, save_png)
6465
{
65-
dump_image("files/dump_test.png", test_image_rgb);
66-
auto img = load_image("files/dump_test.png");
67-
EXPECT_TRUE(all(equal(test_image_rgb, img)));
66+
{
67+
// save as 'unsigned char'
68+
dump_image("files/dump_test.png", test_image_rgb);
69+
auto img = load_image("files/dump_test.png");
70+
EXPECT_TRUE(all(equal(test_image_rgb, img)));
71+
}
72+
{
73+
// save as 'int'
74+
dump_image("files/dump_test.png", 1*test_image_rgb);
75+
auto img = load_image("files/dump_test.png");
76+
EXPECT_TRUE(all(equal(test_image_rgb, img)));
77+
}
6878
}
6979

7080
TEST(ximage, save_gif)
@@ -82,4 +92,21 @@ namespace xt
8292
bool test = (amax(img - img_large)() <= 50);
8393
EXPECT_TRUE(test);
8494
}
85-
}
95+
96+
TEST(ximage, float_img)
97+
{
98+
xarray<float> test_float(test_image_rgb);
99+
100+
// TIFF supports float pixels
101+
dump_image("files/dump_test_float.tif", test_float);
102+
auto img = load_image<float>("files/dump_test_float.tif");
103+
EXPECT_EQ(test_float, img);
104+
105+
// PNG does not support float pixels => test automatic conversion
106+
dump_image("files/dump_test_float.png", test_float);
107+
auto cimg = load_image("files/dump_test_float.png");
108+
EXPECT_EQ(test_image_rgb, cimg);
109+
img = load_image<float>("files/dump_test_float.png");
110+
EXPECT_TRUE(allclose(test_float, 255.0*img));
111+
}
112+
}

test/test_xnpz.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ namespace xt
6262
}
6363
// this is date + time == 4 bytes per file + once in global header
6464
std::size_t unequal_allowed = (n_zipped_files + 2) * 4;
65-
if (unequal != unequal_allowed)
65+
if (unequal > unequal_allowed)
6666
{
6767
std::stringstream ss;
6868
ss << "Number of unequal elements not allowed size: " << unequal << " vs allowed: " << unequal_allowed << std::endl;

0 commit comments

Comments
 (0)