/*
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.awt.geom.Line2D;
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) by finding rectangles.
*
* @author Doug Barnum
* @version 1.0
*/
public final class DetectRatingRectangle {
/**
* The rating logo TEXT can be basically black in color. This is a "hint"
* that our code needs.
*/
public static final int BLACK_TYPE = 0;
/**
* The rating logo TEXT can be basically white in color. This is a "hint"
* that our code needs.
*/
public static final int WHITE_TYPE = 1;
public static final int MINIMUM_HEIGHT = 40;
public static final int MINIMUM_WIDTH = 30;
private int backup;
private int span;
/**
* Default empty constructor.
*/
public DetectRatingRectangle() {
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;
}
/**
* Examine a particlur image file as determine whether it is a
* "rating frame".
*
* @param f A File representing an image.
* @param type Black or white symbol is expected.
* @param verbose Print out messages if true.
* @return True if it is a "rating frame".
* @throws IOException on an error.
*/
public boolean examine(File f, int type, int red, int green, int blue, int range, boolean verbose,
int planIndex) throws IOException {
boolean result = false;
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);
double drange = range;
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 + "-" + planIndex + "-crop.png"));
}
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;
double distance = Math.sqrt(Math.pow(red - r, 2) + Math.pow(green - g, 2) + Math.pow(blue - b, 2));
double percentage = distance / Math.sqrt(Math.pow(255, 2) + Math.pow(255, 2) + Math.pow(255, 2));
if (type == BLACK_TYPE) {
if (distance < drange) {
data[i] = 0x00000000;
} else {
data[i] = 0x00ffffff;
}
} else if (type == WHITE_TYPE) {
if (distance < drange) {
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 + "-" + planIndex + "-white.png"));
//ImageIO.write(tryit, "png", new File(fname + "-white.png"));
}
// Ok here we add new code....
boolean done = false;
// No sense checking from the first row as the rating will not be there.
int row = 4;
while (!done) {
//System.out.println("gern");
Line2D.Double line = findLargestLine(data, w, h, row);
//System.out.println("bobby");
if (line != null) {
// Check minimum width;
int y1 = (int) line.getY1();
int x1 = (int) line.getX1();
int x2 = (int) line.getX2();
int linelength = x2 - x1;
if (linelength >= MINIMUM_WIDTH) {
int vertical = findVerticalLength(data, w, h, line);
//System.out.println("minwidth " + linelength + " minheight " + vertical);
if (vertical >= MINIMUM_HEIGHT) {
// Ok we have three sides of a rectangle.
// Now find the bottom Y to really see.
int bottomY = findBottomLineY(data, w, h, x1, x2, y1, vertical + y1 - 1);
int realheight = bottomY - y1 + 1;
//System.out.println("bottomY " + bottomY + " y1 " + y1 + " realheight " + realheight);
if (realheight >= MINIMUM_HEIGHT) {
// We found one that fits the minimum sizes. But we also
// know that these are generally in "portrait mode" where they
// are taller than their width. But we have to handle squareish ones too.
if ((linelength <= realheight) || (Math.abs(linelength - realheight) < 10)) {
// Ok right shape of rectangle. We have one more check,
// the line above our original line should be blank. How ever
// often this line is broken up so lets go up 2.
Line2D.Double aboveline = findLargestLine(data, w, h, y1 - 2);
if (aboveline != null) {
// There is a line. We make it on the same row and check
// intersection. No intersection means Ok.
aboveline.setLine(aboveline.getX1(), y1, aboveline.getX2(), y1);
if (!line.intersectsLine(aboveline)) {
// Ok we take it. Let's declare victory.
done = true;
result = true;
System.out.println("Yes!");
} else {
//System.out.println("tossed because line intersect");
//System.out.println(aboveline);
//System.out.println(line);
}
} else {
done = true;
result = true;
System.out.println("Yes!");
}
} else {
//System.out.println("tossed because not portrait");
}
}
}
}
}
row++;
if ((row + MINIMUM_HEIGHT) > h) {
done = true;
}
}
//System.out.println("examine done");
return (result);
}
private int findBottomLineY(int[] data, int width, int height, int x1, int x2, int y1, int y2) {
//System.out.println("findBottomLineY start y2 " + y2);
int result = y2;
int[] dline = new int[width];
boolean done = false;
while (!done) {
int offset = result * width;
for (int i = 0; i < width; i++) {
dline[i] = data[offset + i];
}
//System.out.println("before next");
int end = next(0x00ffffff, dline, x1);
//System.out.println("after next " + end);
if (end == -1) {
if (width == x2) {
done = true;
} else {
result--;
}
} else if (end == (x2 + 1)) {
done = true;
} else {
result--;
}
if (!done) {
if (result <= y1) {
done = true;
}
}
}
//System.out.println("findBottomLineY end");
return (result);
}
private int findVerticalLength(int[] data, int width, int height, Line2D.Double line) {
//System.out.println("findVerticalLength start " + width + " " + height);
int result = 0;
if ((data != null) && (line != null)) {
int x1 = (int) line.getX1();
int x2 = (int) line.getX2();
int y = (int) line.getY1();
int linewidth = x2 - x1;
if (linewidth <= width) {
int maxMissing = 10;
while (y < height) {
//System.out.println("y " + y + " x2 " + x2);
int left = data[y * width + x1];
int leftOneLess = 0x00ffffff;
if (x1 > 0) {
leftOneLess = data[y * width + x1 - 1];
}
int right = data[y * width + x2];
int rightOneMore = 0x00ffffff;
if ((x2 + 1) < width) {
rightOneMore = data[y * width + x2 + 1];
}
int passes = 0;
if (left == 0x00000000) {
passes++;
}
if (right == 0x00000000) {
passes++;
}
if (leftOneLess == 0x00ffffff) {
passes++;
}
if (rightOneMore == 0x00ffffff) {
passes++;
}
if (passes == 4) {
result++;
y++;
} else {
if ((passes == 3) && (maxMissing > 0)) {
maxMissing--;
result++;
y++;
} else {
y = height;
}
}
}
}
}
//System.out.println("findVerticalLength end");
return (result);
}
private Line2D.Double findLargestLine(int[] data, int width, int height, int y) {
//System.out.println("largest line start");
Line2D.Double result = null;
if ((data != null) && (y < height)) {
int[] dline = new int[width];
int offset = y * width;
for (int i = 0; i < width; i++) {
dline[i] = data[offset + i];
}
boolean done = false;
int index = 0;
ArrayList<Line2D.Double> l = new ArrayList<Line2D.Double>();
while (!done) {
int start = next(0x00000000, dline, index);
if (start >= 0) {
index = start + 1;
int end = next(0x00ffffff, dline, index);
if (end == -1) {
// We came to the end of the line at the end of the data.
done = true;
l.add(new Line2D.Double(start, y, dline.length - 1, y));
} else {
index = end;
l.add(new Line2D.Double(start, y, end - 1, y));
}
} else {
done = true;
}
}
if (l.size() > 0) {
if (l.size() == 1) {
result = l.get(0);
} else {
result = l.get(0);
double length = result.getX2() - result.getX1();
for (int i = 1; i < l.size(); i++) {
Line2D.Double tmp = l.get(i);
double tmplength = tmp.getX2() - tmp.getX1();
if (tmplength > length) {
length = tmplength;
result = tmp;
}
}
}
}
}
//System.out.println("largest line end");
return (result);
}
private int next(int value, int[] line, int index) {
int result = -1;
if (line != null) {
for (int i = index; i < line.length; i++) {
if (line[i] == value) {
result = i;
break;
}
}
}
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);
// The first frame bogus. Plus we start the count at 1
// instead of 0. So we take off 2 from the frame.
result -= 2;
result *= getSpan();
result -= getBackup();
if (result < 0) {
result = 0;
}
}
return (result);
}
/**
* Process a directory of images.
*
* @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 DetectResult instances.
*/
public DetectResult[] processDirectory(File dir, String ext, DetectRatingPlan[] plans, boolean verbose)
throws IOException {
DetectResult[] 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<DetectResult> drlist = new ArrayList<DetectResult>();
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 red = plans[j].getRed();
int green = plans[j].getGreen();
int blue = plans[j].getBlue();
int range = plans[j].getRange();
if (examine(all[i], type, red, green, blue, range, verbose, j)) {
int time = frameToSeconds(all[i]);
DetectResult dr = new DetectResult();
dr.setTime(time);
dr.setFile(all[i]);
drlist.add(dr);
System.out.println(all[i] + " is a rating frame <" + time + ">");
// Since we just found one, lets assume the next
// 30 seconds or so we don't need to check.
int fcount = (int) (30 / 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 (drlist.size() > 0) {
result = drlist.toArray(new DetectResult[drlist.size()]);
}
}
}
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 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:red:green:blue:range")) {
String[] splans = args[i + 1].split(",");
if ((splans != null) && (splans.length > 0)) {
for (int j = 0; j < splans.length; j++) {
String[] breakup = splans[j].split(":");
if ((breakup != null) && (breakup.length == 5)) {
DetectRatingPlan drp = new DetectRatingPlan();
drp.setType(Util.str2int(breakup[0], 0));
drp.setRed(Util.str2int(breakup[1], 0));
drp.setGreen(Util.str2int(breakup[2], 0));
drp.setBlue(Util.str2int(breakup[3], 0));
drp.setRange(Util.str2int(breakup[4], 0));
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()]);
DetectRatingRectangle drr = new DetectRatingRectangle();
drr.setBackup(backup);
drr.setSpan(span);
File file = new File(path);
if (file.isDirectory()) {
DetectResult[] array = drr.processDirectory(file, extension, plans, verbose);
}
}
}
}