You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ZXQRCodeFinderPatternFinder.m 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  1. /*
  2. * Copyright 2012 ZXing authors
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #import "ZXBitMatrix.h"
  17. #import "ZXDecodeHints.h"
  18. #import "ZXErrors.h"
  19. #import "ZXQRCodeFinderPattern.h"
  20. #import "ZXQRCodeFinderPatternInfo.h"
  21. #import "ZXQRCodeFinderPatternFinder.h"
  22. #import "ZXResultPoint.h"
  23. #import "ZXResultPointCallback.h"
  24. const int ZX_CENTER_QUORUM = 2;
  25. const int ZX_FINDER_PATTERN_MIN_SKIP = 3;
  26. const int ZX_FINDER_PATTERN_MAX_MODULES = 57;
  27. @interface ZXQRCodeFinderPatternFinder ()
  28. NSInteger centerCompare(id center1, id center2, void *context);
  29. NSInteger furthestFromAverageCompare(id center1, id center2, void *context);
  30. @property (nonatomic, assign) BOOL hasSkipped;
  31. @property (nonatomic, weak, readonly) id<ZXResultPointCallback> resultPointCallback;
  32. @property (nonatomic, strong) NSMutableArray *possibleCenters;
  33. @end
  34. @implementation ZXQRCodeFinderPatternFinder
  35. - (id)initWithImage:(ZXBitMatrix *)image {
  36. return [self initWithImage:image resultPointCallback:nil];
  37. }
  38. - (id)initWithImage:(ZXBitMatrix *)image resultPointCallback:(id<ZXResultPointCallback>)resultPointCallback {
  39. if (self = [super init]) {
  40. _image = image;
  41. _possibleCenters = [NSMutableArray array];
  42. _resultPointCallback = resultPointCallback;
  43. }
  44. return self;
  45. }
  46. - (ZXQRCodeFinderPatternInfo *)find:(ZXDecodeHints *)hints error:(NSError **)error {
  47. BOOL tryHarder = hints != nil && hints.tryHarder;
  48. BOOL pureBarcode = hints != nil && hints.pureBarcode;
  49. int maxI = self.image.height;
  50. int maxJ = self.image.width;
  51. int iSkip = (3 * maxI) / (4 * ZX_FINDER_PATTERN_MAX_MODULES);
  52. if (iSkip < ZX_FINDER_PATTERN_MIN_SKIP || tryHarder) {
  53. iSkip = ZX_FINDER_PATTERN_MIN_SKIP;
  54. }
  55. BOOL done = NO;
  56. int stateCount[5];
  57. for (int i = iSkip - 1; i < maxI && !done; i += iSkip) {
  58. stateCount[0] = 0;
  59. stateCount[1] = 0;
  60. stateCount[2] = 0;
  61. stateCount[3] = 0;
  62. stateCount[4] = 0;
  63. int currentState = 0;
  64. for (int j = 0; j < maxJ; j++) {
  65. if ([self.image getX:j y:i]) {
  66. if ((currentState & 1) == 1) {
  67. currentState++;
  68. }
  69. stateCount[currentState]++;
  70. } else {
  71. if ((currentState & 1) == 0) {
  72. if (currentState == 4) {
  73. if ([ZXQRCodeFinderPatternFinder foundPatternCross:stateCount]) {
  74. BOOL confirmed = [self handlePossibleCenter:stateCount i:i j:j pureBarcode:pureBarcode];
  75. if (confirmed) {
  76. iSkip = 2;
  77. if (self.hasSkipped) {
  78. done = [self haveMultiplyConfirmedCenters];
  79. } else {
  80. int rowSkip = [self findRowSkip];
  81. if (rowSkip > stateCount[2]) {
  82. i += rowSkip - stateCount[2] - iSkip;
  83. j = maxJ - 1;
  84. }
  85. }
  86. } else {
  87. stateCount[0] = stateCount[2];
  88. stateCount[1] = stateCount[3];
  89. stateCount[2] = stateCount[4];
  90. stateCount[3] = 1;
  91. stateCount[4] = 0;
  92. currentState = 3;
  93. continue;
  94. }
  95. currentState = 0;
  96. stateCount[0] = 0;
  97. stateCount[1] = 0;
  98. stateCount[2] = 0;
  99. stateCount[3] = 0;
  100. stateCount[4] = 0;
  101. } else {
  102. stateCount[0] = stateCount[2];
  103. stateCount[1] = stateCount[3];
  104. stateCount[2] = stateCount[4];
  105. stateCount[3] = 1;
  106. stateCount[4] = 0;
  107. currentState = 3;
  108. }
  109. } else {
  110. stateCount[++currentState]++;
  111. }
  112. } else {
  113. stateCount[currentState]++;
  114. }
  115. }
  116. }
  117. if ([ZXQRCodeFinderPatternFinder foundPatternCross:stateCount]) {
  118. BOOL confirmed = [self handlePossibleCenter:stateCount i:i j:maxJ pureBarcode:pureBarcode];
  119. if (confirmed) {
  120. iSkip = stateCount[0];
  121. if (self.hasSkipped) {
  122. done = [self haveMultiplyConfirmedCenters];
  123. }
  124. }
  125. }
  126. }
  127. NSMutableArray *patternInfo = [self selectBestPatterns];
  128. if (!patternInfo) {
  129. if (error) *error = ZXNotFoundErrorInstance();
  130. return nil;
  131. }
  132. [ZXResultPoint orderBestPatterns:patternInfo];
  133. return [[ZXQRCodeFinderPatternInfo alloc] initWithPatternCenters:patternInfo];
  134. }
  135. /**
  136. * Given a count of black/white/black/white/black pixels just seen and an end position,
  137. * figures the location of the center of this run.
  138. */
  139. - (float)centerFromEnd:(const int[])stateCount end:(int)end {
  140. return (float)(end - stateCount[4] - stateCount[3]) - stateCount[2] / 2.0f;
  141. }
  142. + (BOOL)foundPatternCross:(const int[])stateCount {
  143. int totalModuleSize = 0;
  144. for (int i = 0; i < 5; i++) {
  145. int count = stateCount[i];
  146. if (count == 0) {
  147. return NO;
  148. }
  149. totalModuleSize += count;
  150. }
  151. if (totalModuleSize < 7) {
  152. return NO;
  153. }
  154. float moduleSize = totalModuleSize / 7.0f;
  155. float maxVariance = moduleSize / 2.0f;
  156. // Allow less than 50% variance from 1-1-3-1-1 proportions
  157. return
  158. ABS(moduleSize - stateCount[0]) < maxVariance &&
  159. ABS(moduleSize - stateCount[1]) < maxVariance &&
  160. ABS(3.0f * moduleSize - stateCount[2]) < 3 * maxVariance &&
  161. ABS(moduleSize - stateCount[3]) < maxVariance &&
  162. ABS(moduleSize - stateCount[4]) < maxVariance;
  163. }
  164. /**
  165. * After a vertical and horizontal scan finds a potential finder pattern, this method
  166. * "cross-cross-cross-checks" by scanning down diagonally through the center of the possible
  167. * finder pattern to see if the same proportion is detected.
  168. *
  169. * @param startI row where a finder pattern was detected
  170. * @param centerJ center of the section that appears to cross a finder pattern
  171. * @param maxCount maximum reasonable number of modules that should be
  172. * observed in any reading state, based on the results of the horizontal scan
  173. * @param originalStateCountTotal The original state count total.
  174. * @return true if proportions are withing expected limits
  175. */
  176. - (BOOL)crossCheckDiagonal:(int)startI centerJ:(int)centerJ maxCount:(int)maxCount originalStateCountTotal:(int)originalStateCountTotal {
  177. int stateCount[5] = {0, 0, 0, 0, 0};
  178. // Start counting up, left from center finding black center mass
  179. int i = 0;
  180. while (startI >= i && centerJ >= i && [self.image getX:centerJ - i y:startI - i]) {
  181. stateCount[2]++;
  182. i++;
  183. }
  184. if (startI < i || centerJ < i) {
  185. return NO;
  186. }
  187. // Continue up, left finding white space
  188. while (startI >= i && centerJ >= i && ![self.image getX:centerJ - i y:startI - i] &&
  189. stateCount[1] <= maxCount) {
  190. stateCount[1]++;
  191. i++;
  192. }
  193. // If already too many modules in this state or ran off the edge:
  194. if (startI < i || centerJ < i || stateCount[1] > maxCount) {
  195. return NO;
  196. }
  197. // Continue up, left finding black border
  198. while (startI >= i && centerJ >= i && [self.image getX:centerJ - i y:startI - i] &&
  199. stateCount[0] <= maxCount) {
  200. stateCount[0]++;
  201. i++;
  202. }
  203. if (stateCount[0] > maxCount) {
  204. return NO;
  205. }
  206. int maxI = self.image.height;
  207. int maxJ = self.image.width;
  208. // Now also count down, right from center
  209. i = 1;
  210. while (startI + i < maxI && centerJ + i < maxJ && [self.image getX:centerJ + i y:startI + i]) {
  211. stateCount[2]++;
  212. i++;
  213. }
  214. // Ran off the edge?
  215. if (startI + i >= maxI || centerJ + i >= maxJ) {
  216. return NO;
  217. }
  218. while (startI + i < maxI && centerJ + i < maxJ && ![self.image getX:centerJ + i y:startI + i] &&
  219. stateCount[3] < maxCount) {
  220. stateCount[3]++;
  221. i++;
  222. }
  223. if (startI + i >= maxI || centerJ + i >= maxJ || stateCount[3] >= maxCount) {
  224. return NO;
  225. }
  226. while (startI + i < maxI && centerJ + i < maxJ && [self.image getX:centerJ + i y:startI + i] &&
  227. stateCount[4] < maxCount) {
  228. stateCount[4]++;
  229. i++;
  230. }
  231. if (stateCount[4] >= maxCount) {
  232. return NO;
  233. }
  234. // If we found a finder-pattern-like section, but its size is more than 100% different than
  235. // the original, assume it's a false positive
  236. int stateCountTotal = stateCount[0] + stateCount[1] + stateCount[2] + stateCount[3] + stateCount[4];
  237. return
  238. abs(stateCountTotal - originalStateCountTotal) < 2 * originalStateCountTotal &&
  239. [ZXQRCodeFinderPatternFinder foundPatternCross:stateCount];
  240. }
  241. /**
  242. * After a horizontal scan finds a potential finder pattern, this method
  243. * "cross-checks" by scanning down vertically through the center of the possible
  244. * finder pattern to see if the same proportion is detected.
  245. *
  246. * @param startI row where a finder pattern was detected
  247. * @param centerJ center of the section that appears to cross a finder pattern
  248. * @param maxCount maximum reasonable number of modules that should be
  249. * observed in any reading state, based on the results of the horizontal scan
  250. * @return vertical center of finder pattern, or {@link Float#NaN} if not found
  251. */
  252. - (float)crossCheckVertical:(int)startI centerJ:(int)centerJ maxCount:(int)maxCount originalStateCountTotal:(int)originalStateCountTotal {
  253. int maxI = self.image.height;
  254. int stateCount[5] = {0, 0, 0, 0, 0};
  255. int i = startI;
  256. while (i >= 0 && [self.image getX:centerJ y:i]) {
  257. stateCount[2]++;
  258. i--;
  259. }
  260. if (i < 0) {
  261. return NAN;
  262. }
  263. while (i >= 0 && ![self.image getX:centerJ y:i] && stateCount[1] <= maxCount) {
  264. stateCount[1]++;
  265. i--;
  266. }
  267. if (i < 0 || stateCount[1] > maxCount) {
  268. return NAN;
  269. }
  270. while (i >= 0 && [self.image getX:centerJ y:i] && stateCount[0] <= maxCount) {
  271. stateCount[0]++;
  272. i--;
  273. }
  274. if (stateCount[0] > maxCount) {
  275. return NAN;
  276. }
  277. i = startI + 1;
  278. while (i < maxI && [self.image getX:centerJ y:i]) {
  279. stateCount[2]++;
  280. i++;
  281. }
  282. if (i == maxI) {
  283. return NAN;
  284. }
  285. while (i < maxI && ![self.image getX:centerJ y:i] && stateCount[3] < maxCount) {
  286. stateCount[3]++;
  287. i++;
  288. }
  289. if (i == maxI || stateCount[3] >= maxCount) {
  290. return NAN;
  291. }
  292. while (i < maxI && [self.image getX:centerJ y:i] && stateCount[4] < maxCount) {
  293. stateCount[4]++;
  294. i++;
  295. }
  296. if (stateCount[4] >= maxCount) {
  297. return NAN;
  298. }
  299. int stateCountTotal = stateCount[0] + stateCount[1] + stateCount[2] + stateCount[3] + stateCount[4];
  300. if (5 * abs(stateCountTotal - originalStateCountTotal) >= 2 * originalStateCountTotal) {
  301. return NAN;
  302. }
  303. return [ZXQRCodeFinderPatternFinder foundPatternCross:stateCount] ? [self centerFromEnd:stateCount end:i] : NAN;
  304. }
  305. /**
  306. * Like crossCheckVertical, and in fact is basically identical,
  307. * except it reads horizontally instead of vertically. This is used to cross-cross
  308. * check a vertical cross check and locate the real center of the alignment pattern.
  309. */
  310. - (float)crossCheckHorizontal:(int)startJ centerI:(int)centerI maxCount:(int)maxCount originalStateCountTotal:(int)originalStateCountTotal {
  311. int maxJ = self.image.width;
  312. int stateCount[5] = {0, 0, 0, 0, 0};
  313. int j = startJ;
  314. while (j >= 0 && [self.image getX:j y:centerI]) {
  315. stateCount[2]++;
  316. j--;
  317. }
  318. if (j < 0) {
  319. return NAN;
  320. }
  321. while (j >= 0 && ![self.image getX:j y:centerI] && stateCount[1] <= maxCount) {
  322. stateCount[1]++;
  323. j--;
  324. }
  325. if (j < 0 || stateCount[1] > maxCount) {
  326. return NAN;
  327. }
  328. while (j >= 0 && [self.image getX:j y:centerI] && stateCount[0] <= maxCount) {
  329. stateCount[0]++;
  330. j--;
  331. }
  332. if (stateCount[0] > maxCount) {
  333. return NAN;
  334. }
  335. j = startJ + 1;
  336. while (j < maxJ && [self.image getX:j y:centerI]) {
  337. stateCount[2]++;
  338. j++;
  339. }
  340. if (j == maxJ) {
  341. return NAN;
  342. }
  343. while (j < maxJ && ![self.image getX:j y:centerI] && stateCount[3] < maxCount) {
  344. stateCount[3]++;
  345. j++;
  346. }
  347. if (j == maxJ || stateCount[3] >= maxCount) {
  348. return NAN;
  349. }
  350. while (j < maxJ && [self.image getX:j y:centerI] && stateCount[4] < maxCount) {
  351. stateCount[4]++;
  352. j++;
  353. }
  354. if (stateCount[4] >= maxCount) {
  355. return NAN;
  356. }
  357. int stateCountTotal = stateCount[0] + stateCount[1] + stateCount[2] + stateCount[3] + stateCount[4];
  358. if (5 * abs(stateCountTotal - originalStateCountTotal) >= originalStateCountTotal) {
  359. return NAN;
  360. }
  361. return [ZXQRCodeFinderPatternFinder foundPatternCross:stateCount] ? [self centerFromEnd:stateCount end:j] : NAN;
  362. }
  363. - (BOOL)handlePossibleCenter:(const int[])stateCount i:(int)i j:(int)j pureBarcode:(BOOL)pureBarcode {
  364. int stateCountTotal = stateCount[0] + stateCount[1] + stateCount[2] + stateCount[3] + stateCount[4];
  365. float centerJ = [self centerFromEnd:stateCount end:j];
  366. float centerI = [self crossCheckVertical:i centerJ:(int)centerJ maxCount:stateCount[2] originalStateCountTotal:stateCountTotal];
  367. if (!isnan(centerI)) {
  368. centerJ = [self crossCheckHorizontal:(int)centerJ centerI:(int)centerI maxCount:stateCount[2] originalStateCountTotal:stateCountTotal];
  369. if (!isnan(centerJ) &&
  370. (!pureBarcode || [self crossCheckDiagonal:(int)centerI centerJ:(int) centerJ maxCount:stateCount[2] originalStateCountTotal:stateCountTotal])) {
  371. float estimatedModuleSize = (float)stateCountTotal / 7.0f;
  372. BOOL found = NO;
  373. int max = (int)[self.possibleCenters count];
  374. for (int index = 0; index < max; index++) {
  375. ZXQRCodeFinderPattern *center = self.possibleCenters[index];
  376. if ([center aboutEquals:estimatedModuleSize i:centerI j:centerJ]) {
  377. self.possibleCenters[index] = [center combineEstimateI:centerI j:centerJ newModuleSize:estimatedModuleSize];
  378. found = YES;
  379. break;
  380. }
  381. }
  382. if (!found) {
  383. ZXResultPoint *point = [[ZXQRCodeFinderPattern alloc] initWithPosX:centerJ posY:centerI estimatedModuleSize:estimatedModuleSize];
  384. [self.possibleCenters addObject:point];
  385. if (self.resultPointCallback != nil) {
  386. [self.resultPointCallback foundPossibleResultPoint:point];
  387. }
  388. }
  389. return YES;
  390. }
  391. }
  392. return NO;
  393. }
  394. /**
  395. * @return number of rows we could safely skip during scanning, based on the first
  396. * two finder patterns that have been located. In some cases their position will
  397. * allow us to infer that the third pattern must lie below a certain point farther
  398. * down in the image.
  399. */
  400. - (int)findRowSkip {
  401. int max = (int)[self.possibleCenters count];
  402. if (max <= 1) {
  403. return 0;
  404. }
  405. ZXResultPoint *firstConfirmedCenter = nil;
  406. for (int i = 0; i < max; i++) {
  407. ZXQRCodeFinderPattern *center = self.possibleCenters[i];
  408. if ([center count] >= ZX_CENTER_QUORUM) {
  409. if (firstConfirmedCenter == nil) {
  410. firstConfirmedCenter = center;
  411. } else {
  412. self.hasSkipped = YES;
  413. return (int)(fabsf([firstConfirmedCenter x] - [center x]) - fabsf([firstConfirmedCenter y] - [center y])) / 2;
  414. }
  415. }
  416. }
  417. return 0;
  418. }
  419. /**
  420. * @return true iff we have found at least 3 finder patterns that have been detected
  421. * at least ZX_CENTER_QUORUM times each, and, the estimated module size of the
  422. * candidates is "pretty similar"
  423. */
  424. - (BOOL)haveMultiplyConfirmedCenters {
  425. int confirmedCount = 0;
  426. float totalModuleSize = 0.0f;
  427. int max = (int)[self.possibleCenters count];
  428. for (int i = 0; i < max; i++) {
  429. ZXQRCodeFinderPattern *pattern = self.possibleCenters[i];
  430. if ([pattern count] >= ZX_CENTER_QUORUM) {
  431. confirmedCount++;
  432. totalModuleSize += [pattern estimatedModuleSize];
  433. }
  434. }
  435. if (confirmedCount < 3) {
  436. return NO;
  437. }
  438. float average = totalModuleSize / (float)max;
  439. float totalDeviation = 0.0f;
  440. for (int i = 0; i < max; i++) {
  441. ZXQRCodeFinderPattern *pattern = self.possibleCenters[i];
  442. totalDeviation += fabsf([pattern estimatedModuleSize] - average);
  443. }
  444. return totalDeviation <= 0.05f * totalModuleSize;
  445. }
  446. /**
  447. * Orders by ZXFinderPattern count, descending.
  448. */
  449. NSInteger centerCompare(id center1, id center2, void *context) {
  450. float average = [(__bridge NSNumber *)context floatValue];
  451. if ([((ZXQRCodeFinderPattern *)center2) count] == [((ZXQRCodeFinderPattern *)center1) count]) {
  452. float dA = fabsf([((ZXQRCodeFinderPattern *)center2) estimatedModuleSize] - average);
  453. float dB = fabsf([((ZXQRCodeFinderPattern *)center1) estimatedModuleSize] - average);
  454. return dA < dB ? 1 : dA == dB ? 0 : -1;
  455. } else {
  456. return [((ZXQRCodeFinderPattern *)center2) count] - [((ZXQRCodeFinderPattern *)center1) count];
  457. }
  458. }
  459. /**
  460. * Orders by furthest from average
  461. */
  462. NSInteger furthestFromAverageCompare(id center1, id center2, void *context) {
  463. float average = [(__bridge NSNumber *)context floatValue];
  464. float dA = fabsf([((ZXQRCodeFinderPattern *)center2) estimatedModuleSize] - average);
  465. float dB = fabsf([((ZXQRCodeFinderPattern *)center1) estimatedModuleSize] - average);
  466. return dA < dB ? -1 : dA == dB ? 0 : 1;
  467. }
  468. /**
  469. * @return the 3 best ZXFinderPatterns from our list of candidates. The "best" are
  470. * those that have been detected at least ZXCENTER_QUORUM times, and whose module
  471. * size differs from the average among those patterns the least
  472. * @return nil if 3 such finder patterns do not exist
  473. */
  474. - (NSMutableArray *)selectBestPatterns {
  475. int startSize = (int)[self.possibleCenters count];
  476. if (startSize < 3) {
  477. return nil;
  478. }
  479. if (startSize > 3) {
  480. float totalModuleSize = 0.0f;
  481. float square = 0.0f;
  482. for (int i = 0; i < startSize; i++) {
  483. float size = [self.possibleCenters[i] estimatedModuleSize];
  484. totalModuleSize += size;
  485. square += size * size;
  486. }
  487. float average = totalModuleSize / (float)startSize;
  488. float stdDev = (float)sqrt(square / startSize - average * average);
  489. [self.possibleCenters sortUsingFunction: furthestFromAverageCompare context: (__bridge void *)@(average)];
  490. float limit = MAX(0.2f * average, stdDev);
  491. for (int i = 0; i < [self.possibleCenters count] && [self.possibleCenters count] > 3; i++) {
  492. ZXQRCodeFinderPattern *pattern = self.possibleCenters[i];
  493. if (fabsf([pattern estimatedModuleSize] - average) > limit) {
  494. [self.possibleCenters removeObjectAtIndex:i];
  495. i--;
  496. }
  497. }
  498. }
  499. if ([self.possibleCenters count] > 3) {
  500. float totalModuleSize = 0.0f;
  501. for (int i = 0; i < [self.possibleCenters count]; i++) {
  502. totalModuleSize += [self.possibleCenters[i] estimatedModuleSize];
  503. }
  504. float average = totalModuleSize / (float)[self.possibleCenters count];
  505. [self.possibleCenters sortUsingFunction:centerCompare context:(__bridge void *)(@(average))];
  506. self.possibleCenters = [[NSMutableArray alloc] initWithArray:[self.possibleCenters subarrayWithRange:NSMakeRange(0, 3)]];
  507. }
  508. return [@[self.possibleCenters[0], self.possibleCenters[1], self.possibleCenters[2]] mutableCopy];
  509. }
  510. @end