jenkins-bot has submitted this change and it was merged.

Change subject: Face detection in lead image.
......................................................................


Face detection in lead image.

A few notes so far:
- on my test device (Galaxy S4), detection takes between 0.5 and 3
  seconds. May end up being too slow on other devices.
- fortunately, this will be easy to disable/reenable if necessary.

TODO: research a better way of passing the bitmap from Picasso directly to
the FaceDetector. Right now, we have to create an intermediate bitmap,
because the FaceDetector requires a 5-6-5 bitmap to work properly.

Change-Id: Ic255a0974345270c10164df84508527b6f2767fc
---
M wikipedia/res/layout/fragment_page.xml
A wikipedia/src/main/java/org/wikipedia/page/leadimages/ImageViewWithFace.java
M wikipedia/src/main/java/org/wikipedia/page/leadimages/LeadImagesHandler.java
3 files changed, 207 insertions(+), 11 deletions(-)

Approvals:
  BearND: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/wikipedia/res/layout/fragment_page.xml 
b/wikipedia/res/layout/fragment_page.xml
index 1d0cced..d2685b3 100644
--- a/wikipedia/res/layout/fragment_page.xml
+++ b/wikipedia/res/layout/fragment_page.xml
@@ -30,10 +30,16 @@
                 android:orientation="vertical"
                 android:paddingTop="48dp"
                 >
-                <ImageView
+                <org.wikipedia.page.leadimages.ImageViewWithFace
                     android:id="@+id/page_image_1"
                     android:layout_width="match_parent"
                     android:layout_height="match_parent"
+                    android:scaleType="centerCrop"
+                    android:visibility="invisible"/>
+                <ImageView
+                    android:id="@+id/page_image_placeholder"
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent"
                     android:scaleType="centerCrop"/>
                 <FrameLayout
                     android:id="@+id/page_title_container"
