2

I am using the CommonsWare CWAC-Camera library (https://github.com/commonsguy/cwac-camera) to wrap the native Android Camera API.

I would like to allow the user to select a point on the preview image before taking a picture, then (accurately) pick out that same point on the final image after they have taken the picture. While this works approximately, the markup on the final image is typically off-target by about 100-200 pixels.

To start with, I am capturing the user's original selected point by setting an OnTouchListener on the CameraView. I am then storing the X and Y coordinates from the MotionEvent. Just to help with visual verification, I am also immediately drawing a circle at the coordinates the user selected, before they take the picture.

When the user takes the picture, I have subclassed from SimpleCameraHost to override saveImage, in order to draw a corresponding circle on the final image before it is written to storage. In order to calculate where to draw the circle on the final image, I am doing the following:

  1. Create a Bitmap the same size and shape as the TextureView that was used to draw the preview
  2. Draw a circle on that Bitmap at the same coordinates as was drawn on-screen
  3. Scale the markup-Bitmap up to the final image size (using Matrix.ScaleToFit.Center) and overlay it on top of the actual final photo image

This is not sufficiently accurate, however - at least, not on the Nexus 7 (2013) that I am testing with. I am not sure what I am missing in the placement calculations for adding the markup to the final image.

A simplified example to demonstrate the problem follows:

MainActivity.java:

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    mCameraContainer = findViewById(R.id.camera_container);
    mCanvasView = (CanvasView)findViewById(R.id.canvas_view);

    FragmentManager fmgr = getFragmentManager();
    FragmentTransaction ft = fmgr.beginTransaction();
    mCameraFragment = (CameraFragment)fmgr.findFragmentByTag(CAMERA_FRAGMENT_TAG);
    if (null == mCameraFragment) {
        mCameraFragment = new CameraFragment();
        ft.add(R.id.camera_container,  mCameraFragment, CAMERA_FRAGMENT_TAG);
    }
    ft.commit();

    if (null == mCameraHost) {
        mCameraHost = new MyCameraHost(this);
    }
    mCameraFragment.setHost(mCameraHost);

    ViewTreeObserver ccObserver = mCameraContainer.getViewTreeObserver();
    ccObserver.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            CameraView cv = (CameraView)mCameraFragment.getView();
            View previewWidget = ((ViewGroup)cv).getChildAt(0);
            mCameraHost.setPreviewSize(previewWidget.getWidth(), previewWidget.getHeight());

            cv.setOnTouchListener(new OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent e) {
                    mCanvasView.setPoint(e.getX(), e.getY());
                    mCameraHost.setPoint(e.getX(), e.getY());
                    return true;
                }
            });

            mCameraContainer.getViewTreeObserver().removeOnGlobalLayoutListener(this);
        }
    });

    Button photoButton = (Button)findViewById(R.id.takePhotoButton);
    photoButton.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) {
            mCameraFragment.takePicture();
        }
    });
}

MyCameraHost.java:

@Override
public void saveImage(PictureTransaction xact, byte[] image) {
    // decode the final image as a Bitmap
    BitmapFactory.Options opt = new BitmapFactory.Options();
    opt.inMutable = true;
    Bitmap imageBitmap = BitmapFactory.decodeByteArray(image, 0, image.length, opt);

    // draw a blank Bitmap with just the markup, using a canvas size equivalent to the preview view
    Bitmap markerBitmap = Bitmap.createBitmap(previewViewWidth, previewViewHeight, Bitmap.Config.ARGB_8888);
    Canvas c = new Canvas(markerBitmap);
    Paint p = new Paint();
    p.setStrokeWidth(10);
    p.setStyle(Style.FILL);
    p.setColor(Color.BLUE);
    c.drawCircle(previewMarkerX, previewMarkerY, 20, p);

    // scale the markup bitmap up to final-image size using Matrix.ScaleToFit.CENTER
    Matrix m = new Matrix();
    m.setRectToRect(new RectF(0, 0, previewViewWidth, previewViewHeight), new RectF(0, 0, imageBitmap.getWidth(), imageBitmap.getHeight()), Matrix.ScaleToFit.CENTER);

    // overlay the scaled marker Bitmap onto the image
    Canvas imageCanvas = new Canvas(imageBitmap);
    imageCanvas.drawBitmap(markerBitmap, m, null);

    // save the combined image
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    imageBitmap.compress(CompressFormat.JPEG, 70, bos);
    byte[] image2 = bos.toByteArray();
    super.saveImage(xact, image2);
}

layout XML:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" >

<FrameLayout 
    android:id="@+id/camera_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

