Elkezdtem fejleszteni egy Android appot, közösen egy cimbivel. Az app kamerát használ, ami kapcsán nem kevés szívást okozott, hogy hogyan is kellene Java-ban leprogramozni legalábbis az alapfunkciókat, miszerint előnézeti kép, és annak feldolgozása lenne szükséges. Korántsem olyan egyszerű, amilyennek látszik...
Mivel én sem tudok igazán Java-ban programozni (ööö, mármint hogy másban sem ), sem Androidra nem fejlesztettem még, igen sokáig tartott egyáltalán működő példákat összekukázni. A kutatás során derült ki, hogy a Google CameraX API-ja elég sok mindent megold a kódoló kisiparos helyett. A Camera2 API-t is lehet használni (most azt gyűröm), de ha nem kell különösebben uralni a kamera paramétereit (fehéregyensúly, ISO, expókorrekció,stb.), akkor egyszerűbb a CameraX. Viszont a CameraX példái szinte mind Kotlinban íródtak, ami nyelv működésére a mai napig nem sikerült rájönnöm... Nem mellesleg persze a szokásos probléma, hogy a fellelhető példaprogramok egy része le se fordul, vagy túl bonyolult, a "hogyan kell ezt" kérdésre talált válaszok meg csak töredékek, ott kezdve, hogy "ezt hova is kéne a programban...".
Hogy ne kelljen másoknak is ezt végigjárni, gondoltam egy teljesen alap kódot kiteszek, ami ugyan nem szép, és nagyon nem teljes (például a kijelző forgatása nincs jól lekezelve, holott ott van rá az eljárás) de nálam tutibiztos lefordult, és úgy általában működik, van kép, lefut a képfeldolgozó.
Javaban, Manifest-tel, build.gradle-el, stb. együtt. Android 7 és 9 (mind a kettő Lineage)-en tesztelve, illetve egy 5.1-en, ami gyári Samsung, de az kicsit más kód volt.
MainActivity.java------------------------------------------------------------------------------------------------------------
package com.example.camera_base; //csomag neve, ezt mondjuk hagyd meg az új projectben :D
//importálunk egy rakás csomagot
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.ImageFormat;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.YuvImage;
import android.media.Image;
import android.os.Bundle;
import android.os.Looper;
import android.util.Log;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.ImageAnalysisConfig;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureConfig;
import androidx.camera.core.ImageProxy;
import androidx.core.app.ActivityCompat;
import android.view.WindowManager;
import android.widget.Toast;
import android.view.TextureView;
import androidx.camera.core.CameraX;
import androidx.camera.core.Preview;
import androidx.camera.core.PreviewConfig;
import androidx.core.content.ContextCompat;
import android.util.Rational;
import android.util.Size;
import android.view.Surface;
import android.view.ViewGroup;
import android.media.Image;
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
//-------------------------------------------------------------------MainActivity------------------------------------------
//A program UI
public class MainActivity extends AppCompatActivity {
static TextureView textureView; //ezen fog megjelenni a kamera képe
Context context;
//Az OnCreate a program indulásakor fut le.
//--------------------------------------------------------Oncreate and permission setup-------------------------------------
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
context=getApplicationContext();
//Jogkérés a kamerához - bizonyos Android verzó felett nem elég a Manifest-ben megadni, hanem meg kell kérni a programban is
ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.CAMERA}, 1);
while (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, 1);
}
//A kamera külön szálban fut; ez azért jó, mert hiba esetén nem dől össze az egész program
//--------------------CAMERA thread start --------------------------------------------------
textureView = findViewById(R.id.view_finder); //a layout-on megadott TextureView öszekapcsolása a programon belüli változóval
CamThread camThread = new CamThread(); //új kameraszál
camThread.start(); //elindul
}
//---------------------------------------------------------------------MainActivity vége
//------------------------------------------Camera handler --------------------------------------------
//based on https://camposha.info/android-camerax/
public class CamThread extends Thread { //ez maga a kamera szál
public void run() { //ez az eljárás fut le a szál indulásakor
Looper.prepare();
runOnUiThread(new Runnable() { //favágó módon megjelenít egy Toast-ot, hogy elindult a kamera
@Override
public void run() {
Toast.makeText(MainActivity.this, "Camera started", Toast.LENGTH_SHORT).show();
}
});
Log.v("Cam : ", "startCamera()"); //Le is loggolja
CameraX.unbindAll(); //lejön minden addigi use case-ról (a CameraX use case-ket kezel, meg ne kérdezzétek, mi az... :D )
//széleség, magasság begyűjtése a megjelenítős view-ről
Rational aspectRatio = new Rational(textureView.getWidth(), textureView.getHeight());
Size screen = new Size(textureView.getWidth(), textureView.getHeight()); //size of the screen
PreviewConfig.Builder builder = new PreviewConfig.Builder(); // ez építi fel a konfigot
/* //Ez az Extender érdekes dolog. A kamera tulajdonságait lehet vele állítgatni, de a CameraX fejlesztők szerint sem működhet, mert a CameraX felülírja, amit beállít...
new Camera2Config.Extender(builder)
.setCaptureRequestOption(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, 0)
.setCaptureRequestOption(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_OFF);*/
//beállítunk a preview konfig buildernek pár tulajdonságot... és építhet
builder.setTargetAspectRatio(aspectRatio);
builder.setTargetRotation(0);
//letrejön az új preview
Preview preview;
preview = new Preview(builder.build());
//ez intézi a preview frissítését, a végén meghívott updateTransform -al lehet pl. a forgatott nézetet intézni - a textureView-t pedig valamiért le kell szedni és újra feltenni az UI-ra, nem én találtam ki, így működik...
preview.setOnPreviewOutputUpdateListener(new Preview.OnPreviewOutputUpdateListener() {
@Override
public void onUpdated(Preview.PreviewOutput output) {
runOnUiThread(new Runnable() {
@Override
public void run() {
ViewGroup parent = (ViewGroup) textureView.getParent();
parent.removeView(textureView);
parent.addView(textureView, 0);
textureView.setSurfaceTexture(output.getSurfaceTexture());
updateTransform();
}
});
}
});
//Az ImageAnalysis az egyik értelme az egésznek - ez az, ami megkapja a preview-ből a képeket, és elemzi.
//Felépül a konfigja...
ImageAnalysisConfig.Builder builder1 = new ImageAnalysisConfig.Builder();
builder1.setImageReaderMode(ImageAnalysis.ImageReaderMode.ACQUIRE_LATEST_IMAGE);
builder1.setTargetResolution(new Size(1280, 720));
ImageAnalysisConfig iAC = builder1
.build();
//Itt is lehet opciókat megadni, pl. hogy mekkora felbontásban kapja a képeket, stb.
//.setTargetResolution(new Size(1280, 720))
//.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
//.build();
ImageAnalysis imageAnalyzer =
new ImageAnalysis(iAC);
//Ez maga az analyzer, ami lefut a preview képeire
imageAnalyzer.setAnalyzer(
new ImageAnalysis.Analyzer() {
@Override
public void analyze(@NonNull ImageProxy image, int rotationDegrees) {
Log.d("Analyzer:", "Analyzer called");
// ide jön a feldolgozás kódja. Mondjuk a bitmap kinyerése az ImageProx-ból szintén nem triviális
//Kép kinyerése bitmap formájában :
Image img = image.getImage();
Bitmap bmp=toBitmap(img);
}
});
//Képmentés
//Ezt én nem használtam, de a képmentés kb. így néz ki :
/* ImageCaptureConfig imageCaptureConfig = new ImageCaptureConfig.Builder()
.setTargetAspectRatio(new Rational(1, 1))
.setCaptureMode(ImageCapture.CaptureMode.MIN_LATENCY)
.build();
final ImageCapture imageCapture = new ImageCapture(imageCaptureConfig);*/
/* //És ha megvan az ImageCaptureConfig , akkor pl. egy Button Onclick eseményre ezt rá lehet tenni.
//Persze kell neki egy file, stb.
imageCapture.takePicture(file, new ImageCapture.OnImageSavedListener() {
@Override
public void onImageSaved(@NonNull File file) {
Toast.makeText(MainActivity.this, "Photo saved as " + file.getAbsolutePath(), Toast.LENGTH_SHORT).show();
}
@Override
public void onError(@NonNull ImageCapture.ImageCaptureError imageCaptureError, @NonNull String message, @Nullable Throwable cause) {
Toast.makeText(MainActivity.this, "Couldn't save photo: " + message, Toast.LENGTH_SHORT).show();
if (cause != null)
cause.printStackTrace();
}
});*/
//A lifecycle kell a CameraX-nek
//bind to lifecycle:
CameraX.bindToLifecycle(MainActivity.this, preview, imageAnalyzer); //az imageAnalyzer után kell még az imageCapture, ha menteni is akarsz
}
//Ez kell az imageProxy által szolgáltatott kép bitmap-é alakításához, amivel utána egyszerűbb dolgozni
//--------------https://stackoverflow.com/questions/56772967/converting-imageproxy-to-bitmap ----------------------------
private Bitmap toBitmap(Image image) {
Image.Plane[] planes = image.getPlanes();
ByteBuffer yBuffer = planes[0].getBuffer();
ByteBuffer uBuffer = planes[1].getBuffer();
ByteBuffer vBuffer = planes[2].getBuffer();
int ySize = yBuffer.remaining();
int uSize = uBuffer.remaining();
int vSize = vBuffer.remaining();
byte[] nv21 = new byte[ySize + uSize + vSize];
//U and V are swapped
yBuffer.get(nv21, 0, ySize);
vBuffer.get(nv21, ySize, vSize);
uBuffer.get(nv21, ySize + vSize, uSize);
YuvImage yuvImage = new YuvImage(nv21, ImageFormat.NV21, image.getWidth(), image.getHeight(), null);
ByteArrayOutputStream out = new ByteArrayOutputStream();
yuvImage.compressToJpeg(new Rect(0, 0, yuvImage.getWidth(), yuvImage.getHeight()), 75, out);
byte[] imageBytes = out.toByteArray();
return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
}
//Az updateTransform-ban lehet lekezelni a megváltozott képernyőgeometriát. pl. forgatás esetén. Nem csináltam meg rendesen, elfordul, de pl. nem méretezi a textureView-t az új képernyőméretekhez
private void updateTransform() {
//debughoz jól jön, ha loggol
//Log.d("Camera:", "updateTransform() : rotation : " + getRotation(context));
String orientation = getRotation(context);
if (orientation == "landscape") {
textureView.setRotation(-90);
}
if (orientation == "reverse landscape") {
textureView.setRotation(90);
}
if (orientation == "portrait") {
textureView.setRotation(0);
}
Matrix mx = new Matrix();
float w = textureView.getMeasuredWidth();
float h = textureView.getMeasuredHeight();
float cX = w / 2f;
float cY = h / 2f;
int rotationDgr;
int rotation = (int) textureView.getRotation();
switch (rotation) {
case Surface.ROTATION_0:
rotationDgr = 0;
break;
case Surface.ROTATION_90:
rotationDgr = 90;
break;
case Surface.ROTATION_180:
rotationDgr = 180;
break;
case Surface.ROTATION_270:
rotationDgr = 270;
break;
default:
return;
}
mx.postRotate((float) rotationDgr, cX, cY);
textureView.setTransform(mx);
textureView.setRotation(rotationDgr);
}
//Ez a getRotation eljárás meg azért kell, mert némely telefonon (Samsung...) a int orientation = getResources().getConfiguration().orientation; függvényre csak 0 vagy 90 jön vissza, függetlenül attól, merre lett elfordítva a teló... Nem szép megoldás, de nem volt jobb
//https://stackoverflow.com/questions/2795833/check-orientation-on-android-phone
public String getRotation(Context context) {
final int rotation = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getOrientation();
switch (rotation) {
case Surface.ROTATION_0:
return "portrait";
case Surface.ROTATION_90:
return "landscape";
case Surface.ROTATION_180:
return "reverse portrait";
default:
return "reverse landscape";
}
}
}
}
//---------------------------------------Camera end -----------------------------------------
Itt pedig a Manifest file : semmi különös, kamera megkövetelése, jogkérés
-------------------------------manifest
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.camera_base">
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
---------------------------------------------------------------------
A layout sem egy nagy valami, van rajta 1db. Textureview :
------------------activitiy_main.xml-------------
<?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">
<TextureView
android:id="@+id/view_finder"
android:layout_width="408dp"
android:layout_height="646dp"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.045"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.067"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
----------------------------------------------------------------
A build.gradle (:app) , az app szintű build konfig file már érdekesebb : ebben behúzzuk a CameraX libraryt. Ehhez szükséges az is, hogy a Java verzió 1.8 legyen, és a minimum SDK 23-as (5.1 Android, ha jól emlékszem?)
-----------------build.grade (app)------------
apply plugin: 'com.android.application'
android {
compileSdkVersion 29
buildToolsVersion "29.0.2"
defaultConfig {
applicationId "com.example.camera_base"
minSdkVersion 23
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
def cameraxVersion = "1.0.0-alpha02"
implementation "androidx.camera:camera-core:${cameraxVersion}"
implementation "androidx.camera:camera-camera2:${cameraxVersion}"
}
----------------------------------------------------------------
...szóval csak ennyi, és máris lehet képfeldolgozni Androidon...
(Update : lassú. Kellett egy appban folyamatosan a képet elemezni, és a Camera2-n "natívan" futó elemzés kb. ezerszer gyorsabb volt, azaz másodperceként 4-5x lefutott egy gyengusz telón, a CameraX-es meg jó ha 300ms-onként képes volt, szaggatott a preview, stb. Érdemes ezt is figyelembe venni, ha komolyabb képelemzésre van szükség.)
(Update2 : Samsung eszközökön az ImageAnalyzer által látott kép 90 fokkal balra döntött a preview-hez képest. Lehet kezelni, de ahhoz külön forgatórutin kell a telefon állása alapján. Camera2-vel ez nincs...)
Ha hiba van benne, szóljatok