NDK Camera2 API를 사용하여 안드로이드 QR 스캐너를 구현하는 방법
예비 지식
Dynamsoft Barcode SDK for Android v8.8
SDK를 활성화하려면 valid license가 필요합니다.
Dynamsoft Android SDK의 C++ API 정보
Dynamsoft는 안드로이드 개발에 AAR 패키지를 제공합니다.우리는
libDynamsoftBarcodeReaderAndroid.so
에서 C++ 공유 라이브러리DynamsoftBarcodeReaderAndroid.aar
를 추출할 수 있다.헤더 파일은 Dynamsoft C++ SDK 가방에서 얻을 수 있습니다.Android NDK Camera2 샘플 시작
NDK 샘플 라이브러리는 NDK 개발을 위한 속성 강좌를 제공합니다.한번 봅시다camera2 samples.
camera 폴더에는 다음 두 가지 예가 있습니다.
본 컴퓨터 활동에서 어떻게 카메라 미리보기를 실현합니까
자바로 확장
Activity
된 안드로이드NativeActivity
를 만듭니다.public class CameraActivity extends NativeActivity {}
네이티브 활동의 JNI 엔트리 점을 작성하려면 다음과 같이 하십시오.
extern "C" void android_main(struct android_app* state) {
CameraEngine engine(state);
pEngineObj = &engine;
state->userData = reinterpret_cast<void*>(&engine);
state->onAppCmd = ProcessAndroidCmd;
// loop waiting for stuff to do.
while (1) {
// Read all pending events.
int events;
struct android_poll_source* source;
while (ALooper_pollAll(0, NULL, &events, (void**)&source) >= 0) {
// Process this event.
if (source != NULL) {
source->process(state, source);
}
// Check if we are exiting.
if (state->destroyRequested != 0) {
LOGI("CameraEngine thread destroy requested!");
engine.DeleteCamera();
pEngineObj = nullptr;
return;
}
}
pEngineObj->DrawFrame();
}
}
무한 순환에서 카메라 미리보기를 그립니다. 이 순환은 계속 호출됩니다.
pEngineObj->DrawFrame()
ImageReader
카메라 미리보기 프레임을 가져올 수 있습니다.void CameraEngine::DrawFrame(void) {
if (!cameraReady_ || !yuvReader_) return;
AImage* image = yuvReader_->GetNextImage();
if (!image) {
return;
}
ANativeWindow_acquire(app_->window);
ANativeWindow_Buffer buf;
if (ANativeWindow_lock(app_->window, &buf, nullptr) < 0) {
yuvReader_->DeleteImage(image);
return;
}
yuvReader_->DisplayImage(&buf, image);
ANativeWindow_unlockAndPost(app_->window);
ANativeWindow_release(app_->window);
}
Android TextureView 예
이 예에서는 먼저 Java를 사용하여 텍스쳐 뷰를 생성합니다.
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true">
<TextureView
android:id="@+id/texturePreview"
android:layout_gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</FrameLayout>
private void createTextureView() {
textureView_ = (TextureView) findViewById(R.id.texturePreview);
textureView_.setSurfaceTextureListener(this);
if (textureView_.isAvailable()) {
onSurfaceTextureAvailable(textureView_.getSurfaceTexture(),
textureView_.getWidth(), textureView_.getHeight());
}
}
public void onSurfaceTextureAvailable(SurfaceTexture surface,
int width, int height) {
createNativeCamera();
resizeTextureView(width, height);
surface.setDefaultBufferSize(cameraPreviewSize_.getWidth(),
cameraPreviewSize_.getHeight());
surface_ = new Surface(surface);
onPreviewSurfaceCreated(ndkCamera_, surface_);
scheduleTask();
}
무늬 보기를 초기화할 때, 그 곡면은 본 컴퓨터 코드에 연결된 카메라의 출력에 귀속됩니다.extern "C" JNIEXPORT void JNICALL
Java_com_sample_textureview_ViewActivity_onPreviewSurfaceCreated(
JNIEnv *env, jobject instance, jlong ndkCameraObj, jobject surface) {
ASSERT(ndkCameraObj && (jlong)pEngineObj == ndkCameraObj,
"NativeObject should not be null Pointer");
CameraAppEngine *pApp = reinterpret_cast<CameraAppEngine *>(ndkCameraObj);
pApp->CreateCameraSession(surface);
pApp->StartPreview(true);
}
camera_->CreateSession(ANativeWindow_fromSurface(env_, surface));
void NDKCamera::CreateSession(ANativeWindow* previewWindow) {
requests_[PREVIEW_REQUEST_IDX].outputNativeWindow_ = previewWindow;
requests_[PREVIEW_REQUEST_IDX].template_ = TEMPLATE_PREVIEW;
CALL_CONTAINER(create(&outputContainer_));
for (auto& req : requests_) {
if (!req.outputNativeWindow_) continue;
ANativeWindow_acquire(req.outputNativeWindow_);
CALL_OUTPUT(create(req.outputNativeWindow_, &req.sessionOutput_));
CALL_CONTAINER(add(outputContainer_, req.sessionOutput_));
CALL_TARGET(create(req.outputNativeWindow_, &req.target_));
CALL_DEV(createCaptureRequest(cameras_[activeCameraId_].device_,
req.template_, &req.request_));
CALL_REQUEST(addTarget(req.request_, req.target_));
}
captureSessionState_ = CaptureSessionState::READY;
CALL_DEV(createCaptureSession(cameras_[activeCameraId_].device_,
outputContainer_, GetSessionListener(),
&captureSession_));
}
TextureView 및 ImageReader 결합
QR 스캐너를 실현하기 위해
TextureView
를 사용하여 카메라 미리보기를 표시하고 ImageReader
를 사용하여 카메라 프레임을 가져오고 디코딩합니다.어떻게 동시에 TextureView
와 ImageReader
로 카메라 프레임을 출력합니까?Java 코드를 살펴보겠습니다.
mCameraDevice.createCaptureSession(Arrays.asList(surface, mImageReader.getSurface()),
new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
// The camera is already closed
if (null == mCameraDevice) {
return;
}
mCaptureSession = cameraCaptureSession;
startPreview();
}
@Override
public void onConfigureFailed(
@NonNull CameraCaptureSession cameraCaptureSession) {
showToast("Failed");
}
}, null
);
두 개의 표면이 있는 카메라 세션을 만드는 자바 코드에 따르면 하나는 TextureView
, 다른 하나는 ImageReader
에서 우리는 camera_manager.cpp
에서 해당하는 본체 코드를 작성할 수 있다. 아래와 같다.void NDKCamera::CreateSession(ANativeWindow* textureViewWindow, ANativeWindow* imgReaderWindow) {
auto& req = requests_[PREVIEW_REQUEST_IDX];
req.outputNativeWindow_ = textureViewWindow;
req.yuvWindow = imgReaderWindow;
req.template_ = TEMPLATE_PREVIEW;
ACaptureSessionOutputContainer_create(&outputContainer_);
CALL_DEV(createCaptureRequest(cameras_[activeCameraId_].device_,
req.template_, &req.request_));
// Add the texture view surface to the container
ANativeWindow_acquire(req.outputNativeWindow_);
CALL_OUTPUT(create(req.outputNativeWindow_, &req.sessionOutput_));
CALL_CONTAINER(add(outputContainer_, req.sessionOutput_));
CALL_TARGET(create(req.outputNativeWindow_, &req.target_));
CALL_REQUEST(addTarget(req.request_, req.target_));
// Add the image reader surface to the container
ANativeWindow_acquire(req.yuvWindow);
CALL_OUTPUT(create(req.yuvWindow, &req.yuvOutput));
CALL_CONTAINER(add(outputContainer_, req.yuvOutput));
CALL_TARGET(create(req.yuvWindow, &req.yuvTarget));
CALL_REQUEST(addTarget(req.request_, req.yuvTarget));
captureSessionState_ = CaptureSessionState::READY;
ACameraDevice_createCaptureSession(cameras_[activeCameraId_].device_,
outputContainer_, GetSessionListener(),
&captureSession_);
}
그런 다음 ACameraCaptureSession_setRepeatingRequest()
를 사용하여 미리 보기를 시작할 수 있습니다.void NDKCamera::StartPreview(bool start) {
if (start) {
ACaptureRequest* requests[] = { requests_[PREVIEW_REQUEST_IDX].request_};
ACameraCaptureSession_setRepeatingRequest(captureSession_, nullptr, 1,
requests,
nullptr);
} else if (!start && captureSessionState_ == CaptureSessionState::ACTIVE) {
ACameraCaptureSession_stopRepeating(captureSession_);
}
}
나는 그것이 잘 작동할 수 있을 것이라고 생각했지만, 사실은 그렇지 않았다. 코드를 실행할 때, 화면은 카메라 프레임을 받은 후 동결되었다.다음은 로그입니다.2021-12-14 08:42:20.316 24536-24556/com.sample.textureview D/ACameraDevice: Device error received, code 3, frame number 13, request ID 0, subseq ID 0
2021-12-14 08:42:21.319 24536-24556/com.sample.textureview D/ACameraDevice: Device error received, code 3, frame number 14, request ID 0, subseq ID 0
2021-12-14 08:42:22.321 24536-24584/com.sample.textureview D/ACameraDevice: Device error received, code 3, frame number 15, request ID 0, subseq ID 0
2021-12-14 08:42:23.323 24536-24584/com.sample.textureview D/ACameraDevice: Device error received, code 3, frame number 16, request ID 0, subseq ID 0
2021-12-14 08:42:24.325 24536-24556/com.sample.textureview D/ACameraDevice: Device error received, code 3, frame number 17, request ID 0, subseq ID 0
2021-12-14 08:42:25.328 24536-24584/com.sample.textureview D/ACameraDevice: Device error received, code 3, frame number 18, request ID 0, subseq ID 0
2021-12-14 08:42:26.330 24536-24584/com.sample.textureview D/ACameraDevice: Device error received, code 3, frame number 19, request ID 0, subseq ID 0
캡처 세션이 여러 요청을 지원하기 때문에 두 개의 곡면을 각각 두 개의 요청에 넣을 수 있습니까?FPS의 급격한 감소를 야기하는 실험을 수행했습니다.yuvReader_ = new ImageReader(&compatibleCameraRes_, AIMAGE_FORMAT_YUV_420_888);
camera_->CreateSession(ANativeWindow_fromSurface(env_, surface), yuvReader_->GetNativeWindow());
void NDKCamera::CreateSession(ANativeWindow* previewWindow, ANativeWindow* yuvWindow) {
// Create output from this app's ANativeWindow, and add into output container
requests_[PREVIEW_REQUEST_IDX].outputNativeWindow_ = previewWindow;
requests_[PREVIEW_REQUEST_IDX].template_ = TEMPLATE_PREVIEW;
requests_[YUV_REQUEST_IDX].outputNativeWindow_ = yuvWindow;
requests_[YUV_REQUEST_IDX].template_ = TEMPLATE_PREVIEW;
CALL_CONTAINER(create(&outputContainer_));
for (auto& req : requests_) {
if (!req.outputNativeWindow_) continue;
ANativeWindow_acquire(req.outputNativeWindow_);
CALL_OUTPUT(create(req.outputNativeWindow_, &req.sessionOutput_));
CALL_CONTAINER(add(outputContainer_, req.sessionOutput_));
CALL_TARGET(create(req.outputNativeWindow_, &req.target_));
CALL_DEV(createCaptureRequest(cameras_[activeCameraId_].device_,
req.template_, &req.request_));
CALL_REQUEST(addTarget(req.request_, req.target_));
}
// Create a capture session for the given preview request
captureSessionState_ = CaptureSessionState::READY;
CALL_DEV(createCaptureSession(cameras_[activeCameraId_].device_,
outputContainer_, GetSessionListener(),
&captureSession_));
}
void NDKCamera::StartPreview(bool start) {
if (start) {
ACaptureRequest* requests[] = { requests_[PREVIEW_REQUEST_IDX].request_, requests_[YUV_REQUEST_IDX].request_};
CALL_SESSION(setRepeatingRequest(captureSession_, nullptr, 2,
requests,
nullptr));
} else if (!start && captureSessionState_ == CaptureSessionState::ACTIVE) {
ACameraCaptureSession_stopRepeating(captureSession_);
}
}
나는 StackOverflow에 이 문제를 만들었다.절충된 해결 방안은 두 가지 요청을 만드는 것이다. 하나는 미리 보기 (
TextureView
이고 다른 하나는 정지 포획 ImageReader
이다.void NDKCamera::CreateSession(ANativeWindow* previewWindow,
ANativeWindow* jpgWindow, bool manualPreview,
int32_t imageRotation) {
requests_[PREVIEW_REQUEST_IDX].outputNativeWindow_ = previewWindow;
requests_[PREVIEW_REQUEST_IDX].template_ = TEMPLATE_PREVIEW;
requests_[JPG_CAPTURE_REQUEST_IDX].outputNativeWindow_ = jpgWindow;
requests_[JPG_CAPTURE_REQUEST_IDX].template_ = TEMPLATE_STILL_CAPTURE;
CALL_CONTAINER(create(&outputContainer_));
for (auto& req : requests_) {
if (!req.outputNativeWindow_) continue;
ANativeWindow_acquire(req.outputNativeWindow_);
CALL_OUTPUT(create(req.outputNativeWindow_, &req.sessionOutput_));
CALL_CONTAINER(add(outputContainer_, req.sessionOutput_));
CALL_TARGET(create(req.outputNativeWindow_, &req.target_));
CALL_DEV(createCaptureRequest(cameras_[activeCameraId_].device_,
req.template_, &req.request_));
CALL_REQUEST(addTarget(req.request_, req.target_));
}
// Create a capture session for the given preview request
captureSessionState_ = CaptureSessionState::READY;
CALL_DEV(createCaptureSession(cameras_[activeCameraId_].device_,
outputContainer_, GetSessionListener(),
&captureSession_));
if (jpgWindow) {
ACaptureRequest_setEntry_i32(requests_[JPG_CAPTURE_REQUEST_IDX].request_,
ACAMERA_JPEG_ORIENTATION, 1, &imageRotation);
}
}
Java에서 이미지 캡처를 트리거하는 버튼을 만들었습니다. 나중에 QR코드를 디코딩하는 데 사용합니다.<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true">
<TextureView
android:id="@+id/texturePreview"
android:layout_gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textSize="10pt"
android:textColor="@android:color/white"/>
<ImageButton
android:id="@+id/takePhoto"
android:layout_width="80dp"
android:layout_height="60dp"
android:layout_gravity="bottom|center"
android:src = "@drawable/camera_button"
android:background="@android:color/transparent"
android:adjustViewBounds ="true"
android:scaleType="fitCenter"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="60dp"/>
</FrameLayout>
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
onWindowFocusChanged(true);
setContentView(R.layout.activity_main);
textView = findViewById(R.id.textView);
_takePhoto = (ImageButton)findViewById(R.id.takePhoto);
_takePhoto.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
scanPhoto(ndkCamera_);
}
});
if (isCamera2Device()) {
RequestCamera();
} else {
Log.e("CameraSample", "Found legacy camera device, this sample needs camera2 device");
}
}
이미지가 캡처되면 image_reader.cpp
에서 다음 콜백 함수가 호출됩니다.void ImageReader::ImageCallback(AImageReader *reader) {
int32_t format;
media_status_t status = AImageReader_getFormat(reader, &format);
ASSERT(status == AMEDIA_OK, "Failed to get the media format");
if (format == AIMAGE_FORMAT_JPEG) {
AImage *image = nullptr;
media_status_t status = AImageReader_acquireNextImage(reader, &image);
ASSERT(status == AMEDIA_OK && image, "Image is not available");
}
else if (format == AIMAGE_FORMAT_YUV_420_888) {
}
}
Android 프로젝트에서 타사 공유 라이브러리 링크
우리는
DynamsoftBarcodeReader.h
, DynamsoftCommon.h
및 libDynamsoftBarcodeReaderAndroid.so
를 JNI 폴더로 복사합니다.CMakeLists.txt
에서 다음 행 링크 공유 라이브러리를 추가합니다.target_include_directories(camera_textureview PRIVATE ${COMMON_SOURCE_DIR} ./dbr)
add_library(DynamsoftBarcodeReaderAndroid
SHARED
IMPORTED)
set_target_properties( # Specifies the target library.
DynamsoftBarcodeReaderAndroid
# Specifies the parameter you want to define.
PROPERTIES IMPORTED_LOCATION
# Provides the path to the library you want to import.
${CMAKE_CURRENT_SOURCE_DIR}/dbr/libDynamsoftBarcodeReaderAndroid.so)
target_link_libraries(camera_textureview dl android log m camera2ndk mediandk DynamsoftBarcodeReaderAndroid)
참고: set_target_properties
에서 상대 경로를 사용하지 마십시오.2차원 스캐너를 만들다
우리는
DecodeImage()
에서 image_reader.cpp
함수를 만들었다.void ImageReader::DecodeImage(AImage* image) {
int planeCount;
media_status_t status = AImage_getNumberOfPlanes(image, &planeCount);
ASSERT(status == AMEDIA_OK && planeCount == 1,
"Error: getNumberOfPlanes() planeCount = %d", planeCount);
uint8_t *data = nullptr;
int len = 0;
AImage_getPlaneData(image, 0, &data, &len);
DBR_DecodeFileInMemory(barcode_reader, data, len, "");
TextResultArray *handler = NULL;
DBR_GetAllTextResults(barcode_reader, &handler);
TextResult **results = handler->results;
int count = handler->resultsCount;
std::string out = "No QR Detected";
if (count > 0)
{
out = "";
for (int index = 0; index < count; index++)
{
out += "Index: " + std::to_string(index) + "\n";;
out += "Barcode format: " + std::string(results[index]->barcodeFormatString) + "\n";
out += "Barcode value: " + std::string(results[index]->barcodeText) + "\n";
out += "\n";
}
DBR_FreeTextResults(&handler);
}
if (callback_) {
LOGI("QR detection %s ", out.c_str());
callback_(callbackCtx_, out.c_str());
}
AImage_delete(image);
}
void ImageReader::ImageCallback(AImageReader *reader) {
int32_t format;
media_status_t status = AImageReader_getFormat(reader, &format);
ASSERT(status == AMEDIA_OK, "Failed to get the media format");
if (format == AIMAGE_FORMAT_JPEG) {
AImage *image = nullptr;
media_status_t status = AImageReader_acquireNextImage(reader, &image);
ASSERT(status == AMEDIA_OK && image, "Image is not available");
std::thread decodeQRHandler(&ImageReader::DecodeImage, this, image);
decodeQRHandler.detach();
}
}
또한 QR코드 결과를 표시하기 위해 camera_engine.cpp
에 콜백 함수를 등록했습니다.void CameraAppEngine::OnQRDetected(const char *result) {
JNIEnv* env;
jvm->AttachCurrentThread(&env, nullptr);
jmethodID methodID = env->GetMethodID(globalClass, "onQRDetected", "(Ljava/lang/String;)V");
jstring javaName = env->NewStringUTF(result);
env->CallVoidMethod(javaInstance_, methodID, javaName);
jvm->DetachCurrentThread();
}
void CameraAppEngine::CreateCameraSession(jobject surface) {
surface_ = env_->NewGlobalRef(surface);
jpgReader_ = new ImageReader(&compatibleCameraRes_, AIMAGE_FORMAT_JPEG);
jpgReader_->SetPresentRotation(GetCameraSensorOrientation(ACAMERA_LENS_FACING_BACK));
jpgReader_->RegisterCallback(this, [this](void* ctx, const char* str) -> void {
reinterpret_cast<CameraAppEngine* >(ctx)->OnQRDetected(str);
});
camera_->CreateSession(ANativeWindow_fromSurface(env_, surface), jpgReader_->GetNativeWindow(), false, GetCameraSensorOrientation(ACAMERA_LENS_FACING_BACK));
}
이제 우리는 사진을 찍어서 QR코드를 디코딩할 수 있다.만약 우리가 QR코드를 실시간으로 디코딩하고 싶다면?정답:Timer timer = new Timer();
public void scheduleTask() {
timer.schedule(new TimerTask() {
@Override
public void run() {
if (ndkCamera_ != 0 && surface_ != null) {
scanPhoto(ndkCamera_);
}
}
}, 0);
}
public void onQRDetected(String result) {
final String content = result;
runOnUiThread(new Runnable() {
@Override
public void run() {
textView.setText(content);
scheduleTask();
}
});
}
자바에서 C++로 Camera2 코드를 마이그레이션하는 것이 좋습니까?
전체 과정은 제가camera2 API가 엔진 뚜껑 아래에서 어떻게 작동하는지 이해하는 데 도움을 주었지만 제 답은
No
입니다.NDK Camera2 API를 사용한 경험이 기대에 미치지 못했습니다.그것은 심지어 자바API를 사용하는 것보다 더 나쁘다.실제로 자바에서 ImageReader
대상을 만들 때 관련 메모리는 본 컴퓨터 더미에 분배됩니다.따라서 이미지 버퍼를 가져오고 본 컴퓨터 코드로 이미지 처리 API를 호출하면 추가 메모리 복사와 GC가 없습니다.관련 기사
만약 당신이 안드로이드 C++ 프로그래밍에 관심이 있다면, 당신도 이 글을 읽을 수 있습니다. How to Build a QR Code Scanner for Windows and Android with Qt QML
도구책
소스 코드
https://github.com/yushulx/ndk-native-camera2-
Reference
이 문제에 관하여(NDK Camera2 API를 사용하여 안드로이드 QR 스캐너를 구현하는 방법), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/yushulx/how-to-implement-android-qr-scanner-with-ndk-camera2-api-2ilc텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)