
* preprocessing/deskew.hh, * preprocessing/deskew_crop.hh: New. * preprocessing/unskew.hh: Remove. Deprecated. * tests/preprocessing/unskew.cc: Rename as... * tests/preprocessing/deskew.cc: ... this. * tests/preprocessing/Makefile.am: Update. * tests/img/text_to_group.pgm: New test image. --- scribo/ChangeLog | 16 + scribo/preprocessing/deskew.hh | 437 ++++++++++++++++++++ scribo/preprocessing/deskew_crop.hh | 187 +++++++++ scribo/preprocessing/unskew.hh | 115 ----- scribo/tests/img/text_to_group.pgm | Bin 0 -> 3053 bytes scribo/tests/preprocessing/Makefile.am | 4 +- .../tests/preprocessing/{unskew.cc => deskew.cc} | 22 +- 7 files changed, 657 insertions(+), 124 deletions(-) create mode 100644 scribo/preprocessing/deskew.hh create mode 100644 scribo/preprocessing/deskew_crop.hh delete mode 100644 scribo/preprocessing/unskew.hh create mode 100644 scribo/tests/img/text_to_group.pgm rename scribo/tests/preprocessing/{unskew.cc => deskew.cc} (74%) diff --git a/scribo/ChangeLog b/scribo/ChangeLog index c5476be..623147b 100644 --- a/scribo/ChangeLog +++ b/scribo/ChangeLog @@ -1,5 +1,21 @@ 2010-05-25 Guillaume Lazzara <z@lrde.epita.fr> + Add a new deskew algorithm. + + * preprocessing/deskew.hh, + * preprocessing/deskew_crop.hh: New. + + * preprocessing/unskew.hh: Remove. Deprecated. + + * tests/preprocessing/unskew.cc: Rename as... + * tests/preprocessing/deskew.cc: ... this. + + * tests/preprocessing/Makefile.am: Update. + + * tests/img/text_to_group.pgm: New test image. + +2010-05-25 Guillaume Lazzara <z@lrde.epita.fr> + Add crop tools. * preprocessing/crop.hh, diff --git a/scribo/preprocessing/deskew.hh b/scribo/preprocessing/deskew.hh new file mode 100644 index 0000000..27d38a1 --- /dev/null +++ b/scribo/preprocessing/deskew.hh @@ -0,0 +1,437 @@ +// Copyright (C) 2010 EPITA Research and Development Laboratory (LRDE) +// +// This file is part of Olena. +// +// Olena is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free +// Software Foundation, version 2 of the License. +// +// Olena is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Olena. If not, see <http://www.gnu.org/licenses/>. +// +// As a special exception, you may use this file as part of a free +// software project without restriction. Specifically, if other files +// instantiate templates or use macros or inline functions from this +// file, or you compile this file and link it with other files to produce +// an executable, this file does not by itself cause the resulting +// executable to be covered by the GNU General Public License. This +// exception does not however invalidate any other reasons why the +// executable file might be covered by the GNU General Public License. + +#ifndef SCRIBO_PREPROCESSING_DESKEW_HH +# define SCRIBO_PREPROCESSING_DESKEW_HH + +/// \file +/// +/// Deskew an image. + +/// \FIXME: provide a version for binary images. + + +# include <queue> +# include <mln/core/image/image2d.hh> +# include <mln/math/pi.hh> +# include <mln/geom/rotate.hh> + +# define PI 3.1415926535897932384 + +namespace scribo +{ + + namespace preprocessing + { + + using namespace mln; + + namespace internal + { + + class Hough + { + public: + Hough(int width, int height); + + ~Hough(); + + void look_up_table(); + + int width() const; + + int height() const; + + double mtheta() const; + double mrho() const; + + int mrhoi() const; + int mthetai() const; + + double get_cos(int index) const; + double get_sin(int index) const; + + image2d<unsigned>& acc(); + + private: + int width_; + int height_; + + double max_rho_; + double max_theta_; + + int max_rho_index_; + int max_theta_index_; + + double* cos_; + double* sin_; + + image2d<unsigned> acc_; + }; + + + struct s_angle + { + int pos; + unsigned max; + }; + + + struct QCompare + { + bool operator()(const s_angle& s1, const s_angle& s2); + }; + + } // end of namespace scribo::preprocessing::internal + + + +# ifndef MLN_INCLUDE_ONLY + + namespace internal + { + + bool + QCompare::operator()(const s_angle& s1, const s_angle& s2) + { + return (s1.max > s2.max); + } + + + Hough::Hough(int width, int height) + : width_(width / 2), + height_(height / 2), + max_rho_(sqrt((width * width) + (height * height))), + max_theta_(math::pi), + max_rho_index_(this->max_rho_ + 1), + max_theta_index_(500), + acc_(this->max_rho_index_, this->max_theta_index_) + { + look_up_table(); + } + + + Hough::~Hough() + { + delete[] this->cos_; + delete[] this->sin_; + } + + inline + void Hough::look_up_table() + { + this->cos_ = new double[this->max_theta_index_]; + this->sin_ = new double[this->max_theta_index_]; + + for (int i = 0; i < this->max_theta_index_; ++i) + { + double i_val = (i + 650) * this->max_theta_ / 1800.0f; + + this->cos_[i] = cos(i_val); + this->sin_[i] = sin(i_val); + } + } + + inline + int Hough::width() const + { + return this->width_; + } + + inline + int Hough::height() const + { + return this->height_; + } + + inline + double Hough::mtheta() const + { + return this->max_theta_; + } + + inline + double Hough::mrho() const + { + return this->max_rho_; + } + + inline + int Hough::mrhoi() const + { + return this->max_rho_index_; + } + + inline + int Hough::mthetai() const + { + return this->max_theta_index_; + } + + inline + double Hough::get_cos(int index) const + { + return this->cos_[index]; + } + + inline + double Hough::get_sin(int index) const + { + return this->sin_[index]; + } + + inline + image2d<unsigned>& Hough::acc() + { + return this->acc_; + } + + inline + static + void + vote(int x, int y, Hough& hough, int theta) + { + int theta_min = std::max(theta - 25, 0); + int theta_max = std::min(theta + 25, hough.mthetai()); + + x -= hough.width(); + y -= hough.height(); + + for (int i = theta_min; i < theta_max; ++i) + { + double rho = x * hough.get_cos(i) + y * hough.get_sin(i); + double rho_index = (0.5 + (rho / hough.mrho() + 0.5) + * hough.mrhoi()); + + ++(opt::at(hough.acc(), rho_index, i)); + } + } + + + inline + static + void + init_hist(Hough& hough, int hist[500], + std::priority_queue<s_angle, std::vector<s_angle>, QCompare>& q, + int nb_elm) + { + int max_rho = hough.mrhoi(); + int max_theta = hough.mthetai(); + unsigned max_elm = (nb_elm > max_rho) ? (nb_elm / max_rho) << 5 : 1; + + for (int j = 0; j < max_theta; ++j) + { + hist[j] = 0; + + if (q.size() < max_elm) + { + if (opt::at(hough.acc(), 0, j) > 0) + { + s_angle s; + + s.max = opt::at(hough.acc(), 0, j); + s.pos = j; + + q.push(s); + } + } + else if (opt::at(hough.acc(), 0, j) > q.top().max) + { + s_angle s; + + s.max = opt::at(hough.acc(), 0, j); + s.pos = j; + + q.pop(); + q.push(s); + } + } + } + + + inline + static + double + get_max(Hough& hough, int hist[500], + std::priority_queue<s_angle, std::vector<s_angle>, QCompare>& q, + int nb_elm) + { + int max = 0; + int h_value = 0; + int max_rho = hough.mrhoi(); + int max_theta = hough.mthetai(); + double pos = 0.f; + unsigned max_elm = (nb_elm > max_rho) ? (nb_elm / max_rho) << 5 : 1; + + + for (int i = 1; i < max_rho; ++i) + { + for (int j = 0; j < max_theta; ++j) + { + if (q.size() < max_elm) + { + if (opt::at(hough.acc(), i, j) > 0) + { + s_angle s; + + s.max = opt::at(hough.acc(), i, j); + s.pos = j; + + q.push(s); + } + } + else if (opt::at(hough.acc(), i, j) > q.top().max) + { + s_angle s; + + s.max = opt::at(hough.acc(), i, j); + s.pos = j; + + q.pop(); + q.push(s); + } + } + } + + while (!q.empty()) + { + hist[q.top().pos] += q.top().max; + h_value = hist[q.top().pos]; + + if (h_value > max) + { + max = h_value; + pos = q.top().pos; + } + + q.pop(); + } + + return pos; + } + + + double + perform_deskew(const image2d<value::int_u8>& gray) + { + Hough hough(gray.ncols(), gray.nrows()); + std::priority_queue<s_angle, std::vector<s_angle>, QCompare> q; + int hist[500]; + int nb_elm = 0; + + for (unsigned i = 0; i < gray.nrows() - 1; ++i) + { + for (unsigned j = 1; j < gray.ncols() - 1; ++j) + { + unsigned up = 1; + unsigned down = 1; + unsigned mean = ((opt::at(gray, i, j) * opt::at(gray, i + 1, j))) >> 8; + + for (unsigned k = j - 1; k <= j + 1; ++k) + { + up *= opt::at(gray, i, k); + down *= opt::at(gray, i + 1, k); + } + + up = 255 - (up >> 16); + down = down >> 16; + + if (up > down && down > mean && down > 130) + { + + ++nb_elm; + double gy = opt::at(gray, i - 1, j - 1) + 2 * opt::at(gray, i - 1, j) + + opt::at(gray, i - 1, j + 1); + gy += -opt::at(gray, i + 1, j - 1) - 2 * opt::at(gray, i + 1, j) - + opt::at(gray, i + 1, j + 1); + + double gx = opt::at(gray, i - 1, j - 1) + 2 * opt::at(gray, i, j - 1) + + opt::at(gray, i + 1, j - 1); + gx += -opt::at(gray, i - 1, j + 1) - 2 * opt::at(gray, i, j + 1) - + opt::at(gray, i + 1, j + 1); + + double tanv = (PI / 2.0 - atan(gy / gx)) * 180.0 / PI; + + if (tanv <= 25.0 || tanv >= 155.0) + { + ++nb_elm; + vote(j, i, hough, (tanv <= 25.0 ? 250.0 - tanv * 10.0 : + (180.0 - tanv) * 10.0 + 250.0)); + } + } + } + } + + init_hist(hough, hist, q, nb_elm); + + return 90 - (get_max(hough, hist, q, nb_elm) + 650) / 10; + } + + + + } // end of namespace scribo::preprocessing::internal + + + + template <typename I> + mln_concrete(I) + deskew(const Image<I>& input_gl_) + { + const I& input_gl = exact(input_gl_); + + trace::entering("scribo::preprocessing::deskew"); + mln_assertion(input_gl.is_valid()); + mlc_is(mln_domain(I), box2d)::check(); + mlc_is_not(mln_value(I), bool)::check(); + mlc_is_not_a(mln_value(I), value::Vectorial)::check(); + + double angle = internal::perform_deskew(input_gl); +// std::cout << angle << std::endl; + + mln_concrete(I) output = input_gl; + + // FIXME: trick to make this routine faster for really small + // angles (no impact on the results) + if (angle > 0.5 || angle < -0.5) + output = geom::rotate(input_gl, - angle, + //mln_max(mln_value(I)), + extend(input_gl, mln_max(mln_value(I))), + make::box2d(input_gl.nrows(), + input_gl.ncols())); + + trace::exiting("scribo::preprocessing::deskew"); + return output; + } + + +# endif // ! MLN_INCLUDE_ONLY + + + } // end of namespace scribo::preprocessing + +} // end of namespace scribo + + +# endif // SCRIBO_PREPROCESSING_DESKEW_HH diff --git a/scribo/preprocessing/deskew_crop.hh b/scribo/preprocessing/deskew_crop.hh new file mode 100644 index 0000000..3b15cb1 --- /dev/null +++ b/scribo/preprocessing/deskew_crop.hh @@ -0,0 +1,187 @@ +// Copyright (C) 2010 EPITA Research and Development Laboratory (LRDE) +// +// This file is part of Olena. +// +// Olena is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free +// Software Foundation, version 2 of the License. +// +// Olena is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Olena. If not, see <http://www.gnu.org/licenses/>. +// +// As a special exception, you may use this file as part of a free +// software project without restriction. Specifically, if other files +// instantiate templates or use macros or inline functions from this +// file, or you compile this file and link it with other files to produce +// an executable, this file does not by itself cause the resulting +// executable to be covered by the GNU General Public License. This +// exception does not however invalidate any other reasons why the +// executable file might be covered by the GNU General Public License. + +#ifndef SCRIBO_PREPROCESSING_DESKEW_CROP_HH +# define SCRIBO_PREPROCESSING_DESKEW_CROP_HH + +/// \file +/// +/// \brief Deskew a region of interest. + +# include <scribo/preprocessing/deskew.hh> + +namespace scribo +{ + + namespace preprocessing + { + + /*! \brief Deskew a region of interest. + + \param[in] input_bin A binary image. + \param[in] input_gray A gray-level image. + + \return A deskewed binary image. + + + + Handles skew angles from -25 to +25 degrees. + + \p input_bin and \p input_gray must be 2D images and must be + identical (e.g. only the value differs). + + This algorithm is designed for images created from a region of + interest (e.g. Not a full document). + + */ + template <typename I, typename J> + mln_concrete(I) + deskew(const Image<I>& crop_gl_, const Image<I>& input_gl_); + +# ifndef MLN_INCLUDE_ONLY + + namespace internal + { + + + /*! \brief Compute the skew angle using a Hough transform. + + \param[in] input_bin A binary image. + \param[in] input_gray A gray-level image. + \param[in] threshold + \param[in] length + + \return A deskewed binary image. + + This algorithm tries to use only the sites located on the + baselines (the most relevant sites for deskewing). + A site is considered as relevant for this algorithm if : + - \p threshold sites in the window of length \p length are set to True. + - The bottom left, bottom and bottom right sites are set to False. + + Note: Increasing \p length value while keeping threshold value + low increases the number of relevant sites. + + Handles skew angles from -25 to +25 degrees. + + \p input_bin and \p input_gray must be 2D images and must be + identical (e.g. only the value differs). + + This algorithm is designed for images created from a region of + interest (e.g. Not a full document). + + */ +// double +// perform_deskew_crop(const image2d<bool>& input_bin, +// const image2d<value::int_u8>& input_gray, +// int threshold, +// int length) +// { +// Hough hough(input_bin.ncols(), input_bin.nrows()); +// int +// mid = length >> 1, +// max = 0; +// double pos = 0; + +// for (int i = 1; i < input_bin.nrows() - 1; ++i) +// { +// int +// acc = 0, +// begin = 0, +// end = length; + +// for (int j = 0; j < length; ++j) +// acc += opt::at(input_bin, i, j); + +// for (int j = mid + 1; end < input_bin.ncols(); ++j, ++end, ++begin) +// { +// acc += opt::at(input_bin, i, end) - opt::at(input_bin, i, begin); + +// if (acc > threshold && !opt::at(input_bin, i + 1, j) && +// !opt::at(input_bin, i + 1, j - 1) && !opt::at(input_bin, i + 1, j + 1)) +// { +// double gy = opt::at(input_gray, i - 1, j - 1) + 2 * opt::at(input_gray, i - 1, j) + opt::at(input_gray, i - 1, j + 1); +// gy += - opt::at(input_gray, i + 1, j - 1) - 2 * opt::at(input_gray, i + 1, j) - opt::at(input_gray, i + 1, j + 1); + +// double gx = opt::at(input_gray, i - 1, j - 1) + 2 * opt::at(input_gray, i, j - 1) + opt::at(input_gray, i + 1, j - 1); +// gx += - opt::at(input_gray, i - 1, j + 1) - 2 * opt::at(input_gray, i, j + 1) - opt::at(input_gray, i + 1, j + 1); + +// if (abs(gx) + abs(gy) >= 255) +// vote(j, i, deskew, &max, &pos); +// } +// } +// } + +// return 90 - (pos + 650) / 10.0f; +// } + + +// } // end of namespace scribo::preprocessing::internal + + + // Facade + + + template <typename I> + mln_concrete(I) + deskew(const Image<I>& crop_gl_, const Image<I>& input_gl_) + { + const I& crop_gl = exact(crop_gl_); + const I& input_gl = exact(input_gl_); + + trace::entering("scribo::preprocessing::deskew_crop"); + mln_assertion(crop_gl.is_valid()); + mln_assertion(input_gl.is_valid()); + mlc_is(mln_domain(I), box2d)::check(); + mlc_is_not(mln_value(I), bool)::check(); + mlc_is_not_a(mln_value(I), value::Vectorial)::check(); + + double angle = internal::perform_deskew(crop_gl); + std::cout << angle << std::endl; + + mln_concrete(I) output = crop_gl; + + // FIXME: trick to make this routine faster for really small + // angles (no impact on the results) + if (angle > 0.5 || angle < -0.5) + output = geom::rotate(crop_gl, - angle, + //mln_max(mln_value(I)), + extend(input_gl, mln_max(mln_value(I))), + make::box2d(crop_gl.nrows(), + crop_gl.ncols())); + + trace::exiting("scribo::preprocessing::deskew_crop"); + return output; + } + +# endif // ! MLN_INCLUDE_ONLY + + + } // end of namespace scribo::preprocessing + +} // end of namespace scribo + + +# endif // SCRIBO_PREPROCESSING_DESKEW_CROP_HH diff --git a/scribo/preprocessing/unskew.hh b/scribo/preprocessing/unskew.hh deleted file mode 100644 index a01cae3..0000000 --- a/scribo/preprocessing/unskew.hh +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright (C) 2009 EPITA Research and Development Laboratory (LRDE) -// -// This file is part of Olena. -// -// Olena is free software: you can redistribute it and/or modify it under -// the terms of the GNU General Public License as published by the Free -// Software Foundation, version 2 of the License. -// -// Olena is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Olena. If not, see <http://www.gnu.org/licenses/>. -// -// As a special exception, you may use this file as part of a free -// software project without restriction. Specifically, if other files -// instantiate templates or use macros or inline functions from this -// file, or you compile this file and link it with other files to produce -// an executable, this file does not by itself cause the resulting -// executable to be covered by the GNU General Public License. This -// exception does not however invalidate any other reasons why the -// executable file might be covered by the GNU General Public License. - -#ifndef SCRIBO_PREPROCESSING_UNSKEW_HH -# define SCRIBO_PREPROCESSING_UNSKEW_HH - -/// \file -/// -/// Unskew an image. - -# include <mln/core/image/image2d.hh> - -# include <mln/accu/compute.hh> -# include <mln/accu/max_site.hh> - -# include <mln/transform/hough.hh> -# include <mln/geom/rotate.hh> - -# include <mln/io/pgm/save.hh> -# include <mln/core/image/vmorph/cast_image.hh> -# include <mln/value/int_u8.hh> - -# include <mln/util/couple.hh> - -namespace scribo -{ - - namespace preprocessing - { - - using namespace mln; - - /// Unskew a document. - /// Based on the Hough transform. - /// - /// \param[in] input_ A binary image. Objects to be unskewed must be set - /// to "true". - /// - /// \return A binary image. - // - template <typename I> - mln::util::couple<mln_concrete(I), double> - unskew(const Image<I>& input_); - - - -# ifndef MLN_INCLUDE_ONLY - - - template <typename I> - mln::util::couple<mln_concrete(I), double> - unskew(const Image<I>& input_) - { - trace::entering("scribo::preprocessing::unskew"); - - const I& input = exact(input_); - mlc_equal(mln_value(I), bool)::check(); - mln_precondition(input.is_valid()); - - image2d<float> hough_ima = transform::hough(input); - - point2d max_p = accu::compute(accu::max_site<image2d<float> >(), hough_ima); - - double angle = 0; - int max_angle = max_p.row(); - -// std::cout << "max_angle = " << max_angle << std::endl; - if (max_angle > 180) - max_angle = - max_angle % 180; - - if (max_angle < 90 && max_angle > 0) - angle = - max_angle; - else if (max_angle < 0 && max_angle > -90) - angle = max_angle; - else if (max_angle < 180 && max_angle > 90) - angle = 180 - max_angle; - else if (max_angle < -90 && max_angle > -180) - angle = 180 + max_angle; - -// std::cout << "effective angle = " << angle << std::endl; - mln_concrete(I) output = geom::rotate(input, angle); - - trace::exiting("scribo::preprocessing::unskew"); - return mln::make::couple(output, angle); - } - -# endif // ! MLN_INCLUDE_ONLY - - } // end of namespace scribo::preprocessing - -} // end of namespace mln - -# endif // SCRIBO_PREPROCESSING_UNSKEW_HH diff --git a/scribo/tests/img/text_to_group.pgm b/scribo/tests/img/text_to_group.pgm new file mode 100644 index 0000000000000000000000000000000000000000..220ffbaa2e49e0b1e8fe644de06e4d627748d19e GIT binary patch literal 3053 zcmWGA<x*B~4svx2@ei_6aQE~LPzdnzRdCD9DM>9-2um$0&dkqKFw`^T;xaQ(Fg4&Z zGBxF57zLvtFd70wHw6B}0Rx=%pD+{b;r}pi12vGq9MuaAEFyw!V8aTYA_yl4xKemZ zfTf60nwBAeJ=}>Cg+~>f0dgsv17~0pg-gO2*i?bUFinJOfitkF0&B%^4O|PHF{&35 YQdq(no(y0rz+pv#D6+m$_Am+o0N4yHPXGV_ literal 0 HcmV?d00001 diff --git a/scribo/tests/preprocessing/Makefile.am b/scribo/tests/preprocessing/Makefile.am index d180604..f94a852 100644 --- a/scribo/tests/preprocessing/Makefile.am +++ b/scribo/tests/preprocessing/Makefile.am @@ -23,11 +23,11 @@ check_PROGRAMS = \ crop \ crop_without_localization \ rotate_90 \ - unskew + deskew crop_SOURCES = crop.cc crop_without_localization_SOURCES = crop_without_localization.cc rotate_90_SOURCES = rotate_90.cc -unskew_SOURCES = unskew.cc +deskew_SOURCES = deskew.cc TESTS = $(check_PROGRAMS) diff --git a/scribo/tests/preprocessing/unskew.cc b/scribo/tests/preprocessing/deskew.cc similarity index 74% rename from scribo/tests/preprocessing/unskew.cc rename to scribo/tests/preprocessing/deskew.cc index 9c84951..5b55889 100644 --- a/scribo/tests/preprocessing/unskew.cc +++ b/scribo/tests/preprocessing/deskew.cc @@ -1,4 +1,5 @@ -// Copyright (C) 2009 EPITA Research and Development Laboratory (LRDE) +// Copyright (C) 2009, 2010 EPITA Research and Development Laboratory +// (LRDE) // // This file is part of Olena. // @@ -23,11 +24,16 @@ // exception does not however invalidate any other reasons why the // executable file might be covered by the GNU General Public License. +/// \file +/// +/// \fixme Add a better test..... + #include <mln/core/image/image2d.hh> -#include <mln/io/pbm/load.hh> -#include <mln/io/pbm/save.hh> +#include <mln/io/pgm/all.hh> +#include <mln/value/int_u8.hh> +#include <mln/data/compare.hh> -#include <scribo/preprocessing/unskew.hh> +#include <scribo/preprocessing/deskew.hh> #include <scribo/tests/data.hh> @@ -37,8 +43,10 @@ int main(int argc, char *argv[]) (void) argv; using namespace mln; - image2d<bool> ima; - io::pbm::load(ima, SCRIBO_IMG_DIR "/text_to_group.pbm"); + image2d<value::int_u8> ima; + io::pgm::load(ima, SCRIBO_IMG_DIR "/text_to_group.pgm"); + + image2d<value::int_u8> tmp = scribo::preprocessing::deskew(ima); - io::pbm::save(scribo::preprocessing::unskew(ima).first(), "unskew.pbm"); + mln_assertion(ima == tmp); } -- 1.5.6.5