diff --git 
a/wikipedia/src/main/java/org/wikipedia/page/leadimages/ImageViewWithFace.java 
b/wikipedia/src/main/java/org/wikipedia/page/leadimages/ImageViewWithFace.java
new file mode 100644
index 0000000..80b84a1
--- /dev/null
+++ 
b/wikipedia/src/main/java/org/wikipedia/page/leadimages/ImageViewWithFace.java
@@ -0,0 +1,100 @@
+package org.wikipedia.page.leadimages;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PointF;
+import android.graphics.drawable.Drawable;
+import android.media.FaceDetector;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.widget.ImageView;
+
+import com.squareup.picasso.Picasso;
+import com.squareup.picasso.Target;
+
+import org.wikipedia.concurrency.SaneAsyncTask;
+
+public class ImageViewWithFace extends ImageView implements Target {
+    private static final String TAG = "ImageViewWithFace";
+
+    public ImageViewWithFace(Context context) {
+        super(context);
+    }
+
+    public ImageViewWithFace(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public ImageViewWithFace(Context context, AttributeSet attrs, int 
defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    public void onBitmapLoaded(final Bitmap bitmap, Picasso.LoadedFrom from) {
+        (new SaneAsyncTask<Void>(SaneAsyncTask.LOW_CONCURRENCY) {
+            @Override
+            public Void performTask() throws Throwable {
+                // boost this thread's priority a bit
+                Thread.currentThread().setPriority(Thread.MAX_PRIORITY - 1);
+                long millis = System.currentTimeMillis();
+                // create a new bitmap onto which we'll draw the original 
bitmap,
+                // because the FaceDetector requires it to be a 565 bitmap, 
and it
+                // must also be even width.
+                Bitmap tempbmp = Bitmap.createBitmap(bitmap.getWidth() - 
(bitmap.getWidth() % 2),
+                        bitmap.getHeight(), Bitmap.Config.RGB_565);
+                Canvas canvas = new Canvas(tempbmp);
+                Paint paint = new Paint();
+                paint.setColor(Color.BLACK);
+                canvas.drawBitmap(bitmap, 0, 0, paint);
+
+                // initialize the face detector, and look for only one face...
+                FaceDetector fd = new FaceDetector(tempbmp.getWidth(), 
tempbmp.getHeight(), 1);
+                FaceDetector.Face[] faces = new FaceDetector.Face[1];
+                PointF facePos = new PointF();
+                int numFound = fd.findFaces(tempbmp, faces);
+
+                if (numFound > 0) {
+                    faces[0].getMidPoint(facePos);
+                    android.util.Log.d(TAG, "Found face at " + facePos.x + ", 
" + facePos.y);
+                }
+                // free our temporary bitmap
+                tempbmp.recycle();
+
+                Log.d(TAG, "Face detection took " + 
(System.currentTimeMillis() - millis) + "ms");
+
+                if (onImageLoadListener != null) {
+                    onImageLoadListener.onImageLoaded(bitmap, facePos);
+                }
+                return null;
+            }
+        }).execute();
+        // and, of course, set the original bitmap as our image
+        this.setImageBitmap(bitmap);
+    }
+
+    @Override
+    public void onBitmapFailed(Drawable errorDrawable) {
+        if (onImageLoadListener != null) {
+            onImageLoadListener.onImageFailed();
+        }
+    }
+
+    @Override
+    public void onPrepareLoad(Drawable placeHolderDrawable) {
+    }
+
+
+    public interface OnImageLoadListener {
+        void onImageLoaded(Bitmap bitmap, PointF faceLocation);
+        void onImageFailed();
+    }
+
+    private OnImageLoadListener onImageLoadListener;
+
+    public void setOnImageLoadListener(OnImageLoadListener listener) {
+        onImageLoadListener = listener;
+    }
+}
\ No newline at end of file
diff --git 
a/wikipedia/src/main/java/org/wikipedia/page/leadimages/LeadImagesHandler.java 
b/wikipedia/src/main/java/org/wikipedia/page/leadimages/LeadImagesHandler.java
index 3c41314..5e67388 100644
--- 
a/wikipedia/src/main/java/org/wikipedia/page/leadimages/LeadImagesHandler.java
+++ 
b/wikipedia/src/main/java/org/wikipedia/page/leadimages/LeadImagesHandler.java
@@ -1,19 +1,23 @@
 package org.wikipedia.page.leadimages;
 
+import android.graphics.Bitmap;
 import android.graphics.Color;
 import android.graphics.Point;
 import android.os.Build;
 import android.util.TypedValue;
+import android.graphics.PointF;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.animation.Animation;
 import android.view.animation.Transformation;
+import android.webkit.WebView;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 import android.widget.TextView;
 
 import com.nineoldandroids.view.ViewHelper;
 import com.squareup.picasso.Picasso;
+import com.squareup.picasso.Target;
 
 import org.json.JSONException;
 import org.json.JSONObject;
@@ -28,9 +32,10 @@
 
 import java.util.Map;
 
-public class LeadImagesHandler implements 
ObservableWebView.OnScrollChangeListener {
+public class LeadImagesHandler implements 
ObservableWebView.OnScrollChangeListener, ImageViewWithFace.OnImageLoadListener 
{
     private final PageViewFragment parentFragment;
     private final CommunicationBridge bridge;
+    private final WebView webView;
 
     /**
      * Minimum screen height for enabling lead images. If the screen is 
smaller than
@@ -86,25 +91,30 @@
     private boolean leadImagesEnabled = true;
 
     private final ViewGroup imageContainer;
-    private ImageView image1;
+    private ImageView imagePlaceholder;
+    private ImageViewWithFace image1;
     private View pageTitleContainer;
     private TextView pageTitleText;
     private TextView pageDescriptionText;
 
     private int displayHeight;
+    private int imageBaseYOffset = 0;
     private float displayDensity;
 
     public interface OnLeadImageLayoutListener {
         void onLayoutComplete();
     }
 
-    public LeadImagesHandler(PageViewFragment parentFragment, 
CommunicationBridge bridge,
+    public LeadImagesHandler(final PageViewFragment parentFragment, 
CommunicationBridge bridge,
                              ObservableWebView webview, ViewGroup hidingView) {
         this.parentFragment = parentFragment;
         this.imageContainer = hidingView;
         this.bridge = bridge;
+        this.webView = webview;
+        displayDensity = 
parentFragment.getResources().getDisplayMetrics().density;
 
-        image1 = (ImageView)imageContainer.findViewById(R.id.page_image_1);
+        imagePlaceholder = 
(ImageView)imageContainer.findViewById(R.id.page_image_placeholder);
+        image1 = 
(ImageViewWithFace)imageContainer.findViewById(R.id.page_image_1);
         pageTitleContainer = 
imageContainer.findViewById(R.id.page_title_container);
         pageTitleText = 
(TextView)imageContainer.findViewById(R.id.page_title_text);
         pageDescriptionText = 
(TextView)imageContainer.findViewById(R.id.page_description_text);
@@ -125,6 +135,10 @@
 
         // hide ourselves by default
         hide();
+
+        
imagePlaceholder.setImageResource(Utils.getThemedAttributeId(parentFragment.getActivity(),
+                R.attr.lead_image_drawable));
+        image1.setOnImageLoadListener(this);
     }
 
     @Override
@@ -134,7 +148,7 @@
             ViewHelper.setTranslationY(image1, 0);
         } else {
             ViewHelper.setTranslationY(imageContainer, -scrollY);
-            ViewHelper.setTranslationY(image1, scrollY / 2); //parallax, baby
+            ViewHelper.setTranslationY(image1, imageBaseYOffset + scrollY / 
2); //parallax, baby
         }
     }
 
@@ -144,6 +158,70 @@
      */
     public void hide() {
         imageContainer.setVisibility(View.INVISIBLE);
+    }
+
+    @Override
+    public void onImageLoaded(Bitmap bitmap, final PointF faceLocation) {
+        final int bmpHeight = bitmap.getHeight();
+        final float aspect = (float)bitmap.getHeight() / 
(float)bitmap.getWidth();
+        imageContainer.post(new Runnable() {
+            @Override
+            public void run() {
+                if (!parentFragment.isAdded()) {
+                    return;
+                }
+                int newWidth = image1.getWidth();
+                int newHeight = (int)(newWidth * aspect);
+
+                // give our image an offset based on the location of the face,
+                // relative to the image container
+                float scale = (float)newHeight / (float)bmpHeight;
+                if (faceLocation.y > 0) {
+                    int faceY = (int)(faceLocation.y * scale);
+                    // if we have a face, then offset to the face location
+                    imageBaseYOffset = -(faceY - (imagePlaceholder.getHeight() 
/ 2));
+                    // give it a slight artificial boost, so that it appears 
slightly
+                    // above the page title...
+                    final int faceBoost = 24;
+                    imageBaseYOffset -= (faceBoost * displayDensity);
+                } else {
+                    // if we don't have a face, then center on the midpoint of 
the image
+                    imageBaseYOffset = -(newHeight / 2
+                            - (imagePlaceholder.getHeight() / 2));
+                }
+                // is the offset too far to the top?
+                if (imageBaseYOffset > 0) {
+                    imageBaseYOffset = 0;
+                }
+                // is the offset too far to the bottom?
+                if (imageBaseYOffset < imagePlaceholder.getHeight() - 
newHeight) {
+                    imageBaseYOffset = imagePlaceholder.getHeight() - 
newHeight;
+                }
+
+                // resize our image to have the same proportions as the 
acquired bitmap
+                if (newHeight < imagePlaceholder.getHeight()) {
+                    // if the height of the image is less than the container, 
then just
+                    // make it fill-parent
+                    image1.setLayoutParams(new 
FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
+                            FrameLayout.LayoutParams.MATCH_PARENT));
+                    imageBaseYOffset = 0;
+                } else {
+                    image1.setLayoutParams(new 
FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
+                            newHeight));
+                }
+
+                // and force a refresh of its position within the container 
view.
+                onScrollChanged(webView.getScrollY(), webView.getScrollY());
+
+                // fade in the new image!
+                ViewAnimations.crossFade(imagePlaceholder, image1);
+            }
+        });
+    }
+
+    @Override
+    public void onImageFailed() {
+        // just keep showing the placeholder image...
     }
 
 
@@ -192,9 +270,8 @@
             thumbUrl = WikipediaApp.getInstance().getNetworkProtocol() + ":" + 
thumbUrl;
             Picasso.with(parentFragment.getActivity())
                     .load(thumbUrl)
-                    
.placeholder(Utils.getThemedAttributeId(parentFragment.getActivity(), 
R.attr.lead_image_drawable))
-                    
.error(Utils.getThemedAttributeId(parentFragment.getActivity(), 
R.attr.lead_image_drawable))
-                    .into(image1);
+                    .noFade()
+                    .into((Target) image1);
         }
     }
 
@@ -269,6 +346,7 @@
                     (int) ((titleContainerHeight) * displayDensity)));
             // hide the lead image
             image1.setVisibility(View.GONE);
