com.jeffreybosboom.region.Region.java Source code

Java tutorial

Introduction

Here is the source code for com.jeffreybosboom.region.Region.java

Source

/*
 * Copyright 2014 Jeffrey Bosboom.
 * This file is part of lynebot.
 *
 * lynebot is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * lynebot 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with lynebot.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.jeffreybosboom.region;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Deque;
import java.util.IntSummaryStatistics;
import java.util.List;
import java.util.Set;

/**
 *
 * @author Jeffrey Bosboom <jbosboom@csail.mit.edu>
 * @since 8/16/2014
 */
public final class Region {
    /**
     * as from BufferedImage.getRGB
     */
    private final int color;
    private final ImmutableList<Point> points;
    private final IntSummaryStatistics xStats, yStats;

    private Region(int color, List<Point> points) {
        this.color = color;
        this.points = ImmutableList.copyOf(points);
        //TODO: lazy initialize?
        //TODO: compute in one pass?
        this.xStats = this.points.stream().mapToInt(Point::x).summaryStatistics();
        this.yStats = this.points.stream().mapToInt(Point::y).summaryStatistics();
    }

    private static final int[][] NEIGHBORHOOD = { { -1, -1 }, { -1, 0 }, { -1, 1 }, { 0, -1 }, { 0, 1 }, { 1, -1 },
            { 1, 0 }, { 1, 1 }, };

    public static ImmutableSet<Region> connectedComponents(BufferedImage image, Set<Integer> interestingColors) {
        final int imageSize = image.getWidth() * image.getHeight();
        BitSet processed = new BitSet(imageSize);
        for (int x = 0; x < image.getWidth(); ++x)
            for (int y = 0; y < image.getHeight(); ++y)
                if (!interestingColors.contains(image.getRGB(x, y)))
                    processed.set(y * image.getWidth() + x);

        ImmutableSet.Builder<Region> builder = ImmutableSet.builder();
        int lastClearBit = 0;
        while ((lastClearBit = processed.nextClearBit(lastClearBit)) != imageSize) {
            int fillY = lastClearBit / image.getWidth(), fillX = lastClearBit % image.getWidth();
            int color = image.getRGB(fillX, fillY);
            List<Point> points = new ArrayList<>();

            //flood fill
            Deque<Point> frontier = new ArrayDeque<>();
            frontier.push(new Point(fillX, fillY));
            while (!frontier.isEmpty()) {
                Point p = frontier.pop();
                int bitIndex = p.row() * image.getWidth() + p.col();
                if (processed.get(bitIndex))
                    continue;
                if (image.getRGB(p.x, p.y) != color)
                    continue;

                points.add(p);
                processed.set(bitIndex);
                for (int[] n : NEIGHBORHOOD) {
                    int nx = p.x + n[0], ny = p.y + n[1];
                    int nBitIndex = nx + ny * image.getWidth();
                    if (0 <= nx && nx < image.getWidth() && 0 <= ny && ny < image.getHeight()
                            && !processed.get(nBitIndex))
                        frontier.push(new Point(nx, ny));
                }
            }
            assert !points.isEmpty();
            builder.add(new Region(color, points));
        }
        return builder.build();
    }

    public int color() {
        return color;
    }

    public ImmutableList<Point> points() {
        return points;
    }

    public Point centroid() {
        return new Point((int) xStats.getAverage(), (int) yStats.getAverage());
    }

    public Rectangle boundingBox() {
        return new Rectangle(xStats.getMin(), yStats.getMin(), xStats.getMax() - xStats.getMin(),
                yStats.getMax() - yStats.getMin());
    }

    public static final class Point {
        public final int x, y;

        public Point(int x, int y) {
            this.x = x;
            this.y = y;
        }

        //these are mostly for method references (field refs don't exist)
        public int x() {
            return x;
        }

        public int y() {
            return y;
        }

        //row/col is reversed, of course
        public int row() {
            return y;
        }

        public int col() {
            return x;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            final Point other = (Point) obj;
            if (this.x != other.x)
                return false;
            if (this.y != other.y)
                return false;
            return true;
        }

        @Override
        public int hashCode() {
            int hash = 3;
            hash = 13 * hash + this.x;
            hash = 13 * hash + this.y;
            return hash;
        }

        @Override
        public String toString() {
            return String.format("(%d, %d)", x, y);
        }
    }
}