/** Checks to see if the two ways of specifying interpolation work */
  @Test
  public void scale_InterpTypeStyle() {
    ImageFloat32 input = new ImageFloat32(width, height);
    ImageFloat32 output = new ImageFloat32(width, height);

    GImageMiscOps.fillUniform(input, rand, 0, 100);

    DistortImageOps.scale(input, output, TypeInterpolate.BILINEAR);

    InterpolatePixel<ImageFloat32> interp = FactoryInterpolation.bilinearPixel(input);
    interp.setImage(input);

    float scaleX = (float) input.width / (float) output.width;
    float scaleY = (float) input.height / (float) output.height;

    if (input.getTypeInfo().isInteger()) {
      for (int i = 0; i < output.height; i++) {
        for (int j = 0; j < output.width; j++) {
          float val = interp.get(j * scaleX, i * scaleY);
          assertEquals((int) val, output.get(j, i), 1e-4);
        }
      }
    } else {
      for (int i = 0; i < output.height; i++) {
        for (int j = 0; j < output.width; j++) {
          float val = interp.get(j * scaleX, i * scaleY);
          assertEquals(val, output.get(j, i), 1e-4);
        }
      }
    }
  }
示例#2
0
  public static BufferedImage standard(ImageSingleBand<?> src, BufferedImage dst) {
    if (src.getDataType().isInteger()) {
      ImageInteger srcInt = (ImageInteger) src;

      if (src.getDataType().isSigned()) {
        double max = GImageStatistics.maxAbs(srcInt);
        return colorizeSign(srcInt, dst, (int) max);
      } else {
        if (src.getDataType().getNumBits() == 8) {
          dst = ConvertBufferedImage.convertTo((ImageUInt8) src, dst);
        } else {
          double max = GImageStatistics.maxAbs(srcInt);
          dst = grayUnsigned(srcInt, dst, (int) max);
        }
      }
    } else if (ImageFloat32.class.isAssignableFrom(src.getClass())) {
      ImageFloat32 img = (ImageFloat32) src;
      float max = ImageStatistics.maxAbs(img);

      boolean hasNegative = false;
      for (int i = 0; i < img.getHeight(); i++) {
        for (int j = 0; j < img.getWidth(); j++) {
          if (img.get(j, i) < 0) {
            hasNegative = true;
            break;
          }
        }
      }

      if (hasNegative) return colorizeSign(img, dst, (int) max);
      else return grayMagnitude((ImageFloat32) src, dst, max);
    }

    return dst;
  }
