tilt.image.Picture.java Source code

Java tutorial

Introduction

Here is the source code for tilt.image.Picture.java

Source

/*
 * This file is part of TILT.
 *
 *  TILT 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 2 of the License, or
 *  (at your option) any later version.
 *
 *  TILT 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 TILT.  If not, see <http://www.gnu.org/licenses/>.
 *  (c) copyright Desmond Schmidt 2014
 */

package tilt.image;

import java.net.URL;
import java.net.InetAddress;
import tilt.exception.ImageException;
import org.json.simple.*;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.File;
import tilt.exception.TiltException;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.Channels;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.Graphics2D;
import java.awt.Color;

import java.util.Iterator;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;

import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster;
import tilt.image.page.Page;
import tilt.handler.TextIndex;
import tilt.align.Matchup;
import tilt.image.page.Word;
import tilt.handler.Options;

/**
 * Handle everything related to the abstract image in all its forms
 * @author desmond
 */
public class Picture {
    private final static String PNG_TYPE = "PNG";
    String id;
    InetAddress poster;
    float ppAverage;
    Page page;
    File orig;
    File greyscale;
    File twotone;
    File cleaned;
    File baselines;
    File reduced;
    File blurred;
    File words;
    int blur;
    Double[][] coords;
    TextIndex text;
    boolean linked;
    Options options;

    /**
     * Create a picture. Pictures stores links to the various image files.
     * @param options options from the geoJSON file
     * @param text the text to align with
     * @param poster the ipaddress of the poster of the image (DDoS prevention)
     * @throws TiltException 
     */
    public Picture(Options options, TextIndex text, InetAddress poster) throws TiltException {
        try {
            URL url = new URL(options.url);
            // use url as id for now
            id = options.url;
            this.text = text;
            this.poster = poster;
            // try to register the picture
            PictureRegistry.register(this, options.url);
            // fetch picture from url
            ReadableByteChannel rbc = Channels.newChannel(url.openStream());
            orig = File.createTempFile(PictureRegistry.PREFIX, PictureRegistry.SUFFIX);
            FileOutputStream fos = new FileOutputStream(orig);
            fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
            fos.close();
            String mimeType = getFormatName();
            if (!mimeType.equals(PNG_TYPE))
                convertToPng();
            this.options = options;
            this.coords = new Double[4][2];
            for (int i = 0; i < 4; i++) {
                JSONArray vector = (JSONArray) options.coords.get(i);
                for (int j = 0; j < 2; j++) {
                    this.coords[i][j] = (Double) vector.get(j);
                }
            }
        } catch (Exception e) {
            throw new TiltException(e);
        }
    }

    /**
     * Ensure that all files are deleted when we go
     * @throws ImageException 
     */
    public void dispose() throws ImageException {
        try {
            if (orig != null)
                orig.delete();
            if (greyscale != null)
                greyscale.delete();
            if (twotone != null)
                twotone.delete();
            if (baselines != null)
                baselines.delete();
            if (words != null)
                words.delete();
            // dispose of other temporary files here
        } catch (Exception e) {
            throw new ImageException(e);
        }
    }

    /**
     * Do the coordinates cover the entire picture or only part of it?
     * @return true if the coords cover the entire picture area
     */
    boolean isWholePicture() {
        boolean whole = true;
        // top-left
        if (coords[0][0].doubleValue() > 0.0)
            whole = false;
        if (coords[0][1].doubleValue() > 0.0)
            whole = false;
        // top-right
        if (coords[1][0].doubleValue() < 100.0)
            whole = false;
        if (coords[1][1].doubleValue() > 0.0)
            whole = false;
        // bot-right
        if (coords[2][0].doubleValue() < 100.0)
            whole = false;
        if (coords[2][1].doubleValue() < 100.0)
            whole = false;
        // bot-left
        if (coords[3][0].doubleValue() > 0.0)
            whole = false;
        if (coords[3][1].doubleValue() < 100.0)
            whole = false;
        return whole;
    }

