/*
* Copyright 2015 Daniel Dittmar
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package dan.dit.whatsthat.util.mosaic.reconstruction;
import android.graphics.Bitmap;
import dan.dit.whatsthat.util.image.ColorAnalysisUtil;
import dan.dit.whatsthat.util.image.ColorMetric;
/**
* This class models an abstract ShapeReconstructor. For a concrete
* ShapeReconstructor see {@link MultiRectReconstructor}. A ShapeReconstructor
* is able to reconstruct an image with a fragmentation shape and algorithm
* defined by the extending reconstructor.
* @author Daniel
*
*/
abstract class ShapeReconstructor extends Reconstructor {
private int rowCount;
private int columnCount;
private int totalHeight;
private int totalWidth;
private int rectHeight;
private int rectWidth;
private boolean[][][] raster;
private int[][] averageRGB;
ColorMetric mColorMetric;
/**
* Creates a new ShapeReconstructor for the given image with the wanted amount of
* rows and columns for basic fragmentation. The ShapeConstructor does the basic analyzing
* and fragmentation and sets up a raster of unconnected fragments which can later be connected
* to form different shapes.
* @param source The image to be analyzed and fragmented.
* @param maxRows The wanted amount of maximum rows for the basic fragmentation.
* @param maxColumns The wanted amount of maximum columns for the basic fragmentation.
* @param allowCornerConnections If corner connection should be allowed. If the specific
* Reconstructor does not use corner connections (TOP_LEFT, BOTTOM_RIGHT,...), set this
* @param metric The color metric to use to compare colors for similarity.
*/
ShapeReconstructor(Bitmap source, int maxRows, int maxColumns,
boolean allowCornerConnections, ColorMetric metric) {
if (source == null) {
throw new NullPointerException();
}
if (maxRows <= 0 || maxColumns <= 0) {
throw new IllegalArgumentException("MaxRows/Columns need to be positive and not zero.");
}
mColorMetric = metric == null ? ColorMetric.Euclid2.INSTANCE : metric;
this.init(source,
Reconstructor.getClosestCount(source.getHeight(), maxRows),
Reconstructor.getClosestCount(source.getWidth(), maxColumns),
allowCornerConnections);
}
private void init(Bitmap image, int maxRows, int maxColumns,
boolean allowCornerConnections) {
this.rowCount = maxRows;
this.columnCount = maxColumns;
this.totalHeight = image.getHeight();
this.totalWidth = image.getWidth();
this.rectHeight = this.totalHeight / maxRows;
this.rectWidth = this.totalWidth / maxColumns;
this.raster = new boolean[maxRows][maxColumns]
[(allowCornerConnections) ? FragmentNeighbor.COUNT : FragmentNeighbor.SIDES.length];
this.averageRGB = new int[maxRows][maxColumns];
for (int r = 0; r < maxRows; r++) {
for (int c = 0; c < maxColumns; c++) {
int startX = c * this.rectWidth;
int startY = r * this.rectHeight;
int endX = (c + 1) * this.rectWidth;
int endY = (r + 1) * this.rectHeight;
this.averageRGB[r][c] = ColorAnalysisUtil.getAverageColor(image, startX, endX, startY, endY);
}
}
}
/**
* Returns the total height of the image to reconstruct.
* @return The total height of the image to reconstruct.
*/
int getTotalHeight() {
return totalHeight;
}
/**
* Returns the total width of the image to reconstruct.
* @return The total widht of the image to reconstruct.
*/
int getTotalWidth() {
return totalWidth;
}
/**
* Returns the index of the last row which is connected to the given fragment in up
* or down connection.
* @param startRow The row index of the start fragment.
* @param column The column index of the start fragment and end fragment.
* @param up If the search should go up or down. If <code>true</code> it will
* search up and the returned index will be lower than or equal startRow.
* @return The index of the last row which is connected without interruption to the
* given fragment.
*/
int getLastConnectedRow(int startRow, int column, boolean up) {
int dirIndex = up ? FragmentNeighbor.UP.ordinal() : FragmentNeighbor.DOWN.ordinal();
int delta = up ? -1 : 1;
int currRow = startRow;
while ((currRow < this.rowCount)
&& (currRow >= 0)
&& this.raster[currRow][column][dirIndex]) {
currRow += delta;
}
return currRow;
}
/**
* Returns the index of the last column which is connected to the given fragment in left
* or right connection.
* @param startColumn The column index of the start fragment.
* @param row The row index of the start fragment and end fragment.
* @param left If the search should go left or right. If <code>true</code> it will
* search left and the returned index will be lower than or equal startColumn.
* @return The index of the last column which is connected without interruption to the
* given fragment.
*/
int getLastConnectedColumn(int startColumn, int row, boolean left) {
int dirIndex = left ? FragmentNeighbor.LEFT.ordinal() : FragmentNeighbor.RIGHT.ordinal();
int delta = left ? -1 : 1;
int currColumn = startColumn;
while ((currColumn < this.columnCount)
&& (currColumn >= 0)
&& this.raster[row][currColumn][dirIndex]) {
currColumn += delta;
}
return currColumn;
}
/**
* Returns <code>true</code> if the Fragment at the given row and column is connected
* to the specified neighbor. Will throw an {@link ArrayIndexOutOfBoundsException} if
* using a corner neighbor and this ShapeReconstructor did not allow corner connection.
* @param row The row index of the Fragment.
* @param column The column index of the Fragment.
* @param to The neighbor to check for a connection.
* @return <code>true</code> if the Fragment is connected to the neighbor.
*/
boolean isConnected(int row, int column, FragmentNeighbor to) {
return this.raster[row][column][to.ordinal()];
}
/**
* Sets or removes the connection between two fragments. Removing of connections
* is not primarly supported, but possible. Information about each single Fragment
* is lost when connecting Fragments, so removing a connection leaves the average RGB
* value unchanged. If the neighbor does not exist, nothing is done. A connection is
* symmetric.
* @param row The row index of the Fragment to connect to the neighbor.
* @param column The column index of the Fragment to connect to the neighbor.
* @param neighbor The neighbor the connection should be set for.
* @param connected If <code>true</code>, the fragments will be connected, else
* the connection will be removed.
* @return <code>true</code> if the neighbor existed and the connection between the
* neighbors changed.
*/
boolean setConnected(int row, int column, FragmentNeighbor neighbor, boolean connected) {
int neighborRow = row + neighbor.getRowDelta();
int neighborColumn = column + neighbor.getColumnDelta();
if (neighborRow >= 0 && neighborRow < this.rowCount
&& neighborColumn >= 0 && neighborColumn < this.columnCount
&& this.raster[row][column][neighbor.ordinal()] != connected) {
// the given neighbor exists for this fragment and the connection will change
this.raster[row][column][neighbor.ordinal()] = connected;
FragmentNeighbor neighborPartner = neighbor.getPartner();
if (neighborPartner == null) {
return false;
}
this.raster[neighborRow][neighborColumn][neighborPartner.ordinal()] = connected;
int mixedRGB = ColorAnalysisUtil.mix(this.getAverageRGB(row, column),
this.getAverageRGB(neighborRow, neighborColumn),
this.rectWidth * this.rectHeight, this.rectWidth * this.rectHeight);
this.averageRGB[row][column] = mixedRGB;
this.averageRGB[neighborRow][neighborColumn] = mixedRGB;
return true;
}
return false;
}
/**
* Returns the height of a basic fragment rect. The same
* for all rects.
* @return The height of a basic single fragment rect.
*/
int getRectHeight() {
return rectHeight;
}
/**
* Returns the width of a basic fragment rect. The same
* for all rects.
* @return The widht of a basic single fragment rect.
*/
int getRectWidth() {
return rectWidth;
}
/**
* Returns the average RGB value of the fragment at the given row
* and column.
* @param row The row index of the fragment.
* @param column The column index of the fragment.
* @return The average RGB value of the fragment.
*/
int getAverageRGB(int row, int column) {
return this.averageRGB[row][column];
}
/**
* Returns the amount of rows of the basic fragmentation. Does not change.
* @return The amount of rows of the basic fragmentation.
*/
int getRowCount() {
return this.rowCount;
}
/**
* Returns the amount of columns of the basic fragmentation. Does not change.
* @return The amount of columns of the basic fragmentation.
*/
int getColumnCount() {
return this.columnCount;
}
}