/**
   * Creates an image of cube edges, similar to this:
   *
   * <p>*---------* ** ** * * * * *********** * | * | * | *------|--* | / | * |/ |* ***********
   *
   * <p>Typical planes are as follow: z = 0 z=1,2,3 z = 4 X * * * * . . . . . * * * * * . . . . * .
   * . . . . * . . . * . . . . * . . . . . * . . . * . . . . * . . . . . * . . . * Z . . . * * . . .
   * * * . . . *
   *
   * <p>(reconstruction starts from the X, and terminates at the Z)
   */
  private ImageStack createThinCubicMeshImage() {
    int sizeX = 5;
    int sizeY = 5;
    int sizeZ = 5;
    int bitDepth = 8;

    // create empty stack
    ImageStack stack = ImageStack.create(sizeX, sizeY, sizeZ, bitDepth);

    // First, the edges in the x direction
    for (int x = 0; x < 5; x++) {
      stack.setVoxel(x, 0, 0, 255);
      stack.setVoxel(x, 0, 4, 255);
    }

    // then, the edges in the y direction
    for (int y = 0; y < 5; y++) {
      stack.setVoxel(4, y, 0, 255);
      stack.setVoxel(0, y, 4, 255);
      stack.setVoxel(4, y, 4, 255);
    }

    // Finally, the edges in the z direction
    for (int z = 0; z < 5; z++) {
      stack.setVoxel(0, 4, z, 255);
      stack.setVoxel(4, 4, z, 255);
    }

    return stack;
  }
  private ImageStack createLeveledCubeGraphImage() {
    ImageStack stack = createCubeGraphImage();
    stack.setVoxel(5, 1, 1, 224);
    stack.setVoxel(9, 5, 1, 192);
    stack.setVoxel(9, 9, 5, 160);
    stack.setVoxel(9, 5, 9, 128);
    stack.setVoxel(5, 1, 9, 96);
    stack.setVoxel(1, 5, 9, 64);
    stack.setVoxel(1, 9, 5, 32);

    return stack;
  }
  @Test
  public final void testRegionalMaxima_CubicMeshC26() {
    // load the reference image, and get its size
    ImageStack image = createCubicMeshImage();
    int sizeX = image.getWidth();
    int sizeY = image.getHeight();
    int sizeZ = image.getSize();

    // create test image: add a band with value 127 in the middle
    int zMid = sizeZ / 2;
    for (int y = 0; y < sizeY; y++) {
      for (int x = 0; x < sizeX; x++) {
        double val = image.getVoxel(x, y, zMid);
        if (val == 255) image.setVoxel(x, y, zMid, 127);
      }
    }

    ImageStack maxima = MinimaAndMaxima3D.regionalMaxima(image, 26);

    for (int z = 0; z < sizeZ; z++) {
      for (int y = 0; y < sizeY; y++) {
        for (int x = 0; x < sizeX; x++) {
          int v0 = (int) image.getVoxel(x, y, z);
          int v = (int) maxima.getVoxel(x, y, z);
          if (v0 == 255) assertEquals(255, v);
          else assertEquals(0, v);
        }
      }
    }
  }
  @Test
  public final void testErosionCubicMeshC6() {
    ImageStack mask = createCubicMeshImage();
    invertGray8Stack(mask);

    ImageStack marker = ImageStack.create(20, 20, 20, 8);
    marker.setVoxel(5, 5, 5, 255);
    invertGray8Stack(marker);

    GeodesicReconstruction3DHybrid0Gray8 algo = new GeodesicReconstruction3DHybrid0Gray8();
    algo.setReconstructionType(GeodesicReconstructionType.BY_EROSION);
    algo.setConnectivity(6);

    ImageStack result = algo.applyTo(marker, mask);

    assertEquals(0, result.getVoxel(5, 15, 5), .01);
    assertEquals(255, result.getVoxel(0, 0, 0), .01);

    int sizeX = mask.getWidth();
    int sizeY = mask.getHeight();
    int sizeZ = mask.getSize();

    for (int z = 0; z < sizeZ; z++) {
      for (int y = 0; y < sizeY; y++) {
        for (int x = 0; x < sizeX; x++) {
          assertEquals(result.getVoxel(x, y, z), mask.getVoxel(x, y, z), .01);
        }
      }
    }
  }
  @Test
  public final void testDilationCochleaVolumeC6() {
    String fileName = getClass().getResource("/files/bat-cochlea-volume.tif").getFile();
    ImagePlus imagePlus = IJ.openImage(fileName);

    assertNotNull(imagePlus);

    assertTrue(imagePlus.getStackSize() > 0);

    ImageStack mask = imagePlus.getStack();
    // Ensure regularity of the mask
    mask = Morphology.opening(mask, CubeStrel.fromRadius(1));

    int width = mask.getWidth();
    int height = mask.getHeight();
    int depth = mask.getSize();
    int bitDepth = mask.getBitDepth();
    ImageStack marker = ImageStack.create(width, height, depth, bitDepth);

    marker.setVoxel(20, 80, 50, 255);

    GeodesicReconstruction3DHybrid0Gray8 algo = new GeodesicReconstruction3DHybrid0Gray8();
    algo.setConnectivity(6);
    algo.verbose = false;

    ImageStack result = algo.applyTo(marker, mask);

    for (int z = 0; z < depth; z++) {
      for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
          assertEquals(result.getVoxel(x, y, z), mask.getVoxel(x, y, z), .01);
        }
      }
    }
  }
  @Test
  public final void testDilationHilbertCurveC26() {
    String fileName = getClass().getResource("/files/hilbert3d.tif").getFile();
    ImagePlus imagePlus = IJ.openImage(fileName);

    assertNotNull(imagePlus);

    assertTrue(imagePlus.getStackSize() > 0);

    ImageStack mask = imagePlus.getStack();

    int width = mask.getWidth();
    int height = mask.getHeight();
    int depth = mask.getSize();
    int bitDepth = mask.getBitDepth();
    ImageStack marker = ImageStack.create(width, height, depth, bitDepth);

    marker.setVoxel(3, 0, 0, 255);

    GeodesicReconstruction3DHybrid0Gray8 algo = new GeodesicReconstruction3DHybrid0Gray8();
    algo.setConnectivity(26);
    algo.verbose = false;

    ImageStack result = algo.applyTo(marker, mask);

    for (int z = 0; z < depth; z++) {
      for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
          assertEquals(result.getVoxel(x, y, z), mask.getVoxel(x, y, z), .01);
        }
      }
    }
  }
  @Test
  public final void testApplyTo() {
    GeodesicReconstructionByDilation3DGray8 algo = new GeodesicReconstructionByDilation3DGray8();

    String fileName = getClass().getResource("/files/bat-cochlea-volume.tif").getFile();
    ImagePlus imagePlus = IJ.openImage(fileName);
    assertNotNull(imagePlus);

    assertTrue(imagePlus.getStackSize() > 0);

    ImageStack mask = imagePlus.getStack();
    int width = mask.getWidth();
    int height = mask.getHeight();
    int depth = mask.getSize();
    int bitDepth = mask.getBitDepth();
    ImageStack marker = ImageStack.create(width, height, depth, bitDepth);

    marker.setVoxel(20, 80, 50, 255);

    ImageStack result = algo.applyTo(marker, mask);

    for (int z = 0; z < depth; z++) {
      for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
          assertEquals(result.getVoxel(x, y, z), mask.getVoxel(x, y, z), .01);
        }
      }
    }
  }
  private ImageStack createInvertedLeveledCubeGraphImage() {
    ImageStack stack = createCubeGraphImage();
    for (int z = 0; z < stack.getSize(); z++) {
      for (int y = 0; y < stack.getHeight(); y++) {
        for (int x = 0; x < stack.getWidth(); x++) {
          stack.setVoxel(x, y, z, 255 - stack.getVoxel(x, y, z));
        }
      }
    }
    stack.setVoxel(5, 1, 1, 32);
    stack.setVoxel(9, 5, 1, 64);
    stack.setVoxel(9, 9, 5, 96);
    stack.setVoxel(9, 5, 9, 128);
    stack.setVoxel(5, 1, 9, 160);
    stack.setVoxel(1, 5, 9, 192);
    stack.setVoxel(1, 9, 5, 224);

    return stack;
  }
  @Test
  public final void testDilationCubicHollowMesh() {
    ImageStack mask = createCubicHollowMeshImage();

    ImageStack marker = ImageStack.create(20, 20, 20, 8);
    for (int z = 0; z < 20; z++) {
      for (int y = 0; y < 20; y++) {
        for (int x = 0; x < 20; x++) {
          marker.setVoxel(x, y, z, 255);
        }
      }
    }
    marker.setVoxel(5, 5, 5, 0);

    GeodesicReconstruction3DHybrid0Gray8 algo = new GeodesicReconstruction3DHybrid0Gray8();

    ImageStack result = algo.applyTo(marker, mask);

    assertEquals(0, result.getVoxel(5, 15, 5), .01);
    assertStackEquals(mask, result);
  }
  private final void invertGray8Stack(ImageStack image) {
    int sizeX = image.getWidth();
    int sizeY = image.getHeight();
    int sizeZ = image.getSize();

    for (int z = 0; z < sizeZ; z++) {
      for (int y = 0; y < sizeY; y++) {
        for (int x = 0; x < sizeX; x++) {
          image.setVoxel(x, y, z, 255 - image.getVoxel(x, y, z));
        }
      }
    }
  }
  /** Creates a 3D image containing thin cube mesh. */
  private ImageStack createCubeGraphImage() {
    int sizeX = 11;
    int sizeY = 11;
    int sizeZ = 11;
    int bitDepth = 8;

    // create empty stack
    ImageStack stack = ImageStack.create(sizeX, sizeY, sizeZ, bitDepth);

    // coordinates of the cube edges
    int x1 = 1;
    int x2 = 9;
    int y1 = 1;
    int y2 = 9;
    int z1 = 1;
    int z2 = 9;

    // First, the edges in the x direction
    for (int x = x1; x <= x2; x++) {
      stack.setVoxel(x, y1, z1, 255);
      stack.setVoxel(x, y1, z2, 255);
    }

    // then, the edges in the y direction
    for (int y = y1; y <= y2; y++) {
      stack.setVoxel(x2, y, z1, 255);
      stack.setVoxel(x1, y, z2, 255);
      stack.setVoxel(x2, y, z2, 255);
    }

    // Finally, the edges in the z direction
    for (int z = z1; z <= z2; z++) {
      stack.setVoxel(x1, y2, z, 255);
      stack.setVoxel(x2, y2, z, 255);
    }

    return stack;
  }
  @Test
  public final void testDilationThinCubicMeshC26() {
    ImageStack mask = createThinCubicMeshImage();

    ImageStack marker = ImageStack.create(5, 5, 5, 8);
    marker.setVoxel(0, 0, 0, 255);

    GeodesicReconstruction3DHybrid0Gray8 algo = new GeodesicReconstruction3DHybrid0Gray8();
    algo.setConnectivity(26);

    ImageStack result = algo.applyTo(marker, mask);

    assertEquals(255, result.getVoxel(0, 4, 0), .01);
  }
  @Test
  public final void testErosionCochleaVolumeC26() {
    String fileName = getClass().getResource("/files/bat-cochlea-volume.tif").getFile();
    ImagePlus imagePlus = IJ.openImage(fileName);

    assertNotNull(imagePlus);
    assertTrue(imagePlus.getStackSize() > 0);

    ImageStack mask = imagePlus.getStack();
    // Ensure regularity of the mask
    mask = Morphology.opening(mask, CubeStrel.fromRadius(1));
    invertGray8Stack(mask);

    int width = mask.getWidth();
    int height = mask.getHeight();
    int depth = mask.getSize();
    int bitDepth = mask.getBitDepth();
    ImageStack marker = ImageStack.create(width, height, depth, bitDepth);
    marker.setVoxel(20, 80, 50, 255);
    invertGray8Stack(marker);

    GeodesicReconstruction3DHybrid0Gray8 algo =
        new GeodesicReconstruction3DHybrid0Gray8(GeodesicReconstructionType.BY_EROSION);
    algo.setConnectivity(26);

    ImageStack result = algo.applyTo(marker, mask);

    for (int z = 0; z < depth; z++) {
      for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
          if (Math.abs(result.getVoxel(x, y, z) - mask.getVoxel(x, y, z)) > .1) {
            System.out.println("x=" + x + " y=" + y + " z=" + z);
            System.out.println("  mask = " + (int) mask.getVoxel(x, y, z));
            System.out.println("  res  = " + (int) result.getVoxel(x, y, z));
            assertTrue(false);
          }
        }
      }
    }
  }
  private ImageStack createCubicMeshImage() {
    int sizeX = 20;
    int sizeY = 20;
    int sizeZ = 20;
    int bitDepth = 8;

    // create empty stack
    ImageStack stack = ImageStack.create(sizeX, sizeY, sizeZ, bitDepth);

    // number of voxels between edges and 'tube' borders
    int gap = 2;

    // First, the edges in the x direction
    for (int z = 5 - gap - 1; z <= 5 + gap + 1; z++) {
      for (int y = 5 - gap - 1; y <= 5 + gap + 1; y++) {
        for (int x = 5 - gap - 1; x <= 15 + gap + 1; x++) {
          stack.setVoxel(x, y, z, 255);
          stack.setVoxel(x, y, z + 10, 255);
        }
      }
    }

    // then, the edges in the y direction
    for (int z = 5 - gap - 1; z <= 5 + gap + 1; z++) {
      for (int x = 5 - gap - 1; x <= 5 + gap + 1; x++) {
        for (int y = 5 - gap - 1; y <= 15 + gap + 1; y++) {
          stack.setVoxel(x + 10, y, z, 255);
          stack.setVoxel(x, y, z + 10, 255);
          stack.setVoxel(x + 10, y, z + 10, 255);
        }
      }
    }

    // Finally, the edges in the z direction
    for (int y = 5 - gap - 1; y <= 5 + gap + 1; y++) {
      for (int x = 5 - gap - 1; x <= 5 + gap + 1; x++) {
        for (int z = 5 - gap - 1; z <= 15 + gap + 1; z++) {
          stack.setVoxel(x, y + 10, z, 255);
          stack.setVoxel(x + 10, y + 10, z, 255);
        }
      }
    }

    return stack;
  }
  @Test
  public final void testRegionalMaxima_BatCochlea() {
    String fileName = getClass().getResource("/files/bat-cochlea-volume.tif").getFile();
    ImagePlus imagePlus = IJ.openImage(fileName);
    assertNotNull(imagePlus);
    assertTrue(imagePlus.getStackSize() > 0);

    // load the reference image, and get its size
    ImageStack image = imagePlus.getStack();
    int sizeX = image.getWidth();
    int sizeY = image.getHeight();
    int sizeZ = image.getSize();

    // create test image:
    // use cochlea volume, but add a band with value 127 in the middle
    int zMid = sizeZ / 2;
    for (int y = 0; y < sizeY; y++) {
      for (int x = 0; x < sizeX; x++) {
        double val = image.getVoxel(x, y, zMid);
        if (val == 255) image.setVoxel(x, y, zMid, 127);
      }
    }

    ImageStack maxima = MinimaAndMaxima3D.regionalMaxima(image);

    for (int z = 0; z < sizeZ; z++) {
      for (int y = 0; y < sizeY; y++) {
        for (int x = 0; x < sizeX; x++) {
          int v0 = (int) image.getVoxel(x, y, z);
          int v = (int) maxima.getVoxel(x, y, z);
          if (v0 == 255) assertEquals(255, v);
          else assertEquals(0, v);
        }
      }
    }
  }
  private ImageStack createCubicHollowMeshImage() {
    // create filled cubic mesh
    ImageStack stack = createCubicMeshImage();

    // number of voxels between edges and 'tube' borders
    int gap = 2;

    // First, the edges in the x direction
    for (int z = 5 - gap; z <= 5 + gap; z++) {
      for (int y = 5 - gap; y <= 5 + gap; y++) {
        for (int x = 5 - gap; x <= 15 + gap; x++) {
          stack.setVoxel(x, y, z, 0);
          stack.setVoxel(x, y, z + 10, 0);
        }
      }
    }

    // then, the edges in the y direction
    for (int z = 5 - gap; z <= 5 + gap; z++) {
      for (int x = 5 - gap; x <= 5 + gap; x++) {
        for (int y = 5 - gap; y <= 15 + gap; y++) {
          stack.setVoxel(x + 10, y, z, 0);
          stack.setVoxel(x, y, z + 10, 0);
          stack.setVoxel(x + 10, y, z + 10, 0);
        }
      }
    }

    // Finally, the edges in the z direction
    for (int y = 5 - gap; y <= 5 + gap; y++) {
      for (int x = 5 - gap; x <= 5 + gap; x++) {
        for (int z = 5 - gap; z <= 15 + gap; z++) {
          stack.setVoxel(x, y + 10, z, 0);
          stack.setVoxel(x + 10, y + 10, z, 0);
        }
      }
    }

    return stack;
  }