/* * Copyright 2015 Lafayette College * * This file is part of OpenCVTour. * * OpenCVTour 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. * * OpenCVTour 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 OpenCVTour. If not, see <http://www.gnu.org/licenses/>. */ package alicrow.opencvtour; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Matrix; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.media.ExifInterface; import android.media.ThumbnailUtils; import android.net.Uri; import android.os.AsyncTask; import android.provider.MediaStore; import android.util.Log; import android.util.LruCache; import android.widget.ImageView; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.lang.ref.WeakReference; import java.text.SimpleDateFormat; import java.util.Date; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; /** * Created by daniel on 6/2/15. * * Various utility functions * * Most of the image-loading code was adapted from sample code at http://developer.android.com/training/displaying-bitmaps/index.html, which was licensed under the Apache 2.0 License. */ public class Utilities { private static final String TAG = "Utilities"; public static final int REQUEST_IMAGE_CAPTURE = 1; private static final LruCache<String, Bitmap> _bitmap_cache = new LruCache<>(32); /** * Creates and returns a Bitmap of the image at the given filepath, scaled down to fit the area the Bitmap will be displayed in * @param image_file_path location of the image to sample * @param reqWidth width at which the resultant Bitmap will be displayed * @param reqHeight height at which the resultant Bitmap will be displayed * @return a Bitmap large enough to cover the given area */ public static Bitmap decodeSampledBitmap(String image_file_path, int reqWidth, int reqHeight) { Log.v(TAG, "creating bitmap for " + image_file_path); long start = android.os.SystemClock.uptimeMillis(); final BitmapFactory.Options options = getBitmapBounds(image_file_path); options.inSampleSize = calculateInSampleSize(options.outWidth, options.outHeight, reqWidth, reqHeight); options.inJustDecodeBounds = false; Bitmap sampled = BitmapFactory.decodeFile(image_file_path, options); long sampled_time = android.os.SystemClock.uptimeMillis(); Log.v(TAG, "created sampled bitmap for " + image_file_path + ", took " + (sampled_time-start) + "ms"); Bitmap oriented = fixOrientation(sampled, image_file_path); long orientation_fixed_time = android.os.SystemClock.uptimeMillis(); Log.v(TAG, "fixed orientation for " + image_file_path + ", took " + (orientation_fixed_time-sampled_time) + "ms"); Bitmap final_bitmap = ThumbnailUtils.extractThumbnail(oriented, reqWidth, reqHeight); long end = android.os.SystemClock.uptimeMillis(); Log.v(TAG, "finished resizing bitmap for " + image_file_path + ", took " + (end-orientation_fixed_time) + "ms"); Log.v(TAG, "finished creating bitmap for " + image_file_path + ", took " + (end-start) + "ms"); return final_bitmap; } /** * Calculates the sample size to scale the image to be displayed at a given size * @param raw_width raw width of the image * @param raw_height raw height of the image * @param reqWidth the width the image will be displayed at * @param reqHeight the height the image will be displayed at * @return the sample size */ private static int calculateInSampleSize(int raw_width, int raw_height, int reqWidth, int reqHeight) { int inSampleSize = 1; if (raw_height > reqHeight || raw_width > reqWidth) { final int halfHeight = raw_height / 2; final int halfWidth = raw_width / 2; // Calculate the largest inSampleSize value that is a power of 2 and keeps both height and width larger than the requested height and width. while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) { inSampleSize *= 2; } } return inSampleSize; } /** * Retrieves the bounds of the given image * @param filepath path to the image file * @return a BitmapFactory.Options object where outWidth and outHeight are the width and height of the image */ public static BitmapFactory.Options getBitmapBounds(String filepath) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFile(filepath, options); return options; } /** * creates a new Bitmap rotated according to the orientation specified in a source file * @param image the Bitmap to rotate * @param image_file_path the file containing the Bitmap's source * @return a new Bitmap with the correct rotation */ public static Bitmap fixOrientation(Bitmap image, String image_file_path) { try { /// Retrieve orientation info from the file ExifInterface exif = new ExifInterface(image_file_path); int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); float rotation; switch (orientation) { case ExifInterface.ORIENTATION_NORMAL: rotation = 0; break; case ExifInterface.ORIENTATION_ROTATE_90: rotation = 90; break; case ExifInterface.ORIENTATION_ROTATE_180: rotation = 180; break; case ExifInterface.ORIENTATION_ROTATE_270: rotation = 270; break; default: rotation = 0; break; } return rotateImage(image, rotation); } catch(IOException e) { Log.w(TAG, "Unable to open '" + image_file_path + "' to read orientation"); return image; } } /** * Returns a rotated version of a Bitmap * @param image the image to rotate * @param angle the amount to rotate the image by, in degrees * @return a new Bitmap with the correct rotation */ public static Bitmap rotateImage(Bitmap image, float angle) { Matrix matrix = new Matrix(); matrix.postRotate(angle); return Bitmap.createBitmap(image, 0, 0, image.getWidth(), image.getHeight(), matrix, true); } /** * Contains the parameters used to generate a smaller Bitmap from a larger image. */ public static class ReducedBitmapInfo { public String full_image_filepath; public int width; public int height; public ReducedBitmapInfo(String filepath, int width, int height) { this.full_image_filepath = filepath; this.width = width; this.height = height; } /// Using ReducedBitmapInfo as a key in our cache doesn't work (no clue why), so we use Strings instead. @Override public String toString() { return full_image_filepath + " " + width + " " + height; } } /** * Adds an image thumbnail to our cache * @param info information about the thumbnail (name of full imgae file, and dimensions of the thumbnail) * @param bitmap the bitmap to cache */ public static void addToCache(ReducedBitmapInfo info, Bitmap bitmap) { if(_bitmap_cache.get(info.toString()) == null) { Log.v(TAG, "Adding bitmap to cache"); _bitmap_cache.put(info.toString(), bitmap); } } /** * Extension of BitmapDrawable that keeps track of what BitmapWorkerTask is loading its image */ static class AsyncDrawable extends BitmapDrawable { private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference; public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) { super(res, bitmap); bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask); } public BitmapWorkerTask getBitmapWorkerTask() { return bitmapWorkerTaskReference.get(); } } /** * Asynchronously loads a thumbnail of the given image into the given view * @param view the ImageView to load the bitmap into * @param filepath filepath of the (full-size) image to load * @param width desired width of the thumbnail * @param height desired height of the thumbnail * @param context a context, used to retireve the path where we're caching thumbnails on disk. */ public static void loadBitmap(ImageView view, String filepath, int width, int height, Context context) { ReducedBitmapInfo info = new ReducedBitmapInfo(filepath, width, height); Log.v(TAG, "loading bitmap " + info.toString()); Bitmap bitmap = _bitmap_cache.get(info.toString()); if(bitmap != null) { Log.v(TAG, "bitmap already created"); view.setImageBitmap(bitmap); } else { /// Find (or create) the file containing the cached version of this image at the right size File cached_image_file = new File(context.getExternalCacheDir(), info.toString().substring(Tour.getToursDirectory(context).getPath().length()) + ".jpg"); cached_image_file.getParentFile().mkdir(); Utilities.BitmapWorkerTask task = new Utilities.BitmapWorkerTask(view, width, height, cached_image_file); Bitmap placeholder_bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.loading_thumbnail); final AsyncDrawable asyncDrawable = new AsyncDrawable(context.getResources(), placeholder_bitmap, task); view.setImageDrawable(asyncDrawable); task.execute(filepath); } } /** * @return the BitmapWorkerTask associated with imageView */ private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { if (imageView != null) { final Drawable drawable = imageView.getDrawable(); if (drawable instanceof AsyncDrawable) { final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; return asyncDrawable.getBitmapWorkerTask(); } } return null; } /** * Task to load a smaller version of a picture into an ImageView */ public static class BitmapWorkerTask extends AsyncTask<String, Void, Bitmap> { private final WeakReference<ImageView> imageViewReference; private int _width, _height; private File _cached_image_file; public BitmapWorkerTask(ImageView imageView, int width, int height, File cached_image_file) { // Use a WeakReference to ensure the ImageView can be garbage collected imageViewReference = new WeakReference<>(imageView); Log.v(TAG, "Creating BitmapWorkerTask"); _width = width; _height = height; _cached_image_file = cached_image_file; } // Decode image in background. @Override protected Bitmap doInBackground(String... params) { try { String image_filepath = params[0]; Bitmap image; if(_cached_image_file != null && _cached_image_file.exists()) { Log.v(TAG, "loading cached bitmap file " + _cached_image_file.getPath()); long start = android.os.SystemClock.uptimeMillis(); image = BitmapFactory.decodeFile(_cached_image_file.getAbsolutePath()); long end = android.os.SystemClock.uptimeMillis(); Log.v(TAG, "finished loading cached bitmap file " + _cached_image_file.getPath() + ", took " + (end-start) + "ms"); } else { image = Utilities.decodeSampledBitmap(image_filepath, _width, _height); /// Save this thumbnail to disk, so we don't need to run sampling for it again. FileOutputStream out = null; try { out = new FileOutputStream(_cached_image_file); image.compress(Bitmap.CompressFormat.JPEG, 97, out); } catch (Exception e) { e.printStackTrace(); } finally { try { if (out != null) { out.close(); } } catch (IOException e) { e.printStackTrace(); } } } addToCache(new ReducedBitmapInfo(image_filepath, _width, _height), image); return image; } catch(Exception e) { Log.e(TAG, e.toString()); return null; } } // Once complete, see if ImageView is still around and set bitmap. @Override protected void onPostExecute(Bitmap bitmap) { if (isCancelled()) { Log.d(TAG, "BitmapWorkerTask was canceled"); bitmap.recycle(); bitmap = null; } if (imageViewReference != null && bitmap != null) { final ImageView imageView = imageViewReference.get(); final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); if (this == bitmapWorkerTask && imageView != null) { imageView.setImageBitmap(bitmap); } } } } /** * Converts a size in density-independent pixels to the actual pixel value on this device. * @param dp size in density-independent pixels * @return actual pixel value on this device */ public static int dp_to_px(float dp) { final float scale = Resources.getSystem().getDisplayMetrics().density; return (int)( dp * scale + 0.5f); } /** * Creates a file to store an image in * @param context a Context so we can retrieve our app's storage directory * @param is_temp whether the image is meant to be temporary. If so, we'll use the "temp.jpg" filename, so it will be overwritten the next time we take a temporary photo * @return a Uri for the file to save the photo in */ public static Uri createImageFile(Context context, boolean is_temp) { String timestamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); File file; if(is_temp) /// this is the file we use for temporary images. If this changes, it needs to be changed in FollowTourActivity.identifyItem() as well. file = new File(context.getExternalCacheDir(), "temp" + ".jpg"); else file = new File(Tour.getCurrentTour().getDirectory(), timestamp + ".jpg"); return Uri.fromFile(file); } /** * Starts an activity to take a picture * @param activity an Activity, necessary in order to start activities * @param is_temp whether the image is meant to be temporary. If so, we'll use the "temp.jpg" filename, so it will be overwritten the next time we take a temporary photo * @return the Uri of the file the photo will be saved in */ public static Uri takePicture(Activity activity, boolean is_temp) { Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); Uri photo_uri = null; if (intent.resolveActivity(activity.getPackageManager()) != null) { // Create a file to save the photo to photo_uri = Utilities.createImageFile(activity, is_temp); if (photo_uri != null) { intent.putExtra(MediaStore.EXTRA_OUTPUT, photo_uri); activity.startActivityForResult(intent, REQUEST_IMAGE_CAPTURE); Log.i(TAG, "started image capture request"); } else { Log.e(TAG, "failed to create file for image"); } } return photo_uri; } /** * Compresses a folder * @param input_path filepath of the folder to compress * @param output_path filepath of the archive to save * @param skip_images whether to ignore image files in the folder. These can take up a *lot* of space, so it's best to skip them if they're not needed (the image descriptors are still saved, so OpenCV has what it needs from the images) */ public static void compressFolder(String input_path, String output_path, boolean skip_images) { try { FileOutputStream fos = new FileOutputStream(output_path); ZipOutputStream zos = new ZipOutputStream(fos); File srcFile = new File(input_path); File[] files = srcFile.listFiles(); Log.d(TAG, "Zip directory: " + srcFile.getName()); for (File file : files) { if(skip_images && file.getName().endsWith(".jpg")) { Log.d(TAG, "Skipping image file " + file.getName()); } else { Log.d(TAG, "Adding file: " + file.getName()); byte[] buffer = new byte[1024]; FileInputStream fis = new FileInputStream(file); zos.putNextEntry(new ZipEntry(file.getName())); int length; while ((length = fis.read(buffer)) > 0) { zos.write(buffer, 0, length); } zos.closeEntry(); fis.close(); } } zos.close(); } catch (IOException ioe) { Log.e(TAG, ioe.getMessage()); } } public static void extractFolder(String input_path, String output_path) { try { extractFolder(new FileInputStream(input_path), output_path); } catch(Exception ex) { Log.e(TAG, ex.getMessage()); } } public static void extractFolder(InputStream is, String output_path) { try { File output_folder = new File(output_path); if(!output_folder.exists()) output_folder.mkdir(); if(!output_path.endsWith("/")) output_path = output_path + "/"; ZipInputStream zis = new ZipInputStream(new BufferedInputStream(is)); ZipEntry ze; byte[] buffer = new byte[1024]; int count; String filename; while ((ze = zis.getNextEntry()) != null) { filename = ze.getName(); Log.d(TAG, "Extracting file " + filename); if (ze.isDirectory()) { File fmd = new File(output_path + filename); fmd.mkdirs(); continue; } FileOutputStream fout = new FileOutputStream(output_path + filename); while ((count = zis.read(buffer)) != -1) { fout.write(buffer, 0, count); } fout.close(); zis.closeEntry(); } zis.close(); } catch (IOException ioe) { Log.e(TAG, ioe.getMessage()); } } }