All Downloads are FREE. Search and download functionalities are using the official Maven repository.

jaitools.imageutils.FloodFiller Maven / Gradle / Ivy

Go to download

Provides a single jar containing all JAI-tools modules which you can use instead of including individual modules in your project. Note: It does not include the Jiffle scripting language or Jiffle image operator.

The newest version!
/*
 * Copyright 2009-2011 Michael Bedward
 *
 * This file is part of jai-tools.
 *
 * jai-tools is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * jai-tools is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with jai-tools.  If not, see .
 *
 */

package jaitools.imageutils;

import java.awt.Rectangle;
import java.awt.geom.Ellipse2D;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.awt.image.WritableRenderedImage;
import java.util.ArrayList;
import java.util.Queue;
import java.util.LinkedList;
import java.util.List;

import javax.media.jai.PlanarImage;
import javax.media.jai.ROI;
import javax.media.jai.ROIShape;
import javax.media.jai.iterator.RandomIter;
import javax.media.jai.iterator.RandomIterFactory;
import javax.media.jai.iterator.WritableRandomIter;

import jaitools.numeric.CompareOp;


/**
 * A flood-filling algorithm to use with rasters.
 * 

* The code is adapted from an algorithm published in C# by J. Dunlap at: *

   http://www.codeproject.com/KB/GDI-plus/queuelinearfloodfill.aspx
* which was subsequently ported to Java by Owen Kaluza. The JAI-tools implementation * is substantially different and any bugs should not be blamed on the above authors. *

* This version works with a source {@code RenderedImage} and a destination * {@code WritableRenderedImage}, both of which are accessed using JAI iterators. * * @author Michael Bedward * @since 1.0 * @version $Id: FloodFiller.java 1610 2011-03-31 04:44:28Z michael.bedward $ */ public class FloodFiller { private RandomIter srcIter; private WritableRenderedImage destImage; private Rectangle destBounds; private int fillValue; private double tolerance; private boolean diagonal; private double refValue; private int srcBand; private int destBand; private double fillRadius; private boolean usingRadius; private ROI roi; /** * Records a segment of contiguous pixels in a single row that will * become part of a filled region. */ public static class ScanSegment implements Comparable { int startX; int endX; int y; /** * Creates a new segment. * * @param startX start X ordinate * @param endX end X ordinate * @param y Y ordinate */ public ScanSegment(int startX, int endX, int y) { this.startX = startX; this.endX = endX; this.y = y; } /** * Checks if the given location lies within this segment. * * @param x location X ordinate * @param y location Y ordinate * * @return the result as boolean */ public boolean contains(int x, int y) { return this.y == y && startX <= x && endX >= x; } /** * Compares this segment to another. The comparison is first by * Y ordinate, then by left and right X ordinates. * * @param other the other segment */ public int compareTo(ScanSegment other) { if (y < other.y) { return -1; } else if (y > other.y) { return 1; } else if (startX < other.startX) { return -1; } else if (startX > other.startX) { return 1; } else if (endX < other.endX) { return -1; } else if (endX > other.endX) { return 1; } else { return 0; } } } private Queue segmentsPending; private List segmentsFilled; private ScanSegment lastSegmentChecked; /** * Create a FloodFiller to work with the given source image * * @param sourceImage the source image * * @param destImage the destination image * * @param sourceBand the source image band to be processed * * @param destBand the destination image band to write to * * @param tolerance the maximum absolute difference in value for a pixel to be * included in the region * * @param diagonal set to true to include sub-regions that are only connected * diagonally; set to false to require orthogonal connections */ public FloodFiller( RenderedImage sourceImage, int sourceBand, WritableRenderedImage destImage, int destBand, double tolerance, boolean diagonal) { this.srcBand = sourceBand; this.destImage = destImage; this.destBand = destBand; this.tolerance = tolerance; this.diagonal = diagonal; if (destImage instanceof PlanarImage) { this.destBounds = ((PlanarImage)destImage).getBounds(); } else if (destImage instanceof BufferedImage) { BufferedImage bImg = (BufferedImage) destImage; this.destBounds = new Rectangle( bImg.getMinX(), bImg.getMinY(), bImg.getWidth(), bImg.getHeight()); } else { // @todo I suppose we could try reflection instead of giving up here throw new IllegalArgumentException("regionImage arg must be a PlanarImage or a BufferedImage"); } this.srcIter = RandomIterFactory.create(sourceImage, null); // Note: the destination iterator is created in the fillRadius // method to avoid tile ownership problems } /** * Fills the region connected to the specified start pixel. * A pixel belongs to this region if there is a path between it and the starting * pixel which passes only through pixels of value {@code v} within the range * {@code start_pixel_value - tolerance <= v <= start_pixel_value + tolerance}. * * @param x start pixel x coordinate * * @param y start pixel y coordinate * * @param fillValue the value to write to the destination image for this region * * @return a new {@linkplain FillResult} */ public FillResult fill(int x, int y, int fillValue) { return fill(x, y, fillValue, srcIter.getSampleDouble(x, y, srcBand)); } /** * Fills the region connected to the specified start pixel and lying within * {@code radius} pixels of the start pixel. *

* A pixel belongs to this region if there is a path between it and the starting * pixel which passes only through pixels of value {@code v} within the range * {@code start_pixel_value - tolerance <= v <= start_pixel_value + tolerance}. * * @param x start pixel x coordinate * * @param y start pixel y coordinate * * @param fillValue the value to write to the destination image for this region * * @param radius maximum distance (pixels) that a candidate pixel can be from * the start pixel * * @return a new {@linkplain FillResult} */ public FillResult fillRadius(int x, int y, int fillValue, double radius) { return fillRadius(x, y, fillValue, srcIter.getSampleDouble(x, y, srcBand), radius); } /** * Fills the region connected to the specified start pixel. * A pixel belongs to this region if there is a path between it and the starting * pixel which passes only through pixels of value {@code v} within the range * {@code refValue - tolerance <= v <= refValue + tolerance}. * * @param x start pixel x coordinate * * @param y start pixel y coordinate * * @param fillValue the value to write to the destination image for this region * * @param refValue the source image reference value for the region * * @return a new {@linkplain FillResult} */ public FillResult fill(int x, int y, int fillValue, double refValue) { return fillRadius(x, y, fillValue, refValue, Double.NaN); } /** * Fills the region connected to the specified start pixel and lying within * {@code radius} pixels of the start pixel. *

* A pixel belongs to this region if there is a path between it and the starting * pixel which passes only through pixels of value {@code v} within the range * {@code refValue - tolerance <= v <= refValue + tolerance}. * * @param x start pixel x coordinate * * @param y start pixel y coordinate * * @param fillValue the value to write to the destination image for this region * * @param refValue the source image reference value for the region * * @param radius maximum distance (pixels) that a candidate pixel can be from * the start pixel * * @return a new {@linkplain FillResult} */ public FillResult fillRadius(int x, int y, int fillValue, double refValue, double radius) { this.fillValue = fillValue; this.refValue = refValue; this.fillRadius = radius; if (!Double.isNaN(radius)) { Ellipse2D shp = new Ellipse2D.Double(x-radius, y-radius, 2*radius, 2*radius); roi = new ROIShape(shp); roi = roi.intersect(new ROIShape(destBounds)); } else { roi = new ROIShape(destBounds); } WritableRandomIter destIter = RandomIterFactory.createWritable(destImage, null); segmentsPending = new LinkedList(); segmentsFilled = new ArrayList(); fillSegment(x, y, destIter); ScanSegment segment; ScanSegment newSegment; while ((segment = segmentsPending.poll()) != null) { int startX, endX; if (diagonal) { startX = segment.startX-1; endX = segment.endX + 1; } else { startX = segment.startX; endX = segment.endX; } if (segment.y > destBounds.y) { int xi = startX; while (xi <= endX) { newSegment = fillSegment(xi, segment.y - 1, destIter); xi = newSegment != null ? newSegment.endX+1 : xi+1; } } if (segment.y < destBounds.y + destBounds.height - 1) { int xi = startX; while (xi <= endX) { newSegment = fillSegment(xi, segment.y + 1, destIter); xi = newSegment != null ? newSegment.endX+1 : xi+1; } } } destIter.done(); return new FillResult(fillValue, refValue, segmentsFilled); } /** * Fills pixels that: *

    *
  • are on the same horizontal scan line as the start pixel *
  • have the same value (plus or minus tolerance) as the start pixel *
  • have no intervening pixels with other values between them and * the start pixel *
* * @param x start X ordinate * @param y start Y ordinate * * @return one of FILL_NEW_SEGMENT, FILL_NONE or FILL_NEED_NEXT_RASTER */ private ScanSegment fillSegment(int x, int y, WritableRandomIter destIter) { if (!roi.contains(x, y)) { return null; } boolean fill = false; ScanSegment segment = null; int left = x, right = x, xi = x; while (roi.contains(xi, y)) { if (checkPixel(xi, y)) { destIter.setSample(xi, y, destBand, fillValue); fill = true; left = xi; xi-- ; } else { break; } } if (!fill) { return null; } xi = x+1; while (roi.contains(xi, y)) { if (checkPixel(xi, y)) { destIter.setSample(xi, y, destBand, fillValue); right = xi; xi++ ; } else { break; } } segment = new ScanSegment(left, right, y); segmentsFilled.add(segment); segmentsPending.offer(segment); return segment; } /** * Tests if a pixel is a candidate to be filled. * * @param x X ordinate * @param y Y ordinate * * @return true if a fill candidate; false otherwise */ private boolean checkPixel(int x, int y) { /* * First check if the pixel has already been filled */ if (lastSegmentChecked != null) { if (lastSegmentChecked.contains(x, y)) { return false; } } for (ScanSegment segment : segmentsFilled) { if (segment.contains(x, y)) { lastSegmentChecked = segment; return false; } } lastSegmentChecked = null; /* * Now test if the pixel's value is within range * of the flood fill reference value */ double val = srcIter.getSampleDouble(x, y, srcBand); return CompareOp.acompare(Math.abs(val - refValue), tolerance) <= 0; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy