1 /**
2  * libdmtx - Data Matrix Encoding/Decoding Library
3  * Copyright 2010 Mike Laughton. All rights reserved.
4  *
5  * See LICENSE file in the main project directory for full
6  * terms of use and distribution.
7  *
8  * Contact: Mike Laughton <mike@dragonflylogic.com>
9  *
10  * \file dmtxregion2.c
11  */
12 
13 #include <string.h>
14 #include <math.h>
15 #include <assert.h>
16 #include "../../dmtx.h"
17 #include "multi_test.h"
18 #include "kiss_fftr.h"
19 
20 #define RETURN_FAIL_IF(c) if(c) { return DmtxFail; }
21 
22 /*
23 struct Deskew {
24    DmtxMatrix3 fit2raw;
25    DmtxMatrix3 raw2fit;
26 }
27 
28 struct Timing {
29    double shift;
30    double period;
31 }
32 
33 struct AlignmentGrid {
34    Deskew align;
35    Timing vTiming;
36    Timing hTiming;
37 }
38 */
39 
40 /**
41  *
42  *
43  */
44 DmtxPassFail
dmtxRegion2FindNext(DmtxDecode2 * dec)45 dmtxRegion2FindNext(DmtxDecode2 *dec)
46 {
47    int i, j;
48    int phiDiff, phiDiffTmp;
49    DmtxBoolean regionFound;
50    DmtxPassFail passFail;
51    VanishPointSort vPoints;
52    DmtxOrient orient;
53 
54    vPoints = dmtxFindVanishPoints(dec->hough->vanish);
55    dec->fn.vanishPointCallback(&vPoints, 0);
56 
57    for(i = 0, regionFound = DmtxFalse; i < vPoints.count && regionFound == DmtxFalse; i++)
58    {
59       for(j = i + 1; j < vPoints.count; j++)
60       {
61          phiDiffTmp = abs(vPoints.bucket[i].phi - vPoints.bucket[j].phi);
62          phiDiff = (phiDiffTmp < 64) ? phiDiffTmp : 128 - phiDiffTmp;
63 
64          /* Reject angle combinations that are too close */
65          if(phiDiff < 36)
66             continue;
67 
68          /* Build oriented (but still untimed) grid from vanish points */
69          passFail = OrientRegion(&orient, vPoints.bucket[i], vPoints.bucket[j], dec);
70          if(passFail == DmtxFail)
71             continue;
72 
73          /* Build timed grid from untimed grid and line hough */
74 /*       align = CalibrateRegion(region, orient, lHough, &passFail);
75          if(passFail == DmtxFail)
76             continue;
77 */
78 /*
79          // timings = dmtxFindGridTiming(dec->hough->line, &vPoints);
80 
81          err = dmtxBuildGridFromTimings(&grid, timings.timing[i], timings.timing[j]);
82          if(err == DmtxFail)
83             continue; // Keep trying
84 
85          dec->fn.timingCallback(&timings.timing[i], &timings.timing[j], 1);
86 
87          // Hack together raw2fitFull and fit2rawFull outside since we need app data
88          AddFullTransforms(&grid);
89 
90          err = dmtxFindRegionWithinGrid(&region, &grid, &houghCache, dec, fn);
91          regionFound = (err == DmtxPass) ? DmtxTrue : DmtxFalse;
92 
93          if(regionFound == DmtxTrue) {
94             region.sizeIdx = dmtxGetSizeIdx(region.width, region.height);
95             if(region.sizeIdx >= DmtxSymbol10x10 && region.sizeIdx <= DmtxSymbol16x48)
96                dmtxDecodeSymbol(&region, dec);
97          }
98 
99          regionFound = DmtxTrue; // break out of outer loop
100          break; // break out of inner loop
101 */
102       }
103    }
104 
105    return DmtxPass;
106 }
107 
108 /**
109  *
110  *
111  */
112 DmtxPassFail
OrientRegion(DmtxOrient * orient,DmtxHoughBucket v0,DmtxHoughBucket v1,DmtxDecode2 * dec)113 OrientRegion(DmtxOrient *orient, DmtxHoughBucket v0, DmtxHoughBucket v1, DmtxDecode2 *dec)
114 {
115    DmtxRay2 v0a, v0b, v1a, v1b;
116 
117    v0a = dec->corners[v0.d][v0.phi].lineA;
118    v0b = dec->corners[v0.d][v0.phi].lineB;
119    v1a = dec->corners[v1.d][v1.phi].lineA;
120    v1b = dec->corners[v1.d][v1.phi].lineB;
121 /*
122    RegionFromSides(v0a, v0b, v1a, v1b);
123 */
124    return DmtxPass;
125 }
126 
127 /**
128  *
129  *
130  */
131 /*
132 DmtxPassFail
133 CalibrateRegion(DmtxOrient *orient)
134 {
135 input:  orientation (transformation matrix)
136         hough line cache
137 
138 output: timed alignment grid
139 
140 description:
141   fourier transform receives evenly spaced samples to be taken in both directions (v and h)
142     -> tranforms into frequency space
143     -> major frequency emerges
144   determine shift as post-processing step (can't remember how I did it before at the moment)
145 
146   need to interpolate values between true locations in line hough because
147     "evenly spaced" describes positions in the normalized fitted region whereas
148     the hough values are aligned along raw image coordinates
149 
150   step size should be determined based on desired fft dimensions (maybe 64
151     steps?) ... check what we did for poc
152 
153 steps:
154    for each step 0-63 upward along Y axis
155       xFit = 0
156       yFit = 1-63
157       xRaw,yRaw = VMult(fit2raw,xFit,yFit)
158       lineFit = (xRaw - 0, yRaw - 0) (duh)
159       phi = lineFit angle (something like atan2)
160       d = vector2Mag(lineFit)
161       interpolate hough[d][phi] and add to fft
162       that was easy
163 
164    return DmtxPass;
165 }
166 */
167 
168 /**
169  * Future structure of dmtxRegion2FindNext() (after getting orientation and timing working again)
170  *
171  * TODO:
172  * o Is there a way to name HoughGridPopulate() and HoughGridMerge() to show they are sister functions?
173  *
174  * hough lines, hough line maxima, and hough vanish can be calculated for entire image at once
175  * for each local:
176  *    for each combination of vpoint pairs:
177  *       step outward
178  *          if step takes us into adjacent local
179  *              add maxima from adjacent local into existing vpoint hough accumulator
180  *              rescan for updated vpoints (using original centerline, adding only portion of region?)
181  *              get new timing (?)
182  *              continue stepping path where we left off?
183  *
184  * progress:
185  *    level
186  *    row
187  *    col
188  *    vanish point combo (?)
189  */
190 /*
191 dmtxRegion2FindNext(DmtxDecode2 *dec)
192 {
193    if(starting new level other than the first)
194       HoughGridMerge();
195 
196    // for each new local
197    while(dec->localIdx < dec->localCount)
198    {
199       // each valid combination of vanish points
200       for(dec->vPointIdx < 28)
201       {
202          vPointI = xyz;
203          vPointJ = xyz;
204 
205          while(perimeter condition not met)
206          {
207             stepOutward()
208 
209             if(first step in new local, including first)
210             {
211                add new region to central vanish hough
212                update vanish points (shouldn't move too far)
213                record that new local was added to stats
214                redo timing
215             }
216 
217             test perimeter for known strip patterns
218             if(perimeter says that barcode region is found) {
219                save current progress ("?")
220                return region;
221             }
222          }
223       }
224    }
225 
226    return NULL;
227 }
228 */
229 
230 /**
231  *
232  *
233  */
234 double
UncompactOffset(double compactedOffset,int phiIdx,int extent)235 UncompactOffset(double compactedOffset, int phiIdx, int extent)
236 {
237    double phiRad;
238    double scale;
239    double posMax, negMax;
240 
241    phiRad = M_PI * phiIdx / 128.0;
242 
243    if(phiIdx < 64) {
244       posMax = extent * (cos(phiRad) + sin(phiRad));
245       negMax = 0.0;
246    }
247    else {
248       posMax = extent * sin(phiRad);
249       negMax = extent * cos(phiRad);
250    }
251 
252    assert(extent > 0);
253    scale = (posMax - negMax) / extent;
254 
255    return (compactedOffset * scale) + negMax;
256 }
257 
258 /**
259  *
260  *
261  */
262 void
AddToVanishPointSort(VanishPointSort * sort,DmtxHoughBucket bucket)263 AddToVanishPointSort(VanishPointSort *sort, DmtxHoughBucket bucket)
264 {
265    int i, startHere;
266    int phiDiff, phiDiffTmp;
267    DmtxBoolean isFull;
268    DmtxHoughBucket *lastBucket;
269 
270    isFull = (sort->count == ANGLE_SORT_MAX_COUNT) ? DmtxTrue : DmtxFalse;
271    lastBucket = &(sort->bucket[ANGLE_SORT_MAX_COUNT - 1]);
272 
273    /* Array is full and incoming bucket is already weakest */
274    if(isFull && bucket.val < lastBucket->val)
275       return;
276 
277    startHere = DmtxUndefined;
278 
279    /* If sort already has entry near this angle then either:
280     *   a) Overwrite the old one without shifting if stronger
281     *   b) Reject the new one completely if weaker
282     */
283    for(i = 0; i < sort->count; i++)
284    {
285       phiDiffTmp = abs(bucket.phi - sort->bucket[i].phi);
286       phiDiff = (phiDiffTmp < 64) ? phiDiffTmp : 128 - phiDiffTmp;
287 
288       if(phiDiff < 10)
289       {
290          /* Similar angle is already represented with stronger magnitude */
291          if(bucket.val < sort->bucket[i].val)
292          {
293             return;
294          }
295          /* Found similar-but-weaker angle that will be overwritten */
296          else
297          {
298             sort->bucket[i] = bucket;
299             startHere = i;
300             break;
301          }
302       }
303    }
304 
305    if(startHere == DmtxUndefined)
306    {
307       if(isFull)
308          *lastBucket = bucket;
309       else
310          sort->bucket[sort->count++] = bucket;
311 
312       startHere = sort->count - 1;
313    }
314 
315    /* Shift weak entries downward */
316    for(i = startHere; i > 0; i--)
317    {
318       if(bucket.val > sort->bucket[i-1].val)
319       {
320          sort->bucket[i] = sort->bucket[i-1];
321          sort->bucket[i-1] = bucket;
322       }
323       else
324       {
325          break;
326       }
327    }
328 }
329 
330 /**
331  *
332  *
333  */
334 VanishPointSort
dmtxFindVanishPoints(DmtxHoughLocal * vHough)335 dmtxFindVanishPoints(DmtxHoughLocal *vHough)
336 {
337    DmtxHoughBucket bucket;
338    VanishPointSort sort;
339 
340    memset(&sort, 0x00, sizeof(VanishPointSort));
341 
342    for(bucket.phi = 0; bucket.phi < 128; bucket.phi++)
343    {
344       for(bucket.d = 0; bucket.d < 64; bucket.d++)
345       {
346          bucket.val = vHough->bucket[bucket.d][bucket.phi];
347          AddToVanishPointSort(&sort, bucket);
348       }
349    }
350 
351    return sort;
352 }
353 
354 /**
355  *
356  *
357  */
358 void
AddToMaximaSort(HoughMaximaSort * sort,int maximaMag)359 AddToMaximaSort(HoughMaximaSort *sort, int maximaMag)
360 {
361    int i;
362 
363    /* If new entry would be weakest (or only) one in list, then append */
364    if(sort->count == 0 || maximaMag < sort->mag[sort->count - 1]) {
365       if(sort->count + 1 < MAXIMA_SORT_MAX_COUNT)
366          sort->mag[sort->count++] = maximaMag;
367       return;
368    }
369 
370    /* Otherwise shift the weaker entries downward */
371    for(i = sort->count - 1; i >= 0; i--) {
372       if(maximaMag > sort->mag[i]) {
373          if(i + 1 < MAXIMA_SORT_MAX_COUNT)
374             sort->mag[i+1] = sort->mag[i];
375          sort->mag[i] = maximaMag;
376       }
377    }
378 
379    if(sort->count < MAXIMA_SORT_MAX_COUNT)
380       sort->count++;
381 }
382 
383 /**
384  * Return sum of top 8 maximum points (hmmm)
385  * btw, can we skip this step entirely?
386  */
387 DmtxHoughBucket
GetAngleSumAtPhi(DmtxHoughLocal * line,int phi)388 GetAngleSumAtPhi(DmtxHoughLocal *line, int phi)
389 {
390    int i, d;
391    int prev, here, next;
392    DmtxHoughBucket bucket;
393    HoughMaximaSort sort;
394 
395    memset(&sort, 0x00, sizeof(HoughMaximaSort));
396 
397    /* Handle last condition separately; one sided comparison */
398    prev = line->bucket[62][phi];
399    here = line->bucket[63][phi];
400    if(here > prev)
401       AddToMaximaSort(&sort, here);
402 
403    /* Handle first condition separately; one sided comparison */
404    here = line->bucket[0][phi];
405    next = line->bucket[1][phi];
406    if(here > next)
407       AddToMaximaSort(&sort, here);
408 
409    /* Handle remaining conditions as two sided comparisons */
410    for(d = 2; d < 64; d++)
411    {
412       prev = here;
413       here = next;
414       next = line->bucket[d][phi];
415 
416       if(here > 0 && here >= prev && here >= next)
417          AddToMaximaSort(&sort, here);
418    }
419 
420    bucket.d = 0;
421    bucket.phi = phi;
422    bucket.val = 0;
423    for(i = 0; i < 8; i++)
424       bucket.val += sort.mag[i];
425 
426    return bucket;
427 }
428 
429 /**
430  *
431  *
432  */
433 void
AddToTimingSort(DmtxTimingSort * sort,Timing timing)434 AddToTimingSort(DmtxTimingSort *sort, Timing timing)
435 {
436    int i;
437 
438    if(timing.mag < 1.0) /* XXX or some minimum threshold */
439       return;
440 
441    /* If new entry would be weakest (or only) one in list, then append */
442    if(sort->count == 0 || timing.mag < sort->timing[sort->count - 1].mag) {
443       if(sort->count + 1 < TIMING_SORT_MAX_COUNT)
444          sort->timing[sort->count++] = timing;
445       return;
446    }
447 
448    /* Otherwise shift the weaker entries downward */
449    for(i = sort->count - 1; i >= 0; i--) {
450       if(timing.mag > sort->timing[i].mag) {
451          if(i + 1 < TIMING_SORT_MAX_COUNT)
452             sort->timing[i+1] = sort->timing[i];
453          sort->timing[i] = timing;
454       }
455    }
456 
457    if(sort->count < TIMING_SORT_MAX_COUNT)
458       sort->count++;
459 }
460 
461 /**
462  *
463  *
464  */
465 DmtxTimingSort
dmtxFindGridTiming(DmtxHoughLocal * line,VanishPointSort * vPoints)466 dmtxFindGridTiming(DmtxHoughLocal *line, VanishPointSort *vPoints)
467 {
468    int x, y, fitMag, fitMax, fitOff, attempts, iter;
469    int i, vSortIdx, phi;
470    kiss_fftr_cfg   cfg = NULL;
471    kiss_fft_scalar rin[NFFT];
472    kiss_fft_cpx    sout[NFFT/2+1];
473    kiss_fft_scalar mag[NFFT/2+1];
474    int maxIdx;
475    Timing timing;
476    DmtxTimingSort timings;
477 
478    memset(&timings, 0x00, sizeof(DmtxTimingSort));
479 
480    for(vSortIdx = 0; vSortIdx < vPoints->count; vSortIdx++) {
481 
482       phi = vPoints->bucket[vSortIdx].phi;
483 
484       /* Load FFT input array */
485       for(i = 0; i < NFFT; i++) {
486          rin[i] = (i < 64) ? line->bucket[i][phi] : 0;
487       }
488 
489       /* Execute FFT */
490       memset(sout, 0x00, sizeof(kiss_fft_cpx) * (NFFT/2 + 1));
491       cfg = kiss_fftr_alloc(NFFT, 0, 0, 0);
492       kiss_fftr(cfg, rin, sout);
493       free(cfg);
494 
495       /* Select best result */
496       maxIdx = NFFT/9-1;
497       for(i = 0; i < NFFT/9-1; i++)
498          mag[i] = 0.0;
499       for(i = NFFT/9-1; i < NFFT/2+1; i++) {
500          mag[i] = sout[i].r * sout[i].r + sout[i].i * sout[i].i;
501          if(mag[i] > mag[maxIdx])
502             maxIdx = i;
503       }
504 
505       timing.phi = phi;
506       timing.period = NFFT / (double)maxIdx;
507       timing.mag = mag[maxIdx];
508 
509       /* Find best offset */
510       fitOff = fitMax = 0;
511       attempts = (int)timing.period + 1;
512       for(x = 0; x < attempts; x++) {
513          fitMag = 0;
514          for(iter = 0; ; iter++) {
515             y = x + (int)(iter * timing.period);
516             if(y >= 64)
517                break;
518             fitMag += line->bucket[y][timing.phi];
519          }
520          if(x == 0 || fitMag > fitMax) {
521             fitMax = fitMag;
522             fitOff = x;
523          }
524       }
525       timing.shift = fitOff;
526 
527       AddToTimingSort(&timings, timing);
528    }
529 
530    return timings;
531 }
532 
533 /**
534  *
535  *
536  */
537 DmtxRay2
HoughCompactToRay(int phi,double d)538 HoughCompactToRay(int phi, double d)
539 {
540    double phiRad;
541    double dScaled;
542    DmtxRay2 rStart, rLine;
543 
544    memset(&rStart, 0x00, sizeof(DmtxRay2));
545    memset(&rLine, 0x00, sizeof(DmtxRay2));
546 
547    rStart.p.X = rStart.p.Y = 0.0;
548 
549    phiRad = phi * M_PI/128.0;
550 
551    rStart.v.X = cos(phiRad);
552    rStart.v.Y = sin(phiRad);
553 
554    rLine.v.X = -rStart.v.Y;
555    rLine.v.Y = rStart.v.X;
556 
557    dScaled = UncompactOffset(d, phi, LOCAL_SIZE);
558 
559    dmtxPointAlongRay2(&(rLine.p), &rStart, dScaled);
560 
561    return rLine;
562 }
563 
564 /**
565  *
566  *
567  *
568  */
569 DmtxPassFail
dmtxBuildGridFromTimings(AlignmentGrid * grid,Timing vp0,Timing vp1)570 dmtxBuildGridFromTimings(AlignmentGrid *grid, Timing vp0, Timing vp1)
571 {
572    RegionLines rl0, rl1, *flat, *steep;
573    DmtxVector2 p00, p10, p11, p01;
574    DmtxMatrix3 fit2raw, raw2fit, mScale;
575 
576    /* (1) -- later compare all possible combinations for strongest pair */
577    rl0.timing = vp0;
578    rl0.gridCount = (int)((64.0 - vp0.shift)/vp0.period);
579    rl0.dA = vp0.shift;
580    rl0.dB = vp0.shift + vp0.period * rl0.gridCount; /* doesn't work but whatever */
581 
582    rl1.timing = vp1;
583    rl1.gridCount = (int)((64.0 - vp1.shift)/vp1.period);
584    rl1.dA = vp1.shift;
585    rl1.dB = vp1.shift + vp1.period * rl1.gridCount; /* doesn't work but whatever */
586 
587    /* flat[0] is the bottom flat line */
588    /* flat[1] is the top flat line */
589    /* steep[0] is the left steep line */
590    /* steep[1] is the right line */
591 
592    /* Line with angle closest to horizontal is flatest */
593    if(abs(64 - rl0.timing.phi) < abs(64 - rl1.timing.phi)) {
594       flat = &rl0;
595       steep = &rl1;
596    }
597    else {
598       flat = &rl1;
599       steep = &rl0;
600    }
601 
602    flat->line[0] = HoughCompactToRay(flat->timing.phi, flat->dA);
603    flat->line[1] = HoughCompactToRay(flat->timing.phi, flat->dB);
604 
605    if(steep->timing.phi < 64) {
606       steep->line[0] = HoughCompactToRay(steep->timing.phi, steep->dA);
607       steep->line[1] = HoughCompactToRay(steep->timing.phi, steep->dB);
608    }
609    else {
610       steep->line[0] = HoughCompactToRay(steep->timing.phi, steep->dB);
611       steep->line[1] = HoughCompactToRay(steep->timing.phi, steep->dA);
612    }
613 
614    RETURN_FAIL_IF(dmtxRay2Intersect(&p00, &(flat->line[0]), &(steep->line[0])) == DmtxFail);
615    RETURN_FAIL_IF(dmtxRay2Intersect(&p10, &(flat->line[0]), &(steep->line[1])) == DmtxFail);
616    RETURN_FAIL_IF(dmtxRay2Intersect(&p11, &(flat->line[1]), &(steep->line[1])) == DmtxFail);
617    RETURN_FAIL_IF(dmtxRay2Intersect(&p01, &(flat->line[1]), &(steep->line[0])) == DmtxFail);
618    RETURN_FAIL_IF(RegionUpdateCorners(fit2raw, raw2fit, p00, p10, p11, p01) == DmtxFail);
619 
620    grid->rowCount = flat->gridCount;
621    grid->colCount = steep->gridCount;
622 
623    /* raw2fit: Final transformation fits single origin module */
624    dmtxMatrix3Identity(mScale);
625    dmtxMatrix3Multiply(grid->raw2fitActive, raw2fit, mScale);
626 
627    /* fit2raw: Abstract away display nuances of multi_test application */
628    dmtxMatrix3Identity(mScale);
629    dmtxMatrix3Multiply(grid->fit2rawActive, mScale, fit2raw);
630 
631    return DmtxPass;
632 }
633 
634 /**
635  *
636  *
637  */
638 StripStats
GenStripPatternStats(unsigned char * strip,int stripLength,int startState,int contrast)639 GenStripPatternStats(unsigned char *strip, int stripLength, int startState, int contrast)
640 {
641    int i, jumpAmount, jumpThreshold;
642    int finderLow, finderHigh, timingLow, timingHigh;
643    int surpriseCount, jumpCount, contrastSum;
644    int newState, currentState;
645    StripStats stats;
646 
647    assert(startState == MODULE_HIGH || startState == MODULE_LOW);
648 
649    jumpThreshold = (contrast*40)/100;
650    finderLow = finderHigh = timingLow = timingHigh = 0;
651    surpriseCount = jumpCount = contrastSum = 0;
652    currentState = startState;
653    memset(&stats, 0x00, sizeof(StripStats));
654 
655    for(i = 0; i < stripLength; i++) {
656 
657       if(i > 0) {
658          jumpAmount = strip[i] - strip[i-1];
659 
660          /* Tally jump statistics if occurred */
661          if(abs(jumpAmount) > jumpThreshold) {
662             jumpCount++;
663             contrastSum += abs(jumpAmount);
664             newState = (jumpAmount > 0) ? MODULE_HIGH : MODULE_LOW;
665 
666             /* Surprise! We jumped but landed in the same state */
667             if(newState == currentState)
668                surpriseCount++;
669             else
670                currentState = newState;
671          }
672       }
673 
674       /* Increment appropriate finder pattern */
675       if(currentState == MODULE_HIGH)
676          finderHigh++;
677       else
678          finderLow++;
679 
680       /* Increment appropriate timing pattern */
681       if(currentState ^ (i & 0x01))
682          timingHigh++;
683       else
684          timingLow++;
685    }
686 
687    stats.jumps = jumpCount;
688    stats.surprises = surpriseCount;
689    stats.finderErrors = (finderHigh < finderLow) ? finderHigh : finderLow;
690    stats.timingErrors = (timingHigh < timingLow) ? timingHigh : timingLow;
691 
692    if(jumpCount > 0) {
693       stats.contrast = (int)((double)contrastSum/jumpCount + 0.5);
694       stats.finderBest = (finderHigh > finderLow) ? MODULE_HIGH : MODULE_LOW;
695       stats.timingBest = (timingHigh > timingLow) ? MODULE_HIGH : MODULE_LOW;
696    }
697    else {
698       stats.contrast = 0;
699       stats.finderBest = MODULE_UNKNOWN;
700       stats.timingBest = MODULE_UNKNOWN;
701    }
702 
703    return stats;
704 }
705 
706 /**
707  *
708  *
709  */
710 DmtxPassFail
dmtxFindRegionWithinGrid(GridRegion * region,AlignmentGrid * grid,DmtxHoughLocal * line,DmtxDecode * dec,DmtxCallbacks * fn)711 dmtxFindRegionWithinGrid(GridRegion *region, AlignmentGrid *grid, DmtxHoughLocal *line, DmtxDecode *dec, DmtxCallbacks *fn)
712 {
713    int goodCount;
714    int finderSides;
715    DmtxDirection sideDir;
716    DmtxBarType innerType, outerType;
717    GridRegion regGrow;
718 
719    memset(&regGrow, 0x00, sizeof(GridRegion));
720 
721    regGrow.grid = *grid; /* Capture local copy of grid for tweaking */
722    regGrow.x = regGrow.grid.colCount / 2;
723    regGrow.y = regGrow.grid.rowCount / 2;
724    regGrow.width = 2;
725    regGrow.height = 2;
726    regGrow.sizeIdx = DmtxUndefined;
727    regGrow.onColor = regGrow.offColor = 0;
728    regGrow.contrast = 20; /* low initial value */
729 
730    /* Assume the starting region will be far enough away from any side that
731     * the expansion rules won't need to work at the very smallest sizes */
732 
733    /* Grow region */
734    finderSides = DmtxDirNone;
735    for(goodCount = 0, sideDir = DmtxDirDown; goodCount < 4; sideDir = RotateCW(sideDir)) {
736       if(regGrow.width > 26 || regGrow.height > 26)
737          return DmtxFail;
738 
739       innerType = TestSideForPattern(&regGrow, dec->image, sideDir, 0);
740       outerType = TestSideForPattern(&regGrow, dec->image, sideDir, 1);
741 
742       /**
743        * Make smarter ... maybe:
744        * 1) Check that next-farther out strips are consistent (easy)
745        * 2) Check that the colors are all consistent (not as easy)
746        */
747 
748       if(innerType == DmtxBarNone || outerType == DmtxBarNone) {
749 /*       RegionExpand(&regGrow, sideDir, line, fn); */
750          finderSides = DmtxDirNone;
751          goodCount = 0;
752       }
753       else {
754          if(innerType == DmtxBarFinder)
755             finderSides |= sideDir;
756 
757          goodCount++;
758       }
759 
760 /*    fn->perimeterCallback(&regGrow, sideDir, innerType); */
761    }
762 
763    regGrow.finderSides = finderSides;
764    *region = regGrow;
765 
766    return DmtxPass;
767 }
768 
769 /**
770  *
771  *
772  */
dmtxReadModuleColor(DmtxImage * img,AlignmentGrid * grid,int symbolRow,int symbolCol,int colorPlane)773 int dmtxReadModuleColor(DmtxImage *img, AlignmentGrid *grid, int symbolRow,
774       int symbolCol, int colorPlane)
775 {
776    int err;
777    int i;
778    int color, colorTmp;
779    double sampleX[] = { 0.5, 0.4, 0.5, 0.6, 0.5 };
780    double sampleY[] = { 0.5, 0.5, 0.4, 0.5, 0.6 };
781    DmtxVector2 p;
782 
783    color = 0;
784    for(i = 0; i < 5; i++) {
785 
786       p.X = (1.0/grid->colCount) * (symbolCol + sampleX[i]);
787       p.Y = (1.0/grid->rowCount) * (symbolRow + sampleY[i]);
788 
789       dmtxMatrix3VMultiplyBy(&p, grid->fit2rawFull);
790 
791       /* Should use dmtxDecodeGetPixelValue() later to properly handle pixel skipping */
792       err = dmtxImageGetPixelValue(img, p.X, p.Y, colorPlane, &colorTmp);
793       if(err == DmtxFail)
794          return 0;
795 
796       color += colorTmp;
797    }
798 
799    return color/5;
800 }
801 
802 /**
803  *
804  *
805  */
806 DmtxBarType
TestSideForPattern(GridRegion * region,DmtxImage * img,DmtxDirection side,int offset)807 TestSideForPattern(GridRegion *region, DmtxImage *img, DmtxDirection side, int offset)
808 {
809    int i;
810    int col, colBeg;
811    int row, rowBeg;
812    int extent;
813    unsigned char colorStrip[26] = { 0 };
814    StripStats *stats, statsHigh, statsLow;
815 
816    assert(region->width <= 26 && region->height <= 26);
817    assert(side == DmtxDirUp || side == DmtxDirLeft ||
818          side == DmtxDirDown || side == DmtxDirRight);
819 
820    /* Note opposite meaning (DmtxDirUp means "top", extent is horizontal) */
821    extent = (side & DmtxDirVertical) ? region->width : region->height;
822    if(extent < 3)
823       return DmtxBarNone;
824 
825    switch(side) {
826       case DmtxDirUp:
827          colBeg = region->x;
828          rowBeg = (region->y + region->height - 1) + offset;
829          break;
830       case DmtxDirLeft:
831          colBeg = region->x - offset;
832          rowBeg = region->y;
833          break;
834       case DmtxDirDown:
835          colBeg = region->x;
836          rowBeg = region->y - offset;
837          break;
838       case DmtxDirRight:
839          colBeg = (region->x + region->width - 1) + offset;
840          rowBeg = region->y;
841          break;
842       default:
843          return DmtxUndefined;
844    }
845 
846    /* Sample and hold colors at each module along both inner and outer edges */
847    col = colBeg;
848    row = rowBeg;
849 
850    for(i = 0; i < extent; i++) {
851       colorStrip[i] = dmtxReadModuleColor(img, &(region->grid), row, col, 0);
852 
853       if(side & DmtxDirVertical)
854          col++;
855       else
856          row++;
857    }
858 
859    statsHigh = GenStripPatternStats(colorStrip, extent, MODULE_HIGH, region->contrast);
860    statsLow = GenStripPatternStats(colorStrip, extent, MODULE_LOW, region->contrast);
861    stats = (statsHigh.surprises > statsLow.surprises) ? &statsLow : &statsHigh;
862 
863    if(stats->contrast > region->contrast) {
864       region->contrast = stats->contrast;
865    }
866 
867    if(stats->finderErrors < stats->timingErrors) {
868       if(stats->finderErrors < 2) {
869          return DmtxBarFinder;
870       }
871    }
872    else if(stats->finderErrors > stats->timingErrors) {
873       if(stats->timingErrors < 2) {
874          return DmtxBarTiming;
875       }
876    }
877 
878    return DmtxBarNone;
879 }
880 
881 #define DmtxAlmostZero          0.000001
882 
883 /**
884  *
885  *
886  */
887 DmtxPassFail
Vector2Norm(DmtxVector2 * v)888 Vector2Norm(DmtxVector2 *v)
889 {
890    double mag;
891 
892    mag = dmtxVector2Mag(v);
893 
894    if(mag <= DmtxAlmostZero)
895       return DmtxFail;
896 
897    dmtxVector2ScaleBy(v, 1/mag);
898 
899    return DmtxTrue;
900 }
901 
902 /**
903  *
904  *
905  */
906 DmtxPassFail
RayFromPoints(DmtxRay2 * ray,DmtxVector2 p0,DmtxVector2 p1)907 RayFromPoints(DmtxRay2 *ray, DmtxVector2 p0, DmtxVector2 p1)
908 {
909    DmtxRay2 r;
910 
911    r.tMin = 0.0;
912    r.tMax = 1.0;
913    r.p = p0;
914 
915    dmtxVector2Sub(&r.v, &p1, &p0);
916    RETURN_FAIL_IF(Vector2Norm(&r.v) == DmtxFail);
917 
918    *ray = r;
919 
920    return DmtxPass;
921 }
922 
923 /**
924  * Not a generic function. Notice that it uses fit2rawActive.
925  *
926  */
927 DmtxPassFail
dmtxRegionToSides(GridRegion * region,DmtxRegionSides * regionSides)928 dmtxRegionToSides(GridRegion *region, DmtxRegionSides *regionSides)
929 {
930    DmtxVector2 p00, p10, p11, p01;
931    DmtxRegionSides rs;
932 
933    p00.X = p01.X = region->x * (1.0/region->grid.colCount);
934    p10.X = p11.X = (region->x + region->width) * (1.0/region->grid.colCount);
935    p00.Y = p10.Y = region->y * (1.0/region->grid.rowCount);
936    p01.Y = p11.Y = (region->y + region->height) * (1.0/region->grid.rowCount);
937 
938    dmtxMatrix3VMultiplyBy(&p00, region->grid.fit2rawActive);
939    dmtxMatrix3VMultiplyBy(&p10, region->grid.fit2rawActive);
940    dmtxMatrix3VMultiplyBy(&p11, region->grid.fit2rawActive);
941    dmtxMatrix3VMultiplyBy(&p01, region->grid.fit2rawActive);
942 
943    RETURN_FAIL_IF(RayFromPoints(&rs.bottom, p00, p10) == DmtxFail);
944    RETURN_FAIL_IF(RayFromPoints(&rs.right, p10, p11) == DmtxFail);
945    RETURN_FAIL_IF(RayFromPoints(&rs.top, p11, p01) == DmtxFail);
946    RETURN_FAIL_IF(RayFromPoints(&rs.left, p01, p00) == DmtxFail);
947 
948    *regionSides = rs;
949 
950    return DmtxPass;
951 }
952 
953 /**
954  *
955  *
956  */
957 DmtxPassFail
dmtxRegionUpdateFromSides(GridRegion * region,DmtxRegionSides regionSides)958 dmtxRegionUpdateFromSides(GridRegion *region, DmtxRegionSides regionSides)
959 {
960    DmtxVector2 p00, p10, p11, p01;
961 
962    RETURN_FAIL_IF(dmtxRay2Intersect(&p00, &(regionSides.left), &(regionSides.bottom)) == DmtxFail);
963    RETURN_FAIL_IF(dmtxRay2Intersect(&p10, &(regionSides.bottom), &(regionSides.right)) == DmtxFail);
964    RETURN_FAIL_IF(dmtxRay2Intersect(&p11, &(regionSides.right), &(regionSides.top)) == DmtxFail);
965    RETURN_FAIL_IF(dmtxRay2Intersect(&p01, &(regionSides.top), &(regionSides.left)) == DmtxFail);
966 
967    RETURN_FAIL_IF(RegionUpdateCorners(region->grid.fit2rawActive, region->grid.raw2fitActive, p00, p10, p11, p01) == DmtxFail);
968    /* need to update fit2rawFull and raw2fitFull here too? */
969 
970    return DmtxPass;
971 }
972 
973 /**
974  *
975  *
976  */
977 DmtxPassFail
RegionExpand(GridRegion * region,DmtxDirection sideDir,DmtxHoughLocal * line,DmtxCallbacks * fn)978 RegionExpand(GridRegion *region, DmtxDirection sideDir, DmtxHoughLocal *line, DmtxCallbacks *fn)
979 {
980    DmtxRegionSides regionSides;
981    DmtxRay2 *sideRay;
982 
983    switch(sideDir) {
984       case DmtxDirDown:
985          region->y--;
986          region->height++;
987          sideRay = &(regionSides.bottom);
988          break;
989       case DmtxDirUp:
990          region->height++;
991          sideRay = &(regionSides.top);
992          break;
993       case DmtxDirLeft:
994          region->x--;
995          region->width++;
996          sideRay = &(regionSides.left);
997          break;
998       case DmtxDirRight:
999          region->width++;
1000          sideRay = &(regionSides.right);
1001          break;
1002       default:
1003          return DmtxFail;
1004    }
1005 
1006    return DmtxPass;
1007 }
1008 
1009 /**
1010  *
1011  *
1012  */
1013 int
dmtxGetSizeIdx(int a,int b)1014 dmtxGetSizeIdx(int a, int b)
1015 {
1016    int i, rows, cols;
1017    const int totalSymbolSizes = 30; /* is there a better way to determine this? */
1018 
1019    for(i = 0; i < totalSymbolSizes; i++) {
1020       rows = dmtxGetSymbolAttribute(DmtxSymAttribSymbolRows, i);
1021       cols = dmtxGetSymbolAttribute(DmtxSymAttribSymbolCols, i);
1022 
1023       if((rows == a && cols == b) || (rows == b && cols == a))
1024          return i;
1025    }
1026 
1027    return DmtxUndefined;
1028 }
1029 
1030 /**
1031  *
1032  *
1033  */
1034 DmtxPassFail
RegionUpdateCorners(DmtxMatrix3 fit2raw,DmtxMatrix3 raw2fit,DmtxVector2 p00,DmtxVector2 p10,DmtxVector2 p11,DmtxVector2 p01)1035 RegionUpdateCorners(DmtxMatrix3 fit2raw, DmtxMatrix3 raw2fit, DmtxVector2 p00,
1036       DmtxVector2 p10, DmtxVector2 p11, DmtxVector2 p01)
1037 {
1038 /* double xMax, yMax; */
1039    double tx, ty, phi, shx, scx, scy, skx, sky;
1040    double dimOT, dimOR, dimTX, dimRX; /* , ratio; */
1041    DmtxVector2 vOT, vOR, vTX, vRX, vTmp;
1042    DmtxMatrix3 m, mtxy, mphi, mshx, mscx, mscy, mscxy, msky, mskx;
1043 
1044 /* xMax = (double)(dmtxDecodeGetProp(dec, DmtxPropWidth) - 1);
1045    yMax = (double)(dmtxDecodeGetProp(dec, DmtxPropHeight) - 1);
1046 
1047    if(p00.X < 0.0 || p00.Y < 0.0 || p00.X > xMax || p00.Y > yMax ||
1048          p01.X < 0.0 || p01.Y < 0.0 || p01.X > xMax || p01.Y > yMax ||
1049          p10.X < 0.0 || p10.Y < 0.0 || p10.X > xMax || p10.Y > yMax)
1050       return DmtxFail; */
1051    dimOT = dmtxVector2Mag(dmtxVector2Sub(&vOT, &p01, &p00)); /* XXX could use MagSquared() */
1052    dimOR = dmtxVector2Mag(dmtxVector2Sub(&vOR, &p10, &p00));
1053    dimTX = dmtxVector2Mag(dmtxVector2Sub(&vTX, &p11, &p01));
1054    dimRX = dmtxVector2Mag(dmtxVector2Sub(&vRX, &p11, &p10));
1055 
1056    /* Verify that sides are reasonably long */
1057 /* if(dimOT <= 8.0 || dimOR <= 8.0 || dimTX <= 8.0 || dimRX <= 8.0)
1058       return DmtxFail; */
1059 
1060    /* Verify that the 4 corners define a reasonably fat quadrilateral */
1061 /* ratio = dimOT / dimRX;
1062    if(ratio <= 0.5 || ratio >= 2.0)
1063       return DmtxFail; */
1064 
1065 /* ratio = dimOR / dimTX;
1066    if(ratio <= 0.5 || ratio >= 2.0)
1067       return DmtxFail; */
1068 
1069    /* Verify this is not a bowtie shape */
1070 /*
1071    if(dmtxVector2Cross(&vOR, &vRX) <= 0.0 ||
1072          dmtxVector2Cross(&vOT, &vTX) >= 0.0)
1073       return DmtxFail; */
1074 
1075 /* if(RightAngleTrueness(p00, p10, p11, M_PI_2) <= dec->squareDevn)
1076       return DmtxFail;
1077    if(RightAngleTrueness(p10, p11, p01, M_PI_2) <= dec->squareDevn)
1078       return DmtxFail; */
1079 
1080    /* Calculate values needed for transformations */
1081    tx = -1 * p00.X;
1082    ty = -1 * p00.Y;
1083    dmtxMatrix3Translate(mtxy, tx, ty);
1084 
1085    phi = atan2(vOT.X, vOT.Y);
1086    dmtxMatrix3Rotate(mphi, phi);
1087    dmtxMatrix3Multiply(m, mtxy, mphi);
1088 
1089    dmtxMatrix3VMultiply(&vTmp, &p10, m);
1090    shx = -vTmp.Y / vTmp.X;
1091    dmtxMatrix3Shear(mshx, 0.0, shx);
1092    dmtxMatrix3MultiplyBy(m, mshx);
1093 
1094    scx = 1.0/vTmp.X;
1095    dmtxMatrix3Scale(mscx, scx, 1.0);
1096    dmtxMatrix3MultiplyBy(m, mscx);
1097 
1098    dmtxMatrix3VMultiply(&vTmp, &p11, m);
1099    scy = 1.0/vTmp.Y;
1100    dmtxMatrix3Scale(mscy, 1.0, scy);
1101    dmtxMatrix3MultiplyBy(m, mscy);
1102 
1103    dmtxMatrix3VMultiply(&vTmp, &p11, m);
1104    skx = vTmp.X;
1105    dmtxMatrix3LineSkewSide(mskx, 1.0, skx, 1.0);
1106    dmtxMatrix3MultiplyBy(m, mskx);
1107 
1108    dmtxMatrix3VMultiply(&vTmp, &p01, m);
1109    sky = vTmp.Y;
1110    dmtxMatrix3LineSkewTop(msky, sky, 1.0, 1.0);
1111    dmtxMatrix3Multiply(raw2fit, m, msky);
1112 
1113    /* Create inverse matrix by reverse (avoid straight matrix inversion) */
1114    dmtxMatrix3LineSkewTopInv(msky, sky, 1.0, 1.0);
1115    dmtxMatrix3LineSkewSideInv(mskx, 1.0, skx, 1.0);
1116    dmtxMatrix3Multiply(m, msky, mskx);
1117 
1118    dmtxMatrix3Scale(mscxy, 1.0/scx, 1.0/scy);
1119    dmtxMatrix3MultiplyBy(m, mscxy);
1120 
1121    dmtxMatrix3Shear(mshx, 0.0, -shx);
1122    dmtxMatrix3MultiplyBy(m, mshx);
1123 
1124    dmtxMatrix3Rotate(mphi, -phi);
1125    dmtxMatrix3MultiplyBy(m, mphi);
1126 
1127    dmtxMatrix3Translate(mtxy, -tx, -ty);
1128    dmtxMatrix3Multiply(fit2raw, m, mtxy);
1129 
1130    return DmtxPass;
1131 }
1132 
1133 /**
1134  *
1135  *
1136  */
1137 DmtxPassFail
dmtxDecodeSymbol(GridRegion * region,DmtxDecode * dec)1138 dmtxDecodeSymbol(GridRegion *region, DmtxDecode *dec)
1139 {
1140    static int prefix = 0;
1141    int onColor, offColor;
1142    DmtxVector2 p00, p10, p11, p01;
1143    DmtxRegion reg;
1144    DmtxMessage *msg;
1145 
1146    /* Since we now hold 2 adjacent timing bars, find colors */
1147    RETURN_FAIL_IF(GetOnOffColors(region, dec, &onColor, &offColor) == DmtxFail);
1148 
1149    p00.X = p01.X = region->x * (1.0/region->grid.colCount);
1150    p10.X = p11.X = (region->x + region->width) * (1.0/region->grid.colCount);
1151    p00.Y = p10.Y = region->y * (1.0/region->grid.rowCount);
1152    p01.Y = p11.Y = (region->y + region->height) * (1.0/region->grid.rowCount);
1153 
1154    dmtxMatrix3VMultiplyBy(&p00, region->grid.fit2rawFull);
1155    dmtxMatrix3VMultiplyBy(&p10, region->grid.fit2rawFull);
1156    dmtxMatrix3VMultiplyBy(&p11, region->grid.fit2rawFull);
1157    dmtxMatrix3VMultiplyBy(&p01, region->grid.fit2rawFull);
1158 
1159    /* Update DmtxRegion with detected corners */
1160    switch(region->finderSides) {
1161       case (DmtxDirLeft | DmtxDirDown):
1162          RETURN_FAIL_IF(dmtxRegionUpdateCorners(dec, &reg, p00, p10, p11, p01) == DmtxFail);
1163          break;
1164       case (DmtxDirDown | DmtxDirRight):
1165          RETURN_FAIL_IF(dmtxRegionUpdateCorners(dec, &reg, p10, p11, p01, p00) == DmtxFail);
1166          break;
1167       case (DmtxDirRight | DmtxDirUp):
1168          RETURN_FAIL_IF(dmtxRegionUpdateCorners(dec, &reg, p11, p01, p00, p10) == DmtxFail);
1169          break;
1170       case (DmtxDirUp | DmtxDirLeft):
1171          RETURN_FAIL_IF(dmtxRegionUpdateCorners(dec, &reg, p01, p00, p10, p11) == DmtxFail);
1172          break;
1173       default:
1174          return DmtxFail;
1175    }
1176 
1177    /* Populate old-style region */
1178    reg.flowBegin.plane = 0; /* or 1, or 2 (0 = red, 1 = green, etc...) */
1179    reg.onColor = onColor;
1180    reg.offColor = offColor;
1181    reg.sizeIdx = region->sizeIdx;
1182    reg.symbolRows = dmtxGetSymbolAttribute(DmtxSymAttribSymbolRows, region->sizeIdx);
1183    reg.symbolCols = dmtxGetSymbolAttribute(DmtxSymAttribSymbolCols, region->sizeIdx);
1184    reg.mappingRows = dmtxGetSymbolAttribute(DmtxSymAttribMappingMatrixRows, region->sizeIdx);
1185    reg.mappingCols = dmtxGetSymbolAttribute(DmtxSymAttribMappingMatrixCols, region->sizeIdx);
1186 
1187    msg = dmtxDecodeMatrixRegion(dec, &reg, DmtxUndefined);
1188    if(msg == NULL)
1189       return DmtxFail;
1190 
1191    fprintf(stdout, "%d: ", prefix);
1192    prefix = (prefix == 9) ? 0 : prefix + 1;
1193    fwrite(msg->output, sizeof(char), msg->outputIdx, stdout);
1194    fputc('\n', stdout);
1195    fflush(stdout);
1196 
1197    return DmtxPass;
1198 }
1199 
1200 /**
1201  *
1202  *
1203  */
1204 DmtxPassFail
GetOnOffColors(GridRegion * region,const DmtxDecode * dec,int * onColor,int * offColor)1205 GetOnOffColors(GridRegion *region, const DmtxDecode *dec, int *onColor, int *offColor)
1206 {
1207    int colBeg, rowBeg;
1208    DmtxDirection d0, d1;
1209    ColorTally t0, t1;
1210 
1211    /* add assertion to guarantee 2 adjancent timing bars */
1212 
1213    /* Start tally at intersection of timing bars */
1214    colBeg = (region->finderSides & DmtxDirLeft) ? region->x + region->width - 1 : region->x;
1215    rowBeg = (region->finderSides & DmtxDirDown) ? region->y + region->height - 1 : region->y;
1216 
1217    switch(region->finderSides) {
1218       case (DmtxDirLeft | DmtxDirDown):
1219          d0 = DmtxDirLeft;
1220          d1 = DmtxDirDown;
1221          break;
1222       case (DmtxDirDown | DmtxDirRight):
1223          d0 = DmtxDirDown;
1224          d1 = DmtxDirRight;
1225          break;
1226       case (DmtxDirRight | DmtxDirUp):
1227          d0 = DmtxDirRight;
1228          d1 = DmtxDirUp;
1229          break;
1230       case (DmtxDirUp | DmtxDirLeft):
1231          d0 = DmtxDirUp;
1232          d1 = DmtxDirLeft;
1233          break;
1234       default:
1235          return DmtxFail;
1236    }
1237 
1238    t0 = GetTimingColors(region, dec, colBeg, rowBeg, d0);
1239    t1 = GetTimingColors(region, dec, colBeg, rowBeg, d1);
1240 
1241    if(t0.evnCount + t1.evnCount == 0 || t0.oddCount + t1.oddCount == 0)
1242       return DmtxFail;
1243 
1244    *onColor = (t0.oddColor + t1.oddColor)/(t0.oddCount + t1.oddCount);
1245    *offColor = (t0.evnColor + t1.evnColor)/(t0.evnCount + t1.evnCount);
1246 
1247    return DmtxPass;
1248 }
1249 
1250 /**
1251  *
1252  *
1253  */
1254 ColorTally
GetTimingColors(GridRegion * region,const DmtxDecode * dec,int colBeg,int rowBeg,DmtxDirection dir)1255 GetTimingColors(GridRegion *region, const DmtxDecode *dec, int colBeg, int rowBeg,
1256       DmtxDirection dir)
1257 {
1258    int i, row, col, extent;
1259    int sample;
1260    ColorTally colors;
1261 
1262    colors.evnColor = 0;
1263    colors.evnCount = 0;
1264 
1265    colors.oddColor = 0;
1266    colors.oddCount = 0;
1267 
1268    /* Note opposite meaning (DmtxDirUp means "top", extent is horizontal) */
1269    extent = (dir & DmtxDirVertical) ? region->width : region->height;
1270 
1271    col = colBeg;
1272    row = rowBeg;
1273    for(i = 0; i < extent; i++) {
1274       sample = dmtxReadModuleColor(dec->image, &(region->grid), row, col, 0);
1275 
1276       if(i & 0x01) {
1277          colors.oddColor += sample;
1278          colors.oddCount++;
1279       }
1280       else {
1281          colors.evnColor += sample;
1282          colors.evnCount++;
1283       }
1284 
1285       switch(dir) {
1286          case DmtxDirUp:
1287             row++;
1288             break;
1289          case DmtxDirLeft:
1290             col--;
1291             break;
1292          case DmtxDirDown:
1293             row--;
1294             break;
1295          case DmtxDirRight:
1296             col++;
1297             break;
1298          default:
1299             return colors; /* XXX should be an error condition */
1300       }
1301    }
1302 
1303     /* consider having PerimeterEdgeTest() call this function when ready? */
1304 
1305    return colors;
1306 }
1307