/*
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.Rectangle;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import javax.imageio.ImageIO;
/**
*
* @author Doug Barnum
* @version 1.0
*/
public final class Detect {
/**
* 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;
/**
* The rating logo can be basically dark gray in color. This is a "hint"
* that our code needs.
*/
public static final int DARK_GRAY_TYPE = 2;
private int backup;
private int span;
/**
* Default empty constructor.
*/
public Detect() {
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 Line2D.Double[] findLines(int[] data, int w, int h, int min) {
Line2D.Double[] result = null;
ArrayList<Line2D.Double> list = new ArrayList<Line2D.Double>();
for (int col = 0; col < w; col++) {
int startrow = -1;
int endrow = -1;
int index = col;
for (int row = 0; row < h; row++) {
if (data[index] == 0) {
if (startrow == -1) {
startrow = row;
endrow = row;
} else {
endrow = row;
}
} else {
// We have white space so the line is ended here (well
// if it was ever started). We also only care if the
// line is at least min length.
if ((startrow != -1) && ((endrow - startrow) > min)) {
list.add(new Line2D.Double((double) col,
(double) startrow, (double) col, (double) endrow));
}
startrow = -1;
endrow = -1;
}
index += w;
}
}
if (list.size() > 0) {
result = list.toArray(new Line2D.Double[list.size()]);
}
return (result);
}
private Point2D.Double computeLeft(boolean top, Line2D.Double[] array,
int[] data, int w, int h, boolean verbose) {
Point2D.Double result = null;
if (array != null) {
// Count the occurences of the proper Y.
HashMap<Integer, Integer> hm = new HashMap<Integer, Integer>();
for (int i = 0; i < array.length; i++) {
int y = 0;
if (top) {
y = (int) array[i].getY1();
} else {
y = (int) array[i].getY2();
}
Integer val = Integer.valueOf(y);
Integer lookup = hm.get(val);
if (lookup == null) {
hm.put(val, Integer.valueOf(1));
} else {
hm.put(val, Integer.valueOf(lookup.intValue() + 1));
}
}
if (verbose) {
System.out.println(hm);
}
Set<Map.Entry<Integer, Integer>> set = hm.entrySet();
Iterator<Map.Entry<Integer, Integer>> iter = set.iterator();
int max = -1;
Integer val = null;
while (iter.hasNext()) {
Map.Entry<Integer, Integer> me = iter.next();
Integer tmax = me.getValue();
if (tmax.intValue() > max) {
max = tmax.intValue();
val = me.getKey();
}
}
if (max != -1) {
// Find the left most line that matches up with our Y value.
int findex = 0;
for (int i = 0; i < array.length; i++) {
double dy = 0.0;
if (top) {
dy = array[i].getY1();
} else {
dy = array[i].getY2();
}
if (Math.abs(dy - val.doubleValue()) <= 2.0) {
// Now we only think we have it if we have a
// non-zero length horizontal line. Let's
// check it.
if (computeLineLength(array[i].getX1(),
val.doubleValue(), data, w, h) > 0) {
findex = i;
break;
}
}
}
result = new Point2D.Double(array[findex].getX1(),
val.doubleValue());
}
}
return (result);
}
private int computeLineLength(double x, double y, int[] data, int w,
int h) {
return computeLineLength(new Point2D.Double(x, y), data, w, h);
}
private int computeLineLength(Point2D.Double p, int[] data, int w, int h) {
int result = 0;
if ((p != null) && (data != null)) {
int col = (int) p.getX();
int row = (int) p.getY();
int index = row * w + col;
boolean done = false;
while (!done) {
if (data[index] == 0) {
result++;
index++;
} else {
done = true;
}
}
if (result > (w - col)) {
result = w - col;
}
}
return (result);
}
private Line2D.Double findLine(Line2D.Double[] array, int y1, int y2,
int mincol, int maxcol, boolean verbose) {
Line2D.Double result = null;
if (array != null) {
double dy1 = (double) y1;
double dy2 = (double) y2;
if (verbose) {
System.out.println("y1: " + y1);
System.out.println("y2: " + y2);
System.out.println("mincol: " + mincol);
System.out.println("maxcol: " + maxcol);
}
// We are going to look for more than one line...
for (int col = mincol; col < maxcol; col++) {
double dcol = (double) col;
for (int i = 0; i < array.length; i++) {
// Now we find a correct line if it contains both
// points defined by our other arguments...
if ((array[i].ptSegDist(dcol, dy1) == 0.0)
&& (array[i].ptSegDist(dcol, dy2) == 0.0)) {
result = array[i];
}
}
}
}
return (result);
}
private double computeWhitespace(Rectangle r, int[] data, int w, int h) {
double result = 0.0;
if ((r != null) && (data != null)) {
double count = 0.0;
int cols = r.width;
for (int row = 0; row < r.height; row++) {
int index = (r.y + row) * w + r.x;
for (int col = 0; col < r.width; col++) {
if (data[index] != 0) {
count += 1.0;
}
index++;
}
}
double max = r.width * r.height;
result = count / max;
}
return (result);
}
private double computeBorder(Rectangle r, int[] data, int w, int h) {
double result = 0.0;
if ((r != null) && (data != null)) {
double max = 2 * r.height + 2 * r.width;
double count = 0.0;
int cols = r.width;
for (int row = 0; row < r.height; row++) {
int index = (r.y + row) * w + r.x;
if ((row == 0) || (row == (r.height - 1))) {
for (int col = 0; col < r.width; col++) {
if (data[index] == 0) {
count += 1.0;
}
index++;
}
} else {
if (data[index] == 0) {
count += 1.0;
}
index += r.width;
if (data[index] == 0) {
count += 1.0;
}
}
}
result = count / max;
}
return (result);
}
/**
* 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 range The range to use.
* @param fudge Wiggle room from full black or full white.
* @param compare True when one should do a compare.
* @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 range, int fudge,
boolean compare, boolean verbose) throws IOException {
boolean result = false;
BufferedImage bi = ImageIO.read(f);
int x = 66;
int y = 0;
int w = 200;
int h = 200;
int min = 44;
int minx = 10;
int miny = 20;
double whitespace = 0.25;
double border = 0.65;
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;
}
} else if (type == DARK_GRAY_TYPE) {
if ((Math.min(r, fudge) == r)
&& (Math.min(g, fudge) == g)
&& (Math.min(b, fudge) == b)) {
// Seems to be dark enough, but we want it not
// to be too dark.
int low = fudge - range;
if ((Math.max(r, low) == r)
&& (Math.max(g, low) == r)
&& (Math.max(b, low) == r)) {
data[i] = 0x00ffffff;
} else {
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"));
}
if (compare) {
// We are going to do more of a brute force search for a
// logo in what is left of our search area of the frame.
} else {
// We are going to look for some sort of "box" by
// computing lines and such. This works pretty well
// on mostbox-line rating symbols.
Line2D.Double[] lines = findLines(data, w, h, min);
if (lines != null) {
if (verbose) {
System.out.println("Found " + lines.length
+ " interesting lines.");
}
for (int i = 0; i < lines.length; i++) {
int x1 = (int) lines[i].getX1();
int y1 = (int) lines[i].getY1();
int x2 = (int) lines[i].getX2();
int y2 = (int) lines[i].getY2();
if (verbose) {
System.out.print("(X1, Y1)-(X2, Y2): (" + x1 + ", " + y1
+ ")");
System.out.print("-(" + x2 + ", " + y2 + ")");
System.out.println(" length " + (y2 - y1));
}
}
Point2D.Double toppt =
computeLeft(true, lines, data, w, h, verbose);
int toplength = computeLineLength(toppt, data, w, h);
Point2D.Double botpt =
computeLeft(false, lines, data, w, h, verbose);
int botlength = computeLineLength(botpt, data, w, h);
if (verbose) {
System.out.println("topleft could be: " + toppt);
System.out.println("top line length looks like: "
+ toplength);
System.out.println("botleft could be: " + botpt);
System.out.println("bottom line length looks like: "
+ botlength);
}
int mincol = Math.min(toplength, botlength) - 5;
mincol += (int) toppt.getX();
int maxcol = Math.max(toplength, botlength);
maxcol += (int) toppt.getX();
int toprow = (int) toppt.getY();
int botrow = (int) botpt.getY();
Line2D.Double rightline =
findLine(lines, toprow, botrow, mincol, maxcol, verbose);
if (rightline != null) {
int x1 = (int) rightline.getX1();
int y1 = (int) rightline.getY1();
int x2 = (int) rightline.getX2();
int y2 = (int) rightline.getY2();
if (verbose) {
System.out.println("rightline");
System.out.print("(X1, Y1)-(X2, Y2): (" + x1 + ", " + y1
+ ")");
System.out.print("-(" + x2 + ", " + y2 + ")");
System.out.println(" length " + (y2 - y1));
}
// At this point we think we have found the rectangle
// that is the "rating box". We could have a false
// positive where we are looking at a blank screen or
// something. Lets check the rectangle - it should have
// a high percentage of white pixels representing the
// text.
int rx = (int) toppt.getX();
int ry = (int) toppt.getY();
int rw = (int) (x1 - rx) + 1;
int rh = (botrow - toprow) + 1;
if ((rx > minx) && (ry > miny)) {
Rectangle rect = new Rectangle(rx, ry, rw, rh);
double percent = computeWhitespace(rect, data, w, h);
result = (percent >= whitespace);
if (result) {
result = computeBorder(rect, data, w, h) > border;
}
if (verbose) {
System.out.println("Rectangle: " + rect);
System.out.println("perc whitespace: " + percent);
}
}
} else {
if (verbose) {
System.out.println("Can't find right vertical line");
}
}
} else {
if (verbose) {
System.out.println("Didn't find any interesting lines.");
}
}
}
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 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 compare True when one should do a compare.
* @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(File dir, String ext, int type,
int fudge, boolean compare, 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(all[i], type, 24, fudge, compare, 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
// 20 seconds or so we don't need to check.
int fcount = (int) (20 / 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;
}
/**
* 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 {
String path = null;
int type = BLACK_TYPE;
boolean compare = false;
boolean verbose = false;
int fudge = 10;
String extension = "jpg";
int backup = 0;
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")) {
type = Util.str2int(args[i + 1], type);
} else if (args[i].equalsIgnoreCase("-fudge")) {
fudge = Util.str2int(args[i + 1], fudge);
} else if (args[i].equalsIgnoreCase("-compare")) {
compare = Util.str2boolean(args[i + 1], compare);
} 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) {
Detect detect = new Detect();
detect.setBackup(backup);
detect.setSpan(span);
File file = new File(path);
if (file.isDirectory()) {
int[] array = detect.processDirectory(file, extension, type,
fudge, compare, verbose);
} else {
detect.examine(file, type, 24, fudge, compare, verbose);
}
}
}
}