I'm wondering if there are any algorithms out there written in Java currently for determining if an image has a low range of different pixel colours contained within it.
I'm trying to detect placeholder images (that typically consist of high percentages of single colours (typically white and grey pixels) as opposed to full colour photos (that consist of a plethora of multiple colours).
If nothing exists, I'll write something myself (was thinking about sampling an arbitrary pixels in random positions across the image or averaging out all pixel colours contained across the image) and then determining quantities of the different colours I find. There may be a trade off between speed and accuracy depending on the methodology used.
Any advice / pointers / reading material appreciated.
A way to do it would be:
final BufferedImage image = // your image;
final Set<Integer> colours = new HashSet<Integer>();
for (int x = 0; x < image.getWidth(); x++) {
for (int y = 0; y < image.getHeight(); y++) {
colours.add(image.getRGB(x, y));
}
}
// Check your pixels here. In grayscale images the R equals G equals B
You can also use the Java Advanced Imaging(JAI) since it provides a Histogram class:
package com.datroop.histogram;
import java.awt.image.renderable.ParameterBlock;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import javax.media.jai.Histogram;
import javax.media.jai.JAI;
import javax.media.jai.PlanarImage;
import javax.media.jai.ROI;
import javax.media.jai.RenderedOp;
public class HistogramCreator {
private HistogramCreator() {
}
public static int[] createHistogram(final PlanarImage image) {
// set up the histogram
final int[] bins = { 256 };
final double[] low = { 0.0D };
final double[] high = { 256.0D };
Histogram histogram = new Histogram(bins, low, high);
final ParameterBlock pb = new ParameterBlock();
pb.addSource(image);
pb.add(null);
pb.add(1);
pb.add(1);
final RenderedOp op = JAI.create("histogram", pb);
histogram = (Histogram) op.getProperty("histogram");
// get histogram contents
final int[] local_array = new int[histogram.getNumBins(0)];
for (int i = 0; i < histogram.getNumBins(0); i++) {
local_array[i] = histogram.getBinSize(0, i);
}
return local_array;
}
public static void main(String[] args) {
try {
String filename = "file://localhost/C:/myimage.jpg";
System.setProperty("com.sun.media.jai.disableMediaLib", "true");
// Create the histogram
int[] myHistogram = createHistogram(JAI.create("url", new URL(filename)));
// Check it out here
System.out.println(Arrays.toString(myHistogram));
} catch (MalformedURLException e) {
e.printStackTrace();
}
}
}
A simple low-overhead approach would be to do a histogram of the Red, Green and Blue component values separately. There would only be 256 colour levels for each so it would be quite efficient - you could build each histogram in an new int[256] array.
Then you just count the number of non-zero values in each of the histograms and check whether they all meet some threshold (say at least 20 different values in each would imply a photograph)
An alternative approach would be to create a HashSet of colour values in the image, and keep adding to the HashSet as you scan the image. Since HashSets hold unique values, it will store only the unqique colours. To avoid the HashSet getting too large, you can bail out when the size of the HashSet hits a pre-determined threshold (maybe 1000 unique colours?) and conclude that you have a photograph.
Related
I want to be able to take the data of an image as an array, modify it, and then use that array to create a modified image. Here is what I attempted:
public class Blue {
public static void main (String [] args) throws AWTException, IOException {
Robot robot = new Robot ();
Dimension d = Toolkit.getDefaultToolkit().getScreenSize();
BufferedImage img = robot.createScreenCapture(new Rectangle(0,0,d.width,d.height));
int[] pixels = ((DataBufferInt)img.getRaster().getDataBuffer()).getData();
int[] newPixels = IntStream.range(0,pixels.length).parallel().filter(i ->{
int p = pixels[i];
// get red
int r = (p >> 16) & 0xff;
// get green
int g = (p >> 8) & 0xff;
// get blue
int b = p & 0xff;
return b >= 200;
}).toArray();
int[] output = new int[pixels.length];
for(int i = 0; i<newPixels.length; i++) {
output[newPixels[i]] = 0x0000FF;
}
File f = new File("Result.jpg");
ByteBuffer byteBuffer = ByteBuffer.allocate(output.length * 4);
for (int i = 0; i< output.length; i++) {
byteBuffer.putInt(output[i]);
}
byte[] array = byteBuffer.array();
InputStream stream = new ByteArrayInputStream(array);
BufferedImage image1 = ImageIO.read(stream);
System.out.println(image1.getWidth());
ImageIO.write(image1, "png", f);
}
}
Here is how it works.
The robot takes a screen capture of the screen, which is then stored into a BufferedImage.
The data of the image is stored in an integer array
An int stream is used to select all pixel locations that correspond to sufficiently blue pixels
These blue pixels are placed in an array called output at the same locations they were taken from. However, the rest of the array has value 0.
A destination file for my modified image is created
I create a byte buffer that is 4 times the length of the output array, and data from the output array is placed in it.
I create a byte array from the buffer then create an input stream with it
Finally, I read the stream to create an Image from it
I use System.out.println() to print some data from the image to see if the image exists.
Step 9 is where the problem shows up.
I keep getting a NullPointerException, meaning that the image doesn't exist, it is null.
I don't understand what I did wrong.
I tried using ByteArrayInputStream instead of InputStream, but that doesn't work as well. Then, I thought that maybe the first couple of bytes encode the coding information for the image, so I tried copying that over to the output array, but that didn't solve the problem either. I am not sure why my byte array isn't turning into an image.
Yo summarize the comments in an answer, the problem is that you have an array of "raw" pixels, and try to pass that to ImageIO.read(). ImageIO.read() reads images stored in a defined file format, like PNG, JPEG or TIFF (while the pixel array is just pixels, it does not contain information on image dimension, color model, compression etc.). If no plugin is found for the input, the method will return null(thus the NullPointerException).
To create a BufferedImage from the pixel array, you could create a raster around the array, pick a suitable color model and create a new BufferedImage using the constructor taking a Raster and ColorModel parameter. You can see how to do that in one of my other answers.
However, as you already have a BufferedImage and access to its pixels, it's much easier (and cheaper CPU/memory wise) to just reuse that.
You can replace your code with the following (see comments for details and relation to your steps):
public class Blue {
public static void main (String [] args) throws AWTException, IOException {
// 1. Create screen capture
Robot robot = new Robot ();
Dimension d = Toolkit.getDefaultToolkit().getScreenSize();
BufferedImage img = robot.createScreenCapture(new Rectangle(0, 0, d.width, d.height));
// 2: Get backing array
int[] pixels = ((DataBufferInt) img.getRaster().getDataBuffer()).getData();
// 3: Find all "sufficiently blue" pixels
int[] bluePixels = IntStream.range(0, pixels.length).parallel()
.filter(i -> pixels[i] & 0xff >= 200).toArray();
// 4a: Clear all pixels to opaque black
for (int i = 0; i < pixels.length; i++) {
pixels[i] = 0xFF000000;
}
// 4b: Set all blue pixels to opaque blue
for (int i = 0; i < bluePixels.length; i++) {
pixels[bluePixels[i]] = 0xFF0000FF;
}
// 5: Make sure the file extension matches the file format for less confusion... 😀
File f = new File("result.png");
// 9: Print & write image (steps 6-8 is not needed)
System.out.println(img);
ImageIO.write(img, "png", f);
}
}
i'm having trouble trying to sort an ArrayListof Color.
I'm retrieving all the colors from this image
imgRed.jpg
The code i'm using:
public static ArrayList<Color> getColors(BufferedImage img){
int height = img.getHeight();
int width = img.getWidth();
boolean found = false;
ArrayList<Color> List = new ArrayList<>();
for(int x=0;x<width;x++) {
for(int y=0;y<height;y++) {
found = false;
Color color = new Color(img.getRGB(x, y));
for(Color c : List) {
if(color.getRGB()<c.getRGB()+100 && color.getRGB()>c.getRGB()-100) {
found=true;
}
}
if(!found) {
List.add(color);
}
}
}
return List;
}
After i've collected all the colors, i sort them:
Collections.sort(Colors, Comparator.comparing(Color::getRed)
.thenComparing(Color::getGreen)
.thenComparing(Color::getBlue));
Subsequently i create a new image containing all the colors sorted:
public static void createImage(ArrayList<Color> Colors) {
int width=500;
int height=Colors.size()*10;
BufferedImage b_img = new BufferedImage(width,height, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = b_img.createGraphics();
int partialHeight = 0;
int amount = Colors.size();
for(Color c : Colors) {
System.out.println("Colors remaining: "+amount);
graphics.setPaint ( c );
graphics.fillRect ( 0, partialHeight, b_img.getWidth(), partialHeight+10 );
partialHeight = partialHeight + 10;
amount--;
}
File outFile = new File("C:/test/img/out/testColor/outputRed4.png");
try {
ImageIO.write(b_img, "png", outFile);
} catch (IOException e) {
e.printStackTrace();
}
}
This function produces this image: outputRed.png
As you can see, the colors are not really sorted. This is because (i think) colors are sorted basing its numeric value (Red, Green, Blue) and because the RGB numeric value isn't ordered by our perspective.
I remember that the image produced hasn't a color twice so all the color in that image are different.
My question is:
How can i order all the colors following all the shades of each color without having such a jagged result?
Thanks to all
The issue you are encountering comes from the fact that you are taking 3-dimensional data (red values, green values, blue values) and trying to order them in 1 dimension (a List with only an index parameter).
The output you are receiving is most likely exactly what you should expect if you sort first, by the red value of a colour, and then by the green, and then blue. Remember, this method only compares the green value to sort colours with identical red values, and similarly only compares the blue value to sort colours with identical red and blue values.
Perhaps the reason it looks "jagged" is because of the suddenly changing intensity. Given that the input image is pretty much entirely shades of red at different intensities, it might be worth using Comparator.comparing(Color::getTotal) where getTotal() is defined as:
int getTotal() {
return getGreen() + getBlue() + getRed();
}
This will sort by intensity (i.e. brightness), and will make the image look less "jagged", but on images with more than just red, the colours will not be in "colour order" or "rainbow order".
Again, this is an issue of trying to map 3-d data into a 1-d space. Compromises will always have to be made.
I am attempting to create a sky-view RPG game, but the initial load times have become a bit of an issue. I am fairly new to programming and Java, so I don't know much about how Java loads things into memory or anything.
So the issue is that when I initially run my program, it can take a very long time for all of my JLabels(set up in my grid) to appear. I am hoping to make a map that is very large, consisting of hundreds of thousands of tiles in total. After doing some math and tests, it turns out that 30 seconds of loading would result in merely about 6,724 tiles. This is only a bit over 8 screens worth of map, which is very small, since my character is a little smaller than 50x50 pixels. So for a map of a size from which I may be satisfied with would take over 6 minutes to load, upon every run. That is insane. And my map creator loads even more slowly.
Each tile is 50x50 pixels. Every tile is set up to have multiple layers of JLabels. For example, the first is for just a JLabel that the other labels will be added to, then the second is for Terrain(like Grass), and the third is for Interactive elements such as doors, so that the door may appear on top of the grass.
All of my Tile objects are created and inserted into a 2-dimensional Tile array. Every Tile instance has:
2 JLabels.
Icons added to at least, and usually only, the first JLabel. Every time a tile requests an icon, it gets it from the MapTiles class.
5 String arrays
7 booleans
4 ints
4 Layer instances in a Layer array of that size
Each Tile instance also implements MouseListener
Each Layer instance has:
1 JLabel with no icon, unless it is Grass. (75% of Layer instances aren't Grass)
4 booleans
1 int
1 String
The MapTiles class will create an ImageIcon for the thing, such as "Grass", if it hasn't already. If it has already created an ImageIcon for it, then it simply returns that ImageIcon. From reading about improving loading times, I was led that this may help speed things up, rather than creating a new ImageIcon for each Tile.
Here, I will provide the beginnings for each of my 3 main classes involved in creating/loading the grid. By beginnings, I mean only the constructor and the creating of the objects. There are a few other methods in each class, but not very many, and none used during the initial loading.
Tile class:
package Tiles;
import Datas.*;
import Images.*;
import MapCreation.*;
import UniversalVariables.*;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.*;
import java.util.*;
public class Tile implements MouseListener{
public Layer[] layers = new Layer[4];
//MAKE SURE THAT THE FOLLOWING INTEGERS MATCH THE ARRAY IN MAPDATA
int terrain = 0;
int interactive = 1;
int destroyable = 2;
int obstruction = 3;
public JLabel image = new JLabel();
JLabel mouseDetector = new JLabel();
boolean hasBarrier = false;
boolean hasModifiedBarrier = false;
boolean isInteractive = false;
boolean leftClickPressed = false;
boolean rightClickPressed = false;
boolean scrollClickPressed = false;
String[] layerTypes = MapData.uni.tileLayerTypes;
String[] terrainTypes = MapData.uni.terrainTypes;
String[] interactiveTypes = MapData.uni.interactiveTypes;
String[] destroyableTypes = MapData.uni.destroyableTypes;
String[] obstructionTypes = MapData.uni.obstructionTypes;
int tileSize = MapData.uni.tilePixelSize;
int row = 0;
int col = 0;
boolean mapCreatorOpen = UVars.uni.mapCreatorRunning;
public Tile(int rowNum, int colNum){
row = rowNum;
col = colNum;
setLayer("Terrain", "Grass");
for(int c = 1; c < layers.length; c++){ //Sets all layers except terrain to have new layerName
layers[c].layerName = MapData.uni.tileLayerTypes[c] + "NLT";
}
mouseDetector.addMouseListener(this);
image.add(mouseDetector);
mouseDetector.setBounds(0, 0, tileSize, tileSize);
//image.setComponentZOrder(mouseDetector, 0);
for(int c = 0; c < layers.length; c++){
image.setComponentZOrder(layers[c].image, c);
}
image.setComponentZOrder(mouseDetector, 0);
}
}
Layer class:
package Tiles;
import Datas.*;
import Images.*;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
public class Layer{
public JLabel image = new JLabel();
int tileSize;
public String layerName = "No Layer Name";
boolean hasBarrier;
boolean hasModifiedBarrier;
boolean isInteractive;
boolean affectsMovement;
public Layer(Tile tile, String layerType){
//For empty layers
tile.add(image);
}
public Layer(Tile tile, String layerType, String name){
if(layerType.equals("Terrain") && name.equals("No Layer Name")){
name = "Grass";
}
layerName = name;
tileSize = tile.tileSize;
tile.add(image);
image.setBounds(0, 0, tileSize, tileSize);
image.setIcon(MapTiles.uni.getIcon(layerName));
if(layerType.equals("Terrain")){
Terrain terrain = new Terrain(layerName);
terrain.exchangeValues(this, layerName);
} else if(layerType.equals("Interactive")){
Interactive interactive = new Interactive(layerName);
interactive.exchangeValues(this, layerName);
} else if(layerType.equals("Destroyable")){
Destroyable destroyable = new Destroyable(layerName);
destroyable.exchangeValues(this, layerName);
} else if(layerType.equals("Obstruction")){
Obstruction obstruction = new Obstruction(layerName);
obstruction.exchangeValues(this, layerName);
}
}
}
MapTiles class:
package Images;
import Datas.*;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.*;
import java.util.*;
public class MapTiles{
public static MapTiles uni;
String tileLoc = "Tile Graphics\\" + String.valueOf(MapData.uni.tilePixelSize) + "PX\\";
ArrayList<ImageIcon> icons = new ArrayList<ImageIcon>();
ArrayList<String> iconList = new ArrayList<String>();
public MapTiles(){
uni = this;
}
public ImageIcon getIcon(String tileType){
for(int c = 0; c < iconList.size(); c++){
if(iconList.get(c).equals(tileType)){
return icons.get(c);
}
}
iconList.add(tileType);
ImageIcon newIcon = new ImageIcon(tileLoc + tileType + ".png");
icons.add(newIcon);
return newIcon;
}
}
So, why does my grid take so long to load? It is all created in the default Java thread, but that's because currently, I don't have any menu or anything else to distract a user with before getting to the map. The map starts immediately. What appears is the frame is just a blank panel. Eventually, once it finally creates the tiles in the top left corner once it has gotten that far(where the user's view defaults), you can see it creating each and the user may even hover on one. (Hovering over it changes the icon set on it) Hovering over it runs very well. Only the load times is the issue. So first off, are there any better ideas you guys have for creating such a grid? Secondly, are there better ways you can think of for doing the things I am attempting to do? And finally, is there anything that I have done wrong that I should improve upon? The main question I guess that I am asking is... What is it that is making Java take so long to load new Tile instance? By the way, I have a fairly high-end PC. It's not some piece of junk. For example, I can run most pre-2015 PC games on maximum graphical settings, and I have 16GB of RAM. I also have another question you guys may be able to answer. I read that using the GPU to load things rather than mainly loading them into RAM runs MUCH more quickly, but it uses 3D Java or something like that. Would I be able to use my GPU to load many of the tiles somehow and improve the load times that way?
Well, it looks like you're loading the same images multiple times from disk for tiles. Only loading what you need, then sharing that same resource in memory could help.
Also you probably shouldn't try to hold your entire map in memory but instead load each section as needed.
I'd recommend you ditch Swing and use LWJGL or an engine backed by LWJGL. This will give you hardware accelerated rendering typically not available through Swing (unless passing certain runtime flags.) If you're concerned about quality Minecraft uses LWJGL. Its really the way to go if you're going to use Java for game development.
Specifically I recommend LIBGDX as your LWJGL backed game framework. I've used it extensively and its a great way to get your game on multiple OS and mobile devices.
having an endless map is not so difficult to achive, it's mainly difficult to maintain
the trick is that you don't load all tiles at once but only load the tiles that are visible, thus called a viewport.
public class ViewPort{
private Tile[][] visibleTiles;
public ViewPort(int width, int height, Factory factory){
//create the Tiles and add them using a proper layout
....
}
public void setViewLocation(int x, int y){
for(int dy = 0; dy < getHeight(); dy++){ //getHeight() with height from constructor
for(int dx = 0; dx < getWidth(); dy++){ //getWidth() with width from constructor
visibleTiles[dx][dy] = factory.getTiles(dx+x, dy+y);
}
}
}
if you use this design you can jump to any location on your map, you only have to use a proper factory.
The harder problem is creating a good factory:
public class Factory{
public Tile getTile(int x, int y){
if (x==0 && y==0) return getTile(TileType.Grass);
...
return null;
}
//speeding up using a lookup table, so we only create objects
//when we need new one / reUse old ones
private Map<TileType, Tile> lookUpTable = ...;
private Tile getTile(TileType type){
Tile value = lookUpTable.get(type);
if(value == null){
value = new Tile(...);
lookUpTable.put(type, value);
}
return value;
}
and what looks kind of easy now is very hard to maintain:
if (x==0 && y==0) return getTile(TileType.Grass);
if (x==1 && y==0) return getTile(TileType.Grass);
this code is neither maintainable nor efficient, you should use switch/case
switch(x){
case 0:
switch (y){
case 0: return getTile(TileType.Grass);
...
}
}
is neither maintainable nor readable...
I'm trying to take a screenshot and then look through it for a pixel that has a certain color. Firstly, I tried to just print the color of an image at a certain xy coordinate but I could not even do that. What am I doing wrong?
static int ScreenWidth;
static int ScreenHeight;
static Robot robot;
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
// TODO code application logic
callibrateScreenSize();
findSquares();
//takeScreenShot();
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void callibrateScreenSize() {
try {
Rectangle captureSize = new Rectangle(Toolkit.getDefaultToolkit().getScreenSize());
ScreenWidth = captureSize.width;
ScreenHeight = captureSize.height;
System.out.println("Width is " + ScreenWidth);
System.out.println("Height is " + ScreenHeight);
} catch (Exception e) {
e.printStackTrace();
}
//return null;
}
public static BufferedImage takeScreenShot() {
Rectangle captureSize = new Rectangle(Toolkit.getDefaultToolkit().getScreenSize());
BufferedImage image = robot.createScreenCapture(captureSize);
return image;
}
public static void findSquares() {
System.out.println(takeScreenShot().getRGB(5,5));
}
Thanks!
You can use BufferedImage#getRGB or byte[] pixels = ((DataBufferByte) bufferedImage.getRaster().getDataBuffer()).getData() to get the pixel data. getRBG is more convenient, but is typically slower than getting the pixel array
getRGB packs the pixel data into an int, getData will return the RGB(A) in each entry of the array (R = n; G = n+1; B=n+2(, A=n+3)), so will need to process this yourself
You can use java.awt.Color, which allows you to access the RGB values of the color, pack it as a int value or convert an int to a Color
Color color = new Color(bufferedImage.getRGB(0, 0), true);
int redColor = Color.RED.getRGB();
The answer to this question provides an example of dealing with the byte[] pixel data
Basically, you will need to loop over the data, comparing the values in the image to the value you are after, either directly (comparing the red, green and blue values) or indirectly, comparing the packed int or Color values.
Personally, I'd grab the pixel data, convert each element to an int and compare it with a previously packed int from a Color object, this creates the less number of short lived objects and should be reasonably efficient
You can take a look at this answer which use getRGB to get the red, green, blue values from a given pixel
Here's something I wrote a while ago using the Robot class. It returns an array of the screen wherever the screen is white, it was not very computationally expensive for my application, but I found probing the values individually using robot was. At first I didn't even read your question, but looking back, I think this will help you A LOT. Good luck. And then I saw the original post date...
public boolean[][] raster() throws AWTException, IOException{
boolean[][] filled= new boolean[720][480];
BufferedImage image = new Robot().createScreenCapture(new Rectangle(0,0,720,480));
//accepts (xCoord,yCoord, width, height) of screen
for (int n =0; n<720; n++){
for (int m=0; m<480; m++){
if(new Color(image.getRGB(n, m)).getRed()<254){
//can check any rgb value, I just chose red in this case to check for white pixels
filled[n][m]=true;
}
}
}
return filled;
}
This is my situation. It involves aligning a scanned image which will account for incorrect scanning. I must align the scanned image with my Java program.
These are more details:
There is a table-like form printed on a sheet of paper, which will be scanned into an image file.
I will open the picture with Java, and I will have an OVERLAY of text boxes.
The text boxes are supposed to align correctly with the scanned image.
In order to align correctly, my Java program must analyze the scanned image and detect the coordinates of the edges of the table on the scanned image, and thus position the image and the textboxes so that the textboxes and the image both align properly (in case of incorrect scanning)
You see, the guy scanning the image might not necessarily place the image in a perfectly correct position, so I need my program to automatically align the scanned image as it loads it. This program will be reusable on many of such scanned images, so I need the program to be flexible in this way.
My question is one of the following:
How can I use Java to detect the y coordinate of the upper edge of the table and the x-coordinate of the leftmost edge of the table. The table is a a regular table with many cells, with black thin border, printed on a white sheet of paper (horizontal printout)
If an easier method exists to automatically align the scanned image in such a way that all scanned images will have the graphical table align to the same x, y coordinates, then share this method :).
If you don't know the answer to the above to questions, do tell me where I should start. I don't know much about graphics java programming and I have about 1 month to finish this program. Just assume that I have a tight schedule and I have to make the graphics part as simple as possible for me.
Cheers and thank you.
Try to start from a simple scenario and then improve the approach.
Detect corners.
Find the corners in the boundaries of the form.
Using the form corners coordinates, calculate the rotation angle.
Rotate/scale the image.
Map the position of each field in the form relative to form origin coordinates.
Match the textboxes.
The program presented at the end of this post does the steps 1 to 3. It was implemented using Marvin Framework. The image below shows the output image with the detected corners.
The program also outputs: Rotation angle:1.6365770416167182
Source code:
import java.awt.Color;
import java.awt.Point;
import marvin.image.MarvinImage;
import marvin.io.MarvinImageIO;
import marvin.plugin.MarvinImagePlugin;
import marvin.util.MarvinAttributes;
import marvin.util.MarvinPluginLoader;
public class FormCorners {
public FormCorners(){
// Load plug-in
MarvinImagePlugin moravec = MarvinPluginLoader.loadImagePlugin("org.marvinproject.image.corner.moravec");
MarvinAttributes attr = new MarvinAttributes();
// Load image
MarvinImage image = MarvinImageIO.loadImage("./res/printedForm.jpg");
// Process and save output image
moravec.setAttribute("threshold", 2000);
moravec.process(image, null, attr);
Point[] boundaries = boundaries(attr);
image = showCorners(image, boundaries, 12);
MarvinImageIO.saveImage(image, "./res/printedForm_output.jpg");
// Print rotation angle
double angle = (Math.atan2((boundaries[1].y*-1)-(boundaries[0].y*-1),boundaries[1].x-boundaries[0].x) * 180 / Math.PI);
angle = angle >= 0 ? angle : angle + 360;
System.out.println("Rotation angle:"+angle);
}
private Point[] boundaries(MarvinAttributes attr){
Point upLeft = new Point(-1,-1);
Point upRight = new Point(-1,-1);
Point bottomLeft = new Point(-1,-1);
Point bottomRight = new Point(-1,-1);
double ulDistance=9999,blDistance=9999,urDistance=9999,brDistance=9999;
double tempDistance=-1;
int[][] cornernessMap = (int[][]) attr.get("cornernessMap");
for(int x=0; x<cornernessMap.length; x++){
for(int y=0; y<cornernessMap[0].length; y++){
if(cornernessMap[x][y] > 0){
if((tempDistance = Point.distance(x, y, 0, 0)) < ulDistance){
upLeft.x = x; upLeft.y = y;
ulDistance = tempDistance;
}
if((tempDistance = Point.distance(x, y, cornernessMap.length, 0)) < urDistance){
upRight.x = x; upRight.y = y;
urDistance = tempDistance;
}
if((tempDistance = Point.distance(x, y, 0, cornernessMap[0].length)) < blDistance){
bottomLeft.x = x; bottomLeft.y = y;
blDistance = tempDistance;
}
if((tempDistance = Point.distance(x, y, cornernessMap.length, cornernessMap[0].length)) < brDistance){
bottomRight.x = x; bottomRight.y = y;
brDistance = tempDistance;
}
}
}
}
return new Point[]{upLeft, upRight, bottomRight, bottomLeft};
}
private MarvinImage showCorners(MarvinImage image, Point[] points, int rectSize){
MarvinImage ret = image.clone();
for(Point p:points){
ret.fillRect(p.x-(rectSize/2), p.y-(rectSize/2), rectSize, rectSize, Color.red);
}
return ret;
}
public static void main(String[] args) {
new FormCorners();
}
}
Edge detection is something that is typically done by enhancing the contrast between neighboring pixels, such that you get a easily detectable line, which is suitable for further processing.
To do this, a "kernel" transforms a pixel according it the pixel's inital value, and the value of that pixel's neighbors. A good edge detection kernel will enhance the differences between neighboring pixels, and reduce the strength of a pixel with similar neigbors.
I would start by looking at the Sobel operator. This might not return results that are immediately useful to you; however, it will get you far closer than you would be if you were to approach the problem with little knowledge of the field.
After you have some crisp clean edges, you can use larger kernels to detect points where it seems that a 90% bend in two lines occurs, that might give you the pixel coordinates of the outer rectangle, which might be enough for your purposes.
With those outer coordinates, it still is a bit of math to make the new pixels be composted with the average values between the old pixels rotated and moved to "match". The results (especially if you do not know about anti-aliasing math) can be pretty bad, adding blur to the image.
Sharpening filters might be a solution, but they come with their own issues, mainly they make the picture sharper by adding graininess. Too much, and it is obvious that the original image is not a high-quality scan.
I researched the libraries but in the end I found it more convenient to code up my own edge detection methods.
The class below will detect black/grayed out edges of a scanned sheet of paper that contains such edges, and will return the x and y coordinate of the edges of the sheet of paper, starting from the rightmost end (reverse = true) or from lower end (reverse = true) or from the top edge (reverse = false) or from left edge (reverse = false). Also...the program will take ranges along vertical edges (rangex) measured in pixels, and horizontal ranges (rangey) measured in pixels. The ranges determine outliers in the points received.
The program does 4 vertical cuts using the specified arrays, and 4 horizontal cuts. It retrieves the values of the dark dots. It uses the ranges to eliminate outliers. Sometimes, a little spot on the paper may cause an outlier point. The smaller the range, the fewer the outliers. However, sometimes the edge is slightly tilted, so you don't want to make the range too small.
Have fun. It works perfectly for me.
import java.awt.image.BufferedImage;
import java.awt.Color;
import java.util.ArrayList;
import java.lang.Math;
import java.awt.Point;
public class EdgeDetection {
public App ap;
public int[] horizontalCuts = {120, 220, 320, 420};
public int[] verticalCuts = {300, 350, 375, 400};
public void printEdgesTest(BufferedImage image, boolean reversex, boolean reversey, int rangex, int rangey){
int[] mx = horizontalCuts;
int[] my = verticalCuts;
//you are getting edge points here
//the "true" parameter indicates that it performs a cut starting at 0. (left edge)
int[] xEdges = getEdges(image, mx, reversex, true);
int edgex = getEdge(xEdges, rangex);
for(int x = 0; x < xEdges.length; x++){
System.out.println("EDGE = " + xEdges[x]);
}
System.out.println("THE EDGE = " + edgex);
//the "false" parameter indicates you are doing your cut starting at the end (image.getHeight)
//and ending at 0
//if the parameter was true, it would mean it would start the cuts at y = 0
int[] yEdges = getEdges(image, my, reversey, false);
int edgey = getEdge(yEdges, rangey);
for(int y = 0; y < yEdges.length; y++){
System.out.println("EDGE = " + yEdges[y]);
}
System.out.println("THE EDGE = " + edgey);
}
//This function takes an array of coordinates...detects outliers,
//and computes the average of non-outlier points.
public int getEdge(int[] edges, int range){
ArrayList<Integer> result = new ArrayList<Integer>();
boolean[] passes = new boolean[edges.length];
int[][] differences = new int[edges.length][edges.length-1];
//THIS CODE SEGMENT SAVES THE DIFFERENCES BETWEEN THE POINTS INTO AN ARRAY
for(int n = 0; n<edges.length; n++){
for(int m = 0; m<edges.length; m++){
if(m < n){
differences[n][m] = edges[n] - edges[m];
}else if(m > n){
differences[n][m-1] = edges[n] - edges[m];
}
}
}
//This array determines which points are outliers or nots (fall within range of other points)
for(int n = 0; n<edges.length; n++){
passes[n] = false;
for(int m = 0; m<edges.length-1; m++){
if(Math.abs(differences[n][m]) < range){
passes[n] = true;
System.out.println("EDGECHECK = TRUE" + n);
break;
}
}
}
//Create a new array only using valid points
for(int i = 0; i<edges.length; i++){
if(passes[i]){
result.add(edges[i]);
}
}
//Calculate the rounded mean... This will be the x/y coordinate of the edge
//Whether they are x or y values depends on the "reverse" variable used to calculate the edges array
int divisor = result.size();
int addend = 0;
double mean = 0;
for(Integer i : result){
addend += i;
}
mean = (double)addend/(double)divisor;
//returns the mean of the valid points: this is the x or y coordinate of your calculated edge.
if(mean - (int)mean >= .5){
System.out.println("MEAN " + mean);
return (int)mean+1;
}else{
System.out.println("MEAN " + mean);
return (int)mean;
}
}
//this function computes "dark" points, which include light gray, to detect edges.
//reverse - when true, starts counting from x = 0 or y = 0, and ends at image.getWidth or image.getHeight()
//verticalEdge - determines whether you want to detect a vertical edge, or a horizontal edge
//arr[] - determines the coordinates of the vertical or horizontal cuts you will do
//set the arr[] array according to the graphical layout of your scanned image
//image - this is the image you want to detect black/white edges of
public int[] getEdges(BufferedImage image, int[] arr, boolean reverse, boolean verticalEdge){
int red = 255;
int green = 255;
int blue = 255;
int[] result = new int[arr.length];
for(int n = 0; n<arr.length; n++){
for(int m = reverse ? (verticalEdge ? image.getWidth():image.getHeight())-1:0; reverse ? m>=0:m<(verticalEdge ? image.getWidth():image.getHeight());){
Color c = new Color(image.getRGB(verticalEdge ? m:arr[n], verticalEdge ? arr[n]:m));
red = c.getRed();
green = c.getGreen();
blue = c.getBlue();
//determine if the point is considered "dark" or not.
//modify the range if you want to only include really dark spots.
//occasionally, though, the edge might be blurred out, and light gray helps
if(red<239 && green<239 && blue<239){
result[n] = m;
break;
}
//count forwards or backwards depending on reverse variable
if(reverse){
m--;
}else{
m++;
}
}
}
return result;
}
}
A similar such problem I've done in the past basically figured out the orientation of the form, re-aligned it, re-scaled it, and I was all set. You can use the Hough transform to to detect the angular offset of the image (ie: how much it is rotated), but you still need to detect the boundaries of the form. It also had to accommodate for the boundaries of the piece of paper itself.
This was a lucky break for me, because it basically showed a black and white image in the middle of a big black border.
Apply an aggressive, 5x5 median filter to remove some noise.
Convert from grayscale to black and white (rescale intensity values from [0,255] to [0,1]).
Calculate the Principal Component Analysis (ie: calculate the Eigenvectors of the covariance matrix for your image from the calculated Eigenvalues) (http://en.wikipedia.org/wiki/Principal_component_analysis#Derivation_of_PCA_using_the_covariance_method)
4) This gives you a basis vector. You simply use that to re-orient your image to a standard basis matrix (ie: [1,0],[0,1]).
Your image is now aligned beautifully. I did this for normalizing the orientation of MRI scans of entire human brains.
You also know that you have a massive black border around the actual image. You simply keep deleting rows from the top and bottom, and both sides of the image until they are all gone. You can temporarily apply a 7x7 median or mode filter to a copy of the image so far at this point. It helps rule out too much border remaining in the final image from thumbprints, dirt, etc.