/*
* 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.matching;
import android.util.Log;
import android.util.SparseArray;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
import dan.dit.whatsthat.util.image.ColorMetric;
import dan.dit.whatsthat.util.mosaic.data.MosaicTile;
/**
* Searches for best matching tiles in an approximating way jumping
* from one tile to another, always improving on each step. Best used for large
* data sets as only initialization is in O(n²) but searching approximately constant.
* Good results (and not improving very
* much from this) starting with accuracy = 0.2.<br>
* Initialization will take longer (O(n²) in n = data size), searching is linear in the worst case
* (for accuracy = 1) and else
* Created by daniel on 03.07.15.
*/
public class AdaptiveApproxTileMatcher<S> extends TileMatcher<S> {
private double mAccuracy;
private SparseArray<SortedMap<Double, Integer>> mDataDistances;
private SparseArray<MosaicTile<S>> mData;
private int mCurrentColorApprox;
public AdaptiveApproxTileMatcher(Collection<? extends MosaicTile<S>> data, double accuracy, boolean useAlpha, ColorMetric metric) {
super(useAlpha, metric);
if (data == null || data.isEmpty()) {
throw new IllegalArgumentException("No data given.");
}
setAccuracy(accuracy);
init(data);
}
/**
* Sets the accuracy to the new value, if not exactly equal to the old value
* this will clear the hashed matches.
* @param accuracy The new accuracy to set
* @return true as this matcher uses accuracy.
*/
@Override
public boolean setAccuracy(double accuracy) {
double oldAccuracy = mAccuracy;
mAccuracy = Math.max(Math.min(accuracy, 1.), 0.);
if (oldAccuracy != mAccuracy) {
resetHashMatches();
}
return true;
}
private void init(Collection<? extends MosaicTile<S>> data) {
mData = new SparseArray<>();
for (MosaicTile<S> tile : data) {
mData.put(tile.getAverageARGB(), tile);
}
mDataDistances = new SparseArray<>(data.size());
for (MosaicTile<S> tile : data) {
int tileColor = tile.getAverageARGB();
SortedMap<Double, Integer> distanceMap = new TreeMap<>();
for (MosaicTile<S> otherTile : data) {
int otherTileColor = otherTile.getAverageARGB();
distanceMap.put(mColorMetric.getDistance(otherTileColor, tileColor, useAlpha), otherTileColor);
}
mDataDistances.put(tileColor, distanceMap);
}
mCurrentColorApprox = mData.keyAt(0);
}
@Override
protected MosaicTile<S> calculateBestMatch(int targetColor) {
int currentColorApprox = mCurrentColorApprox;
double tolerance = mColorMetric.maxValue(useAlpha) * mAccuracy;
boolean repeat;
do {
repeat = false;
double distanceToTarget = mColorMetric.getDistance(currentColorApprox, targetColor, useAlpha);
SortedMap<Double, Integer> distancesToColorsInRange = mDataDistances.get(currentColorApprox).subMap(distanceToTarget - tolerance, distanceToTarget + tolerance);
for (Integer colorsInRange : distancesToColorsInRange.values()) {
double colorInRangeDistanceToTarget = mColorMetric.getDistance(colorsInRange, targetColor, useAlpha);
if (colorInRangeDistanceToTarget < distanceToTarget) {
currentColorApprox = colorsInRange;
distanceToTarget = colorInRangeDistanceToTarget;
repeat = true;
}
}
} while (repeat);
mCurrentColorApprox = currentColorApprox;
return mData.get(currentColorApprox);
}
@Override
public double getAccuracy() {
return mAccuracy;
}
@Override
public boolean removeTile(MosaicTile<S> toRemove) {
Log.d("HomeStuff", "Removing tile : " + toRemove);
boolean hadTile = mData.get(toRemove.getAverageARGB(), null) != null;
mData.remove(toRemove.getAverageARGB());
mDataDistances.remove(toRemove.getAverageARGB());
return hadTile;
}
@Override
public int getUsedTilesCount() {
return mData.size();
}
@Override
public void setColorMetric(ColorMetric metric) {
if (metric == null || metric.equals(mColorMetric)) {
return;
}
mColorMetric = metric;
List<MosaicTile<S>> tiles = new ArrayList<>(mData.size());
for (int i = 0; i < mData.size(); i++) {
tiles.add(mData.valueAt(i));
}
init(tiles);
resetHashMatches();
}
}