2024. április 20., szombat

Gyorskeresés

Android CameraX alapkód, hogy ne kellejen neked is napokig Google-özni...

Írta: | Kulcsszavak: android . java . camerax . kotlin . app . fejlesztés . programozás

[ ÚJ BEJEGYZÉS ]

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 :DDD ), 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... :D :W :Y :F 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 :)

Hozzászólások

(#1) Joci93


Joci93
senior tag

Azta, használhatnál Github repót vagy gist-et is :)

Én még csak Quasar-ban csináltam appot, elég sok mindenhez hozzáférést biztosít. (Ez nem natív, crossplatform-ra lett kitalálva, mint pl.: a react native.)

[ Szerkesztve ]

Furcsa, több ezer emberrel találkozunk és egyik sem fog meg igazán. Aztán megismerünk valakit, aki megváltoztatja az életünket. Örökre.

(#2) hcl válasza Joci93 (#1) üzenetére


hcl
félisten
LOGOUT blog

Gitre már van acocuntom, de ehhez szerintem még nem kell. Meg itt lehet mellé normálisan magyarázatot írni.
Ha van igény, a Bluetooth tákolmányt is feltehetem egyszer :D

Mutogatni való hater díszpinty

(#3) 0xmilan


0xmilan
addikt

A cimben es az utolso elotti mondatban van egy typo.

Illetve gist-ek ala is lehet kommentelni. (Normal repokhoz meg ott a README.md)

[ Szerkesztve ]

(#4) hcl válasza 0xmilan (#3) üzenetére


hcl
félisten
LOGOUT blog

Ja, tudom, csak nekem annyira átláthatatlanok a GIT és társai, meg a kommentjeik, Ez egy sokkal látogatottabb oldal, meg a Google-el is megtalálja, aki magyarul keres. Nem hiszem, hogy ennek sok magyar irodalma lenne.

Mutogatni való hater díszpinty

(#5) dabadab válasza hcl (#4) üzenetére


dabadab
titán

Én a helyedben azt csinálnám, hogy átraknám a kódot githubra, ide meg a szöveget meg egy linket rá, de persze én nem te vagyok :)

[ Szerkesztve ]

DRM is theft

(#6) hcl válasza dabadab (#5) üzenetére


hcl
félisten
LOGOUT blog

Lehet ezt sokkféleképpen :) Ecce' lehet, hogy az lesz.

Mutogatni való hater díszpinty

(#7) Oldman2


Oldman2
veterán

Köszi!

További hozzászólások megtekintése...
Copyright © 2000-2024 PROHARDVER Informatikai Kft.