/* * 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 android.graphics.Canvas; import dan.dit.whatsthat.util.image.ColorAnalysisUtil; import dan.dit.whatsthat.util.image.ColorMetric; /** * This class implements a ShapeReconstructor. The given image * is split into rects like the with ordinary {@link RectReconstructor}, * but in comparision to the RectReconstructor, the rects of this * reconstructor will have different widths and heights. To determine * when to merge rects to bigger ones, the mergeFactor is used.<br>The * merged rects will keep the given apect ratio, so for example if the given * parameters height/wantedRows equal width/wantedColumns, this will only produce squares. * Once getReconstructed() returned a valid image, it will return <code>null</code> * for future calls.<br> * This Reconstructor will turn out to be slower than the RectReconstructor since it needs different * sized images, and resizing is pretty expensive (even though the caching helps a lot). * @author Daniel * */ public class MultiRectReconstructor extends ShapeReconstructor { private Bitmap resultImage; private Canvas mResultCanvas; private int lastGivenColumn; private int lastGivenRow; private MosaicFragment next; private MosaicFragment mFragment; private int mFragmentCount; /** * Creates a new MultiRectReconstructor which is capable of reconstructing * the given image into multi sized rects. The basic splitting is defined by * maxRows and maxColumns which need to be positive. No rectangle will be smaller than * imageHeight/maxRows to imageWidth/maxColumns pixel. * @param source The image to reconstruct. * @param maxRows The amount of rows the basic fragmentation will have. * @param maxColumns The amount of columns the basic fragmentation will have. * @param mergeFactor A factor which influences the merging of rects. A factor of <code>0</code> * indicates strict merging which will only allow rects to merge if they have exactly the same * average color and a factor of <code>1</code> will make the reconstructor * very tolerant and merge rects easily. If out of bounds, factor will be adjusted to the * corresponding bound. * @param useAlpha If the alpha value of images should be taken into account. * @throws IllegalArgumentException If maxRows or maxColumns lower than or equal zero. * @throws NullPointerException If image is <code>null</code>. */ public MultiRectReconstructor(Bitmap source, int maxRows, int maxColumns, double mergeFactor, boolean useAlpha, ColorMetric metric) { super(source, maxRows, maxColumns, false, metric); mFragment = new MosaicFragment(0, 0, 0); this.init(ColorAnalysisUtil.factorToSimilarityBound(mergeFactor), useAlpha); this.lastGivenColumn = -1; this.lastGivenRow = -1; } /** * Inits the reconstructor and merges the rects. This connects ShapeFragments, but * ensures that only rects are created, though the sizes may be of any kind, so * also a complete row of pixels could be merged if it was of equal color. * When ShapeFragments are connected, their average color will be mixed and both ShapeFragments * will have this mixed color as a new average color. * @param mergeFactor The factor for merging, a value between 0 and 1, inclusive. * If the similarity between two color vectors divided by the maximum similarity is lower than * this factor, the rects will be able to be merged. * @param useAlpha If alpha should be considered for similarity comparisions. */ private void init(double mergeFactor, boolean useAlpha) { int rows = this.getRowCount(); int columns = this.getColumnCount(); int columnCandidatesStartRow; int columnCandidatesEndRow; int columnCandidatesColumn; int rowCandidatesStartColumn; int rowCandidatesEndColumn; int rowCandidatesRow; final double maxSim = mColorMetric.maxValue(useAlpha); for (int r = 0; r < rows - 1; r++) { for (int c = 0; c < columns - 1; c++) { if (!this.isConnected(r, c, FragmentNeighbor.UP) && !this.isConnected(r, c + 1, FragmentNeighbor.UP)) { // this one is not connected to up and the right one is not connected to up too columnCandidatesStartRow = r; columnCandidatesColumn = c + 1; columnCandidatesEndRow = r; // test if the column right of the current square could be added boolean columnTestOk; do { double simFactor = mColorMetric.getDistance(this.getAverageRGB(r, c), this.getAverageRGB(columnCandidatesEndRow, columnCandidatesColumn), useAlpha) / maxSim; // if simFactor is greater than mergeFactor for the first time, this terminates the loop columnTestOk = simFactor <= mergeFactor; columnCandidatesEndRow++; } while (columnTestOk && columnCandidatesEndRow < rows && this.isConnected(columnCandidatesEndRow, c, FragmentNeighbor.UP)); // could all at the right side be added to this rect and is there a corner ? if (columnTestOk && columnCandidatesEndRow < rows) { // test bottom right corner that should be added boolean cornerTestOk = (mColorMetric.getDistance(this.getAverageRGB(r, c), this.getAverageRGB(columnCandidatesEndRow, columnCandidatesColumn), useAlpha) / maxSim) <= mergeFactor; if (cornerTestOk) { // test if the row on the down side of the current square could be added boolean rowTestOk; rowCandidatesStartColumn = this.getLastConnectedColumn(c, r, true); rowCandidatesRow = this.getLastConnectedRow(r, rowCandidatesStartColumn, false) + 1; rowCandidatesEndColumn = rowCandidatesStartColumn; do { double simFactor = mColorMetric.getDistance( this.getAverageRGB(rowCandidatesRow, rowCandidatesEndColumn), this.getAverageRGB(r, c), useAlpha) / maxSim; rowTestOk = simFactor <= mergeFactor; rowCandidatesEndColumn++; } while (rowTestOk && this.isConnected(rowCandidatesRow - 1, rowCandidatesEndColumn, FragmentNeighbor.LEFT)); if (rowTestOk) { // the column at the right side of the current square can be added, // the bottom right corner // and also the row below the square, so we got all we need to make a new square // setup connections for (int i = columnCandidatesStartRow; i < columnCandidatesEndRow; i++) { if (i != columnCandidatesStartRow) { this.setConnected(i, columnCandidatesColumn, FragmentNeighbor.UP, true); } this.setConnected(i, columnCandidatesColumn, FragmentNeighbor.LEFT, true); } for (int i = rowCandidatesStartColumn; i < rowCandidatesEndColumn; i++) { if (i != rowCandidatesStartColumn) { this.setConnected(rowCandidatesRow, i, FragmentNeighbor.LEFT, true); } this.setConnected(rowCandidatesRow, i, FragmentNeighbor.UP, true); } this.setConnected(columnCandidatesEndRow, rowCandidatesEndColumn, FragmentNeighbor.UP, true); this.setConnected(columnCandidatesEndRow, rowCandidatesEndColumn, FragmentNeighbor.LEFT, true); } } } } } } } @Override public boolean giveNext(Bitmap nextFragmentImage) { if (this.next != null && nextFragmentImage != null && nextFragmentImage.getWidth() == this.next.getWidth() && nextFragmentImage.getHeight() == this.next.getHeight()) { // given image was valid and has correct height and width if (this.resultImage == null) { // if resulting image not yet created, do so now this.resultImage = obtainBaseBitmap(this.getTotalWidth(), this.getTotalHeight(), Bitmap.Config.ARGB_8888); mResultCanvas = new Canvas(resultImage); } mResultCanvas.drawBitmap(nextFragmentImage, lastGivenColumn * getRectWidth(), lastGivenRow * getRectHeight(), null); this.next = null; // clear next to show that the lastGiven was satisfied mFragmentCount++; return true; } // no image expected or given image did not match requirements return false; } @Override public MosaicFragment nextFragment() { if (this.lastGivenColumn < 0 || this.lastGivenRow < 0) { this.lastGivenColumn = 0; this.lastGivenRow = 0; this.next = this.unite(this.lastGivenRow, this.lastGivenColumn); return this.next; } else if (this.next == null) { do { this.lastGivenColumn++; if (this.lastGivenColumn == this.getColumnCount()) { this.lastGivenRow++; if (this.lastGivenRow < this.getRowCount()) { this.lastGivenColumn = 0; } } } while (this.lastGivenRow < this.getRowCount() && (this.isConnected(this.lastGivenRow, this.lastGivenColumn, FragmentNeighbor.LEFT) || this.isConnected(this.lastGivenRow, this.lastGivenColumn, FragmentNeighbor.UP))); if (this.lastGivenRow >= this.getRowCount()) { // reached the last row, end, we got all images return null; } else { this.next = this.unite(this.lastGivenRow, this.lastGivenColumn); return this.next; } } else { return this.next; } } /** * Unites all Fragments that are connected to the given Fragment indexed by row/column, * which must be the top left corner of the rect. As only rects are * created (and only rects need to be united), this is a pretty fast straightforward * method. * @param row The row index. * @param column The column index. * @return A fragment of height equal to the sum of heights of all connected fragments * in a column and of width equal to the sum of widths of all connected fragments * in a row. The average color is the mixed average color of all rects. */ private MosaicFragment unite(int row, int column) { // calculate width of the Fragment rect int rectColumnCount = 0; int currColumn = column; do { rectColumnCount++; currColumn++; } while (currColumn < this.getColumnCount() && this.isConnected(row, currColumn, FragmentNeighbor.LEFT)); // calculate height of the Fragment rect int rectRowCount = 0; int currRow = row; do { rectRowCount++; currRow++; } while (currRow < this.getRowCount() && this.isConnected(currRow, column, FragmentNeighbor.UP)); // as all connected ShapeFragments have equal average RGB, I can simly use the corners one mFragment.reset(rectColumnCount * this.getRectWidth(), rectRowCount * this.getRectHeight(), this.getAverageRGB(row, column)); return mFragment; } @Override public boolean hasAll() { return this.nextFragment() == null; } @Override public Bitmap getReconstructed() { if (this.hasAll()) { Bitmap result = this.resultImage; this.resultImage = null; return result; } else { return null; } } @Override public int estimatedProgressPercent() { return (int) (100 * mFragmentCount / (double) (getColumnCount() * getRowCount())); } }