001package icy.roi; 002 003import icy.type.collection.array.DynamicArray; 004import icy.type.point.Point3D; 005import icy.type.rectangle.Rectangle3D; 006 007import java.awt.Rectangle; 008import java.util.Map.Entry; 009import java.util.TreeMap; 010 011/** 012 * Class to define a 3D boolean mask region and make basic boolean operation between masks.<br> 013 * The bounds property of this object represents the region defined by the boolean mask. 014 * 015 * @author Stephane 016 */ 017public class BooleanMask3D implements Cloneable 018{ 019 // Internal use only 020 private static BooleanMask2D doUnion2D(BooleanMask2D m1, BooleanMask2D m2) 021 { 022 if (m1 == null) 023 { 024 // only use the 2D mask from second mask 025 if (m2 != null) 026 return (BooleanMask2D) m2.clone(); 027 028 return null; 029 } 030 else if (m2 == null) 031 // only use the 2D mask from first mask 032 return (BooleanMask2D) m1.clone(); 033 034 // process union of 2D mask 035 return BooleanMask2D.getUnion(m1, m2); 036 } 037 038 // Internal use only 039 private static BooleanMask2D doIntersection2D(BooleanMask2D m1, BooleanMask2D m2) 040 { 041 if ((m1 == null) || (m2 == null)) 042 return null; 043 044 // process intersection of 2D mask 045 return BooleanMask2D.getIntersection(m1, m2); 046 } 047 048 // Internal use only 049 private static BooleanMask2D doExclusiveUnion2D(BooleanMask2D m1, BooleanMask2D m2) 050 { 051 if (m1 == null) 052 { 053 // only use the 2D mask from second mask 054 if (m2 != null) 055 return (BooleanMask2D) m2.clone(); 056 057 return null; 058 } 059 else if (m2 == null) 060 // only use the 2D mask from first mask 061 return (BooleanMask2D) m1.clone(); 062 063 // process exclusive union of 2D mask 064 return BooleanMask2D.getExclusiveUnion(m1, m2); 065 } 066 067 // Internal use only 068 private static BooleanMask2D doSubtraction2D(BooleanMask2D m1, BooleanMask2D m2) 069 { 070 if (m1 == null) 071 return null; 072 // only use the 2D mask from first mask 073 if (m2 == null) 074 return (BooleanMask2D) m1.clone(); 075 076 // process subtraction of 2D mask 077 return BooleanMask2D.getSubtraction(m1, m2); 078 } 079 080 /** 081 * Build resulting mask from union of the mask1 and mask2: 082 * 083 * <pre> 084 * mask1 + mask2 = result 085 * 086 * ################ ################ ################ 087 * ############## ############## ################ 088 * ############ ############ ################ 089 * ########## ########## ################ 090 * ######## ######## ################ 091 * ###### ###### ###### ###### 092 * #### #### #### #### 093 * ## ## ## ## 094 * </pre> 095 */ 096 public static BooleanMask3D getUnion(BooleanMask3D mask1, BooleanMask3D mask2) 097 { 098 if ((mask1 == null) && (mask2 == null)) 099 return new BooleanMask3D(); 100 101 if ((mask1 == null) || mask1.isEmpty()) 102 return (BooleanMask3D) mask2.clone(); 103 if ((mask2 == null) || mask2.isEmpty()) 104 return (BooleanMask3D) mask1.clone(); 105 106 final Rectangle3D.Integer bounds = (Rectangle3D.Integer) mask1.bounds.createUnion(mask2.bounds); 107 108 if (!bounds.isEmpty()) 109 { 110 final BooleanMask2D[] mask; 111 112 // special case of infinite Z dimension 113 if (bounds.sizeZ == Integer.MAX_VALUE) 114 { 115 // we can allow merge ROI only if they both has infinite Z dimension 116 if ((mask1.bounds.sizeZ != Integer.MAX_VALUE) || (mask2.bounds.sizeZ != Integer.MAX_VALUE)) 117 throw new UnsupportedOperationException( 118 "Cannot merge an infinite Z dimension ROI with a finite Z dimension ROI"); 119 120 mask = new BooleanMask2D[1]; 121 122 final BooleanMask2D m2d1 = mask1.mask.firstEntry().getValue(); 123 final BooleanMask2D m2d2 = mask2.mask.firstEntry().getValue(); 124 125 mask[0] = doUnion2D(m2d1, m2d2); 126 } 127 else 128 { 129 mask = new BooleanMask2D[bounds.sizeZ]; 130 131 for (int z = 0; z < bounds.sizeZ; z++) 132 { 133 final BooleanMask2D m2d1 = mask1.getMask2D(z + bounds.z); 134 final BooleanMask2D m2d2 = mask2.getMask2D(z + bounds.z); 135 136 mask[z] = doUnion2D(m2d1, m2d2); 137 } 138 } 139 140 return new BooleanMask3D(bounds, mask); 141 } 142 143 return new BooleanMask3D(); 144 } 145 146 /** 147 * Build resulting mask from intersection of the mask1 and mask2: 148 * 149 * <pre> 150 * mask1 intersect mask2 = result 151 * 152 * ################ ################ ################ 153 * ############## ############## ############ 154 * ############ ############ ######## 155 * ########## ########## #### 156 * ######## ######## 157 * ###### ###### 158 * #### #### 159 * ## ## 160 * </pre> 161 */ 162 public static BooleanMask3D getIntersection(BooleanMask3D mask1, BooleanMask3D mask2) 163 { 164 if ((mask1 == null) || (mask2 == null)) 165 return new BooleanMask3D(); 166 167 final Rectangle3D.Integer bounds = (Rectangle3D.Integer) mask1.bounds.createIntersection(mask2.bounds); 168 169 if (!bounds.isEmpty()) 170 { 171 final BooleanMask2D[] mask; 172 173 // special case of infinite Z dimension 174 if (bounds.sizeZ == Integer.MAX_VALUE) 175 { 176 // we can allow merge ROI only if they both has infinite Z dimension 177 if ((mask1.bounds.sizeZ != Integer.MAX_VALUE) || (mask2.bounds.sizeZ != Integer.MAX_VALUE)) 178 throw new UnsupportedOperationException( 179 "Cannot merge an infinite Z dimension ROI with a finite Z dimension ROI"); 180 181 mask = new BooleanMask2D[1]; 182 183 final BooleanMask2D m2d1 = mask1.mask.firstEntry().getValue(); 184 final BooleanMask2D m2d2 = mask2.mask.firstEntry().getValue(); 185 186 mask[0] = doIntersection2D(m2d1, m2d2); 187 } 188 else 189 { 190 mask = new BooleanMask2D[bounds.sizeZ]; 191 192 for (int z = 0; z < bounds.sizeZ; z++) 193 { 194 final BooleanMask2D m2d1 = mask1.getMask2D(z + bounds.z); 195 final BooleanMask2D m2d2 = mask2.getMask2D(z + bounds.z); 196 197 mask[z] = doIntersection2D(m2d1, m2d2); 198 } 199 } 200 201 return new BooleanMask3D(bounds, mask); 202 } 203 204 return new BooleanMask3D(); 205 } 206 207 /** 208 * Build resulting mask from exclusive union of the mask1 and mask2: 209 * 210 * <pre> 211 * mask1 xor mask2 = result 212 * 213 * ################ ################ 214 * ############## ############## ## ## 215 * ############ ############ #### #### 216 * ########## ########## ###### ###### 217 * ######## ######## ################ 218 * ###### ###### ###### ###### 219 * #### #### #### #### 220 * ## ## ## ## 221 * </pre> 222 */ 223 public static BooleanMask3D getExclusiveUnion(BooleanMask3D mask1, BooleanMask3D mask2) 224 { 225 if ((mask1 == null) && (mask2 == null)) 226 return new BooleanMask3D(); 227 228 if ((mask1 == null) || mask1.isEmpty()) 229 return (BooleanMask3D) mask2.clone(); 230 if ((mask2 == null) || mask2.isEmpty()) 231 return (BooleanMask3D) mask1.clone(); 232 233 final Rectangle3D.Integer bounds = (Rectangle3D.Integer) mask1.bounds.createUnion(mask2.bounds); 234 235 if (!bounds.isEmpty()) 236 { 237 final BooleanMask2D[] mask; 238 239 // special case of infinite Z dimension 240 if (bounds.sizeZ == Integer.MAX_VALUE) 241 { 242 // we can allow merge ROI only if they both has infinite Z dimension 243 if ((mask1.bounds.sizeZ != Integer.MAX_VALUE) || (mask2.bounds.sizeZ != Integer.MAX_VALUE)) 244 throw new UnsupportedOperationException( 245 "Cannot merge an infinite Z dimension ROI with a finite Z dimension ROI"); 246 247 mask = new BooleanMask2D[1]; 248 249 final BooleanMask2D m2d1 = mask1.mask.firstEntry().getValue(); 250 final BooleanMask2D m2d2 = mask2.mask.firstEntry().getValue(); 251 252 mask[0] = doExclusiveUnion2D(m2d1, m2d2); 253 } 254 else 255 { 256 mask = new BooleanMask2D[bounds.sizeZ]; 257 258 for (int z = 0; z < bounds.sizeZ; z++) 259 { 260 final BooleanMask2D m2d1 = mask1.getMask2D(z + bounds.z); 261 final BooleanMask2D m2d2 = mask2.getMask2D(z + bounds.z); 262 263 mask[z] = doExclusiveUnion2D(m2d1, m2d2); 264 } 265 } 266 267 return new BooleanMask3D(bounds, mask); 268 } 269 270 return new BooleanMask3D(); 271 } 272 273 /** 274 * Build resulting mask from the subtraction of mask2 from mask1: 275 * 276 * <pre> 277 * mask1 - mask2 = result 278 * 279 * ################ ################ 280 * ############## ############## ## 281 * ############ ############ #### 282 * ########## ########## ###### 283 * ######## ######## ######## 284 * ###### ###### ###### 285 * #### #### #### 286 * ## ## ## 287 * </pre> 288 */ 289 public static BooleanMask3D getSubtraction(BooleanMask3D mask1, BooleanMask3D mask2) 290 { 291 if (mask1 == null) 292 return new BooleanMask3D(); 293 if (mask2 == null) 294 return (BooleanMask3D) mask1.clone(); 295 296 final Rectangle3D.Integer bounds = (Rectangle3D.Integer) mask1.bounds.createIntersection(mask2.bounds); 297 298 // need to subtract something ? 299 if (!bounds.isEmpty()) 300 { 301 final BooleanMask2D[] mask; 302 303 // special case of infinite Z dimension 304 if (bounds.sizeZ == Integer.MAX_VALUE) 305 { 306 // we can allow merge ROI only if they both has infinite Z dimension 307 if ((mask1.bounds.sizeZ != Integer.MAX_VALUE) || (mask2.bounds.sizeZ != Integer.MAX_VALUE)) 308 throw new UnsupportedOperationException( 309 "Cannot merge an infinite Z dimension ROI with a finite Z dimension ROI"); 310 311 mask = new BooleanMask2D[1]; 312 313 final BooleanMask2D m2d1 = mask1.mask.firstEntry().getValue(); 314 final BooleanMask2D m2d2 = mask2.mask.firstEntry().getValue(); 315 316 mask[0] = doSubtraction2D(m2d1, m2d2); 317 } 318 else 319 { 320 mask = new BooleanMask2D[bounds.sizeZ]; 321 322 for (int z = 0; z < bounds.sizeZ; z++) 323 { 324 final BooleanMask2D m2d1 = mask1.getMask2D(z + bounds.z); 325 final BooleanMask2D m2d2 = mask2.getMask2D(z + bounds.z); 326 327 mask[z] = doSubtraction2D(m2d1, m2d2); 328 } 329 } 330 331 return new BooleanMask3D(bounds, mask); 332 } 333 334 return (BooleanMask3D) mask1.clone(); 335 } 336 337 /** 338 * Fast 2x up scaling (each point become 2x2x2 bloc points).<br> 339 * This method create a new boolean mask. 340 */ 341 public static BooleanMask3D upscale(BooleanMask3D mask) 342 { 343 final TreeMap<Integer, BooleanMask2D> srcMask = mask.mask; 344 final TreeMap<Integer, BooleanMask2D> resMask = new TreeMap<Integer, BooleanMask2D>(); 345 346 synchronized (mask) 347 { 348 final int minZ = srcMask.firstKey().intValue(); 349 final int maxZ = srcMask.lastKey().intValue(); 350 351 // single Z --> check for special MAX_INTEGER case 352 if ((minZ == maxZ) && (mask.bounds.sizeZ == Integer.MAX_VALUE)) 353 { 354 // put up scaled version for all Z 355 resMask.put(Integer.valueOf(Integer.MIN_VALUE), srcMask.firstEntry().getValue().upscale()); 356 } 357 else 358 { 359 for (Entry<Integer, BooleanMask2D> entry : srcMask.entrySet()) 360 { 361 final int key = entry.getKey().intValue(); 362 // get upscaled 2D mask 363 final BooleanMask2D bm = entry.getValue().upscale(); 364 365 // duplicate it at (Z pos) * 2 366 resMask.put(Integer.valueOf((key * 2) + 0), bm); 367 resMask.put(Integer.valueOf((key * 2) + 1), (BooleanMask2D) bm.clone()); 368 } 369 } 370 } 371 372 return new BooleanMask3D(resMask); 373 } 374 375 /** 376 * Internal use only 377 */ 378 protected static BooleanMask2D mergeForDownscale(TreeMap<Integer, BooleanMask2D> masks, int destZ, 379 int nbPointForTrue) 380 { 381 final BooleanMask2D bm1 = masks.get(Integer.valueOf((destZ * 2) + 0)); 382 final BooleanMask2D bm2 = masks.get(Integer.valueOf((destZ * 2) + 1)); 383 final Rectangle bounds; 384 385 if (bm1 == null) 386 { 387 if (bm2 == null) 388 return null; 389 390 bounds = new Rectangle(bm2.bounds); 391 } 392 else if (bm2 == null) 393 bounds = new Rectangle(bm1.bounds); 394 else 395 bounds = bm1.bounds.union(bm2.bounds); 396 397 final byte[] maskValues1; 398 final byte[] maskValues2; 399 final int resW = bounds.width / 2; 400 final int resH = bounds.height / 2; 401 402 // get mask values from both mask 403 if (bm1 != null) 404 { 405 bm1.moveBounds(bounds); 406 maskValues1 = BooleanMask2D.getDownscaleValues(bm1); 407 } 408 else 409 maskValues1 = new byte[resW * resH]; 410 if (bm2 != null) 411 { 412 bm2.moveBounds(bounds); 413 maskValues2 = BooleanMask2D.getDownscaleValues(bm2); 414 } 415 else 416 maskValues2 = new byte[resW * resH]; 417 418 final int validPt = Math.min(Math.max(nbPointForTrue, 1), 8); 419 420 final boolean[] resMask = new boolean[resW * resH]; 421 422 for (int i = 0; i < resMask.length; i++) 423 resMask[i] = (maskValues1[i] + maskValues2[i]) >= validPt; 424 425 return new BooleanMask2D(new Rectangle(bounds.x / 2, bounds.y / 2, resW, resH), resMask); 426 } 427 428 /** 429 * Fast 2x down scaling (each 2x2x2 block points become 1 point).<br> 430 * This method create a new boolean mask. 431 * 432 * @param mask 433 * the boolean mask to download 434 * @param nbPointForTrue 435 * the minimum number of <code>true</code>points from a 2x2x2 block to give a <code>true</code> resulting 436 * point.<br> 437 * Accepted value: 1 to 8 (default is 5) 438 */ 439 public static BooleanMask3D downscale(BooleanMask3D mask, int nbPointForTrue) 440 { 441 final TreeMap<Integer, BooleanMask2D> srcMask = mask.mask; 442 final TreeMap<Integer, BooleanMask2D> resMask = new TreeMap<Integer, BooleanMask2D>(); 443 444 synchronized (mask) 445 { 446 final int minZ = srcMask.firstKey().intValue(); 447 final int maxZ = srcMask.lastKey().intValue(); 448 449 // single Z --> check for special MAX_INTEGER case 450 if ((minZ == maxZ) && (mask.bounds.sizeZ == Integer.MAX_VALUE)) 451 // put down scaled version for all Z 452 resMask.put(Integer.valueOf(Integer.MIN_VALUE), mergeForDownscale(srcMask, -1, nbPointForTrue)); 453 else 454 { 455 for (int z = minZ; z < maxZ; z += 2) 456 { 457 final int destZ = z / 2; 458 resMask.put(Integer.valueOf(destZ), mergeForDownscale(srcMask, destZ, nbPointForTrue)); 459 } 460 } 461 } 462 463 return new BooleanMask3D(resMask); 464 } 465 466 /** 467 * Fast 2x down scaling (each 2x2x2 block points become 1 point).<br> 468 * This method create a new boolean mask. 469 */ 470 public static BooleanMask3D downscale(BooleanMask3D mask) 471 { 472 return downscale(mask, 5); 473 } 474 475 /** 476 * Fast 2x up scaling (each point become 2x2 bloc points).<br> 477 * 2D version (down scale is done on XY dimension only).<br> 478 * This method create a new boolean mask. 479 */ 480 public static BooleanMask3D upscale2D(BooleanMask3D mask) 481 { 482 final TreeMap<Integer, BooleanMask2D> srcMask = mask.mask; 483 final TreeMap<Integer, BooleanMask2D> resMask = new TreeMap<Integer, BooleanMask2D>(); 484 485 synchronized (mask) 486 { 487 final int minZ = srcMask.firstKey().intValue(); 488 final int maxZ = srcMask.lastKey().intValue(); 489 490 // single Z --> check for special MAX_INTEGER case 491 if ((minZ == maxZ) && (mask.bounds.sizeZ == Integer.MAX_VALUE)) 492 { 493 // put up scaled version for all Z 494 resMask.put(Integer.valueOf(Integer.MIN_VALUE), srcMask.firstEntry().getValue().upscale()); 495 } 496 else 497 { 498 // put up scaled version for each Z 499 for (Entry<Integer, BooleanMask2D> entry : srcMask.entrySet()) 500 resMask.put(entry.getKey(), entry.getValue().upscale()); 501 } 502 } 503 504 return new BooleanMask3D(resMask); 505 } 506 507 /** 508 * Fast 2x down scaling (each 2x2 block points become 1 point).<br> 509 * 2D version (down scale is done on XY dimension only).<br> 510 * This method create a new boolean mask. 511 * 512 * @param mask 513 * the boolean mask to download 514 * @param nbPointForTrue 515 * the minimum number of <code>true</code>points from a 2x2 block to give a <code>true</code> resulting 516 * point.<br> 517 * Accepted value: 1 to 4 (default is 3) 518 */ 519 public static BooleanMask3D downscale2D(BooleanMask3D mask, int nbPointForTrue) 520 { 521 final TreeMap<Integer, BooleanMask2D> srcMask = mask.mask; 522 final TreeMap<Integer, BooleanMask2D> resMask = new TreeMap<Integer, BooleanMask2D>(); 523 524 synchronized (mask) 525 { 526 final int minZ = srcMask.firstKey().intValue(); 527 final int maxZ = srcMask.lastKey().intValue(); 528 529 // single Z --> check for special MAX_INTEGER case 530 if ((minZ == maxZ) && (mask.bounds.sizeZ == Integer.MAX_VALUE)) 531 { 532 // put down scaled version for all Z 533 resMask.put(Integer.valueOf(Integer.MIN_VALUE), 534 srcMask.firstEntry().getValue().downscale(nbPointForTrue)); 535 } 536 else 537 { 538 // put down scaled version for each Z 539 for (Entry<Integer, BooleanMask2D> entry : srcMask.entrySet()) 540 resMask.put(entry.getKey(), entry.getValue().downscale(nbPointForTrue)); 541 } 542 } 543 544 return new BooleanMask3D(resMask); 545 } 546 547 /** 548 * Fast 2x down scaling (each 2x2 block points become 1 point).<br> 549 * 2D version (down scale is done on XY dimension only).<br> 550 * This method create a new boolean mask. 551 */ 552 public static BooleanMask3D downscale2D(BooleanMask3D mask) 553 { 554 return downscale2D(mask, 2); 555 } 556 557 /** 558 * Region represented by the mask. 559 */ 560 public Rectangle3D.Integer bounds; 561 /** 562 * Boolean mask 2D array. 563 */ 564 public final TreeMap<Integer, BooleanMask2D> mask; 565 566 public BooleanMask3D(Rectangle3D.Integer bounds, TreeMap<Integer, BooleanMask2D> mask) 567 { 568 super(); 569 570 this.mask = mask; 571 this.bounds = bounds; 572 } 573 574 public BooleanMask3D(TreeMap<Integer, BooleanMask2D> mask) 575 { 576 this(new Rectangle3D.Integer(), mask); 577 578 // bounds need to exist before calling getOptimizedBounds() 579 bounds = getOptimizedBounds(false); 580 } 581 582 /** 583 * Build a new 3D boolean mask with specified bounds and 2D mask array.<br> 584 * The 2D mask array length should be >= to <code>bounds.getSizeZ()</code>. 585 */ 586 public BooleanMask3D(Rectangle3D.Integer bounds, BooleanMask2D[] mask) 587 { 588 super(); 589 590 this.bounds = bounds; 591 this.mask = new TreeMap<Integer, BooleanMask2D>(); 592 593 // special case of infinite Z dim 594 if (bounds.sizeZ == Integer.MAX_VALUE) 595 this.mask.put(Integer.valueOf(Integer.MIN_VALUE), mask[0]); 596 else 597 { 598 for (int z = 0; z < bounds.sizeZ; z++) 599 if (mask[z] != null) 600 this.mask.put(Integer.valueOf(bounds.z + z), mask[z]); 601 } 602 } 603 604 /** 605 * Build a new 3D boolean mask from the specified array of {@link Point3D}.<br> 606 */ 607 public BooleanMask3D(Point3D.Integer[] points) 608 { 609 super(); 610 611 mask = new TreeMap<Integer, BooleanMask2D>(); 612 613 if ((points == null) || (points.length == 0)) 614 bounds = new Rectangle3D.Integer(); 615 else 616 { 617 int minX = Integer.MAX_VALUE; 618 int minY = Integer.MAX_VALUE; 619 int minZ = Integer.MAX_VALUE; 620 int maxX = Integer.MIN_VALUE; 621 int maxY = Integer.MIN_VALUE; 622 int maxZ = Integer.MIN_VALUE; 623 624 for (Point3D.Integer pt : points) 625 { 626 final int x = pt.x; 627 final int y = pt.y; 628 final int z = pt.z; 629 630 if (x < minX) 631 minX = x; 632 if (x > maxX) 633 maxX = x; 634 if (y < minY) 635 minY = y; 636 if (y > maxY) 637 maxY = y; 638 if (z < minZ) 639 minZ = z; 640 if (z > maxZ) 641 maxZ = z; 642 } 643 644 // define bounds 645 bounds = new Rectangle3D.Integer(minX, minY, minZ, (maxX - minX) + 1, (maxY - minY) + 1, (maxZ - minZ) + 1); 646 647 // set mask 648 for (Point3D.Integer pt : points) 649 { 650 BooleanMask2D m = mask.get(Integer.valueOf(pt.z)); 651 652 // allocate boolean mask if needed 653 if (m == null) 654 { 655 m = new BooleanMask2D(new Rectangle(minX, minY, bounds.sizeX, bounds.sizeY), 656 new boolean[bounds.sizeX * bounds.sizeY]); 657 // set 2D mask for position Z 658 mask.put(Integer.valueOf(pt.z), m); 659 } 660 661 // set mask point 662 m.mask[((pt.y - minY) * bounds.sizeX) + (pt.x - minX)] = true; 663 } 664 665 // optimize mask 2D bounds 666 for (BooleanMask2D m : mask.values()) 667 m.optimizeBounds(); 668 } 669 } 670 671 /** 672 * Build a new boolean mask from the specified array of {@link Point3D}.<br> 673 */ 674 public BooleanMask3D(Point3D[] points) 675 { 676 super(); 677 678 mask = new TreeMap<Integer, BooleanMask2D>(); 679 680 if ((points == null) || (points.length == 0)) 681 bounds = new Rectangle3D.Integer(); 682 else 683 { 684 int minX = Integer.MAX_VALUE; 685 int minY = Integer.MAX_VALUE; 686 int minZ = Integer.MAX_VALUE; 687 int maxX = Integer.MIN_VALUE; 688 int maxY = Integer.MIN_VALUE; 689 int maxZ = Integer.MIN_VALUE; 690 691 for (Point3D pt : points) 692 { 693 final int x = (int) pt.getX(); 694 final int y = (int) pt.getY(); 695 final int z = (int) pt.getZ(); 696 697 if (x < minX) 698 minX = x; 699 if (x > maxX) 700 maxX = x; 701 if (y < minY) 702 minY = y; 703 if (y > maxY) 704 maxY = y; 705 if (z < minZ) 706 minZ = z; 707 if (z > maxZ) 708 maxZ = z; 709 } 710 711 // define bounds 712 bounds = new Rectangle3D.Integer(minX, minY, minZ, (maxX - minX) + 1, (maxY - minY) + 1, (maxZ - minZ) + 1); 713 714 // set mask 715 for (Point3D pt : points) 716 { 717 BooleanMask2D m = mask.get(Integer.valueOf((int) pt.getZ())); 718 719 // allocate boolean mask if needed 720 if (m == null) 721 { 722 m = new BooleanMask2D(new Rectangle(minX, minY, bounds.sizeX, bounds.sizeY), 723 new boolean[bounds.sizeX * bounds.sizeY]); 724 // set 2D mask for position Z 725 mask.put(Integer.valueOf((int) pt.getZ()), m); 726 } 727 728 // set mask point 729 m.mask[(((int) pt.getY() - minY) * bounds.sizeX) + ((int) pt.getX() - minX)] = true; 730 } 731 732 // optimize mask 2D bounds 733 for (BooleanMask2D m : mask.values()) 734 m.optimizeBounds(); 735 } 736 } 737 738 public BooleanMask3D() 739 { 740 this(new Rectangle3D.Integer(), new BooleanMask2D[0]); 741 } 742 743 /** 744 * Returns the 2D boolean mask for the specified Z position 745 */ 746 public BooleanMask2D getMask2D(int z) 747 { 748 // special case of infinite Z dimension 749 if (bounds.sizeZ == Integer.MAX_VALUE) 750 return mask.firstEntry().getValue(); 751 752 return mask.get(Integer.valueOf(z)); 753 } 754 755 /** 756 * Return <code>true</code> if boolean mask is empty 757 */ 758 public boolean isEmpty() 759 { 760 return bounds.isEmpty(); 761 } 762 763 /** 764 * Return true if mask contains the specified point 765 */ 766 public boolean contains(int x, int y, int z) 767 { 768 if (bounds.contains(x, y, z)) 769 { 770 final BooleanMask2D m2d = getMask2D(z); 771 772 if (m2d != null) 773 return m2d.contains(x, y); 774 } 775 776 return false; 777 } 778 779 /** 780 * Return true if mask contains the specified 2D mask at position Z. 781 */ 782 public boolean contains(BooleanMask2D booleanMask, int z) 783 { 784 if (isEmpty()) 785 return false; 786 787 final BooleanMask2D mask2d = getMask2D(z); 788 789 if (mask2d != null) 790 return mask2d.contains(booleanMask); 791 792 return false; 793 } 794 795 /** 796 * Return true if mask contains the specified 3D mask. 797 */ 798 public boolean contains(BooleanMask3D booleanMask) 799 { 800 if (isEmpty()) 801 return false; 802 803 final int sizeZ = booleanMask.bounds.sizeZ; 804 805 // check for special MAX_INTEGER case (infinite Z dim) 806 if (sizeZ == Integer.MAX_VALUE) 807 { 808 // we cannot contains it if we are not on infinite Z dim too 809 if (bounds.sizeZ != Integer.MAX_VALUE) 810 return false; 811 812 return booleanMask.mask.firstEntry().getValue().contains(mask.firstEntry().getValue()); 813 } 814 815 final int offZ = booleanMask.bounds.z; 816 817 for (int z = offZ; z < offZ + sizeZ; z++) 818 if (!contains(booleanMask.getMask2D(z), z)) 819 return false; 820 821 return true; 822 } 823 824 /** 825 * Return true if mask intersects (contains at least one point) the specified 2D mask at 826 * position Z. 827 */ 828 public boolean intersects(BooleanMask2D booleanMask, int z) 829 { 830 if (isEmpty()) 831 return false; 832 833 final BooleanMask2D mask2d = getMask2D(z); 834 835 if (mask2d != null) 836 return mask2d.intersects(booleanMask); 837 838 return false; 839 } 840 841 /** 842 * Return true if mask intersects (contains at least one point) the specified 3D mask region. 843 */ 844 public boolean intersects(BooleanMask3D booleanMask) 845 { 846 if (isEmpty()) 847 return false; 848 849 final int sizeZ = booleanMask.bounds.sizeZ; 850 851 // check for special MAX_INTEGER case (infinite Z dim) 852 if (sizeZ == Integer.MAX_VALUE) 853 { 854 // get the single Z slice 855 final BooleanMask2D mask2d = booleanMask.mask.firstEntry().getValue(); 856 857 // test with every slice 858 for (BooleanMask2D m : mask.values()) 859 if (m.intersects(mask2d)) 860 return true; 861 862 return false; 863 } 864 865 // check for special MAX_INTEGER case (infinite Z dim) 866 if (bounds.sizeZ == Integer.MAX_VALUE) 867 { 868 // get the single Z slice 869 final BooleanMask2D mask2d = mask.firstEntry().getValue(); 870 871 // test with every slice 872 for (BooleanMask2D m : booleanMask.mask.values()) 873 if (m.intersects(mask2d)) 874 return true; 875 876 return false; 877 } 878 879 final int offZ = booleanMask.bounds.z; 880 881 for (int z = offZ; z < offZ + sizeZ; z++) 882 if (intersects(booleanMask.getMask2D(z), z)) 883 return true; 884 885 return false; 886 } 887 888 /** 889 * Optimize mask bounds so it fits mask content. 890 */ 891 public Rectangle3D.Integer getOptimizedBounds(boolean compute2DBounds) 892 { 893 final Rectangle3D.Integer result = new Rectangle3D.Integer(); 894 895 if (mask.isEmpty()) 896 return result; 897 898 Rectangle bounds2D = null; 899 900 for (BooleanMask2D m2d : mask.values()) 901 { 902 // get optimized 2D bounds for each Z 903 final Rectangle optB2d; 904 905 if (compute2DBounds) 906 optB2d = m2d.getOptimizedBounds(); 907 else 908 optB2d = new Rectangle(m2d.bounds); 909 910 // only add non empty bounds 911 if (!optB2d.isEmpty()) 912 { 913 if (bounds2D == null) 914 bounds2D = optB2d; 915 else 916 bounds2D.add(optB2d); 917 } 918 } 919 920 // empty ? 921 if ((bounds2D == null) || bounds2D.isEmpty()) 922 return result; 923 924 int minZ = mask.firstKey().intValue(); 925 int maxZ = mask.lastKey().intValue(); 926 927 // set 2D bounds to start with 928 result.setX(bounds2D.x); 929 result.setY(bounds2D.y); 930 result.setSizeX(bounds2D.width); 931 result.setSizeY(bounds2D.height); 932 933 // single Z --> check for special infinite Z case 934 if ((minZ == maxZ) && ((minZ == Integer.MIN_VALUE) || (bounds.sizeZ == Integer.MAX_VALUE))) 935 { 936 result.setZ(Integer.MIN_VALUE); 937 result.setSizeZ(Integer.MAX_VALUE); 938 } 939 else 940 { 941 result.setZ(minZ); 942 result.setSizeZ((maxZ - minZ) + 1); 943 } 944 945 return result; 946 } 947 948 /** 949 * Optimize mask bounds so it fits mask content. 950 */ 951 public Rectangle3D.Integer getOptimizedBounds() 952 { 953 return getOptimizedBounds(true); 954 } 955 956 /** 957 * Optimize mask bounds so it fits mask content. 958 */ 959 public void optimizeBounds() 960 { 961 // start by optimizing 2D bounds 962 for (BooleanMask2D m : mask.values()) 963 m.optimizeBounds(); 964 965 moveBounds(getOptimizedBounds(false)); 966 } 967 968 /** 969 * Change the bounds of BooleanMask.<br> 970 * Keep mask data intersecting from old bounds. 971 */ 972 public void moveBounds(Rectangle3D.Integer value) 973 { 974 // bounds changed ? 975 if (!bounds.equals(value)) 976 { 977 // changed to empty mask 978 if (value.isEmpty()) 979 { 980 // clear bounds and mask 981 bounds = new Rectangle3D.Integer(); 982 mask.clear(); 983 return; 984 } 985 986 final Rectangle bounds2D = new Rectangle(value.x, value.y, value.sizeX, value.sizeY); 987 988 // it was infinite Z dim ? 989 if (bounds.sizeZ == Integer.MAX_VALUE) 990 { 991 // get the single 2D mask 992 final BooleanMask2D m2d = mask.firstEntry().getValue(); 993 994 // adjust 2D bounds if needed to the single 2D mask 995 m2d.moveBounds(bounds2D); 996 997 // we passed from infinite Z to defined Z range 998 if (value.sizeZ != Integer.MAX_VALUE) 999 { 1000 // assign the same 2D mask for all Z position 1001 mask.clear(); 1002 for (int z = 0; z <= value.sizeZ; z++) 1003 mask.put(Integer.valueOf(z + value.z), (BooleanMask2D) m2d.clone()); 1004 } 1005 } 1006 // we pass to infinite Z dim 1007 else if (value.sizeZ == Integer.MAX_VALUE) 1008 { 1009 // try to use the 2D mask at Z position 1010 BooleanMask2D mask2D = getMask2D(value.z); 1011 1012 // otherwise we use the first found 2D mask 1013 if ((mask2D == null) && !mask.isEmpty()) 1014 mask2D = mask.firstEntry().getValue(); 1015 1016 // set new mask 1017 mask.clear(); 1018 if (mask2D != null) 1019 mask.put(Integer.valueOf(Integer.MIN_VALUE), mask2D); 1020 } 1021 else 1022 { 1023 // create new mask array 1024 final BooleanMask2D[] newMask = new BooleanMask2D[value.sizeZ]; 1025 1026 for (int z = 0; z < value.sizeZ; z++) 1027 { 1028 final BooleanMask2D mask2D = getMask2D(value.z + z); 1029 1030 if (mask2D != null) 1031 // adjust 2D bounds 1032 mask2D.moveBounds(bounds2D); 1033 1034 newMask[z] = mask2D; 1035 } 1036 1037 // set new mask 1038 mask.clear(); 1039 for (int z = 0; z < value.sizeZ; z++) 1040 mask.put(Integer.valueOf(value.z + z), newMask[z]); 1041 } 1042 1043 bounds = value; 1044 } 1045 } 1046 1047 /** 1048 * Fast 2x up scaling (each point become 2x2x2 bloc point).<br> 1049 * This method create a new boolean mask. 1050 */ 1051 public BooleanMask3D upscale() 1052 { 1053 return upscale(this); 1054 } 1055 1056 /** 1057 * Fast 2x down scaling (each 2x2x2 block points become 1 point).<br> 1058 * This method create a new boolean mask. 1059 * 1060 * @param nbPointForTrue 1061 * the minimum number of <code>true</code>points from a 2x2x2 block to give a <code>true</code> resulting 1062 * point.<br> 1063 * Accepted value: 1-8 (default is 5). 1064 */ 1065 public BooleanMask3D downscale(int nbPointForTrue) 1066 { 1067 return downscale(this, nbPointForTrue); 1068 } 1069 1070 /** 1071 * Fast 2x down scaling (each 2x2x2 block points become 1 point).<br> 1072 * This method create a new boolean mask. 1073 */ 1074 public BooleanMask3D downscale() 1075 { 1076 return downscale(this); 1077 } 1078 1079 /** 1080 * Fast 2x up scaling (each point become 2x2 bloc point).<br> 1081 * 2D version (down scale is done on XY dimension only).<br> 1082 * This method create a new boolean mask. 1083 */ 1084 public BooleanMask3D upscale2D() 1085 { 1086 return upscale2D(this); 1087 } 1088 1089 /** 1090 * Fast 2x down scaling (each 2x2 block points become 1 point).<br> 1091 * 2D version (down scale is done on XY dimension only).<br> 1092 * This method create a new boolean mask. 1093 * 1094 * @param nbPointForTrue 1095 * the minimum number of <code>true</code>points from a 2x2 block to give a <code>true</code> resulting 1096 * point.<br> 1097 * Accepted value: 1-4 (default is 3). 1098 */ 1099 public BooleanMask3D downscale2D(int nbPointForTrue) 1100 { 1101 return downscale2D(this, nbPointForTrue); 1102 } 1103 1104 /** 1105 * Fast 2x down scaling (each 2x2 block points become 1 point).<br> 1106 * 2D version (down scale is done on XY dimension only).<br> 1107 * This method create a new boolean mask. 1108 */ 1109 public BooleanMask3D downscale2D() 1110 { 1111 return downscale2D(this); 1112 } 1113 1114 /** 1115 * Transforms the specified 3D coordinates int array [x,y,z] in 4D coordinates int array [x,y,z,t] with the 1116 * specified T value. 1117 */ 1118 public static int[] toInt3D(int[] source2D, int z) 1119 { 1120 final int[] result = new int[(source2D.length * 3) / 2]; 1121 1122 int pt = 0; 1123 for (int i = 0; i < source2D.length; i += 2) 1124 { 1125 result[pt++] = source2D[i + 0]; 1126 result[pt++] = source2D[i + 1]; 1127 result[pt++] = z; 1128 } 1129 1130 return result; 1131 } 1132 1133 /** 1134 * Return the number of points contained in this boolean mask. 1135 */ 1136 public int getNumberOfPoints() 1137 { 1138 int result = 0; 1139 1140 for (BooleanMask2D mask2d : mask.values()) 1141 result += mask2d.getNumberOfPoints(); 1142 1143 return result; 1144 } 1145 1146 /** 1147 * Return an array of {@link icy.type.point.Point3D.Integer} representing all points of the 1148 * current 3D mask.<br> 1149 * Points are returned in ascending XYZ order. 1150 */ 1151 public Point3D.Integer[] getPoints() 1152 { 1153 return Point3D.Integer.toPoint3D(getPointsAsIntArray()); 1154 } 1155 1156 /** 1157 * Return an array of integer representing all points of the current 3D mask.<br> 1158 * <code>result.length</code> = number of point * 3<br> 1159 * <code>result[(pt * 3) + 0]</code> = X coordinate for point <i>pt</i>.<br> 1160 * <code>result[(pt * 3) + 1]</code> = Y coordinate for point <i>pt</i>.<br> 1161 * <code>result[(pt * 3) + 2]</code> = Z coordinate for point <i>pt</i>.<br> 1162 * Points are returned in ascending XYZ order. 1163 */ 1164 public int[] getPointsAsIntArray() 1165 { 1166 final DynamicArray.Int result = new DynamicArray.Int(8); 1167 1168 for (Entry<Integer, BooleanMask2D> entry : mask.entrySet()) 1169 result.add(toInt3D(entry.getValue().getPointsAsIntArray(), entry.getKey().intValue())); 1170 1171 return result.asArray(); 1172 } 1173 1174 /** 1175 * Return an array of {@link icy.type.point.Point3D.Integer} containing the contour/surface 1176 * points of the 3D mask.<br> 1177 * Points are returned in ascending XYZ order. <br> 1178 * <br> 1179 * WARNING: The default implementation is not totally accurate.<br> 1180 * It returns all points from the first and the last Z slices + contour points for intermediate 1181 * Z slices. 1182 * 1183 * @see #getContourPointsAsIntArray() 1184 */ 1185 public Point3D.Integer[] getContourPoints() 1186 { 1187 return Point3D.Integer.toPoint3D(getContourPointsAsIntArray()); 1188 } 1189 1190 /** 1191 * Return an array of integer containing the contour/surface points of the 3D mask.<br> 1192 * <code>result.length</code> = number of point * 3<br> 1193 * <code>result[(pt * 3) + 0]</code> = X coordinate for point <i>pt</i>.<br> 1194 * <code>result[(pt * 3) + 1]</code> = Y coordinate for point <i>pt</i>.<br> 1195 * <code>result[(pt * 3) + 2]</code> = Z coordinate for point <i>pt</i>.<br> 1196 * Points are returned in ascending XYZ order.<br> 1197 * <br> 1198 * WARNING: The default implementation is not totally accurate.<br> 1199 * It returns all points from the first and the last Z slices + contour points for intermediate 1200 * Z slices. 1201 * 1202 * @see #getContourPoints() 1203 */ 1204 public int[] getContourPointsAsIntArray() 1205 { 1206 final DynamicArray.Int result = new DynamicArray.Int(8); 1207 1208 // TODO: fix this method and use real 3D contour point 1209 if (mask.size() <= 2) 1210 { 1211 for (Entry<Integer, BooleanMask2D> entry : mask.entrySet()) 1212 result.add(toInt3D(entry.getValue().getPointsAsIntArray(), entry.getKey().intValue())); 1213 } 1214 else 1215 { 1216 final Entry<Integer, BooleanMask2D> firstEntry = mask.firstEntry(); 1217 final Entry<Integer, BooleanMask2D> lastEntry = mask.lastEntry(); 1218 final Integer firstKey = firstEntry.getKey(); 1219 final Integer lastKey = lastEntry.getKey(); 1220 1221 result.add(toInt3D(firstEntry.getValue().getPointsAsIntArray(), firstKey.intValue())); 1222 1223 for (Entry<Integer, BooleanMask2D> entry : mask.subMap(firstKey, false, lastKey, false).entrySet()) 1224 result.add(toInt3D(entry.getValue().getContourPointsAsIntArray(), entry.getKey().intValue())); 1225 1226 result.add(toInt3D(lastEntry.getValue().getPointsAsIntArray(), lastKey.intValue())); 1227 } 1228 1229 return result.asArray(); 1230 } 1231 1232 /** 1233 * Computes and returns the length of the contour.<br/> 1234 * This is different from the number of contour point as it takes care of approximating 1235 * correctly distance between each contour point. 1236 * 1237 * @author Alexandre Dufour 1238 * @author Stephane Dallongeville 1239 * @return the length of the contour 1240 */ 1241 public double getContourLength() 1242 { 1243 double result = 0; 1244 1245 final int[] edge = getContourPointsAsIntArray(); 1246 1247 // count the edges and corners in 2D/3D 1248 double sideEdges = 0, cornerEdges = 0; 1249 1250 for (int i = 0; i < edge.length; i += 3) 1251 { 1252 final int x = edge[i + 0]; 1253 final int y = edge[i + 1]; 1254 final int z = edge[i + 2]; 1255 1256 // start on current plan 1257 BooleanMask2D mask2D = getMask2D(z); 1258 1259 final boolean leftConnected = mask2D.contains(x - 1, y); 1260 final boolean rightConnected = mask2D.contains(x + 1, y); 1261 final boolean topConnected = mask2D.contains(x, y - 1); 1262 final boolean bottomConnected = mask2D.contains(x + 1, y + 1); 1263 // lower plan 1264 mask2D = getMask2D(z - 1); 1265 final boolean southConnected = (mask2D != null) && mask2D.contains(x, y); 1266 // upper plan 1267 mask2D = getMask2D(z + 1); 1268 final boolean northConnected = (mask2D != null) && mask2D.contains(x, y); 1269 1270 // count the connections (6 max) 1271 int connection = 0; 1272 1273 if (leftConnected) 1274 connection++; 1275 if (rightConnected) 1276 connection++; 1277 if (topConnected) 1278 connection++; 1279 if (bottomConnected) 1280 connection++; 1281 if (southConnected) 1282 connection++; 1283 if (northConnected) 1284 connection++; 1285 1286 switch (connection) 1287 { 1288 // case 0: // isolated point 1289 // cornerEdges += 3; 1290 // sideEdges++; 1291 // result += 1 + Math.sqrt(2) + (2 * Math.sqrt(3)); 1292 // break; 1293 // 1294 // case 1: // filament end 1295 // cornerEdges += 2; 1296 // sideEdges++; 1297 // result += 1 + (2 * Math.sqrt(3)); 1298 // break; 1299 // 1300 // case 2: // filament point 1301 // if ((leftConnected && rightConnected) || (topConnected && bottomConnected) 1302 // || (northConnected && southConnected)) 1303 // { 1304 // // quadruple "side" edge 1305 // sideEdges += 4; 1306 // result += 4; 1307 // } 1308 // else 1309 // { 1310 // cornerEdges += 3; 1311 // result += 3 * Math.sqrt(2); 1312 // } 1313 // // cornerEdges += 3; 1314 // // perimeter += Math.sqrt(3); 1315 // break; 1316 // 1317 // case 3: // "salient" point 1318 // if ((leftConnected && rightConnected) || (topConnected && bottomConnected) 1319 // || (northConnected && southConnected)) 1320 // { 1321 // // triple "side" edge 1322 // sideEdges += 3; 1323 // result += 3; 1324 // } 1325 // else 1326 // { 1327 // cornerEdges += 2; 1328 // result += 2 * Math.sqrt(2); 1329 // } 1330 default: 1331 cornerEdges++; 1332 result += Math.sqrt(3); 1333 break; 1334 1335 case 4: 1336 if (leftConnected && rightConnected && topConnected && bottomConnected) 1337 { 1338 // double "side" edge 1339 sideEdges += 2; 1340 result += 2; 1341 } 1342 else if (leftConnected && rightConnected && northConnected && southConnected) 1343 { 1344 // double "side" edge 1345 sideEdges += 2; 1346 result += 2; 1347 } 1348 else if (topConnected && bottomConnected && northConnected && southConnected) 1349 { 1350 // double "side" edge 1351 sideEdges += 2; 1352 result += 2; 1353 } 1354 else 1355 { 1356 // "corner" edge 1357 cornerEdges++; 1358 result += Math.sqrt(2); 1359 } 1360 break; 1361 1362 case 5: // "side" edge 1363 sideEdges++; 1364 result++; 1365 break; 1366 1367 // internal point --> should not happen 1368 case 6: 1369 break; 1370 } 1371 1372 // case 0: 1373 // break; 1374 // case 1: 1375 // sideEdges++; 1376 // perimeter++; 1377 // break; 1378 // case 2: 1379 // cornerEdges++; 1380 // perimeter += Math.sqrt(2); 1381 // break; 1382 // case 3: 1383 // cornerEdges += 2; 1384 // perimeter += 2 * Math.sqrt(2); 1385 // break; 1386 // default: 1387 // cornerEdges += 3; 1388 // perimeter += Math.sqrt(3); 1389 } 1390 1391 // adjust the surface area empirically according to the edge distribution 1392 double overShoot = Math.min(sideEdges / 10, cornerEdges); 1393 1394 return result - overShoot; 1395 } 1396 1397 @Override 1398 public Object clone() 1399 { 1400 final BooleanMask3D result = new BooleanMask3D(); 1401 1402 result.bounds = new Rectangle3D.Integer(bounds); 1403 for (Entry<Integer, BooleanMask2D> entry : mask.entrySet()) 1404 result.mask.put(entry.getKey(), (BooleanMask2D) entry.getValue().clone()); 1405 1406 return result; 1407 } 1408 1409}