示例#3
0
  private void constructHistograms(double c_x, double c_y, double scale, double orientation) {
    double c = Math.cos(orientation);
    double s = Math.sin(orientation);

    int gridRadius = gridWidth / 2;

    // This is the distance between samples
    // The size is computed by finding the width of one block in the grid, then dividing by the
    // number of samples along that side
    double sampleUnit = (2.0 * scale * sigmaToRadius) / numSamples;
    // how wide a grid cell is in pixels
    double gridCellLength = numSamples * sampleUnit;
    int gridCellLengthI = (int) (gridCellLength + 0.5); // round to int

    //		System.out.println("-----------------------------------------");
    //		System.out.println("  cell length "+gridCellLength);
    //		System.out.println("  sampleUnit "+sampleUnit);

    int allSampleIndex = 0;
    for (int gy = 0; gy < gridWidth; gy++) {
      double gridY = (gy - gridRadius) * gridCellLength;
      for (int gx = 0; gx < gridWidth; gx++) {
        // top left coordinate of grid in pixels
        double gridX = (gx - gridRadius) * gridCellLength;

        // TODO Sample all pixels here
        for (int sy = 0; sy < numSamples; sy++) {
          double y = sy * sampleUnit + gridY;
          for (int sx = 0; sx < numSamples; sx++, allSampleIndex++) {
            // Sample point in pixels in grid coordinate system
            double x = sx * sampleUnit + gridX;

            // Rotate and translate into image pixel coordinates, then round
            int px = (int) (x * c - y * s + c_x + 0.5);
            int py = (int) (x * s + y * c + c_y + 0.5);

            if (image.isInBounds(px, py)) {
              double dx = derivX.unsafe_get(px, py);
              double dy = derivY.unsafe_get(px, py);

              // Gaussian weighting applied to whole sample area
              double w = gridWeights[allSampleIndex];

              // rotate derivative into grid coordinate system
              double adjX = (dx * c + dy * s) * w;
              double adjY = (-dx * s + dy * c) * w;

              addToHistograms(
                  gx - gridRadius,
                  gy - gridRadius,
                  x / gridCellLength,
                  y / gridCellLength,
                  adjX,
                  adjY);
            }
          }
        }
      }
    }
  }
  public void checkInner(ImageFloat32 image, int c_x, int c_y, int w, int h) {
    ImplDescribePointPixelRegionNCC_F32 alg = new ImplDescribePointPixelRegionNCC_F32(w, h);

    NccFeature desc = new NccFeature(alg.getDescriptorLength());
    alg.setImage(image);
    assertTrue(alg.isInBounds(c_x, c_y));
    alg.process(c_x, c_y, desc);

    int y0 = c_y - h / 2;
    int x0 = c_x - w / 2;
    double mean = 0;
    for (int y = y0; y < y0 + h; y++) {
      for (int x = x0; x < x0 + w; x++) {
        mean += image.get(x, y);
      }
    }
    mean /= w * h;
    double variance = 0;
    for (int y = y0; y < y0 + h; y++) {
      for (int x = x0; x < x0 + w; x++) {
        double a = image.get(x, y) - mean;
        variance += a * a;
      }
    }
    variance /= w * h;
    assertEquals(desc.mean, mean, 1e-8);
    assertEquals(desc.sigma, Math.sqrt(variance), 1e-8);

    int index = 0;
    for (int y = y0; y < y0 + h; y++) {
      for (int x = x0; x < x0 + w; x++, index++) {
        assertEquals(image.get(x, y) - mean, desc.value[index], 1e-4);
      }
    }
  }
  /**
   * Runs a canny edge detector on the input image given the provided thresholds. If configured to
   * save a list of trace points then the output image is optional.
   *
   * <p>NOTE: Input and output can be the same instance, if the image type allows it.
   *
   * @param input Input image. Not modified.
   * @param threshLow Lower threshold. >= 0.
   * @param threshHigh Upper threshold. >= 0.
   * @param output (Might be option) Output binary image. Edge pixels are marked with 1 and
   *     everything else 0.
   */
  public void process(T input, float threshLow, float threshHigh, ImageUInt8 output) {

    if (threshLow < 0 || threshHigh < 0)
      throw new IllegalArgumentException("Threshold must be >= zero!");

    if (hysteresisMark != null) {
      if (output == null)
        throw new IllegalArgumentException(
            "An output image must be specified when configured to mark edge points");
    }

    // setup internal data structures
    blurred.reshape(input.width, input.height);
    derivX.reshape(input.width, input.height);
    derivY.reshape(input.width, input.height);
    intensity.reshape(input.width, input.height);
    suppressed.reshape(input.width, input.height);
    angle.reshape(input.width, input.height);
    direction.reshape(input.width, input.height);
    work.reshape(input.width, input.height);

    // run canny edge detector
    blur.process(input, blurred);
    gradient.process(blurred, derivX, derivY);
    GGradientToEdgeFeatures.intensityAbs(derivX, derivY, intensity);
    GGradientToEdgeFeatures.direction(derivX, derivY, angle);
    GradientToEdgeFeatures.discretizeDirection4(angle, direction);
    GradientToEdgeFeatures.nonMaxSuppression4(intensity, direction, suppressed);

    performThresholding(threshLow, threshHigh, output);
  }
  /** Sets a rectangle inside the image with the specified value. */
  public static void fillRectangle(
      ImageFloat32 img, float value, int x0, int y0, int width, int height) {
    int x1 = x0 + width;
    int y1 = y0 + height;

    for (int y = y0; y < y1; y++) {
      for (int x = x0; x < x1; x++) {
        if (img.isInBounds(x, y)) img.set(x, y, value);
      }
    }
  }
  private void shiftCopy(int offX, int offY, ImageFloat32 src, ImageFloat32 dst) {
    for (int y = 0; y < src.height; y++) {
      for (int x = 0; x < src.width; x++) {
        int xx = x + offX;
        int yy = y + offY;

        if (xx >= 0 && xx < src.width && yy >= 0 && yy < src.height) {
          dst.set(xx, yy, src.get(x, y));
        }
      }
    }
  }
  @Test
  public void checkIntensity() {
    ImageUInt8 input = new ImageUInt8(40, 50);
    ImageFloat32 intensity = new ImageFloat32(input.width, input.height);

    int[] offsets = DiscretizedCircle.imageOffsets(3, input.stride);
    createCircle(4, 5, offsets, minContinuous, detectDifference + 1, input);
    createCircle(12, 20, offsets, minContinuous, detectDifference + 10, input);

    alg.process(input, intensity);

    assertTrue(intensity.get(4, 5) < intensity.get(12, 20));
  }
  /**
   * Fills the whole image with the specified pixel value
   *
   * @param img An image.
   * @param value The value that the image is being filled with.
   */
  public static void fill(ImageFloat32 img, float value) {
    final int h = img.getHeight();
    final int w = img.getWidth();

    float[] data = img.data;

    for (int y = 0; y < h; y++) {
      int index = img.getStartIndex() + y * img.getStride();
      for (int x = 0; x < w; x++) {
        data[index++] = value;
      }
    }
  }
