Sibainu Relax Room

愛犬の柴犬とともに過ごす部屋

Android CameraX に Zoom 機能を追加

公園で遊ぶ子供たちの姿を真剣に見ている柴犬です。柴犬らしくキリっとしています。

朝の散歩で鶯が鳴いていました。しばらく柴犬は耳を傾けていました。

概要

SeekBar を使て画像の拡大を考えてみました。

バーのメモリを動かすことで倍率を変えることができましたので記録します。

タブレット用に作成した layout ですが、タブレットのスクリーンショットの調子が悪いのでスマホのスクリーンショットを載せてあります。

起動させたときのスクリーンショットです。初期値のメモリが 3 になっていますがミニマムの倍率です。この後、コードに seekbar.setProgress(0) を加え起動時にメモリが 0 になるようにしました。

倍率をマックスにした時のスクリーンショットです。

res/layout/activity_main.wml

copy

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.camera.view.PreviewView
        android:id="@+id/viewFinder"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@+id/guideline3"
        app:layout_constraintEnd_toStartOf="@+id/guideline"
        app:layout_constraintHorizontal_bias="1.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

    </androidx.camera.view.PreviewView>

    <Button
        android:id="@+id/bt_shutter"
        android:layout_width="100dp"
        android:layout_height="48dp"
        android:layout_marginTop="5dp"
        android:layout_marginEnd="5dp"
        android:text="@string/bt_shutter"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="@+id/guideline"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/bt_post"
        android:layout_width="100dp"
        android:layout_height="48dp"
        android:layout_marginEnd="5dp"
        android:text="@string/bt_post"
        app:layout_constraintBottom_toTopOf="@+id/scrollView2"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="@+id/guideline"
        app:layout_constraintTop_toBottomOf="@+id/bt_shutter" />

    <ScrollView
        android:id="@+id/scrollView2"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="1.0"
        app:layout_constraintStart_toStartOf="@+id/guideline"
        app:layout_constraintTop_toTopOf="@+id/guideline2"
        app:layout_constraintVertical_bias="1.0">

        <TextView
            android:id="@+id/tvinfo"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="TextView" />
    </ScrollView>

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintGuide_percent="0.85" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_percent="0.20" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_percent="0.88" />

    <SeekBar
        android:id="@+id/seekBar"
        style="@style/Widget.AppCompat.SeekBar.Discrete"
        android:layout_width="500dp"
        android:layout_height="wrap_content"
        android:max="10"
        android:progress="3"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/guideline"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/viewFinder" />

</androidx.constraintlayout.widget.ConstraintLayout>

こんな感じになります。

import/onCreate

onCreate に次のコードを追加しています。

        seekbar = findViewById(R.id.seekBar);
        seekbar.setProgress(0);

        changeListener cl = new changeListener();
        seekbar.setOnSeekBarChangeListener(cl);

それに伴い変数、import が変わっていますが、追記していませんので適宜修正してください。

copy

package org.sibainu.relax.room.cameracjavanas_call1;

import static java.lang.String.join;
import androidx.annotation.UiThread;
import androidx.annotation.WorkerThread;
import androidx.appcompat.app.AppCompatActivity;
//-----追加しています
import androidx.camera.core.Camera;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureException;
import androidx.camera.core.Preview;
import androidx.camera.core.resolutionselector.ResolutionSelector;
import androidx.camera.core.resolutionselector.ResolutionStrategy;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.camera.view.PreviewView;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import android.Manifest;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore;
import android.util.Log;
import android.util.Size;
import android.view.Surface;
import android.view.View;
import android.widget.Button;
//-----追加しています
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;
import com.google.common.util.concurrent.ListenableFuture;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class MainActivity extends AppCompatActivity {

    private final static String TAG = "camerax";
    private final static String JSON_TAG = "jason_process";
    private static String FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS";
    private ImageCapture imagecapture;
    private Uri imageuri;
    private ExecutorService cameraExecutor;
    private Button bt;
    private TextView tv;
    //-----追加しています
    private SeekBar seekbar;
    private Camera camera;
    private final int REQUEST_CODE_PERMISSIONS = 100;
    private final String[] REQUIRED_PERMISSIONS = {Manifest.permission.CAMERA,
            Manifest.permission.WRITE_EXTERNAL_STORAGE};
    private final String FTPUSERNAME = "□□□□";
    private final String FTPPASSWORD = "□□□□□□□□□□□□";
    private final String FTPSERVER = "192.168.□□.□□";
    private final String FTPDIRECTORY = "photo/□□□□□□□□□□□□/";
    private final String JSONSERVER = "https://www.sibainu.org/";
    private final String JSONNAME = "jsonusername";
    private final String JSONID = "jsonid";
    private final String JSONACCESS = "jsonaccess";
    private final String JSONKEY = "jsonkey";
    private ListenableFuture<ProcessCameraProvider> cameraProviderFuture;

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

        cameraExecutor = Executors.newSingleThreadExecutor();
        cameraProviderFuture = ProcessCameraProvider.getInstance(this);

        if (allPermissionsGranted()) {
            startCamera();
        } else {
            ActivityCompat.requestPermissions(
                    this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS
            );
        }

        clickListener ls = new clickListener();

        bt = findViewById(R.id.bt_shutter);
        bt.setOnClickListener(ls);

        bt = findViewById(R.id.bt_post);
        bt.setOnClickListener(ls);

        tv = findViewById(R.id.tvinfo);

        //-----ここから追加しています
        seekbar = findViewById(R.id.seekBar);
        seekbar.setProgress(0);

        changeListener cl = new changeListener();
        seekbar.setOnSeekBarChangeListener(cl);
    }

