【Android】カメラ画像編集:タッチで赤丸を描画し、合成して保存する完全版

Java / Android

AndroidのCameraXを使うと、カメラプレビューの上に独自の描画レイヤーを重ねることができます。 本記事では、プレビュー上をタッチすると赤丸を描画し、カメラ画像と合成して保存する機能を実装します。

さらに、座標ずれ問題・サイズ不一致・2回目以降の保存エラーなど、実装時に発生しやすい問題の対処方法もまとめています。

1. CameraXでカメラプレビューを表示する

java

private void startCamera() {
    ProcessCameraProvider cameraProviderFuture =
            ProcessCameraProvider.getInstance(requireContext());

    cameraProviderFuture.addListener(() -> {
        try {
            Preview preview = new Preview.Builder().build();
            imageCapture = new ImageCapture.Builder().build();

            ProcessCameraProvider cameraProvider = cameraProviderFuture.get();

            CameraSelector cameraSelector = new CameraSelector.Builder()
                    .requireLensFacing(CameraSelector.LENS_FACING_BACK)
                    .build();

            preview.setSurfaceProvider(previewView.getSurfaceProvider());

            cameraProvider.unbindAll();
            cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }, ContextCompat.getMainExecutor(requireContext()));
}

2. DrawingView:タッチした位置に赤丸を描画する

java

public class DrawingView extends View {

    private Paint paint;
    private List<Point> points = new ArrayList<>();

    public DrawingView(Context context, AttributeSet attrs) {
        super(context, attrs);
        paint = new Paint();
        paint.setColor(Color.RED);
        paint.setStyle(Paint.Style.FILL);
        paint.setAntiAlias(true);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        for (Point p : points) {
            canvas.drawCircle(p.x, p.y, 20, paint);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN ||
            event.getAction() == MotionEvent.ACTION_MOVE) {

            points.add(new Point(event.getX(), event.getY()));
            invalidate();
            return true;
        }
        return false;
    }

    public void reset() {
        points.clear();
        invalidate();
    }

    private static class Point {
        float x, y;
        Point(float x, float y) { this.x = x; this.y = y; }
    }
}

3. レイアウト:PreviewView と DrawingView を重ねる

xml

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.camera.view.PreviewView
        android:id="@+id/previewView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

    <faithit.image.ad_98list.ui.home.DrawingView
        android:id="@+id/drawingView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="@android:color/transparent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

4. ImageProxy → Bitmap に変換する

java

private Bitmap imageProxyToBitmap(ImageProxy image) {
    ByteBuffer buffer = image.getPlanes()[0].getBuffer();
    byte[] bytes = new byte[buffer.remaining()];
    buffer.get(bytes);
    return BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
}

5. カメラ画像と描画画像を合成して保存する

java

private void saveImage(Bitmap cameraBitmap) {

    Bitmap drawingBitmap = Bitmap.createBitmap(
            drawingView.getWidth(),
            drawingView.getHeight(),
            Bitmap.Config.ARGB_8888);

    Canvas drawingCanvas = new Canvas(drawingBitmap);
    drawingView.draw(drawingCanvas);

    Bitmap combinedBitmap = Bitmap.createBitmap(
            cameraBitmap.getWidth(),
            cameraBitmap.getHeight(),
            Bitmap.Config.ARGB_8888);

    Canvas combinedCanvas = new Canvas(combinedBitmap);
    combinedCanvas.drawBitmap(cameraBitmap, 0, 0, null);
    combinedCanvas.drawBitmap(drawingBitmap, 0, 0, null);

    String fileName = "IMG_" +
            new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.JAPAN)
                    .format(new Date()) + ".png";

    ContentValues values = new ContentValues();
    values.put(MediaStore.Images.Media.DISPLAY_NAME, fileName);
    values.put(MediaStore.Images.Media.MIME_TYPE, "image/png");
    values.put(MediaStore.Images.Media.RELATIVE_PATH,
            Environment.DIRECTORY_PICTURES + "/MyAppFolder");

    ContentResolver resolver = requireActivity().getContentResolver();
    Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);

    if (uri != null) {
        try (OutputStream out = resolver.openOutputStream(uri)) {
            combinedBitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    drawingView.reset();
}

6. よくある問題と対処方法(カメラ画像編集2の内容)

● タッチ座標がカメラ画像とずれる

PreviewView は内部で画像を拡大・縮小するため、 DrawingView の座標と一致しないことがあります。

→ PreviewView の幅・高さを使ってスケール補正する。

● DrawingView のサイズとカメラ画像のサイズが一致しない

保存時にズレる原因の多くはこれ。

→ DrawingView の Bitmap を cameraBitmap のサイズに合わせる。

● 2回目以降の保存で失敗する

原因:ImageProxy が close されていない、または DrawingView の状態が残っている。

java

image.close();
drawingView.reset();

● DrawingView が PreviewView を隠してしまう

DrawingView の背景を透明にする:

xml

android:background="@android:color/transparent"

6. これを利用したアプリ

【プライバシー保護】撮影と同時にデータを暗号化、写真閲覧アプリ。

撮影同時に暗号化: 毎日残したい写真(寝起き顔、体重計、体形、髪型)を撮影と同時にローカルに暗号化保存。閲覧機能も装備。

徹底したプライバシー保護: ファイルはローカル環境で暗号化・管理されます。クラウドに依存しません。

📥 広告 アプリ紹介「暗号化動画プレイヤー守」をPlayストアでチェックする

・価格:無料
・仕様:完全ローカル・通信なし
・広告:ファイル一覧下部のみ(写真・動画再生中はなし)

👉 Google Play ストアでダウンロード

▼ アプリのダウンロードはこちら(Google Playに移動します) Google Play で手に入れよう

コメント