1 /*
2     Scan Tailor - Interactive post-processing tool for scanned pages.
3     Copyright (C)  Joseph Artsimovich <joseph.artsimovich@gmail.com>
4 
5     This program is free software: you can redistribute it and/or modify
6     it under the terms of the GNU General Public License as published by
7     the Free Software Foundation, either version 3 of the License, or
8     (at your option) any later version.
9 
10     This program is distributed in the hope that it will be useful,
11     but WITHOUT ANY WARRANTY; without even the implied warranty of
12     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13     GNU General Public License for more details.
14 
15     You should have received a copy of the GNU General Public License
16     along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 #include "DistortionModel.h"
20 #include <QDomDocument>
21 #include <QRectF>
22 #include <QTransform>
23 #include "CylindricalSurfaceDewarper.h"
24 
25 namespace dewarping {
26 DistortionModel::DistortionModel() = default;
27 
DistortionModel(const QDomElement & el)28 DistortionModel::DistortionModel(const QDomElement& el)
29     : m_topCurve(el.namedItem("top-curve").toElement()), m_bottomCurve(el.namedItem("bottom-curve").toElement()) {}
30 
toXml(QDomDocument & doc,const QString & name) const31 QDomElement DistortionModel::toXml(QDomDocument& doc, const QString& name) const {
32   if (!isValid()) {
33     return QDomElement();
34   }
35 
36   QDomElement el(doc.createElement(name));
37   el.appendChild(m_topCurve.toXml(doc, "top-curve"));
38   el.appendChild(m_bottomCurve.toXml(doc, "bottom-curve"));
39 
40   return el;
41 }
42 
isValid() const43 bool DistortionModel::isValid() const {
44   if (!m_topCurve.isValid() || !m_bottomCurve.isValid()) {
45     return false;
46   }
47 
48   const Vec2d poly[4] = {m_topCurve.polyline().front(), m_topCurve.polyline().back(), m_bottomCurve.polyline().back(),
49                          m_bottomCurve.polyline().front()};
50 
51   double min_dot = NumericTraits<double>::max();
52   double max_dot = NumericTraits<double>::min();
53 
54   for (int i = 0; i < 4; ++i) {
55     const Vec2d cur(poly[i]);
56     const Vec2d prev(poly[(i + 3) & 3]);
57     const Vec2d next(poly[(i + 1) & 3]);
58 
59     Vec2d prev_normal(cur - prev);
60     std::swap(prev_normal[0], prev_normal[1]);
61     prev_normal[0] = -prev_normal[0];
62 
63     const double dot = prev_normal.dot(next - cur);
64     if (dot < min_dot) {
65       min_dot = dot;
66     }
67     if (dot > max_dot) {
68       max_dot = dot;
69     }
70   }
71 
72   if (min_dot * max_dot <= 0) {
73     // Not convex.
74     return false;
75   }
76 
77   if ((std::fabs(min_dot) < 0.01) || (std::fabs(max_dot) < 0.01)) {
78     // Too close - possible problems with calculating homography.
79     return false;
80   }
81 
82   return true;
83 }  // DistortionModel::isValid
84 
matches(const DistortionModel & other) const85 bool DistortionModel::matches(const DistortionModel& other) const {
86   const bool this_valid = isValid();
87   const bool other_valid = other.isValid();
88   if (!this_valid && !other_valid) {
89     return true;
90   } else if (this_valid != other_valid) {
91     return false;
92   }
93 
94   if (!m_topCurve.matches(other.m_topCurve)) {
95     return false;
96   } else if (!m_bottomCurve.matches(other.m_bottomCurve)) {
97     return false;
98   }
99 
100   return true;
101 }
102 
modelDomain(const CylindricalSurfaceDewarper & dewarper,const QTransform & to_output,const QRectF & output_content_rect) const103 QRectF DistortionModel::modelDomain(const CylindricalSurfaceDewarper& dewarper,
104                                     const QTransform& to_output,
105                                     const QRectF& output_content_rect) const {
106   QRectF model_domain(boundingBox(to_output));
107 
108   // We not only uncurl the lines, but also stretch them in curved areas.
109   // Because we don't want to reach out of the content box, we shrink
110   // the model domain vertically, rather than stretching it horizontally.
111   const double vert_scale = 1.0 / dewarper.directrixArcLength();
112   // When scaling model_domain, we want the following point to remain where it is.
113   const QPointF scale_origin(output_content_rect.center());
114 
115   const double new_upper_part = (scale_origin.y() - model_domain.top()) * vert_scale;
116   const double new_height = model_domain.height() * vert_scale;
117   model_domain.setTop(scale_origin.y() - new_upper_part);
118   model_domain.setHeight(new_height);
119 
120   return model_domain;
121 }
122 
boundingBox(const QTransform & transform) const123 QRectF DistortionModel::boundingBox(const QTransform& transform) const {
124   double top = NumericTraits<double>::max();
125   double left = top;
126   double bottom = NumericTraits<double>::min();
127   double right = bottom;
128 
129   for (QPointF pt : m_topCurve.polyline()) {
130     pt = transform.map(pt);
131     left = std::min<double>(left, pt.x());
132     right = std::max<double>(right, pt.x());
133     top = std::min<double>(top, pt.y());
134     bottom = std::max<double>(bottom, pt.y());
135   }
136 
137   for (QPointF pt : m_bottomCurve.polyline()) {
138     pt = transform.map(pt);
139     left = std::min<double>(left, pt.x());
140     right = std::max<double>(right, pt.x());
141     top = std::min<double>(top, pt.y());
142     bottom = std::max<double>(bottom, pt.y());
143   }
144 
145   if ((top > bottom) || (left > right)) {
146     return QRectF();
147   } else {
148     return QRectF(left, top, right - left, bottom - top);
149   }
150 }
151 }  // namespace dewarping