    /**
     * Crop the original if needed
     */
    Rectangle getCropRect() throws IOException {
        int x, y, width, height;
        BufferedImage bi = ImageIO.read(orig);
        if (!isWholePicture()) {
            x = (int) Math.round(bi.getWidth() * coords[0][0].doubleValue() / 100.0);
            y = (int) Math.round(bi.getHeight() * coords[0][1].doubleValue() / 100.0);
            width = (int) Math
                    .round(bi.getWidth() * (coords[1][0].doubleValue() - coords[0][0].doubleValue()) / 100.0);
            height = (int) Math
                    .round(bi.getHeight() * (coords[3][1].doubleValue() - coords[0][1].doubleValue()) / 100.0);
        } else {
            x = 0;
            y = 0;
            width = bi.getWidth();
            height = bi.getHeight();
        }
        return new Rectangle(x, y, width, height);

    }

    /**
     * Get the original Picture data
     * @return the raw picture byte array
     */
    byte[] getOrig() throws ImageException {
        try {
            FileInputStream fis = new FileInputStream(orig);
            byte[] data = new byte[(int) orig.length()];
            fis.read(data);
            fis.close();
            return data;
        } catch (IOException ioe) {
            throw new ImageException(ioe);
        }
    }

    /**
     * Get the format of the picture
     * @return a mime type
     * @throws ImageException 
     */
    final String getFormatName() throws ImageException {
        try {
            ImageInputStream iis = ImageIO.createImageInputStream(orig);
            Iterator<ImageReader> iter = ImageIO.getImageReaders(iis);
            if (!iter.hasNext()) {
                throw new RuntimeException("No readers found for " + id);
            }
            ImageReader reader = iter.next();
            iis.close();
            return reader.getFormatName();
        } catch (Exception e) {
            throw new ImageException(e);
        }
    }

    /**
     * Convert the input file from anything to png
     * @throws ImageException 
     */
    final void convertToPng() throws ImageException {
        try {
            BufferedImage bufferedImage = ImageIO.read(orig);
            File newFile = File.createTempFile(PictureRegistry.PREFIX, PictureRegistry.SUFFIX);
            ImageIO.write(bufferedImage, "png", newFile);
            orig.delete();
            orig = newFile;
        } catch (Exception e) {
            throw new ImageException(e);
        }
    }

    int getPropVal(Double prop, int value) {
        return (int) Math.round(prop.doubleValue() * value / 100.0);
    }

    /**
     * Convert from the original png file to greyscale png. Save original.
     * @throws ImageException 
     */
    void convertToGreyscale() throws ImageException {
        try {
            BufferedImage png = ImageIO.read(orig);
            BufferedImage grey = new BufferedImage(png.getWidth(), png.getHeight(), BufferedImage.TYPE_BYTE_GRAY);
            Graphics g = grey.getGraphics();
            g.drawImage(png, 0, 0, null);
            g.dispose();
            if (!isWholePicture()) {
                // clear excluded regions
                Graphics2D g2d = grey.createGraphics();
                g2d.setColor(Color.white);
                if (coords[0][0].doubleValue() > 0.0) {
                    int w = grey.getWidth();
                    int h = grey.getHeight();
                    g2d.fillRect(0, 0, getPropVal(coords[0][0], w), h);
                }
                if (coords[1][0].doubleValue() < 100.0) {
                    int x = getPropVal(coords[1][0], grey.getWidth());
                    g2d.fillRect(x, 0, grey.getWidth() - x, grey.getHeight());
                }
                if (coords[2][1].doubleValue() < 100.0) {
                    int y = getPropVal(coords[2][1], grey.getHeight());
                    g2d.fillRect(0, y, grey.getWidth(), grey.getHeight() - y);
                }
                if (coords[0][1].doubleValue() > 0.0) {
                    int y = getPropVal(coords[0][1], grey.getHeight());
                    g2d.fillRect(0, 0, grey.getWidth(), y);
                }
                g2d.dispose();
            }
            greyscale = File.createTempFile(PictureRegistry.PREFIX, PictureRegistry.SUFFIX);
            ImageIO.write(grey, "png", greyscale);
        } catch (Exception e) {
            throw new ImageException(e);
        }
    }