<com.example.cwaccameratouchpoint.CanvasView 
    android:id="@+id/canvas_view"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignLeft="@id/camera_container"
    android:layout_alignRight="@id/camera_container"
    android:layout_alignTop="@id/camera_container"
    android:layout_alignBottom="@id/camera_container" />        

<Button
    android:id="@+id/takePhotoButton"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentTop="true"
    android:layout_alignParentRight="true"
    android:text="go" />

</RelativeLayout>

As per comments from CommonsWare, I updated saveImage below to include the full-bleed offset and to spell out the transform calculations. It is an improvement, but still not as accurate as it should be:

// decode the final image as a Bitmap
BitmapFactory.Options opt = new BitmapFactory.Options();
opt.inMutable = true;
Bitmap imageBitmap = BitmapFactory.decodeByteArray(image, 0, image.length, opt);
int finalImageWidth = imageBitmap.getWidth();
int finalImageHeight = imageBitmap.getHeight();

// calculate selected point x and y values as applied to full-size final image
// apply full-bleed-based offset, where xoffset and yoffset represent the
// offset of the TextureView within its parent CameraView
int bleedAdjustedX = (int)previewMarkerX - xoffset;
int bleedAdjustedY = (int)previewMarkerY - yoffset;

// calculate offset for change in aspect ratio
// for now, assume portrait orientation only
double finalImageAspectRatio = (double)finalImageHeight / (double)finalImageWidth;
double aspectAdjustedWidth =  (double)previewViewHeight / finalImageAspectRatio;
double aspectXOffset = (aspectAdjustedWidth - previewViewWidth) / 2;
int aspectAdjustedX = bleedAdjustedX + (int)aspectXOffset;

// scale adjusted coordinates for full-size image
double normalizedAdjustedX = (double)aspectAdjustedX / aspectAdjustedWidth;
double normalizedAdjustedY = (double)bleedAdjustedY / (double)previewViewHeight;
double scaledX = normalizedAdjustedX * (double)finalImageWidth;
double scaledY = normalizedAdjustedY * (double)finalImageHeight;

// draw markup on final image
Canvas c = new Canvas(imageBitmap);
Paint p = new Paint();
p.setStrokeWidth(10);
p.setStyle(Style.FILL);
p.setColor(Color.BLUE);
c.drawCircle((float)scaledX, (float)scaledY, 20, p);

// save marked-up image
ByteArrayOutputStream bos = new ByteArrayOutputStream();
imageBitmap.compress(CompressFormat.JPEG, 70, bos);
byte[] image2 = bos.toByteArray();
super.saveImage(xact, image2);
CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
eshayne
  • 915
  • 9
  • 15
  • Does it work better the *second* time you try it, with the same `CameraFragment`? If yes, my guess is that you're running into the aspect change issue: https://github.com/commonsguy/cwac-camera/issues/71 – CommonsWare Apr 22 '14 at 18:13
  • No. In fact, the second time I try it, for some reason the final image gets rotated 90 degrees counter-clockwise, which of course throws off all of the marker placement. However, even if I explicitly rotate the final image to compensate before applying the markup, I still have the same basic problem. – eshayne Apr 22 '14 at 18:46
  • That may still wind up being a problem for you on other devices. Also, bear in mind that for what you want to work, the full-screen bleed would have to be turned off or taken into account in your calculation of where the user touches. With full-screen bleed on, the preview probably extends off the screen in two directions, based upon aspect ratio. – CommonsWare Apr 22 '14 at 18:49
  • I did notice that the final image aspect ratio (4:3) is not the same as the preview aspect ratio (16:9). This is what led me to use the Matrix.ScaleToFit.CENTER transformation as opposed to a simpler scaling. – eshayne Apr 22 '14 at 18:49
  • You may be better served not scaling the image, but rather calculating where your point is based on the final picture dimensions, full-bleed calculations, etc. If nothing else, it will be faster and more memory-efficient. – CommonsWare Apr 22 '14 at 19:03
  • Thank you - I have incorporated both suggestions into a revised saveImage listed above. It is a definite improvement, but still not as accurate as it should be. I'm beginning to suspect that the mapping of the preview image coordinates to the final image coordinates - even taking into account the change in aspect ratio - is not as straightforward as these calculations allow for, though I'm not sure what the correct mapping would be. – eshayne Apr 22 '14 at 20:06
  • Sorry, I haven't ever really thought about this particular problem, so I don't really have any further advice. Push come to shove, switch to using preview frames rather than `takePicture()`, so your final image *is* the preview image, and so there's no conversion. – CommonsWare Apr 22 '14 at 20:10

0 Answers0