+            imagePlaceholder.setVisibility(View.GONE);
             // set the color of the title
             pageTitleText.setTextColor(parentFragment
                     .getResources()
@@ -294,8 +372,10 @@
             titleContainerHeight = (int) (displayHeight * 
IMAGES_CONTAINER_RATIO);
             imageContainer.setLayoutParams(new 
FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
                     (int) (titleContainerHeight * displayDensity)));
-            // show the lead image
-            image1.setVisibility(View.VISIBLE);
+            // prepare the lead image to be populated
+            image1.setVisibility(View.INVISIBLE);
+            imagePlaceholder.setVisibility(View.VISIBLE);
+
             // set the color of the title
             
pageTitleText.setTextColor(parentFragment.getResources().getColor(R.color.lead_text_color));
             titleBottomPadding = pageTitleText.getPaddingBottom();
@@ -330,6 +410,16 @@
         }
         bridge.sendMessage("setPaddingTop", payload);
 
+        // and start fetching the lead image, if we have one
+        String thumbUrl = 
parentFragment.getFragment().getPage().getPageProperties().getLeadImageUrl();
+        if (thumbUrl != null && leadImagesEnabled) {
+            thumbUrl = WikipediaApp.getInstance().getNetworkProtocol() + ":" + 
thumbUrl;
+            Picasso.with(parentFragment.getActivity())
+                    .load(thumbUrl)
+                    .noFade()
+                    .into((Target)image1);
+        }
+
         // make everything visible!
         imageContainer.setVisibility(View.VISIBLE);
 

-- 
To view, visit https://gerrit.wikimedia.org/r/168773
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: Ic255a0974345270c10164df84508527b6f2767fc
Gerrit-PatchSet: 11
Gerrit-Project: apps/android/wikipedia
Gerrit-Branch: master
Gerrit-Owner: Dbrant <[email protected]>
Gerrit-Reviewer: BearND <[email protected]>
Gerrit-Reviewer: Brion VIBBER <[email protected]>
Gerrit-Reviewer: Deskana <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to