    /**
     * Convert from greyscale to twotone (black and white)
     * Adapted from OCRopus
     * Copyright 2006-2008 Deutsches Forschungszentrum fuer Kuenstliche 
     * Intelligenz or its licensors, as applicable.
     * http://ocropus.googlecode.com/svn/trunk/ocr-binarize/ocr-binarize-sauvola.cc
     * @throws Exception 
     */
    void convertToTwoTone() throws ImageException {
        try {
            int MAXVAL = 256;
            double k = 0.34;
            if (greyscale == null)
                convertToGreyscale();
            BufferedImage grey = ImageIO.read(greyscale);
            WritableRaster grey_image = grey.getRaster();
            WritableRaster bin_image = grey.copyData(null);
            int square = (int) Math.floor(grey_image.getWidth() * 0.025);
            if (square == 0)
                square = Math.min(20, grey_image.getWidth());
            if (square > grey_image.getHeight())
                square = grey_image.getHeight();
            int whalf = square >> 1;
            if (whalf == 0)
                throw new Exception("whalf==0!");
            int image_width = grey_image.getWidth();
            int image_height = grey_image.getHeight();
            // Calculate the integral image, and integral of the squared image
            // original algorithm ate up too much memory, use floats for longs
            float[][] integral_image = new float[image_width][image_height];
            float[][] rowsum_image = new float[image_width][image_height];
            float[][] integral_sqimg = new float[image_width][image_height];
            float[][] rowsum_sqimg = new float[image_width][image_height];
            int xmin, ymin, xmax, ymax;
            double diagsum, idiagsum, diff, sqdiagsum, sqidiagsum, sqdiff, area;
            double mean, std, threshold;
            // for get/setPixel
            int[] iArray = new int[1];
            int[] oArray = new int[1];
            for (int j = 0; j < image_height; j++) {
                grey_image.getPixel(0, j, iArray);
                rowsum_image[0][j] = iArray[0];
                rowsum_sqimg[0][j] = iArray[0] * iArray[0];
            }
            for (int i = 1; i < image_width; i++) {
                for (int j = 0; j < image_height; j++) {
                    grey_image.getPixel(i, j, iArray);
                    rowsum_image[i][j] = rowsum_image[i - 1][j] + iArray[0];
                    rowsum_sqimg[i][j] = rowsum_sqimg[i - 1][j] + iArray[0] * iArray[0];
                }
            }
            for (int i = 0; i < image_width; i++) {
                integral_image[i][0] = rowsum_image[i][0];
                integral_sqimg[i][0] = rowsum_sqimg[i][0];
            }
            for (int i = 0; i < image_width; i++) {
                for (int j = 1; j < image_height; j++) {
                    integral_image[i][j] = integral_image[i][j - 1] + rowsum_image[i][j];
                    integral_sqimg[i][j] = integral_sqimg[i][j - 1] + rowsum_sqimg[i][j];
                }
            }
            // compute mean and std.dev. using the integral image
            for (int i = 0; i < image_width; i++) {
                for (int j = 0; j < image_height; j++) {
                    xmin = Math.max(0, i - whalf);
                    ymin = Math.max(0, j - whalf);
                    xmax = Math.min(image_width - 1, i + whalf);
                    ymax = Math.min(image_height - 1, j + whalf);
                    area = (xmax - xmin + 1) * (ymax - ymin + 1);
                    grey_image.getPixel(i, j, iArray);
                    // area can't be 0 here
                    if (area == 0)
                        throw new Exception("area can't be 0 here!");
                    if (xmin == 0 && ymin == 0) {
                        // Point at origin
                        diff = integral_image[xmax][ymax];
                        sqdiff = integral_sqimg[xmax][ymax];
                    } else if (xmin == 0 && ymin != 0) {
                        // first column
                        diff = integral_image[xmax][ymax] - integral_image[xmax][ymin - 1];
                        sqdiff = integral_sqimg[xmax][ymax] - integral_sqimg[xmax][ymin - 1];
                    } else if (xmin != 0 && ymin == 0) {
                        // first row
                        diff = integral_image[xmax][ymax] - integral_image[xmin - 1][ymax];
                        sqdiff = integral_sqimg[xmax][ymax] - integral_sqimg[xmin - 1][ymax];
                    } else {
                        // rest of the image
                        diagsum = integral_image[xmax][ymax] + integral_image[xmin - 1][ymin - 1];
                        idiagsum = integral_image[xmax][ymin - 1] + integral_image[xmin - 1][ymax];
                        diff = diagsum - idiagsum;
                        sqdiagsum = integral_sqimg[xmax][ymax] + integral_sqimg[xmin - 1][ymin - 1];
                        sqidiagsum = integral_sqimg[xmax][ymin - 1] + integral_sqimg[xmin - 1][ymax];
                        sqdiff = sqdiagsum - sqidiagsum;
                    }
                    mean = diff / area;
                    std = Math.sqrt((sqdiff - diff * diff / area) / (area - 1));
                    threshold = mean * (1 + k * ((std / 128) - 1));
                    if (iArray[0] < threshold)
                        oArray[0] = 0;
                    else
                        oArray[0] = MAXVAL - 1;
                    bin_image.setPixel(i, j, oArray);
                }
            }
            twotone = File.createTempFile(PictureRegistry.PREFIX, PictureRegistry.SUFFIX);
            grey.setData(bin_image);
            ImageIO.write(grey, "png", twotone);
        } catch (Exception e) {
            throw new ImageException(e);
        }
    }

