/* Copyright (C) 2014,2015 Jan Müller, Björn 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 FÜR 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 unterstützt 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 unterstützt 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()
}
/**
* Lädt 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 für den Corner Detection-Algorithmus eignet.
*/
private Bitmap getCDScaledBitmap() {
if (imageFile == null) {
Log.w("EntzerrungsView/getCDScaledBitmap", "imageStream == null");
return null;
}
// Bestimmte optimale Auflösung... Vorerst: fixes Maximum für Höhe und Breite
// TODO sinnvollere Lösung? bessere Konstanten? -> #230
int scaledWidth = Math.min(getImageWidth(), CDALG_MAX_WIDTH);
int scaledHeight = Math.min(getImageHeight(), CDALG_MAX_HEIGHT);
// SampleSize berechnen, maximales Verhältnis zwischen originaler und optimaler Auflösung
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() kümmert.
@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 für das Verschieben der Eckpunkte.)
// Führe 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 für 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 ursprüngliche Bildgröße 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 abhängig 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 höchsten 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] zurück 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 für 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;
}
}