startCamera

形だけだった camera オブジェクトに実体を持たせます。

cameraProvider.bindToLifecycle(this, cameraSelector, preview, imagecapture);

camera = cameraProvider.bindToLifecycle(this, 
                     cameraSelector, 
                     preview, 
                     imagecapture, 
                     imageAnalysis);
camera.getCameraControl().setZoomRatio(0.1f);

に変更します。

copy

    private void startCamera() {
        ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ProcessCameraProvider.getInstance(this);

        cameraProviderFuture.addListener(() -> {
            try {
                // カメラのライフサイクルをアプリのライフサイクルに紐づけ
                ProcessCameraProvider cameraProvider = cameraProviderFuture.get();

                // プレビューの設定
                PreviewView previewView = findViewById(R.id.viewFinder);

                Preview preview = new Preview.Builder().build();
                preview.setSurfaceProvider(previewView.getSurfaceProvider());

                // イメージキャプチャーを受け取るオブジェクト
                imagecapture = new ImageCapture.Builder()
                        //.setTargetResolution(new Size(640,480))
                        .setResolutionSelector(
                                new ResolutionSelector.Builder()
                                        .setResolutionStrategy(
                                                new ResolutionStrategy(new Size(1000, 750),
                                                                       ResolutionStrategy.FALLBACK_RULE_CLOSEST_LOWER_THEN_HIGHER))
                                        .build()
                        )
                        .setTargetRotation(Surface.ROTATION_90)
                        .setJpegQuality(60)
                        .build();

                // 背面カメラをデフォルト
                CameraSelector cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA;
                //-----追加しています
                ImageAnalysis imageAnalysis = new ImageAnalysis.Builder()
                        .build();
                // 既存の廃棄
                cameraProvider.unbindAll();

                //-----カメラをビュー・イメージキャプチャーに紐づけ
                //cameraProvider.bindToLifecycle(this, cameraSelector, preview, imagecapture);
                camera = cameraProvider.bindToLifecycle(this, 
                                                        cameraSelector, 
                                                        preview, 
                                                        imagecapture, 
                                                        imageAnalysis);
                camera.getCameraControl().setZoomRatio(0.1f);

            } catch (ExecutionException | InterruptedException e) {
                Log.e(TAG, e.getLocalizedMessage(), e);
            }
        }, ContextCompat.getMainExecutor(this));
    }

class changeListener

SeekBar のリスナーをクラスで新規に作成しています。

copy

    private class changeListener implements SeekBar.OnSeekBarChangeListener {
        @Override
        public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
            //ドラッグされると呼ばれる
            camera.getCameraControl().setLinearZoom((float) progress/seekbar.getMax());
        }

        @Override
        public void onStartTrackingTouch(SeekBar seekBar) {
            //タッチされた時に呼ばれる
        }

        @Override
        public void onStopTrackingTouch(SeekBar seekBar) {
            //リリースされた時に呼ばれる
        }
    }

onResume

前の Activity の状況が残りますので Progress を 0 に初期化します。

copy

    @Override
    protected void onResume() {
        super.onResume();
        seekbar.setProgress(0);
        startCamera();
    }

    @Override
    public void onPause() {
        super.onPause();
        finish();
        android.os.Process.killProcess(android.os.Process.myPid());
    }

撮影したイメージ

メモリ 0 で撮影したイメージ

メモリ 10 で撮影したイメージ

計ってみると 4 倍ほど拡大となっています。画像はかなり劣化しています。

今回はここまでとします。