    /**
     * Convert to cleaned from twotone
     * @throws Exception 
     */
    void convertToCleaned() throws ImageException {
        try {
            if (twotone == null)
                convertToTwoTone();
            BufferedImage tt = ImageIO.read(twotone);
            RemoveNoise rn = new RemoveNoise(tt, options);
            rn.clean();
            cleaned = File.createTempFile(PictureRegistry.PREFIX, PictureRegistry.SUFFIX);
            ImageIO.write(tt, "png", cleaned);
        } catch (Exception e) {
            throw new ImageException(e);
        }
    }

    /**
     * Convert to cleaned from twotone
     * @throws Exception 
     */
    void convertToBlurred() throws ImageException {
        try {
            if (cleaned == null)
                convertToCleaned();
            BufferedImage tt = ImageIO.read(cleaned);
            BlurImage bi = new BlurImage(tt, options.blur);
            BufferedImage out = bi.blur();
            blurred = File.createTempFile(PictureRegistry.PREFIX, PictureRegistry.SUFFIX);
            ImageIO.write(out, "png", blurred);
        } catch (Exception e) {
            throw new ImageException(e);
        }
    }

    /**
     * Convert to show lines from cleaned
     * @throws ImageException 
     */
    void convertToBaselines() throws ImageException {
        try {
            if (blurred == null)
                convertToBlurred();
            BufferedImage withLines = ImageIO.read(blurred);
            FindLinesBlurred fl = new FindLinesBlurred(withLines, text.numWords(), options);
            page = fl.getPage();
            ppAverage = fl.getPPAverage();
            baselines = File.createTempFile(PictureRegistry.PREFIX, PictureRegistry.SUFFIX);
            ImageIO.write(withLines, "png", baselines);
        } catch (Exception e) {
            throw new ImageException(e);
        }
    }

    /**
     * Convert to reduced lines from baselines
     * @throws ImageException 
     */
    void convertToReduced() throws ImageException {
        try {
            if (baselines == null)
                convertToBaselines();
            BufferedImage reducedLines = ImageIO.read(cleaned);
            ReduceLines rl = new ReduceLines(reducedLines, page, options);
            reduced = File.createTempFile(PictureRegistry.PREFIX, PictureRegistry.SUFFIX);
            ImageIO.write(reducedLines, "png", reduced);
        } catch (Exception e) {
            throw new ImageException(e);
        }

    }

    /**
     * Convert to show identified words
     * @throws ImageException 
     */
    void convertToWords() throws ImageException {
        try {
            if (baselines == null)
                convertToBaselines();
            BufferedImage bandw = ImageIO.read(cleaned);
            BufferedImage originalImage = ImageIO.read(orig);
            FindWords fw = new FindWords(bandw, page, options);
            page.print(originalImage);
            words = File.createTempFile(PictureRegistry.PREFIX, PictureRegistry.SUFFIX);
            ImageIO.write(originalImage, "png", words);
        } catch (Exception e) {
            throw new ImageException(e);
        }
    }

