Android CameraX に Zoom 機能を追加

公園で遊ぶ子供たちの姿を真剣に見ている柴犬です。柴犬らしくキリっとしています。
朝の散歩で鶯が鳴いていました。しばらく柴犬は耳を傾けていました。
概要
SeekBar を使て画像の拡大を考えてみました。
バーのメモリを動かすことで倍率を変えることができましたので記録します。
タブレット用に作成した layout ですが、タブレットのスクリーンショットの調子が悪いのでスマホのスクリーンショットを載せてあります。
起動させたときのスクリーンショットです。初期値のメモリが 3 になっていますがミニマムの倍率です。この後、コードに seekbar.setProgress(0) を加え起動時にメモリが 0 になるようにしました。

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

res/layout/activity_main.wml
<?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 が変わっていますが、追記していませんので適宜修正してください。
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);
を
ImageAnalysis imageAnalysis = new ImageAnalysis.Builder()
.build();
略
camera = cameraProvider.bindToLifecycle(this,
cameraSelector,
preview,
imageAnalysis,
imagecapture);
camera.getCameraControl().setZoomRatio(0.1f);
に変更します。
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,
imageAnalysis,
imagecapture);
camera.getCameraControl().setZoomRatio(0.1f);
} catch (ExecutionException | InterruptedException e) {
Log.e(TAG, e.getLocalizedMessage(), e);
}
}, ContextCompat.getMainExecutor(this));
}
class changeListener
SeekBar のリスナーをクラスで新規に作成しています。
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) {
//リリースされた時に呼ばれる
}
}
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 に初期化します。
@Override
protected void onResume() {
super.onResume();
seekbar.setProgress(0);
startCamera();
}
撮影したイメージ
メモリ 0 で撮影したイメージ

メモリ 10 で撮影したイメージ
計ってみると 4 倍ほど拡大となっています。画像はかなり劣化しています。

こんかいはここまでとします。