示例#10
0
  private void checUpSample(int w, int h) {
    ImageFloat32 input = new ImageFloat32(w, h);
    ImageFloat32 output = new ImageFloat32(w * 2, h * 2);

    GImageMiscOps.fillUniform(input, rand, 0, 100);

    SiftImageScaleSpace.upSample(input, output);

    for (int i = 0; i < output.height; i++) {
      for (int j = 0; j < output.width; j++) {
        assertTrue(input.get(j / 2, i / 2) == output.get(j, i));
      }
    }
  }
示例#11
0
  /**
   * Sets each value in the image to a value drawn from an uniform distribution that has a range of
   * min <= X < max.
   */
  public static void randomize(ImageFloat32 img, Random rand, float min, float max) {
    final int h = img.getHeight();
    final int w = img.getWidth();

    float range = max - min;

    float[] data = img.data;

    for (int y = 0; y < h; y++) {
      int index = img.getStartIndex() + y * img.getStride();
      for (int x = 0; x < w; x++) {
        data[index++] = rand.nextFloat() * range + min;
      }
    }
  }
示例#12
0
  /**
   * Computes the mean squared error (MSE) between the two images.
   *
   * @param imgA first image. Not modified.
   * @param imgB second image. Not modified.
   * @return error between the two images.
   */
  public static double computeMeanSquaredError(ImageFloat32 imgA, ImageFloat32 imgB) {
    final int h = imgA.getHeight();
    final int w = imgA.getWidth();

    double total = 0;

    for (int y = 0; y < h; y++) {
      for (int x = 0; x < w; x++) {
        double difference = imgA.get(x, y) - imgB.get(x, y);
        total += difference * difference;
      }
    }

    return total / (w * h);
  }
示例#13
0
  private static BufferedImage disparity(
      ImageFloat32 src, BufferedImage dst, int minValue, int maxValue, int invalidColor) {
    float range = maxValue - minValue;

    for (int y = 0; y < src.height; y++) {
      for (int x = 0; x < src.width; x++) {
        float v = src.unsafe_get(x, y);

        if (v > range) {
          dst.setRGB(x, y, invalidColor);
        } else {
          int r, b;

          if (v == 0) {
            r = b = 0;
          } else {
            r = (int) (255 * v / maxValue);
            b = (int) (255 * (maxValue - v) / maxValue);
          }

          dst.setRGB(x, y, r << 16 | b);
        }
      }
    }

    return dst;
  }
示例#14
0
  /** Flips the image from top to bottom */
  public static void flipVertical(ImageFloat32 img) {
    int h2 = img.height / 2;

    for (int y = 0; y < h2; y++) {
      int index1 = img.getStartIndex() + y * img.getStride();
      int index2 = img.getStartIndex() + (img.height - y - 1) * img.getStride();

      int end = index1 + img.width;

      while (index1 < end) {
        float tmp = img.data[index1];
        img.data[index1++] = img.data[index2];
        img.data[index2++] = (float) tmp;
      }
    }
  }
  public void checkBorder(ImageFloat32 image, int c_x, int c_y, int w, int h) {
    ImplDescribePointPixelRegion_F32 alg = new ImplDescribePointPixelRegion_F32(w, h);

    TupleDesc_F32 desc = new TupleDesc_F32(alg.getDescriptorLength());
    alg.setImage(image);
    alg.process(c_x, c_y, desc);

    int index = 0;
    int y0 = c_y - h / 2;
    int x0 = c_x - w / 2;
    for (int y = y0; y < y0 + h; y++) {
      for (int x = x0; x < x0 + w; x++, index++) {
        if (image.isInBounds(x, y)) assertEquals(image.get(x, y), desc.value[index], 1e-4);
        else assertEquals(0, desc.value[index], 1e-4);
      }
    }
  }