    /**
     * Generate the text to image links
     * @throws ImageException 
     */
    void convertToLinks() throws ImageException {
        if (this.linked) {
            page.resetShapes();
            words = null;
        }
        if (words == null)
            convertToWords();
        float ppc = page.pixelsPerChar(text.numChars());
        int[] shapeWidths = page.getShapeWidths();
        int[] wordWidths = text.getWordWidths(ppc);
        Matchup m = new Matchup(wordWidths, shapeWidths);
        try {
            int[][][] alignments = m.align();
            int[] shapeOffsets = page.getShapeLineStarts();
            Word[] wordObjs = text.getWords(ppc);
            BufferedImage clean = ImageIO.read(cleaned);
            page.align(alignments, shapeOffsets, wordObjs, clean.getRaster());
            this.linked = true;
        } catch (Exception e) {
            throw new ImageException(e);
        }
    }

    /**
     * Retrieve the data of a picture file
     * @return a byte array
     * @throws ImageException 
     */
    private byte[] getPicData(File src) throws ImageException {
        byte[] data = new byte[(int) src.length()];
        try {
            FileInputStream fis = new FileInputStream(src);
            fis.read(data);
            return data;
        } catch (Exception e) {
            throw new ImageException(e);
        }
    }

    /**
     * Read the original image 
     * @return the image as a byte array
     */
    public byte[] getOrigData() throws ImageException {
        return getPicData(orig);
    }

    public byte[] getBlurredData() throws ImageException {
        if (blurred == null)
            convertToBlurred();
        return getPicData(blurred);
    }

    /**
     * Read the cleaned image 
     * @return the image as a byte array
     */
    public byte[] getCleanedData() throws ImageException {
        if (cleaned == null)
            convertToCleaned();
        return getPicData(cleaned);
    }

    /**
     * Get the greyscale version of the data
     * @return a byte array being the greyscale rendition of it
     * @throws ImageException 
     */
    public byte[] getGreyscaleData() throws ImageException {
        if (greyscale == null)
            convertToGreyscale();
        return getPicData(greyscale);
    }

    /**
     * Get a twotone representation of the original
     * @return a byte array (at 256 bpp)
     * @throws ImageException 
     */
    public byte[] getTwoToneData() throws ImageException {
        if (twotone == null)
            convertToTwoTone();
        return getPicData(twotone);
    }

    /**
     * Get a baselines representation of the original
     * @return a byte array (at 256 bpp)
     * @throws ImageException 
     */
    public byte[] getBaselinesData() throws ImageException {
        if (baselines == null)
            convertToBaselines();
        return getPicData(baselines);
    }

    /**
     * Get a reduced baselines representation of the original
     * @return a byte array (at 256 bpp)
     * @throws ImageException 
     */
    public byte[] getReducedData() throws ImageException {
        if (reduced == null)
            convertToReduced();
        return getPicData(reduced);
    }

    /**
     * Get a baselines representation of the original
     * @return a byte array (at 256 bpp)
     * @throws ImageException 
     */
    public byte[] getWordsData() throws ImageException {
        if (words == null)
            convertToWords();
        return getPicData(words);
    }

    /**
     * Get the GeoJson shapes data
     * @return a GeoJson string
     */
    public String getGeoJson() throws ImageException {
        try {
            // do this always, because the user will want it redone
            convertToLinks();
            BufferedImage image = ImageIO.read(words);
            double hScale = (coords[2][0].doubleValue() - coords[0][0].doubleValue()) / 100.0;
            double vScale = (coords[3][1].doubleValue() - coords[1][1].doubleValue()) / 100.0;
            //            return page.toGeoJson( (int)Math.round(hScale*image.getWidth()), 
            //                (int)Math.round(vScale*image.getHeight()) );
            return page.toGeoJson(image.getWidth(), image.getHeight());
        } catch (Exception e) {
            throw new ImageException(e);
        }
    }
}