de.hu_berlin.informatik.spws2014.mapever.entzerrung.EntzerrungsView.java Source code

Java tutorial

Introduction

Here is the source code for de.hu_berlin.informatik.spws2014.mapever.entzerrung.EntzerrungsView.java

Source

/* Copyright (C) 2014,2015 Jan Mller, Bjrn Stelter
 *
 * This program 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.
 *
 * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>
 */

package de.hu_berlin.informatik.spws2014.mapever.entzerrung;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.PointF;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.ImageButton;

import org.opencv.android.Utils;
import org.opencv.core.CvException;
import org.opencv.core.Mat;
import org.opencv.imgproc.Imgproc;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Comparator;

import de.hu_berlin.informatik.spws2014.mapever.R;
import de.hu_berlin.informatik.spws2014.mapever.largeimageview.LargeImageView;

public class EntzerrungsView extends LargeImageView {

    // ////// KEYS FR ZU SPEICHERNDE DATEN IM SAVEDINSTANCESTATE-BUNDLE

    private static final String SAVEDSHOWCORNERS = "SAVEDSHOWCORNERS";
    private static final String SAVEDCORNERS = "SAVEDCORNERS";
    private static final String SAVEDIMAGETYPESUPPORT = "SAVEDIMAGETYPESUPPORT";

    // ////// OTHER CONSTANTS
    private static final int PICTURE_TRANSPARENT = 200;
    private static final int PICTURE_OPAQUE = 255;

    // Count of rectangle corners
    private static final int CORNERS_COUNT = 4;

    // Maximum size for bitmap scaled for corner detection algorithm
    private static final int CDALG_MAX_WIDTH = 300;
    private static final int CDALG_MAX_HEIGHT = 300;

    // ////// PRIVATE MEMBERS

    // Activity context
    private Entzerren entzerren;

    // InputStream zum Bild
    private File imageFile;
    private boolean imageTypeSupportsDeskew = true;
    private boolean openCVLoadError = true;

    // Eckpunkte als OverlayIcons
    private CornerIcon[] corners = new CornerIcon[CORNERS_COUNT];

    // Zustandsvariablen
    private boolean show_corners = true;
    private boolean punkte_gesetzt = false;

    // Some objects for onDraw
    private Paint white = new Paint();
    private Path wallpath = new Path();

    // ////////////////////////////////////////////////////////////////////////
    // //////////// CONSTRUCTORS AND INITIALIZATION
    // ////////////////////////////////////////////////////////////////////////

    public EntzerrungsView(Context context) {
        super(context);
        init();
    }

    public EntzerrungsView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public EntzerrungsView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    /**
     * Initializes EntzerrungsView (called by constructor), sets background color, image transparency and creates
     * corner icons (each at 0,0).
     */
    private void init() {
        // nichts weiter tun, wenn die View in Eclipse's GUI-Editor angezeigt wird
        if (this.isInEditMode())
            return;

        entzerren = (Entzerren) this.getContext();

        // Set background of view to black
        this.setBackgroundColor(Color.BLACK);

        // Set transparency of LIV image
        setForegroundAlpha(PICTURE_TRANSPARENT);

        // Paint for background: white square to highlight selected part of the map
        white.setColor(Color.WHITE);
        white.setStyle(Style.FILL);

        // Initialize corner icons
        corners[0] = new CornerIcon(this, new Point(0, 0));
        corners[1] = new CornerIcon(this, new Point(0, 0));
        corners[2] = new CornerIcon(this, new Point(0, 0));
        corners[3] = new CornerIcon(this, new Point(0, 0));
    }