示例#16
0
  /** Adds Gaussian/normal i.i.d noise to each pixel in the image. */
  public static void addGaussian(
      ImageFloat32 img, Random rand, double sigma, float min, float max) {
    final int h = img.getHeight();
    final int w = img.getWidth();

    float[] data = img.data;

    for (int y = 0; y < h; y++) {
      int index = img.getStartIndex() + y * img.getStride();
      for (int x = 0; x < w; x++) {
        float value = (data[index]) + (float) (rand.nextGaussian() * sigma);
        if (value < min) value = min;
        if (value > max) value = max;

        data[index++] = value;
      }
    }
  }
示例#17
0
  @Test
  public void scaleSanityCheck() {
    ImageFloat32 input = new ImageFloat32(width, height);
    ImageFloat32 output = new ImageFloat32(width / 2, height / 2);

    GImageMiscOps.fillUniform(input, rand, 0, 100);

    DistortImageOps.scale(input, output, TypeInterpolate.BILINEAR);

    double error = 0;
    for (int y = 0; y < output.height; y++) {
      for (int x = 0; x < output.width; x++) {
        double e = input.get(x * 2, y * 2) - output.get(x, y);
        error += Math.abs(e);
      }
    }
    assertTrue(error / (output.width * output.height) < 0.1);
  }
示例#18
0
 public static void set(ImageSingleBand img, int x, int y, double value) {
   if (ImageInteger.class.isAssignableFrom(img.getClass())) {
     ((ImageInteger) img).set(x, y, (int) value);
   } else if (img instanceof ImageFloat32) {
     ((ImageFloat32) img).set(x, y, (float) value);
   } else if (img instanceof ImageFloat64) {
     ((ImageFloat64) img).set(x, y, value);
   } else {
     throw new IllegalArgumentException(
         "Unknown or incompatible image type: " + img.getClass().getSimpleName());
   }
 }
示例#19
0
  /** Very simple test for rotation accuracy. */
  @Test
  public void rotate_SanityCheck() {
    ImageFloat32 input = new ImageFloat32(width, height);
    ImageFloat32 output = new ImageFloat32(height, width);

    GImageMiscOps.fillUniform(input, rand, 0, 100);

    DistortImageOps.rotate(input, output, TypeInterpolate.BILINEAR, (float) Math.PI / 2f);

    double error = 0;
    // the outside pixels are ignored because numerical round off can cause those to be skipped
    for (int y = 1; y < input.height - 1; y++) {
      for (int x = 1; x < input.width - 1; x++) {
        int xx = output.width - y;
        int yy = x;

        double e = input.get(x, y) - output.get(xx, yy);
        error += Math.abs(e);
      }
    }
    assertTrue(error / (width * height) < 0.1);
  }
示例#20
0
  /**
   * Checks to see if the BufferedImage has the same intensity values as the ImageUInt8
   *
   * @param imgA BufferedImage
   * @param imgB ImageUInt8
   */
  public static void checkEquals(BufferedImage imgA, ImageFloat32 imgB, float tol) {

    if (imgA.getRaster() instanceof ByteInterleavedRaster
        && imgA.getType() != BufferedImage.TYPE_BYTE_INDEXED) {
      ByteInterleavedRaster raster = (ByteInterleavedRaster) imgA.getRaster();

      if (raster.getNumBands() == 1) {
        int strideA = raster.getScanlineStride();
        int offsetA = raster.getDataOffset(0) - raster.getNumBands() + 1;

        // handle a special case where the RGB conversion is screwed
        for (int i = 0; i < imgA.getHeight(); i++) {
          for (int j = 0; j < imgA.getWidth(); j++) {
            float valB = imgB.get(j, i);
            int valA = raster.getDataStorage()[offsetA + i * strideA + j];
            valA &= 0xFF;

            if (Math.abs(valA - valB) > tol)
              throw new RuntimeException("Images are not equal: A = " + valA + " B = " + valB);
          }
        }
        return;
      }
    }

    for (int y = 0; y < imgA.getHeight(); y++) {
      for (int x = 0; x < imgA.getWidth(); x++) {
        int rgb = imgA.getRGB(x, y);

        float gray = (((rgb >>> 16) & 0xFF) + ((rgb >>> 8) & 0xFF) + (rgb & 0xFF)) / 3.0f;
        float grayB = imgB.get(x, y);

        if (Math.abs(gray - grayB) > tol) {
          throw new RuntimeException("images are not equal: A = " + gray + " B = " + grayB);
        }
      }
    }
  }
