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