    @Override
    protected Parcelable onSaveInstanceState() {
        // LargeImageView gibt uns ein Bundle, in dem z.B. die Pan-Daten stecken. Verwende dieses als Basis.
        Bundle bundle = (Bundle) super.onSaveInstanceState();

        // Speichere: "sollen die Ecken angezeigt (= das Bild entzerrt) werden?"
        bundle.putBoolean(SAVEDSHOWCORNERS, show_corners);

        // Speichere Positionen der 4 Eckpunkte
        Point[] cornerPoints = new Point[CORNERS_COUNT];
        for (int i = 0; i < CORNERS_COUNT; i++) {
            cornerPoints[i] = corners[i].getPosition();
        }

        bundle.putSerializable(SAVEDCORNERS, cornerPoints);

        // Speichere, ob Dateityp von den Algorithmen untersttzt wird (GIF z.B. nicht)
        bundle.putBoolean(SAVEDIMAGETYPESUPPORT, imageTypeSupportsDeskew);

        return bundle;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        Bundle bundle = (Bundle) state;

        // Lade: "sollen die Ecken angezeigt (= das Bild entzerrt) werden?"
        boolean _show_corners = bundle.getBoolean(SAVEDSHOWCORNERS);
        showCorners(_show_corners);

        // Lade Positionen der 4 Eckpunkte
        Point[] cornerPoints = (Point[]) bundle.getSerializable(SAVEDCORNERS);
        for (int i = 0; i < CORNERS_COUNT; i++) {
            corners[i].setPosition(cornerPoints[i]);
        }
        punkte_gesetzt = true;

        // Lade, ob Dateityp von den Algorithmen untersttzt wird (GIF z.B. nicht)
        imageTypeSupportsDeskew = bundle.getBoolean(SAVEDIMAGETYPESUPPORT);

        // Im Bundle stecken noch Informationen von LargeImageView, z.B. Pan-Daten. Reiche das Bundle also weiter.
        super.onRestoreInstanceState(bundle); // calls update()
    }

    /**
     * Ldt Bild in die EntzerrungsView. (Benutze dies anstelle von setImage...().)
     */
    public void loadImage(File _imageFile) throws FileNotFoundException {
        // Image-File merken
        imageFile = _imageFile;

        // Bild laden
        setImageFilename(imageFile.getAbsolutePath());
    }

    /**
     * Returns true, if image type is (hopefully?) supported by the deskewing algorithm (not for GIF, for example).
     */
    public boolean isImageTypeSupported() {
        return imageTypeSupportsDeskew;
    }

    /**
     * Returns true, if we failed to load OpenCV
     */
    public boolean isOpenCVLoadError() {
        return openCVLoadError;
    }

    /**
     * Erzeugt eine Bitmap aus dem geladenen Bild mit einer angegebenen SampleSize.
     */
    public Bitmap getSampledBitmap(int sampleSize) {
        if (imageFile == null) {
            Log.w("EntzerrungsView/getSampledBitmap", "imageStream == null");
            return null;
        }

        // Minimum SampleSize 1
        sampleSize = Math.max(1, sampleSize);

        // Stream erzeugen
        InputStream imageStream;
        try {
            // Sollte eigentlich nie schiefgehen...
            imageStream = new FileInputStream(imageFile);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            return null;
        }

        // sampled bitmap dekodieren
        BitmapFactory.Options options = new Options();
        options.inSampleSize = sampleSize;

        Log.d("EntzerrungsView/getSampledBitmap", "Decoding stream with sample size " + sampleSize + "...");

        return BitmapFactory.decodeStream(imageStream, null, options);
    }