示例#21
0
  private static BufferedImage grayMagnitude(
      ImageFloat32 src, BufferedImage dst, float maxAbsValue) {
    for (int y = 0; y < src.height; y++) {
      for (int x = 0; x < src.width; x++) {
        float v = Math.abs(src.get(x, y));

        int rgb = (int) (255 * v / maxAbsValue);

        dst.setRGB(x, y, rgb << 16 | rgb << 8 | rgb);
      }
    }

    return dst;
  }
  @Test
  public void checkRender() {
    // Easier to make up a plane in this direction
    Se3_F64 cameraToPlane = new Se3_F64();
    ConvertRotation3D_F64.eulerToMatrix(
        EulerType.XYZ, UtilAngle.degreeToRadian(0), 0, 0, cameraToPlane.getR());
    cameraToPlane.getT().set(0, -5, 0);

    Se3_F64 planeToCamera = cameraToPlane.invert(null);

    CreateSyntheticOverheadViewMS<ImageFloat32> alg =
        new CreateSyntheticOverheadViewMS<ImageFloat32>(
            TypeInterpolate.BILINEAR, 3, ImageFloat32.class);

    alg.configure(param, planeToCamera, centerX, centerY, cellSize, overheadW, overheadH);

    MultiSpectral<ImageFloat32> input =
        new MultiSpectral<ImageFloat32>(ImageFloat32.class, width, height, 3);
    for (int i = 0; i < 3; i++) ImageMiscOps.fill(input.getBand(i), 10 + i);

    MultiSpectral<ImageFloat32> output =
        new MultiSpectral<ImageFloat32>(ImageFloat32.class, overheadW, overheadH, 3);

    alg.process(input, output);

    for (int i = 0; i < 3; i++) {
      ImageFloat32 o = output.getBand(i);

      // check parts that shouldn't be in view
      assertEquals(0, o.get(0, 300), 1e-8);
      assertEquals(0, o.get(5, 0), 1e-8);
      assertEquals(0, o.get(5, 599), 1e-8);

      // check areas that should be in view
      assertEquals(10 + i, o.get(499, 300), 1e-8);
    }
  }
  public synchronized void process(final BufferedImage input) {
    this.input = input;
    workImage = new BufferedImage(input.getWidth(), input.getHeight(), BufferedImage.TYPE_INT_RGB);

    gray.reshape(input.getWidth(), input.getHeight());
    ConvertBufferedImage.convertFrom(input, gray, true);

    detectTarget();

    SwingUtilities.invokeLater(
        new Runnable() {
          public void run() {
            gui.setPreferredSize(new Dimension(input.getWidth(), input.getHeight()));
            renderOutput();
          }
        });
  }
  public boolean estimateCameraPose(BufferedImage leftEye) {

    if (!hasIntrinsic) return false;

    gray.reshape(leftEye.getWidth(), leftEye.getHeight());
    ConvertBufferedImage.convertFrom(leftEye, gray);

    if (!target.process(gray)) return false;

    if (!computeH.computeHomography(target.getDetectedPoints())) return false;

    DenseMatrix64F H = computeH.getHomography();

    targetToOrigin.set(decomposeH.decompose(H));

    return true;
  }
示例#25
0
  public static BufferedImage graySign(ImageFloat32 src, BufferedImage dst, float maxAbsValue) {
    dst = checkInputs(src, dst);

    if (maxAbsValue < 0) maxAbsValue = ImageStatistics.maxAbs(src);

    for (int y = 0; y < src.height; y++) {
      for (int x = 0; x < src.width; x++) {
        float v = src.get(x, y);

        int rgb = 127 + (int) (127 * v / maxAbsValue);

        dst.setRGB(x, y, rgb << 16 | rgb << 8 | rgb);
      }
    }

    return dst;
  }
