1 /*
2 * Copyright (c) 2005-2019 Libor Pecháček.
3 * Copyright 2020 Kai Pastor
4 *
5 * This file is part of CoVe
6 *
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU Lesser General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU Lesser General Public License for more details.
16 *
17 * You should have received a copy of the GNU Lesser General Public License
18 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21 #include "Vectorizer.h"
22
23 #include <algorithm>
24 #include <cstdlib>
25 #include <iosfwd>
26 #include <iterator>
27 #include <numeric>
28 #include <utility>
29 #include <type_traits>
30
31 #include <Qt>
32 #include <QtGlobal>
33 #include <QColor>
34 #include <QException>
35 #include <QImage>
36 #include <QVector>
37
38 #include "AlphaGetter.h"
39 #include "Concurrency.h"
40 #include "ProgressObserver.h"
41 #include "KohonenMap.h"
42 #include "MapColor.h"
43 #include "Morphology.h"
44 #include "ParallelImageProcessing.h"
45 #include "PatternGetter.h"
46
47 namespace cove {
48
49 /*! \class Vectorizer
50 * \ingroup libvectorizer
51 * \brief Facade for vectorization process.
52 *
53 * This class provides facade for underlying classes MapColor (and its
54 * derivates),
55 * KohonenMap, Morphology and several others. The usual way how to cope with
56 * this class is
57 * - construct Vectorizer with an QImage (Vectorizer(QImage& im))
58 * - set number of recognized colors, learning method and parameters (or leave
59 * them untouched when you are happy with them)
60 * - call performClassification()
61 * - get the resulting colors with getClassifiedColors()
62 * - obtain the classified image with getClassifiedImage()
63 * - after selecting the colors for separation call getBWImage()
64 * - and in case of need thin the lines in the image with getThinnedImage()
65 */
66
67 /*!
68 * \enum Vectorizer::ColorSpace
69 * Type of color space used.
70 */
71 /*! \var Vectorizer::ColorSpace Vectorizer::COLSPC_RGB
72 * Colors are from RGB space.
73 */
74 /*! \var Vectorizer::ColorSpace Vectorizer::COLSPC_HSV
75 * Colors are from HSV space.
76 */
77 /*! \enum Vectorizer::LearningMethod
78 * Learning method to be used.
79 */
80 /*! \var Vectorizer::LearningMethod Vectorizer::KOHONEN_CLASSIC
81 * Classic Kohonen learning with random pattern selection and stepwise lowered
82 * speed of learning.
83 */
84 /*! \var Vectorizer::LearningMethod Vectorizer::KOHONEN_BATCH
85 * Kohonens' batch learning equivalent to ISODATA.
86 */
87 /*! \enum Vectorizer::AlphaStrategy
88 Possible alpha selection strategies for KOHONEN_CLASSIC learning. */
89 /*! \var Vectorizer::AlphaStrategy Vectorizer::ALPHA_CLASSIC
90 Alpha starts at initAlpha, does E cycles of learning, gets multiplied by q and
91 does and returns to the previous step (E cycles) until it falls below
92 minAlpha. \sa ClassicAlphaGetter */
93 /*! \var Vectorizer::AlphaStrategy Vectorizer::ALPHA_NEUQUANT
94 Not implemented yet. */
95
96 /*! \enum Vectorizer::PatternStrategy
97 Selection of pixels from source image. */
98 /*! \var Vectorizer::PatternStrategy Vectorizer::PATTERN_RANDOM
99 Random selection, used with KOHONEN_CLASSIC learning. */
100 /*! \var Vectorizer::PatternStrategy Vectorizer::PATTERN_NEUQUANT
101 Not implemented yet. */
102
103 /*! \enum Vectorizer::MorphologicalOperation
104 Enum of different possible morphological operations. */
105 /*! \var Vectorizer::MorphologicalOperation Vectorizer::EROSION
106 * Perform erosion on BW image.*/
107 /*! \var Vectorizer::MorphologicalOperation Vectorizer::DILATION
108 * Perform dilation on BW image. */
109 /*! \var Vectorizer::MorphologicalOperation Vectorizer::THINNING_ROSENFELD
110 * Perform thinning on BW image. */
111 /*! \var Vectorizer::MorphologicalOperation Vectorizer::PRUNING
112 * Perform pruning on BW image, i.e. line endpoint removal. */
113
114 /*! \enum Vectorizer::VectorizerState
115 Enum of different facade states. */
116 /*! \var Vectorizer::VectorizerState Vectorizer::NO_IMAGE
117 No image has been set. This state cannot be skipped, use Vectorizer(QImage&
118 im) constructor. */
119 /*! \var Vectorizer::VectorizerState Vectorizer::IMAGE
120 Source image gets classified by running performClassification, then proceeds
121 to CLASSIFIED_IMAGE. */
122 /*! \var Vectorizer::VectorizerState Vectorizer::CLASSIFIED_IMAGE
123 Classified image has been created. This state cannot be skipped, use
124 getBWImage to create an BW image. */
125 /*! \var Vectorizer::VectorizerState Vectorizer::BW_IMAGE
126 BW Image gets thinned and proceeds to THINNED_IMAGE. */
127 /*! \var Vectorizer::VectorizerState Vectorizer::THINNED_IMAGE
128 Final state. */
129 /*! \var Vectorizer::VectorizerState Vectorizer::state
130 Data member holding the state of this facade. */
131
132 /*! \var QImage Vectorizer::sourceImage
133 Source image to be operated on. */
134 /*! \var QImage Vectorizer::classifiedImage
135 Image created by classification. \sa getClassifiedImage */
136 /*! \var QImage Vectorizer::bwImage
137 BW Image created from classifiedImage by selectinn colors. \sa getBWImage */
138 /*! \var QImage Vectorizer::thinnedBWImage
139 (bad member name) BW Image after morphological operation. \sa
140 getTransformedImage */
141 /*! \var MapColor** Vectorizer::sourceImageColors
142 Classified color cluster moments. */
143 /*! \var MapColor* Vectorizer::mc
144 Prototype of mapcolor. */
145 /*! \var int Vectorizer::E
146 Parameter E of ClassicAlphaGetter. \sa setE */
147 /*! \var double Vectorizer::initAlpha
148 Initial alpha of ClassicAlphaGetter. \sa setInitAlpha */
149 /*! \var double Vectorizer::q
150 Parameter q of ClassicAlphaGetter. \sa setQ */
151 /*! \var double Vectorizer::minAlpha
152 Minimal alpha of ClassicAlphaGetter. \sa setMinAlpha */
153 /*! \var double Vectorizer::p
154 Minkowski metrics parameter. \sa setP */
155 /*! \var double Vectorizer::quality
156 Quality of learning as returned by getClassifiedImage. \sa getClassifiedImage
157 */
158 /*! \var LearningMethod Vectorizer::learnMethod
159 Selected learning method. \sa setClassificationMethod */
160 /*! \var ColorSpace Vectorizer::colorSpace
161 Selected color space. \sa setColorSpace */
162 /*! \var AlphaStrategy Vectorizer::alphaStrategy
163 Selected alpha selection strategy. \sa setAlphaStrategy */
164 /*! \var PatternStrategy Vectorizer::patternStrategy
165 Selected pattern selection strategy. \sa setPatternStrategy */
166
Vectorizer()167 Vectorizer::Vectorizer()
168 : mc(std::make_unique<MapColorRGB>(2.0))
169 , E(100000)
170 , initAlpha(0.1)
171 , q(0.5)
172 , minAlpha(1e-6)
173 , p(2)
174 , learnMethod(KOHONEN_CLASSIC)
175 , colorSpace(COLSPC_RGB)
176 , alphaStrategy(ALPHA_CLASSIC)
177 , patternStrategy(PATTERN_RANDOM)
178 {
179 }
180
181 //! Constructs Vectorizer with im as sourceImage.
Vectorizer(QImage & im)182 Vectorizer::Vectorizer(QImage& im)
183 : Vectorizer()
184 {
185 sourceImage = im;
186 }
187
deleteColorsTable()188 void Vectorizer::deleteColorsTable()
189 {
190 sourceImageColors.resize(0);
191 }
192
193 /*! Choose classification method to be used.
194 \sa LearningMethod */
setClassificationMethod(LearningMethod learnMethod)195 void Vectorizer::setClassificationMethod(LearningMethod learnMethod)
196 {
197 this->learnMethod = learnMethod;
198 }
199
200 /*! Set color space in which the classification will be performed. */
setColorSpace(ColorSpace colorSpace)201 void Vectorizer::setColorSpace(ColorSpace colorSpace)
202 {
203 this->colorSpace = colorSpace;
204 switch (colorSpace)
205 {
206 case COLSPC_RGB:
207 mc = std::make_unique<MapColorRGB>(p);
208 break;
209 case COLSPC_HSV:
210 mc = std::make_unique<MapColorHSV>(p);
211 break;
212 default:
213 qWarning("UNIMPLEMENTED colorspace");
214 }
215 }
216
217 /*! Set Minkowski metrics parameter.
218 \sa MapColor */
setP(double p)219 void Vectorizer::setP(double p)
220 {
221 this->p = p;
222 mc->setP(p);
223 }
224
225 /*! Choose alpha selection method to be used.
226 \sa AlphaStrategy */
setAlphaStrategy(AlphaStrategy alphaStrategy)227 void Vectorizer::setAlphaStrategy(AlphaStrategy alphaStrategy)
228 {
229 this->alphaStrategy = alphaStrategy;
230 }
231
232 /*! Choose pattern selection method to be used.
233 \sa PatternStrategy */
setPatternStrategy(PatternStrategy patternStrategy)234 void Vectorizer::setPatternStrategy(PatternStrategy patternStrategy)
235 {
236 this->patternStrategy = patternStrategy;
237 }
238
239 /*! Set initial alpha for ClassicAlphaGetter.
240 \sa ClassicAlphaGetter */
setInitAlpha(double initAlpha)241 void Vectorizer::setInitAlpha(double initAlpha)
242 {
243 this->initAlpha = initAlpha;
244 }
245
246 /*! Set minimal alpha for ClassicAlphaGetter.
247 \sa ClassicAlphaGetter */
setMinAlpha(double minAlpha)248 void Vectorizer::setMinAlpha(double minAlpha)
249 {
250 this->minAlpha = minAlpha;
251 }
252
253 /*! Set q for ClassicAlphaGetter.
254 \sa ClassicAlphaGetter */
setQ(double q)255 void Vectorizer::setQ(double q)
256 {
257 this->q = q;
258 }
259
260 /*! Set E for ClassicAlphaGetter.
261 \sa ClassicAlphaGetter */
setE(int E)262 void Vectorizer::setE(int E)
263 {
264 this->E = E;
265 }
266
267 /*! Set number of colors to be recognized in the picture.
268 */
setNumberOfColors(int nColors)269 void Vectorizer::setNumberOfColors(int nColors)
270 {
271 deleteColorsTable();
272 sourceImageColors.resize(nColors);
273 }
274
275 /*! set initial colors for learning. Sets random colors in case initColors is
276 * 0. */
setInitColors(const std::vector<QRgb> & initColors)277 void Vectorizer::setInitColors(const std::vector<QRgb>& initColors)
278 {
279 for (std::size_t i = 0; i < sourceImageColors.size(); i++)
280 {
281 sourceImageColors[i] =
282 std::shared_ptr<MapColor>(dynamic_cast<MapColor*>(mc->clone()));
283
284 if (!initColors.empty())
285 {
286 // Get provided colors
287 sourceImageColors[i]->setRGBTriplet(initColors[i]);
288 }
289 else
290 {
291 // Random colors at beginning
292 sourceImageColors[i]->setRGBTriplet(
293 qRgb(rand() / (RAND_MAX / 255), // NOLINT
294 rand() / (RAND_MAX / 255), // NOLINT
295 rand() / (RAND_MAX / 255))); // NOLINT
296 }
297 }
298 }
299
300 /*! Runs classfication. The progress observer is called during work if set.
301 * \param[in] progressObserver Pointer to class implementing ProgressObserver.
302 * In case it is null pointer no calls take place.
303 */
performClassification(ProgressObserver * progressObserver)304 bool Vectorizer::performClassification(ProgressObserver* progressObserver)
305 {
306 // invalidate previous images
307 bwImage = classifiedImage = QImage();
308 std::unique_ptr<KohonenPatternGetter> pg;
309
310 // if no initial colors have been set, select random ones
311 if (sourceImageColors.empty()) setInitColors({});
312
313 std::vector<OrganizableElement*> classes(sourceImageColors.size());
314 for (std::size_t i = 0; i < sourceImageColors.size(); i++)
315 classes[i] = sourceImageColors[i].get();
316
317 KohonenMap km;
318 km.setClasses(classes);
319
320 switch (learnMethod)
321 {
322 case KOHONEN_CLASSIC:
323 {
324 switch (patternStrategy)
325 {
326 case PATTERN_RANDOM:
327 pg = std::make_unique<RandomPatternGetter>(sourceImage, mc.get());
328 break;
329 default:
330 qWarning("UNIMPLEMENTED pattern getter");
331 }
332
333 std::unique_ptr<KohonenAlphaGetter> ag;
334 switch (alphaStrategy)
335 {
336 case ALPHA_CLASSIC:
337 ag = std::make_unique<ClassicAlphaGetter>(initAlpha, q, E, minAlpha,
338 progressObserver);
339 break;
340 default:
341 qWarning("UNIMPLEMENTED alpha getter");
342 }
343
344 km.performLearning(*ag, *pg);
345 }
346 break;
347 case KOHONEN_BATCH:
348 {
349 auto bg = std::make_unique<SequentialPatternGetter>(
350 sourceImage, mc.get(), progressObserver);
351 quality = km.performBatchLearning(*bg);
352 classifiedImage = *bg->getClassifiedImage();
353 }
354 break;
355 default:
356 qWarning("UNIMPLEMENTED classification method");
357 }
358
359 bool cancel = progressObserver && progressObserver->isInterruptionRequested();
360 if (cancel)
361 {
362 deleteColorsTable();
363 }
364 else
365 {
366 auto classes = km.getClasses();
367
368 for (std::size_t i = 0; i < sourceImageColors.size(); i++)
369 sourceImageColors[i] = std::shared_ptr<MapColor>(
370 dynamic_cast<MapColor*>(classes[i]->clone()));
371 }
372
373 return !cancel;
374 }
375
376 /*! Returns colors found by classification process. */
getClassifiedColors()377 std::vector<QRgb> Vectorizer::getClassifiedColors()
378 {
379 if (!sourceImageColors.size()) return {};
380
381 std::vector<QRgb> retval(sourceImageColors.size());
382 for (std::size_t i = 0; i < sourceImageColors.size(); i++)
383 retval[i] = sourceImageColors[i]->getRGBTriplet();
384
385 return retval;
386 }
387
388 /*! Creates image where original pixel colors are replaced by "closest
389 * momentum" colors. Also computes quality of learning.
390 * \param qualityPtr Pointer to double where quality of learning will be stored.
391 * \param progressObserver Optional progress observer.
392 * \return New image. */
393
394 class ClassificationMapper
395 {
396 const std::vector<std::shared_ptr<MapColor>>& sourceImageColors;
397 const KohonenMap& km;
398
399 public:
ClassificationMapper(const std::vector<std::shared_ptr<MapColor>> & sic,KohonenMap & km)400 ClassificationMapper(const std::vector<std::shared_ptr<MapColor>>& sic, KohonenMap& km)
401 : sourceImageColors(sic)
402 , km(km)
403 {}
404
operator ()(const QImage & sourceImage,QImage & outputImage,ProgressObserver & observer) const405 double operator()(const QImage& sourceImage, QImage& outputImage, ProgressObserver& observer) const
406 {
407 auto const width = outputImage.width();
408 auto const height = outputImage.height();
409
410 auto color = std::unique_ptr<MapColor>(
411 dynamic_cast<MapColor*>(sourceImageColors[0]->clone()));
412
413 double quality = 0;
414 for (int y = 0; y < height && !observer.isInterruptionRequested(); y++)
415 {
416 for (int x = 0; x < width; x++)
417 {
418 double distance;
419 color->setRGBTriplet(sourceImage.pixel(x, y));
420 int index = km.findClosest(*color, distance);
421 outputImage.setPixel(x, y, index);
422 quality += color->squares(*sourceImageColors[index]);
423 }
424 observer.setPercentage((100*y) / height);
425 }
426 return quality;
427 }
428
429 using concurrent_processing = HorizontalStripes;
430 };
431 Q_STATIC_ASSERT((Concurrency::supported<ClassificationMapper>::value));
432
433
getClassifiedImage(double * qualityPtr,ProgressObserver * progressObserver)434 QImage Vectorizer::getClassifiedImage(double* qualityPtr,
435 ProgressObserver* progressObserver)
436 {
437 if (classifiedImage.isNull())
438 {
439 classifiedImage = QImage(sourceImage.size(), QImage::Format_Indexed8);
440 classifiedImage.setColorCount(sourceImageColors.size());
441 for (std::size_t i = 0; i < sourceImageColors.size(); i++)
442 {
443 classifiedImage.setColor(i, sourceImageColors[i]->getRGBTriplet());
444 }
445
446 std::vector<OrganizableElement*> classes(sourceImageColors.size());
447 for (std::size_t i = 0; i < sourceImageColors.size(); i++)
448 classes[i] = sourceImageColors[i].get();
449
450 KohonenMap km;
451 km.setClasses(classes);
452
453 auto mapFunctor = ClassificationMapper(sourceImageColors, km);
454 auto results = Concurrency::process<double>(progressObserver, mapFunctor, sourceImage, classifiedImage);
455 if (progressObserver && progressObserver->isInterruptionRequested())
456 {
457 classifiedImage = QImage();
458 quality = 0;
459 }
460 else
461 {
462 quality = std::accumulate(begin(results), end(results), 0.0, [](auto a, auto& b) { return a + b.future.result(); });
463 }
464 }
465
466 if (qualityPtr) *qualityPtr = quality;
467
468 return classifiedImage;
469 }
470
471 /*! Creates BW image where pixels are on positions where any of the selected
472 * colors were. Final image is also held in data member bwImage.
473 \param[in] selectedColors Boolean array where true means color is selected and
474 pixels of that color will be set to 1 in output image.
475 \param[in] progressObserver Progress observer.
476 */
477 class BWMapper
478 {
479 const std::vector<bool>& selectedColors;
480
481 public:
BWMapper(const std::vector<bool> & sc)482 explicit BWMapper(const std::vector<bool>& sc)
483 : selectedColors(sc)
484 {}
485
operator ()(const QImage & source_image,QImage & output_image,ProgressObserver & progressObserver) const486 void operator()(const QImage& source_image, QImage& output_image, ProgressObserver& progressObserver) const
487 {
488 auto const width = source_image.width();
489 auto const height = source_image.height();
490 for (int y = 0; y < height; y++)
491 {
492 for (int x = 0; x < width; x++)
493 {
494 auto colorIndex = source_image.pixelIndex(x, y);
495 output_image.setPixel(x, y, selectedColors[colorIndex]);
496 }
497 progressObserver.setPercentage((100*y) / height);
498 }
499 }
500
501 using concurrent_processing = HorizontalStripes;
502 };
503 Q_STATIC_ASSERT((Concurrency::supported<BWMapper>::value));
504
getBWImage(std::vector<bool> selectedColors,ProgressObserver * progressObserver)505 QImage Vectorizer::getBWImage(std::vector<bool> selectedColors,
506 ProgressObserver* progressObserver)
507 {
508 if (bwImage.isNull()
509 || !std::equal(begin(selectedColors), end(selectedColors),
510 begin(this->selectedColors), end(this->selectedColors)))
511 {
512 this->selectedColors = selectedColors;
513 bwImage = QImage(sourceImage.size(), QImage::Format_Mono);
514 bwImage.setColorTable(
515 QVector<QRgb>{QColor(Qt::white).rgb(), QColor(Qt::black).rgb()});
516
517 auto mapFunctor = BWMapper(std::move(selectedColors));
518 Concurrency::process(progressObserver, mapFunctor, classifiedImage, bwImage);
519 if (progressObserver && progressObserver->isInterruptionRequested())
520 bwImage = {};
521 }
522
523 return bwImage;
524 }
525
526 /*! Performs selected Morphological operation on data member bwImage.
527 \sa MorphologicalOperation getTransformedImage(QImage bwImage,
528 MorphologicalOperation mo, ProgressObserver* progressObserver) */
getTransformedImage(MorphologicalOperation mo,ProgressObserver * progressObserver)529 QImage Vectorizer::getTransformedImage(MorphologicalOperation mo,
530 ProgressObserver* progressObserver)
531 {
532 QImage i = Vectorizer::getTransformedImage(bwImage, mo, progressObserver);
533 if (!i.isNull()) bwImage = i;
534 return i;
535 }
536
537 /*! Performs selected Morphological operation.
538 \param[in] bwImage BW image to be processed.
539 \param[in] mo Morphological operation to be performed.
540 \param[in] progressObserver Progress observer.
541 \return Transformed BW image.
542 \sa MorphologicalOperation */
getTransformedImage(const QImage & bwImage,MorphologicalOperation mo,ProgressObserver * progressObserver)543 QImage Vectorizer::getTransformedImage(const QImage& bwImage,
544 MorphologicalOperation mo,
545 ProgressObserver* progressObserver)
546 {
547 QImage outputImage;
548 bool (Morphology::*operation)(ProgressObserver*) = nullptr;
549 switch (mo)
550 {
551 case EROSION:
552 operation = &Morphology::erosion;
553 break;
554 case DILATION:
555 operation = &Morphology::dilation;
556 break;
557 case THINNING_ROSENFELD:
558 operation = &Morphology::rosenfeld;
559 break;
560 case PRUNING:
561 operation = &Morphology::pruning;
562 break;
563 }
564 if (operation)
565 {
566 auto functor = [operation](const QImage& source_image, ProgressObserver& progressObserver) -> QImage {
567 Morphology morphology(source_image);
568 auto ok = (morphology.*operation)(&progressObserver);
569 progressObserver.setPercentage(100);
570 return ok ? morphology.getImage() : QImage{};
571 };
572 outputImage = Concurrency::process<QImage>(progressObserver, functor, bwImage);
573 }
574
575 return outputImage;
576 }
577
578
579 } // cove
580
581 //@}
582