    /**
     * Erzeugt eine runterskalierte Version des geladenen Bildes, das sich fr den Corner Detection-Algorithmus eignet.
     */
    private Bitmap getCDScaledBitmap() {
        if (imageFile == null) {
            Log.w("EntzerrungsView/getCDScaledBitmap", "imageStream == null");
            return null;
        }

        // Bestimmte optimale Auflsung... Vorerst: fixes Maximum fr Hhe und Breite
        // TODO sinnvollere Lsung? bessere Konstanten? -> #230
        int scaledWidth = Math.min(getImageWidth(), CDALG_MAX_WIDTH);
        int scaledHeight = Math.min(getImageHeight(), CDALG_MAX_HEIGHT);

        // SampleSize berechnen, maximales Verhltnis zwischen originaler und optimaler Auflsung
        int sampleSize = Math.max(getImageWidth() / scaledWidth, getImageHeight() / scaledHeight);

        try {
            // Skaliertes Bitmap erzeugen
            Bitmap result = getSampledBitmap(sampleSize);

            if (result == null) {
                Log.e("EntzerrungsView/getCDScaledBitmap", "Decoding resulted in null...");
            }
            return result;
        } catch (OutOfMemoryError e) {
            Log.e("EntzerrungsView/getCDScaledBitmap", "Couldn't decode stream, out of memory!");

            return null;
        }
    }

    /**
     * Nach dem Laden des Bildes werden die Ecken per Corner Detection ermittelt.
     */
    @Override
    protected void onPostLoadImage(boolean calledByOnSizeChanged) {
        // LIV: Calculate zoom scale limits and stuff
        super.onPostLoadImage(calledByOnSizeChanged);

        if (getWidth() != 0 && getHeight() != 0) {
            // Find corners with Corner Detection Algorithm
            if (!punkte_gesetzt) {
                calcCornersWithDetector();
            }
        }
    }

    /**
     * Zeichnet das Bild mittels LargeImageView und stellt das helle Entzerrungsrechteck dar.
     */
    @Override
    protected void onDraw(Canvas canvas) {
        // nichts weiter tun, wenn die View in Eclipse's GUI-Editor angezeigt wird
        if (this.isInEditMode()) {
            return;
        }

        // check if we are ready to draw (getWidth and getHeight return non-zero values etc.)
        if (!isReadyToDraw()) {
            return;
        }

        if (show_corners) {
            wallpath.reset();

            // Bildschirmkoordinaten der Punkte ermitteln
            // (Ecken sind bereits sortiert)
            PointF[] canvasPoints = new PointF[CORNERS_COUNT];

            boolean somethingIsNull = false;

            for (int i = 0; i <= 3; i++) {
                canvasPoints[i] = corners[i].getScreenPosition();

                // Sollte eigentlich nie passieren...?
                if (canvasPoints[i] == null) {
                    somethingIsNull = true;
                    Log.w("EntzerrungsView/onDraw", "the " + i + "-th corner screen positions is null!");
                    break;
                }
            }

            if (!somethingIsNull) {
                wallpath.moveTo(canvasPoints[0].x, canvasPoints[0].y);
                wallpath.lineTo(canvasPoints[1].x, canvasPoints[1].y);
                wallpath.lineTo(canvasPoints[2].x, canvasPoints[2].y);
                wallpath.lineTo(canvasPoints[3].x, canvasPoints[3].y);

                canvas.drawPath(wallpath, white);
            }
        }

        // Bild per LargeImageView anzeigen
        super.onDraw(canvas);
    }