示例#26
0
  private static BufferedImage colorizeSign(
      ImageFloat32 src, BufferedImage dst, float maxAbsValue) {
    for (int y = 0; y < src.height; y++) {
      for (int x = 0; x < src.width; x++) {
        float v = src.get(x, y);

        int rgb;
        if (v > 0) {
          rgb = (int) (255 * v / maxAbsValue) << 16;
        } else {
          rgb = (int) (-255 * v / maxAbsValue) << 8;
        }
        dst.setRGB(x, y, rgb);
      }
    }

    return dst;
  }
  @Override
  public void renderTarget(ImageFloat32 original, List<CalibrationObservation> solutions) {
    ImageMiscOps.fill(original, 255);

    int numRows = config.numRows * 2 - 1;
    int numCols = config.numCols * 2 - 1;

    int square = original.getWidth() / (Math.max(numRows, numCols) + 4);

    int targetWidth = square * numCols;
    int targetHeight = square * numRows;

    int x0 = (original.width - targetWidth) / 2;
    int y0 = (original.height - targetHeight) / 2;

    for (int i = 0; i < numRows; i += 2) {
      int y = y0 + i * square;

      for (int j = 0; j < numCols; j += 2) {
        int x = x0 + j * square;
        ImageMiscOps.fillRectangle(original, 0, x, y, square, square);
      }
    }

    int pointsRow = numRows + 1;
    int pointsCol = numCols + 1;

    CalibrationObservation set = new CalibrationObservation();
    int gridIndex = 0;
    for (int i = 0; i < pointsRow; i++) {
      for (int j = 0; j < pointsCol; j++, gridIndex++) {
        double y = y0 + i * square;
        double x = x0 + j * square;
        set.add(new Point2D_F64(x, y), gridIndex);
      }
    }
    solutions.add(set);
  }
  public static void process(ImageFloat32 input, ImageFloat32 output, int radius, float storage[]) {
    int w = 2 * radius + 1;
    if (storage == null) {
      storage = new float[w * w];
    } else if (storage.length < w * w) {
      throw new IllegalArgumentException("'storage' must be at least of length " + (w * w));
    }

    for (int y = 0; y < radius; y++) {
      int minI = y - radius;
      int maxI = y + radius + 1;
      if (minI < 0) minI = 0;
      if (maxI > input.height) maxI = input.height;

      for (int x = 0; x < input.width; x++) {
        int minJ = x - radius;
        int maxJ = x + radius + 1;

        // bound it ot be inside the image
        if (minJ < 0) minJ = 0;
        if (maxJ > input.width) maxJ = input.width;

        int index = 0;

        for (int i = minI; i < maxI; i++) {
          for (int j = minJ; j < maxJ; j++) {
            storage[index++] = input.get(j, i);
          }
        }

        // use quick select to avoid sorting the whole list
        float median = QuickSelectArray.select(storage, index / 2, index);
        output.set(x, y, median);
      }
    }

    for (int y = input.height - radius; y < input.height; y++) {
      int minI = y - radius;
      int maxI = y + radius + 1;
      if (minI < 0) minI = 0;
      if (maxI > input.height) maxI = input.height;

      for (int x = 0; x < input.width; x++) {
        int minJ = x - radius;
        int maxJ = x + radius + 1;

        // bound it ot be inside the image
        if (minJ < 0) minJ = 0;
        if (maxJ > input.width) maxJ = input.width;

        int index = 0;

        for (int i = minI; i < maxI; i++) {
          for (int j = minJ; j < maxJ; j++) {
            storage[index++] = input.get(j, i);
          }
        }

        // use quick select to avoid sorting the whole list
        float median = QuickSelectArray.select(storage, index / 2, index);
        output.set(x, y, median);
      }
    }

    for (int y = radius; y < input.height - radius; y++) {
      int minI = y - radius;
      int maxI = y + radius + 1;
      for (int x = 0; x < radius; x++) {
        int minJ = x - radius;
        int maxJ = x + radius + 1;

        // bound it ot be inside the image
        if (minJ < 0) minJ = 0;
        if (maxJ > input.width) maxJ = input.width;

        int index = 0;

        for (int i = minI; i < maxI; i++) {
          for (int j = minJ; j < maxJ; j++) {
            storage[index++] = input.get(j, i);
          }
        }

        // use quick select to avoid sorting the whole list
        float median = QuickSelectArray.select(storage, index / 2, index);
        output.set(x, y, median);
      }
    }

    for (int y = radius; y < input.height - radius; y++) {
      int minI = y - radius;
      int maxI = y + radius + 1;
      for (int x = input.width - radius; x < input.width; x++) {
        int minJ = x - radius;
        int maxJ = x + radius + 1;

        // bound it ot be inside the image
        if (minJ < 0) minJ = 0;
        if (maxJ > input.width) maxJ = input.width;

        int index = 0;

        for (int i = minI; i < maxI; i++) {
          for (int j = minJ; j < maxJ; j++) {
            storage[index++] = input.get(j, i);
          }
        }

        // use quick select to avoid sorting the whole list
        float median = QuickSelectArray.select(storage, index / 2, index);
        output.set(x, y, median);
      }
    }
  }