1 /******************************************************************************************************
2  * (C) 2018 markummitchell@github.com. This file is part of Engauge Digitizer, which is released      *
3  * under GNU General Public License version 2 (GPLv2) or (at your option) any later version. See file *
4  * LICENSE or go to gnu.org/licenses for details. Distribution requires prior written permission.     *
5  ******************************************************************************************************/
6 
7 #include "Pixels.h"
8 #include <QImage>
9 #include <qmath.h>
10 #include <QRgb>
11 
Pixels()12 Pixels::Pixels ()
13 {
14 }
15 
countBlackPixelsAroundPoint(const QImage & image,int x,int y,int stopCountAt)16 int Pixels::countBlackPixelsAroundPoint (const QImage &image,
17                                          int x,
18                                          int y,
19                                          int stopCountAt)
20 {
21   int count = 0;
22   QueuedPoints queuedPoints;
23   HashLookup hashLookup; // Prevents reprocessing of already-processed pixels
24 
25   // First point
26   if (pixelIsBlack (image, x, y)) {
27     queuedPoints.push_back (QPoint (x, y));
28   }
29 
30   while (queuedPoints.count () > 0) {
31 
32     // Pop off queue
33     QPoint p = queuedPoints.front ();
34     queuedPoints.pop_front ();
35 
36     QString hash = hashForCoordinates (p.x(),
37                                        p.y());
38 
39     // Skip if out of bounds, processed already or not black
40     bool inBounds = (0 <= p.x() &&
41                      0 <= p.y() &&
42                      p.x() < image.width () &&
43                      p.y() < image.height ());
44     if (inBounds &&
45         !hashLookup.contains (hash) &&
46         pixelIsBlack (image, p.x(), p.y())) {
47 
48       // Black pixel. Add to count, and remember to not reprocess later
49       ++count;
50       if (count == stopCountAt) {
51         return count; // Reached limit. Stop immediately (probably for speed)
52       }
53       hashLookup [hash] = true;
54 
55       // Queue neighbors for processing
56       for (int dx = -1; dx <= 1; dx++) {
57         for (int dy = -1; dy <= 1; dy++) {
58           if (dx != 0 || dy != 0) {
59             queuedPoints.push_back (QPoint (p.x() + dx,
60                                             p.y() + dy));
61           }
62         }
63       }
64     }
65   }
66 
67   return count; // Did not reach limit
68 }
69 
fillHole(QImage & image,int row,int col,int thresholdCount) const70 void Pixels::fillHole (QImage &image,
71                        int row,
72                        int col,
73                        int thresholdCount) const
74 {
75   // Square of 1 pixel is surrounded by 3x3 box with indexes -1 to +2
76   //           2-4 pixels               4x4                  -2 to +2
77   //           5-9                      5x5                  -2 to +3
78   //           10-16                    6x6                  -3 to +3
79   int rowStart = qFloor (row - (1 + qSqrt (thresholdCount - 1))); // Inclusive
80   int colStart = qFloor (col - (1 + qSqrt (thresholdCount - 1))); // Inclusive
81   int rowStop = qFloor (row + (1 + qSqrt (thresholdCount))); // Exclusive
82   int colStop = qFloor (col + (1 + qSqrt (thresholdCount))); // Exclusive
83 
84   // First pass is for counting
85   int countWhite = 0;
86   for (int rowIter = rowStart; rowIter < rowStop; rowIter++) {
87     for (int colIter = colStart; colIter < colStop; colIter++) {
88       if (!pixelIsBlack (image,
89                          colIter,
90                          rowIter)) {
91         ++countWhite;
92       }
93     }
94   }
95 
96   // Second pass fills in the hole
97   if (countWhite < thresholdCount) {
98     for (int rowIter = rowStart; rowIter < rowStop; rowIter++) {
99       for (int colIter = colStart; colIter < colStop; colIter++) {
100         image.setPixel (colIter,
101                         rowIter,
102                         Qt::black);
103       }
104     }
105   }
106 }
107 
fillHoles(QImage & image,int thresholdCount)108 void Pixels::fillHoles (QImage &image,
109                         int thresholdCount)
110 {
111   int height = image.height();
112   int width = image.width();
113 
114   // 2d matrix, indexed as 1d vector, of pixel states
115   QVector<PixelFillState> states (image.width() * image.height());
116   states.fill (PIXEL_FILL_STATE_UNPROCESSED);
117 
118   // Search for unprocessed pixels
119   for (int col = 0; col < width; col++) {
120     for (int row = 0; row < height; row++) {
121       if (states [indexCollapse (row, col, width)] == PIXEL_FILL_STATE_UNPROCESSED) {
122 
123         // Found an unprocessed pixel so process it
124         if (pixelIsBlack (image, col, row)) {
125 
126           // Black pixel needs no processing
127           states [indexCollapse (row, col, width)] = PIXEL_FILL_STATE_PROCESSED;
128 
129         } else {
130 
131           // Get this pixel and all of its white neighbors
132           int pixelsInRegion = fillPass (image,
133                                          states,
134                                          row,
135                                          col,
136                                          PIXEL_FILL_STATE_UNPROCESSED,
137                                          PIXEL_FILL_STATE_IN_PROCESS,
138                                          NO_FILL);
139 
140           FillIt fillIt = (pixelsInRegion < thresholdCount) ? YES_FILL : NO_FILL;
141 
142           fillPass (image,
143                     states,
144                     row,
145                     col,
146                     PIXEL_FILL_STATE_IN_PROCESS,
147                     PIXEL_FILL_STATE_PROCESSED,
148                     fillIt);
149         }
150       }
151     }
152   }
153 }
154 
fillIsolatedWhitePixels(QImage & image)155 void Pixels::fillIsolatedWhitePixels (QImage &image)
156 {
157   const int BORDER = 1;
158   const int HALF_NUMBER_NEIGHBORS = 4; // 8 neighbors in each direction from (col,row)
159 
160   int height = image.height();
161   int width = image.width();
162 
163   // 2d matrix, indexed as 1d vector, of neighbor counts
164   QVector<bool> pixelsAreBlack (image.width() * image.height());
165 
166   // Replace slow QImage addressing by faster QVector addressing
167   for (int col = 0; col < width; col++) {
168     for (int row = 0; row < height; row++) {
169       pixelsAreBlack [indexCollapse (row, col, width)] = pixelIsBlack (image, col, row);
170     }
171   }
172 
173   // Search for white pixels. Black pixels will be ignored, and also pixels along the four
174   // borders are ignored so we do not need to worry about going out of bounds
175   for (int col = BORDER; col < width - BORDER; col++) {
176     for (int row = BORDER; row < height - BORDER; row++) {
177       int count = 0;
178       count += pixelsAreBlack [indexCollapse (row - 1, col - 1, width)] ? 1 : 0;
179       count += pixelsAreBlack [indexCollapse (row - 1, col    , width)] ? 1 : 0;
180       count += pixelsAreBlack [indexCollapse (row - 1, col + 1, width)] ? 1 : 0;
181       count += pixelsAreBlack [indexCollapse (row    , col - 1, width)] ? 1 : 0;
182       count += pixelsAreBlack [indexCollapse (row    , col + 1, width)] ? 1 : 0;
183       count += pixelsAreBlack [indexCollapse (row + 1, col - 1, width)] ? 1 : 0;
184       count += pixelsAreBlack [indexCollapse (row + 1, col    , width)] ? 1 : 0;
185       count += pixelsAreBlack [indexCollapse (row + 1, col + 1, width)] ? 1 : 0;
186       if (count > HALF_NUMBER_NEIGHBORS) {
187         image.setPixel (col,
188                           row,
189                           Qt::black);
190       }
191     }
192   }
193 }
194 
fillPass(QImage & image,QVector<PixelFillState> & states,int rowIn,int colIn,PixelFillState stateFrom,PixelFillState stateTo,FillIt fillit)195 int Pixels::fillPass (QImage &image,
196                       QVector<PixelFillState> &states,
197                       int rowIn,
198                       int colIn,
199                       PixelFillState stateFrom,
200                       PixelFillState stateTo,
201                       FillIt fillit)
202 {
203   int height = image.height();
204   int width = image.width ();
205   int count = 0;
206   QList<QPoint> applicablePoints;
207 
208   // Add only applicable points to the running list
209   applicablePoints.append (QPoint (colIn, rowIn));
210 
211   while (applicablePoints.count() > 0) {
212 
213     QPoint p = applicablePoints.front();
214     applicablePoints.pop_front();
215 
216     int col = p.x();
217     int row = p.y();
218 
219     // Double check point is still applicable and that has not changed since added to list
220     PixelFillState stateGot = states [indexCollapse (row, col, width)];
221     if (stateGot == stateFrom &&
222         !pixelIsBlack (image,
223                        col,
224                        row)) {
225 
226       // Still applicable. Do state-specific stuff here
227       if (stateTo == PIXEL_FILL_STATE_IN_PROCESS) {
228         ++count;
229       } else {
230         if (fillit == YES_FILL) {
231           image.setPixel (col, row, Qt::black);
232         }
233       }
234 
235       // Change state to prevent reprocessing
236       states [indexCollapse (row, col, width)] = stateTo;
237 
238       // "Recurse" using list instead of actual recursion (to prevent stack overflow)
239       for (int dx = -1; dx <= 1; dx++) {
240         int colD = col + dx;
241         if (0 <= colD && colD < width) {
242 
243           for (int dy = -1; dy <= 1; dy++) {
244             int rowD = row + dy;
245             if (0 <= rowD && rowD < height) {
246 
247               if (dx != 0 || dy != 0) {
248 
249                 PixelFillState stateGot = states [indexCollapse (rowD, colD, width)];
250                 if (stateGot == stateFrom &&
251                     !pixelIsBlack (image,
252                                    colD,
253                                    rowD)) {
254 
255                   // This point is applicable
256                   applicablePoints.append (QPoint (colD, rowD));
257                 }
258               }
259             }
260           }
261         }
262       }
263     }
264   }
265 
266   return count;
267 }
268 
hashForCoordinates(int x,int y) const269 QString Pixels::hashForCoordinates (int x,
270                                     int y) const
271 {
272   const int FIELD_WIDTH = 6;
273 
274   return QString ("%1/%2")
275       .arg (x, FIELD_WIDTH)
276       .arg (y, FIELD_WIDTH);
277 }
278 
indexCollapse(int row,int col,int width) const279 int Pixels::indexCollapse (int row,
280                            int col,
281                            int width) const
282 {
283   return row * width + col;
284 }
285 
pixelIsBlack(const QImage & image,int x,int y)286 bool Pixels::pixelIsBlack (const QImage &image,
287                            int x,
288                            int y)
289 {
290   QRgb rgb = image.pixel (x, y);
291   return qGray (rgb) < 128;
292 }
293 
294