    /**
     * Behandlung von Touchevents.
     */
    // Lint-Warnung "EntzerrungsView overrides onTouchEvent but not performClick", obwohl sich super.onTouchEvent()
    // um Aufruf von performClick() kmmert.
    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent event) {

        if (entzerren.isLoadingActive())
            return false;

        // Zeigen wir die Schnellhilfe an?
        if (entzerren.isInQuickHelp()) {
            // Hier kein Pan/Zoom, aber Klicks sollen die Hilfe beenden.
            onTouchEvent_clickDetection(event);
            return true;
        }

        // Alle weiteren Touch-Events werden von der LargeImageView sowie anderen EventHandlers behandelt.
        // (Siehe z.B. CornerIcon fr das Verschieben der Eckpunkte.)

        // Fhre Defaulthandler aus (der Klicks, Pan und Zoom behandelt)
        return super.onTouchEvent(event);
    }

    @Override
    public boolean onClickPosition(float clickX, float clickY) {
        // Schnellhilfe deaktivieren, falls aktiv
        if (entzerren.isInQuickHelp()) {
            entzerren.endQuickHelp();
            return true;
        }

        // Kein Klickevent behandelt
        return false;
    }

    /**
     * Calculates default coordinates for corners (20%/80% of screen size).
     */
    public void calcCornerDefaults() {
        // Retrieve image dimensions
        int bitmap_breite = getImageWidth();
        int bitmap_hoehe = getImageHeight();

        // // Retrieve view dimensions
        // int view_breite = this.getWidth();
        // int view_hoehe = this.getHeight();
        //
        // // Choose the smaller one of each
        // int breite = Math.min(bitmap_breite, view_breite);
        // int hoehe = Math.min(bitmap_hoehe, view_hoehe);

        Log.d("EntzerrungsView/calcCornerDefaults", "breite/hoehe: " + bitmap_breite + ", " + bitmap_hoehe);

        // Take 20% of it
        int breite_scaled = (int) (0.2 * bitmap_breite);
        int hoehe_scaled = (int) (0.2 * bitmap_hoehe);

        // Set corner positions to 20% / 80% of the image or view size
        corners[0].setPosition(new Point(breite_scaled, hoehe_scaled));
        corners[1].setPosition(new Point((bitmap_breite - breite_scaled), hoehe_scaled));
        corners[2].setPosition(new Point((bitmap_breite - breite_scaled), (bitmap_hoehe - hoehe_scaled)));
        corners[3].setPosition(new Point(breite_scaled, (bitmap_hoehe - hoehe_scaled)));

        Log.d("EntzerrungsView/calcCornerDefaults",
                "Using default, breite/hoehe_scaled: " + breite_scaled + ", " + hoehe_scaled);

        punkte_gesetzt = true;
    }

    /**
     * Use corner detection algorithm to find and set corners automatically.
     */
    public void calcCornersWithDetector() {
        // Bitmap berechnen, die fr CD-Algorithmus runterskaliert wurde
        Bitmap bmp32 = getCDScaledBitmap();

        if (bmp32 == null || getImageWidth() <= 0) {
            Log.e("EntzerrungsView/calcCornersWithDetector",
                    bmp32 == null ? "getCDScaledBitmap() returned null!" : "getImageWidth() is nonpositive!");
            calcCornerDefaults();
            return;
        }

        float sampleSize = getImageWidth() / bmp32.getWidth();

        org.opencv.core.Point[] corner_points;

        try {
            Mat imgMat = new Mat();
            Utils.bitmapToMat(bmp32, imgMat);
            Mat greyMat = new Mat();
            Imgproc.cvtColor(imgMat, greyMat, Imgproc.COLOR_RGB2GRAY);

            corner_points = CornerDetector.guess_corners(greyMat);
        } catch (CvException e) {
            Log.w("EntzerrungsView/calcCornersWithDetector", "Corner detection failed with CvException");
            e.printStackTrace();

            // it seems that the image type is not supported by the corner detection algorithm (GIF?)
            // it won't be deskewable either, so deactivate that feature
            showCorners(false);
            imageTypeSupportsDeskew = false;

            calcCornerDefaults();
            return;
        } catch (UnsatisfiedLinkError e) {
            Log.w("EntzerrungsView/calcCornersWithDetector", "OpenCV not available");
            openCVLoadError = true;
            calcCornerDefaults();
            return;
        }

        // Im Fehlerfall Standardecken verwenden
        if (corner_points == null) {
            Log.w("EntzerrungsView/calcCornersWithDetector", "Corner detection returned null");
            calcCornerDefaults();
            return;
        }

        // Koordinaten auf ursprngliche Bildgre hochrechnen
        for (int i = 0; i < corner_points.length; i++) {
            corner_points[i].x *= sampleSize;
            corner_points[i].y *= sampleSize;
        }

        Log.d("Corner points", "0: " + corner_points[0] + " 1: " + corner_points[1] + " 2: " + corner_points[2]
                + " 3: " + corner_points[3]);

        // Algorithmusergebnis als Eckpunkte verwenden
        corners[0].setPosition(corner_points[0]);
        corners[1].setPosition(corner_points[1]);
        corners[2].setPosition(corner_points[2]);
        corners[3].setPosition(corner_points[3]);

        // Sortieren (obwohl sie eigentlich sortiert sein sollten...?)
        sortCorners();

        punkte_gesetzt = true;
    }

    /**
     * Shows or hides corners. (Image shall not be rectified if corners are hidden.)
     * 
     * @param show
     */
    public void showCorners(boolean show) {
        ImageButton my_button = (ImageButton) entzerren.findViewById(R.id.entzerrung_ok_button);

        // OverlayIcons verstecken, falls keine Ecken angezeigt werden sollen
        for (int i = 0; i <= 3; i++) {
            corners[i].setVisibility(show);
        }

        // Bild des Buttons abhngig von der Aktion machen (mit (Crop) oder ohne (Done) Entzerrung?)
        // und Transparenz des Bildes setzen (Visualisierung des Entzerrungsvierecks)
        if (show) {
            my_button.setImageResource(R.drawable.ic_action_crop);
            setForegroundAlpha(PICTURE_TRANSPARENT);
        } else {
            my_button.setImageResource(R.drawable.ic_action_done);
            setForegroundAlpha(PICTURE_OPAQUE);
        }

        show_corners = show;
        update();
    }

    /**
     * Returns true if corners are shown. (Image shall not be rectified if corners are hidden.)
     */
    public boolean isShowingCorners() {
        return show_corners;
    }

    /**
     * Eckpunkte sortieren, um "sinnvolles" Rechteck anzuzeigen. (Wird von CornerIcon.onDragMove() aufgerufen.)
     */
    public void sortCorners() {
        // Sortiere Ecken erstmal nach y-Koordinate, d.h. Ecke 1 und 2 sind die beiden mit hchsten y-Koordinaten
        Arrays.sort(corners, new Comparator<CornerIcon>() {
            @Override
            public int compare(CornerIcon lhs, CornerIcon rhs) {
                // return <0 for lhs<rhs, =0 for lhs=rhs, >0 for lhs>rhs
                return lhs.getPosition().y - rhs.getPosition().y;
            }
        });

        // -- Ecke 1 (links oben): die linkeste Ecke der zwei obersten Ecken
        // -- Ecke 2 (rechts oben): die andere Ecke der zwei obersten Ecken
        if (corners[0].getPosition().x > corners[1].getPosition().x) {
            // Swap linkere obere und rechte obere Ecke
            CornerIcon tmp = corners[0];
            corners[0] = corners[1];
            corners[1] = tmp;
        }

        // -- Ecke 3 (rechts unten): die rechte Ecke der verbleibenden
        // -- Ecke 4 (links unten): die verbleibende Ecke
        if (corners[2].getPosition().x < corners[3].getPosition().x) {
            // Swap rechte untere und linke untere Ecke
            CornerIcon tmp = corners[2];
            corners[2] = corners[3];
            corners[3] = tmp;
        }
    }

    /**
     * Gibt ein float[8] zurck mit x- und y-Koordinaten der Eckpunkte in Reihe (x1, y1, ..., x4, y4).
     * 
     * @param sampleSize Koordinaten werden angepasst (durch sampleSize dividiert), mindestens 1.
     * @return float[8] {x1, y1, x2, y2, x3, y3, x4, y4}
     */
    public float[] getPointOffsets(int sampleSize) {
        sampleSize = Math.max(1, sampleSize);

        // Array fr Koordinaten erzeugen
        float[] my_f = new float[2 * CORNERS_COUNT];

        for (int i = 0; i < CORNERS_COUNT; i++) {
            my_f[i * 2] = corners[i].getImagePositionX() / sampleSize;
            my_f[i * 2 + 1] = corners[i].getImagePositionY() / sampleSize;
        }

        return my_f;
    }

}