private void equalsTranVertical(
      WaveletDescription<?> desc,
      ImageSingleBand input,
      ImageSingleBand expected,
      ImageSingleBand found) {
    int w = expected.width;
    int h = expected.height;

    int lowerBorder = UtilWavelet.borderForwardLower(desc.getInverse().getInnerCoefficients());
    int upperBorder =
        input.height
            - UtilWavelet.borderForwardUpper(
                desc.getInverse().getInnerCoefficients(), input.height);

    equalsTranVertical(
        expected.subimage(0, 0, w, h / 2),
        found.subimage(0, 0, w, h / 2),
        lowerBorder / 2,
        upperBorder / 2,
        "top");
    equalsTranVertical(
        expected.subimage(0, h / 2, w, h),
        found.subimage(0, h / 2, w, h),
        lowerBorder / 2,
        upperBorder / 2,
        "bottom");
  }
  /**
   * Compares two wavelet transformations while ignoring the input image borders. Input borders
   * affect the borders of internal segments inside the transformation.
   */
  private void equalsTranHorizontal(
      WaveletDescription<?> desc,
      ImageSingleBand input,
      ImageSingleBand expected,
      ImageSingleBand found) {
    int w = expected.width;
    int h = expected.height;

    int lowerBorder = UtilWavelet.borderForwardLower(desc.getInverse().getInnerCoefficients());
    int upperBorder =
        input.width
            - UtilWavelet.borderForwardUpper(desc.getInverse().getInnerCoefficients(), input.width);

    equalsTranHorizontal(
        expected.subimage(0, 0, w / 2, h),
        found.subimage(0, 0, w / 2, h),
        lowerBorder / 2,
        upperBorder / 2,
        "left");
    equalsTranHorizontal(
        expected.subimage(w / 2, 0, w, h),
        found.subimage(w / 2, 0, w, h),
        lowerBorder / 2,
        upperBorder / 2,
        "right");
  }
  /** See how well it processes an image which is not an GrayS32 */
  @Test
  public void checkOtherType() {
    GrayS32 orig = new GrayS32(width, height);
    GImageMiscOps.fillUniform(orig, rand, 0, 20);
    GrayU8 orig8 = ConvertImage.convert(orig, (GrayU8) null);

    int N = 3;
    ImageDimension dimen = UtilWavelet.transformDimension(orig, N);

    GrayS32 found = new GrayS32(dimen.width, dimen.height);
    GrayS32 expected = new GrayS32(dimen.width, dimen.height);

    WaveletDescription<WlCoef_I32> desc =
        FactoryWaveletDaub.biorthogonal_I32(5, BorderType.REFLECT);

    GrayS32 storage = new GrayS32(dimen.width, dimen.height);
    WaveletTransformOps.transformN(desc, orig.clone(), expected, storage, N);

    WaveletTransformInt<GrayU8> alg = new WaveletTransformInt<>(desc, N, 0, 255, GrayU8.class);
    alg.transform(orig8, found);

    // see if the two techniques produced the same results
    BoofTesting.assertEquals(expected, found, 0);

    // see if it can convert it back
    GrayU8 reconstructed = new GrayU8(width, height);
    alg.invert(found, reconstructed);
    BoofTesting.assertEquals(orig8, reconstructed, 0);
    // make sure the input has not been modified
    BoofTesting.assertEquals(expected, found, 0);
  }
  @Test
  public void compareToWaveletTransformOps() {
    GrayS32 orig = new GrayS32(width, height);
    GImageMiscOps.fillUniform(orig, rand, 0, 20);
    GrayS32 origCopy = orig.clone();

    int N = 3;
    ImageDimension dimen = UtilWavelet.transformDimension(orig, N);

    GrayS32 found = new GrayS32(dimen.width, dimen.height);
    GrayS32 expected = new GrayS32(dimen.width, dimen.height);

    WaveletDescription<WlCoef_I32> desc =
        FactoryWaveletDaub.biorthogonal_I32(5, BorderType.REFLECT);

    GrayS32 storage = new GrayS32(dimen.width, dimen.height);
    WaveletTransformOps.transformN(desc, orig.clone(), expected, storage, N);

    WaveletTransformInt<GrayS32> alg = new WaveletTransformInt<>(desc, N, 0, 255, GrayS32.class);
    alg.transform(orig, found);

    // make sure the original input was not modified like it is in WaveletTransformOps
    BoofTesting.assertEquals(origCopy, orig, 0);
    // see if the two techniques produced the same results
    BoofTesting.assertEquals(expected, found, 0);

    // test inverse transform
    GrayS32 reconstructed = new GrayS32(width, height);
    alg.invert(found, reconstructed);
    BoofTesting.assertEquals(orig, reconstructed, 0);
    // make sure the input has not been modified
    BoofTesting.assertEquals(expected, found, 0);
  }
  /**
   * Computes inverse coefficients
   *
   * @param border
   * @param forward Forward coefficients.
   * @param inverse Inverse used in the inner portion of the data stream.
   * @return
   */
  private static WlBorderCoef<WlCoef_F32> computeBorderCoefficients(
      BorderIndex1D border, WlCoef_F32 forward, WlCoef_F32 inverse) {
    int N = Math.max(forward.getScalingLength(), forward.getWaveletLength());
    N += N % 2;
    N *= 2;
    border.setLength(N);

    // Because the wavelet transform is a linear invertible system the inverse coefficients
    // can be found by creating a matrix and inverting the matrix.  Boundary conditions are then
    // extracted from this inverted matrix.
    DenseMatrix64F A = new DenseMatrix64F(N, N);
    for (int i = 0; i < N; i += 2) {

      for (int j = 0; j < forward.scaling.length; j++) {
        int index = border.getIndex(j + i + forward.offsetScaling);
        A.add(i, index, forward.scaling[j]);
      }

      for (int j = 0; j < forward.wavelet.length; j++) {
        int index = border.getIndex(j + i + forward.offsetWavelet);
        A.add(i + 1, index, forward.wavelet[j]);
      }
    }

    LinearSolver<DenseMatrix64F> solver = LinearSolverFactory.linear(N);
    if (!solver.setA(A) || solver.quality() < 1e-5) {
      throw new IllegalArgumentException("Can't invert matrix");
    }

    DenseMatrix64F A_inv = new DenseMatrix64F(N, N);
    solver.invert(A_inv);

    int numBorder = UtilWavelet.borderForwardLower(inverse) / 2;

    WlBorderCoefFixed<WlCoef_F32> ret = new WlBorderCoefFixed<>(numBorder, numBorder + 1);
    ret.setInnerCoef(inverse);

    // add the lower coefficients first
    for (int i = 0; i < ret.getLowerLength(); i++) {
      computeLowerCoef(inverse, A_inv, ret, i * 2);
    }

    // add upper coefficients
    for (int i = 0; i < ret.getUpperLength(); i++) {
      computeUpperCoef(inverse, N, A_inv, ret, i * 2);
    }

    return ret;
  }