/*
This file is part of JFLICKS.
JFLICKS 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.
JFLICKS 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 JFLICKS. If not, see <http://www.gnu.org/licenses/>.
*/
package org.jflicks.util;
import java.io.File;
import java.io.IOException;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Arrays;
import javax.imageio.ImageIO;
/**
* Simple class that will try to see if a set of Rating Symbol images
* are contained in a larger image (screenshot).
*
* @author Doug Barnum
* @version 1.0
*/
public final class DetectRating {
/**
* The rating logo can be basically black in color. This is a "hint"
* that our code needs.
*/
public static final int BLACK_TYPE = 0;
/**
* The rating logo can be basically white in color. This is a "hint"
* that our code needs.
*/
public static final int WHITE_TYPE = 1;
private int backup;
private int span;
private RatingImage[] ratingImages;
/**
* Default empty constructor.
*/
public DetectRating() {
setBackup(0);
setSpan(5);
}
/**
* The rating frame is usually put up a few seconds after the show has
* restarted, so the backup property allows a constant number of seconds
* to be offset to account for this time.
*
* @return An int value in seconds.
*/
public int getBackup() {
return (backup);
}
/**
* The rating frame is usually put up a few seconds after the show has
* restarted, so the backup property allows a constant number of seconds
* to be offset to account for this time.
*
* @param i An int value in seconds.
*/
public void setBackup(int i) {
backup = i;
}
/**
* The time between each frame. Defaults to five.
*
* @return The span as an int value.
*/
public int getSpan() {
return (span);
}
/**
* The time between each frame. Defaults to five.
*
* @param i The span as an int value.
*/
public void setSpan(int i) {
span = i;
}
private void moveToTop(int index) {
if ((ratingImages != null) && (index > 0)) {
ArrayList<RatingImage> l = new ArrayList<RatingImage>();
l.add(ratingImages[index]);
for (int i = 0; i < ratingImages.length; i++) {
if (i != index) {
l.add(ratingImages[i]);
}
}
ratingImages = l.toArray(new RatingImage[l.size()]);
}
}
private RatingImage[] getRatingImages(String path) {
if (ratingImages == null) {
if (path != null) {
File dir = new File(path);
if ((dir.exists()) && (dir.isDirectory())) {
String[] exts = {
".png",
".jpg"
};
ExtensionsFilter ef = new ExtensionsFilter(exts);
File[] files = dir.listFiles(ef);
if ((files != null) && (files.length > 0)) {
// Ok we have some rating images we can load.
ArrayList<RatingImage> l = new ArrayList<RatingImage>();
for (int i = 0; i < files.length; i++) {
try {
BufferedImage bi = ImageIO.read(files[i]);
int w = bi.getWidth();
int h = bi.getHeight();
int[] data = bi.getRGB(0, 0, w, h, null, 0, w);
RatingImage ri = new RatingImage(
files[i].getPath(), data, w, h);
l.add(ri);
} catch (IOException ex) {
ex.printStackTrace();
}
}
if (l.size() > 0) {
ratingImages = l.toArray(new RatingImage[l.size()]);
}
}
}
}
}
return (ratingImages);
}
private double compare(int[] first, int[] firstalpha, int[] second,
double accept) {
double result = 0.0;
if ((first != null) && (second != null)
&& (first.length == second.length)) {
double dmax = (double) first.length;
int need = (int) (dmax * accept);
int count = 0;
for (int i = 0; i < first.length; i++) {
if (first[i] == second[i]) {
count++;
} else if ((need - count) > (first.length - i)) {
// If we can't possibly make it, then quit now.
break;
}
}
if (count > 0) {
result = (double) (((double) count) / ((double) first.length));
}
}
return (result);
}
private boolean fill(int x, int y, int[] src, int srcw, int srch,
int[] dest, int destw, int desth) {
boolean result = false;
if ((src != null) && (dest != null)) {
if (((x + destw) < srcw) && ((y + desth) < srch)) {
// We have a valid subset.
result = true;
int dindex = 0;
for (int row = 0; row < desth; row++) {
int index = y * srcw + x;
for (int col = 0; col < destw; col++) {
dest[dindex++] = src[index++];
}
y++;
}
}
}
return (result);
}
private double analyzeRatingImage(int[] data, int w, int h,
RatingImage ri, double accept) {
double result = 0.0;
if ((ri != null) && (data != null)) {
int[] ridata = ri.getData();
int[] rialphadata = ri.getAlphaData();
if ((ridata != null) && (rialphadata != null)) {
// We need a "working" buffer to copy data.
int[] dest = ri.getBuffer();
int destw = ri.getWidth();
int desth = ri.getHeight();
// We loop through all (x, y) points of our source
// data array to see how close the given RatingImage
// matches. We return the highest percentage of
// matching pixels. We go by row...
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
if (fill(x, y, data, w, h, dest, destw, desth)) {
// We have valid new data as the destination
// data array "fits" at this (x, y) point.
// The compare method will return a value
// between 0.0 and 1.0. If it is 1.0 (doubtful)
// it means the two images are a perfect match.
// Of course the lower the value the less likely.
double d =
compare(ridata, rialphadata, dest, accept);
if (d > result) {
result = d;
if (result >= accept) {
x = w;
y = h;
}
}
} else {
// No sense in checking the rest of the row...
x = w;
}
}
}
}
}
return (result);
}
/**
* Examine a particlur image file as determine whether it is a
* "rating frame".
*
* @param ratingDir A File directory containing rating images.
* @param f A File representing an image.
* @param type Black or white symbol is expected.
* @param fudge Wiggle room from full black or full white.
* @param verbose Print out messages if true.
* @return True if it is a "rating frame".
* @throws IOException on an error.
*/
public boolean examine(String ratingDir, File f, int type, int fudge,
boolean verbose) throws IOException {
boolean result = false;
// First need to make sure we have images to compare.
RatingImage[] rimages = getRatingImages(ratingDir);
if (rimages != null) {
BufferedImage bi = ImageIO.read(f);
int x = 70;
int y = 10;
int w = 180;
int h = 180;
int[] data = new int[w * h];
bi.getRGB(x, y, w, h, data, 0, w);
String fname = f.getName();
BufferedImage crop =
new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
crop.setRGB(0, 0, w, h, data, 0, w);
if (verbose) {
ImageIO.write(crop, "png", new File(fname + "-crop.png"));
}
// Not lets turn non-grayscale pixels to white.
for (int i = 0; i < data.length; i++) {
int r = data[i] & 0x00ff0000;
r = r >> 16;
int g = data[i] & 0x0000ff00;
g = g >> 8;
int b = data[i] & 0x000000ff;
if (type == BLACK_TYPE) {
// If we are "near black" then turn it black, otherwise
// turn it white.
if ((Math.min(r, fudge) == r)
&& (Math.min(g, fudge) == g)
&& (Math.min(b, fudge) == b)) {
data[i] = 0x00000000;
} else {
data[i] = 0x00ffffff;
}
} else if (type == WHITE_TYPE) {
// If we are "near white" then turn it black, otherwise
// turn it white. This makes the rest of the code in this
// class work when the rating box is black.
if ((Math.max(r, fudge) == r)
&& (Math.max(g, fudge) == g)
&& (Math.max(b, fudge) == b)) {
data[i] = 0x00000000;
} else {
data[i] = 0x00ffffff;
}
}
}
BufferedImage white =
new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
white.setRGB(0, 0, w, h, data, 0, w);
if (verbose) {
ImageIO.write(white, "png", new File(fname + "-white.png"));
}
// Here we have a black and white image that we now try to
// find one of our set of rating images.
double percent = 0.0;
for (int i = 0; i < rimages.length; i++) {
double d = analyzeRatingImage(data, w, h, rimages[i], 0.90);
if (verbose) {
System.out.println("analyzeRatingImage: " + rimages[i].getPath() + ": " + d);
}
if (d > percent) {
percent = d;
if (percent >= 0.90) {
// The idea is that once we find a rating image
// that works, lets use it first on subsequent
// checks.
moveToTop(i);
i = rimages.length;
}
}
}
// Here we have computed the highest percentage any of our
// rating images may be located in our screen data.
result = (percent > 0.90);
}
return (result);
}
private int frameToSeconds(File f) {
int result = 0;
if (f != null) {
String name = f.getName();
int start = name.indexOf("-") + 1;
int end = name.lastIndexOf(".");
result = Util.str2int(name.substring(start, end), result);
result *= getSpan();
result -= getBackup();
if (result < 0) {
result = 0;
}
}
return (result);
}
/**
* This is our main worker method as it checks out all the images in
* a given directory to determine if any are "rating frames".
*
* @param ratingDir The directory to look for rating images.
* @param dir The directory to look.
* @param ext Look for files with this extension.
* @param type Do we look for black or white rating symbol.
* @param fudge This gives us some wiggle room to handle not quite
* black or white - shades of gray if you will.
* @param verbose Print out messages if true.
* @return An array of ints that have "seconds" pointing to time a
* rating frame happened.
* @throws IOException on an error.
*/
public int[] processDirectory(String ratingDir, File dir, String ext,
int type, int fudge, boolean verbose) throws IOException {
int[] result = null;
if ((dir != null) && (ext != null)) {
String[] array = new String[1];
array[0] = ext;
ExtensionsFilter ef = new ExtensionsFilter(array);
File[] all = dir.listFiles(ef);
if ((all != null) && (all.length > 0)) {
ArrayList<Integer> timelist = new ArrayList<Integer>();
Arrays.sort(all);
for (int i = 0; i < all.length; i++) {
if (examine(ratingDir, all[i], type, fudge, verbose)) {
int time = frameToSeconds(all[i]);
timelist.add(Integer.valueOf(time));
System.out.println(all[i] + " is a rating frame <" + time + ">");
// Since we just found one, lets assume the next
// 360 seconds or so we don't need to check.
int fcount = (int) (360 / getSpan());
i += fcount;
}
}
if (timelist.size() > 0) {
result = new int[timelist.size()];
for (int i = 0; i < result.length; i++) {
result[i] = timelist.get(i).intValue();
}
}
}
}
return result;
}
/**
* Process a directory of images.
*
* @param ratingDir The directory of rating symbols.
* @param dir The directory of images.
* @param ext The file extention to use.
* @param plans The palns to use.
* @param verbose More debugging when true.
* @throws IOException on an error.
* @return An array of int values.
*/
public int[] processDirectory(String ratingDir, File dir, String ext,
DetectRatingPlan[] plans, boolean verbose) throws IOException {
int[] result = null;
if ((plans != null) && (plans.length > 0) && (dir != null)
&& (ext != null)) {
String[] array = new String[1];
array[0] = ext;
ExtensionsFilter ef = new ExtensionsFilter(array);
File[] all = dir.listFiles(ef);
if ((all != null) && (all.length > 0)) {
ArrayList<Integer> timelist = new ArrayList<Integer>();
Arrays.sort(all);
// We are going to process each plan until we find one
// that works, then from then on we will just use that
// one. So we have to start with all of them. By default
// our array will be filled with "false" so we don't
// skip any.
boolean[] skipPlan = new boolean[plans.length];
boolean zappedPlans = false;
for (int i = 0; i < all.length; i++) {
for (int j = 0; j < plans.length; j++) {
if (!skipPlan[j]) {
int type = plans[j].getType();
int fudge = plans[j].getValue();
if (examine(ratingDir, all[i], type, fudge,
verbose)) {
int time = frameToSeconds(all[i]);
timelist.add(Integer.valueOf(time));
System.out.println(all[i] + " is a rating frame <" + time + ">");
// Since we just found one, lets assume the next
// 360 seconds or so we don't need to check.
int fcount = (int) (360 / getSpan());
i += fcount;
if (!zappedPlans) {
// Now we need to zap all plans but
// this one.
for (int k = 0; k < plans.length; k++) {
if (k != j) {
skipPlan[k] = true;
}
}
zappedPlans = true;
}
}
}
}
}
if (timelist.size() > 0) {
result = new int[timelist.size()];
for (int i = 0; i < result.length; i++) {
result[i] = timelist.get(i).intValue();
}
}
}
}
return result;
}
/**
* Simple main method that tests this class.
*
* @param args Arguments that happen to be ignored.
* @throws IOException on an error.
*/
public static void main(String[] args) throws IOException {
ArrayList<DetectRatingPlan> drpList = new ArrayList<DetectRatingPlan>();
String ratingDir = "resources/rating";
String path = null;
boolean verbose = false;
String extension = "jpg";
int backup = 3;
int span = 5;
for (int i = 0; i < args.length; i += 2) {
if (args[i].equalsIgnoreCase("-path")) {
path = args[i + 1];
} else if (args[i].equalsIgnoreCase("-type:value")) {
String[] splans = args[i + 1].split(",");
if ((splans != null) && (splans.length > 0)) {
for (int j = 0; j < splans.length; j++) {
int index = splans[j].indexOf(":");
if (index != -1) {
String front = splans[j].substring(0, index);
String back = splans[j].substring(index + 1);
DetectRatingPlan drp = new DetectRatingPlan();
drp.setType(Util.str2int(front, 0));
drp.setValue(Util.str2int(back, 10));
drpList.add(drp);
}
}
}
} else if (args[i].equalsIgnoreCase("-verbose")) {
verbose = Util.str2boolean(args[i + 1], verbose);
} else if (args[i].equalsIgnoreCase("-extension")) {
extension = args[i + 1];
} else if (args[i].equalsIgnoreCase("-backup")) {
backup = Util.str2int(args[i + 1], backup);
} else if (args[i].equalsIgnoreCase("-span")) {
span = Util.str2int(args[i + 1], span);
}
}
if ((path != null) && (drpList.size() > 0)) {
DetectRatingPlan[] plans =
drpList.toArray(new DetectRatingPlan[drpList.size()]);
DetectRating dr = new DetectRating();
dr.setBackup(backup);
dr.setSpan(span);
File file = new File(path);
if (file.isDirectory()) {
int[] array = dr.processDirectory(ratingDir, file, extension,
plans, verbose);
} else {
//dr.examine(ratingDir, file, type, fudge, verbose);
}
}
}
}