Android에서 YOLO5 돌려보기 — 1편

Hyunkil Kim
16 min readAug 1, 2022

--

이 글은 예제 프로젝트ObjectDetection 프로젝트를 기반으로 작성하였습니다. README 번역본을 보려면 여기를 참고하세요.

안드로이드에서 YOLO 모델로 객체 탐지를 간단하게 돌려 보고자 하는 분들이 많습니다. 네, 사실 대학생들이 적당히 프로젝트나 졸업작품으로 때우기 좋은 주제라 그런 것 같습니다. 아무튼 저 같은 경우에는 스마트폰에서 바로 작동시킬 수 있는 모델들에 관심이 가서 검색하던 도중 괜찮은 예제 프로젝트를 발견했습니다. 파이토치 기반으로 생성한 모델을 이용해서 안드로이드에서 돌릴 수 있는 여러 가지 예제 앱들이 잘 구현이 되어 있더군요. 혼자만 알고 있기에 아까워서 README를 번역하기도 했지만, 좀 더 자세히 예제 프로젝트를 분석하는 글도 쓰고 싶어 이렇게 작성하게 되었습니다.

어느정도 안드로이드 앱 개발에 익숙한 분들은 예제 코드 동작 원리같은 것들을 바로 이해하실 수 있습니다. 하지만 인공지능을 공부하고자 하는 분들은 안드로이드 앱의 구조나 동작 관련해서 잘 모르는 분들이 있습니다. 그래서 기본적인 안드로이드 앱 관련 사항은 1편에 작성하도록 하고, YOLO 모델 구동 관련해서는 2편에 작성할 예정입니다. 기본적인 내용을 숙지하고 계시다면 나중에 쓸 2편을 참고하세요.

README.md

기본적인 프로젝트의 설명이 적혀 있는 파일입니다. 깃허브 프로젝트의 첫 페이지가 이 파일을 그대로 보여줍니다. 문제는 파일 이름대로 이 문서를 읽는 것이 좋은데 대충 훑어보는 사람이 많습니다(저도 그렇네요…). 대략적으로 어떤 프로젝트인지, 빌드는 어떻게 하는지, 어떤 라이브러리를 쓰는지, 동작을 제대로 하면 어떤 화면이 뜨는지 등 프로젝트 관련해서 전반적인 내용이 씌여 있습니다. 먼저 앱을 실행해 보거나 소스코드를 보는 것도 도움이 되지만, 잘 작성된 README 파일을 읽는 것도 프로젝트 파악에 큰 도움이 됩니다.

우리가 확인해야 되는 부분은 Prepare the model(모델 준비하기) 섹션입니다. 미리 준비된 모델 파일인 yolov5s.torchscript.pt 파일을 다운로드해서 assets 폴더에 저장해 둡니다. 앱을 실행시켰을 때 이런 기본적인 것들을 빠뜨려서 이상한 동작을 하는 경우가 많으니 조심하도록 합시다.

AndroidManifest.xml

안드로이드 프로젝트를 확인할 때 제일 처음 보는 파일이 manifestgradle 파일입니다. 이 두 종류의 파일만 보더라도 대략적으로 어떤 기능을 사용하고, 그런 기능을 사용하기 위해 어떤 라이브러리를 사용하는지 알 수 있습니다.

먼저 manifest부터 보시죠.

app 폴더 내부에 있습니다.

앱이 특정한 기능을 사용할 때 권한이 필요하다면 manifest 파일에 선언을 해 둬야 합니다. 이런 것만 확인하더라도 어떤 기능을 사용하는지 알 수 있고, 대략적으로 어떤 식으로 구현이 되어 있는지도 알 수 있습니다. 먼저 권한 부분부터 볼까요.

카메라와 외부 저장장치 접근 권한

<uses-permission> 태그를 보면 카메라 사용과 외부 저장장치 READ 권한 두 가지를 사용한다고 선언한 것을 볼 수 있습니다. 여기 선언한 권한은 ‘앱에서 이런 기능을 사용할 것이다’ 라고 선언만 한 것입니다. 안드로이드 버전에 따라 사용자가 실제로 카메라나 외부 저장 장치 접근을 허용한다는 팝업에서 승낙을 해야 해당 기능을 사용할 수 있기도 합니다(최신 버전들은 다 이렇습니다).

앱이 어떤 권한을 쓰는지 알았으니 이제 어떤 화면을 쓰는지도 알아봅시다.

앱이 사용하는 화면 정보가 있네요

<application> 태그 내부에는 앱의 이름, 아이콘 등 앱에 대한 대략적인 정보를 기입할 수 있습니다. 우리가 알고 싶은 것들은 이 앱에서 어떤 화면을 사용하는지이기 때문에, <application > 태그는 대략적으로 어떤 것들을 설정할 수 있는지만 보고 넘어가도 됩니다.

이제 이 앱에서 어떤 화면을 쓰는지 확인해 볼까요. 안드로이드에서는 개별 화면을 activity, 액티비티라고 부릅니다. manifest 내부에 <activity>라고 화면을 선언해 주면 앱에서 선언한 화면을 액티비티라는 형태로 사용할 수 있습니다. <application >과 마찬가지로 여러가지 설정을 할 수도 있습니다. 간략하게 보자면 이름부터 화면 방향, 화면 설정 갱신을 액티비티에서 직접 다룰 것인지 관련된 설정이 되어 있습니다. 화면이 portrait로 선언되어 있으니 세로 방향의 화면은 지원하지 않는구나 정도만 알아두시면 될 것 같네요.

설정할 수 있는 태그는 생각보다 엄청나게 많습니다. <activity> 관련된 설정은 여기 참고하시면 됩니다. 사용하고자 하는 목적에 따라 각 화면 설정을 여러가지로 고민해 보는 것도 좋겠군요.

build.gradle, build.gradle

그럼 이제 안드로이드에서 앱을 빌드하기 위한 gradle 설정 파일을 보도록 하겠습니다. Maven이나 Ant와 같은 빌드 시스템과 마찬가지로 이런 빌드 파일을 보는 것 만으로도 대략적으로 어떤 라이브러리를 사용하고 있는지, 그 버전은 무엇인지도 알 수 있습니다.

한가지 명심하셔야 하는 것은 프로젝트 전체 수준에서의 gradle 파일과 그 프로젝트 내부의 개별 앱에 대한 파일이 개별적으로 존재하고 있다는 것입니다. 프로젝트 전체 gradle 파일은 앱이나 모듈간의 의존관계를 정의하기도 하고, 의존성을 어디에서 가져오는지 등 전체적인 프로젝트를 관리하는 역할을 한다면 개별 앱(정확히는 모듈)의 gradle 파일은 해당 앱에서만 사용하는 라이브러라니 의존성, 빌드 job이나 task를 설정한다고 생각하시면 됩니다. 커다란 조직에서 함께 지켜야 되는 업무 규칙과 개별 팀에서 따라야 되는 규칙 정도로 생각하시면 편하겠군요.

여기에서 프로젝트 수준에서의 gradle 파일은 넘어가도록 하겠습니다. 빌드에 필요한 gradle의 버전이 표시되어 있긴 한데, 특별히 중요한 내용은 아닙니다 :) 앱의 gradle 파일 보시죠.

app 폴더 내부에 존재하는 build.gradle

앱의 컴파일에 필요한 SDK부터 시작해서 다양한 설정을 할 수 있는 것을 볼 수 있습니다. 그 중에서도 우리가 봐야 하는 것은 dependencies부분입니다. 앱에서 사용하고자 하는 라이브러리 중 안드로이드에서 기본적으로 지원해주지 않는 의존성은 여기에 선언을 해 줘야 됩니다.

먼저 implementation fileTree(dir: “libs”, include: [“*.jar”]) 이런 것들이 보이네요. 눈치채신 분들도 계시겠지만 파일 시스템에서 어떤 폴더의 jar 파일을 참조해야 되는지 선언한 부분입니다. jar 파일로 배포되는 라이브러리나 모듈의 경우 libs 폴더에서 받아올 것이라는 선언으로, 혹시라도 jar 파일을 가져와서 사용해야 되는 경우에 유용하겠군요.

testImplementation, androidTestImplementation 과 같은 것들은 이름에서도 짐작하시겠지만 테스트 관련된 의존성을 선언하는 부분입니다. 굳이 여기서 짚고 넘어갈 필요는 없겠지만, jUnit이나 espresso를 이용해서 테스트를 구현하면 되겠구나 하는 정도만 보고 가면 됩니다.

이제 슬슬 중요한 부분들이 나오기 시작합니다. androidx.xxxx 이런 라이브러리들은 안드로이드의 jetpack에서 제공하는 라이브러리들입니다. 모든 안드로이드 버전에서 동일한 최신 기능을 지원하기 위한 라이브러리들이며, 자세한 사항은 여기에서 확인 가능합니다. 어떤 기능을 사용하는지 볼까요.

implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
def camerax_version = "1.0.0-alpha05"
implementation "androidx.camera:camera-core:$camerax_version"
implementation "androidx.camera:camera-camera2:$camerax_version"

윗부분의 appcompat 및 constraintlayout은 화면 구성을 위한 액티비티를 보조하기 위한 라이브러리입니다. 그 밑의 camera x는 카메라를 사용한 기능 개발을 쉽게 하기 위한 라이브러리입니다. manifest에서 선언한 카메라 권한을 이 라이브러리가 이용한다고 보면 되겠군요.

한가지 명심해야 되는 것은 Android Studio가 최신 camerax_version이 존재한다고 suggestion을 해 주는데, 최신 버전을 사용하면 빌드가 되지 않습니다. jetpack이 계속해서 개발이 되고 있고 신기능이 최신 버전으로 업데이트하면 현재 소스코드가 라이브러리를 잘못 사용하고 있다고 컴파일이 되지 않기에 조심해야겠죠.

이제 드디어 파이토치 관련된 라이브러리가 등장합니다.

implementation 'org.pytorch:pytorch_android_lite:1.10.0'
implementation 'org.pytorch:pytorch_android_torchvision_lite:1.10.0'

파이토치 관련된 기능을 사용하기 위한 라이브러리들이 기술되어 있는 부분입니다. camera x도 라이브러리 버전이 맞지 않으면 빌드가 되지 않을 수 있다고 말씀드렸다시피 파이토치 라이브러리들도 최신 버전을 사용하면 동작이 되지 않을 수도 있습니다. 참조하는 라이브러리 버전이 꼬이면 정말 이상한 곳에서 기묘한 문제들이 발생할 수도 있습니다. 그래서 예제 프로젝트의 동작을 확인할 때에는 돌아가는 것을 확인한 다음 라이브러리들을 하나하나씩 최신 버전으로 교체해 가며 최신 기능을 사용하도록 하는 것도 좋은 방법입니다.

이제 드디어 안드로이드 소스코드를 볼 차례입니다. 코틀린이 아닌 자바 버전이라 아쉽긴 하네요.

BaseModuleActivity

생각보다 액티비티가 많네요

manifest에선 선언이 되어 있지 않은 액티비티가 몇 개 보입니다. 부모 클래스로 선언해 놓고, 실제로 보여주는 액티비티만 manifest에 선언을 해서 사용한다고 생각하면 됩니다. 그 중 제일 기본이 되는 BaseModuleActivity를 보도록 합시다.

클래스 변수부터 살펴보도록 하겠습니다. 안드로이드에서 비동기 처리나 멀티쓰레딩을 위한 방법 중 하나인 핸들러를 사용했네요. 핸들러 쓰레드를 하나 생성하고, 액티비티가 화면이 뜰 때 핸들러 쓰레드를 시작하고 화면이 사라질 때 쓰레드를 정지하는 처리를 합니다.

좋은 비동기 처리 방식은 아니에요

안드로이드에서는 main 쓰레드가 화면을 그려주는 역할을 해서 시간이 많이 걸리는 작업은 별도 쓰레드에서 처리를 해 주는 것이 좋습니다. 여기에서도 핸들러 전용 쓰레드를 만들어서 실행하고, 화면이 나타나고 사라지는 생명주기에 따라 알아서 쓰레드 동작을 제어하기 위한 처리를 해 뒀다 정도로 이해하시면 됩니다. 별도로 설명을 덧붙일 정도로 복잡한 코드는 없어 보이네요.

사실 이런 멀티쓰레딩 방식이 좋은 방식은 아닙니다. 예전에는 액티비티에서 멀티쓰레딩을 위한 이런저런 방법이 있었고, AsyncTask 등으로 이런 비동기 멀티쓰레딩을 처리하기도 했습니다. 하지만 최근에는 액티비티는 순수하게 화면과 연관된 처리만 하고, 별도 쓰레드가 필요한 경우에는 ViewModel 같은 곳에서 처리를 하도록 위임하는 방식을 많이 사용합니다. 액티비티의 생명주기에 따라 작업을 종료해야 한다면 ViewModel에서 알아서 처리하도록 위임하는 것이죠. 그 이외에도 코루틴이나 RX 등을 사용해도 되지만, 아무튼 여기에서 알아두셔야 하는 점은 여기서는 예제 프로젝트이기 때문에 상용 서비스에서는 사용하지 않을 단순한 방식을 사용했다 정도로 이해하시면 됩니다.

AbstractCameraXActivity

prefix로 abstract라는 단어가 붙어 있어 추상 클래스라고 생각할 수도 있습니다. 네 맞습니다. 이렇게 명시적으로 파일이 어떤 역할을 하는지 파일 이름에 명기하기도 합니다. 인터페이스를 구현한 파일 같은 경우에는 postfix로 xxxImpl 이런 식으로 이름을 짓기도 합니다. 카메라 관련된 추상 액티비티구나 정도로 생각하고 코드를 보도록 하겠습니다.

카메라 권한 제어 변수

먼저 카메라 권한 획득을 위한 부분부터 볼까요. 앞서 말씀드린대로 특정한 기능을 사용하기 위해서는 사용자의 명시적인 동의가 있어야 되고, 그런 것들을 처리하기 위한 작업을 위한 것들이라고 보시면 됩니다.

권한이 없다면 권한 요청을 합니다

onCreate()에서 카메라 사용 권한이 있는지 체크한 다음 권한이 없으면 사용자의 동의를 얻는 팝업을 띄웁니다. 권한이 이미 있다면 setupCameraX() 메소드를 통해 카메라를 사용할 수 있게 합니다.

카메라 사용 동의를 하지 않는다면?

onRequestPermissionsResult() 메소드에서는 사용자에게 요청한 권한을 획득했는지 확인합니다. 만약에 사용자가 카메라 사용을 동의하지 않는다면 권한이 없이 카메라 사용이 불가능하다는 문구를 출력해 줍니다.

이제 이 액티비티에서 제일 중요한 setupCameraX() 메소드를 보도록 하겠습니다. 카메라를 사용할 수 있도록 준비작업을 하는 메소드로서, camera x에서 제공하는 기능을 사용합니다. 한 가지 주의해야 되는 것은 여기에서 설명하는 내용은 camera x 버전 1.0.0-alpha05 버전 기준으로 카메라 기능을 사용하기 위한 준비작업입니다.

view에 preview를 연결

먼저 메소드 앞 부분부터 살펴보겠습니다. 추상 메소드인 getCameraPreviewTextureView()를 이용해서 미리보기를 뿌려줄 TextureView를 가져옵니다. 컨텐트 스트림을 보여주는 view인데, 동영상이나 미리보기 같은 것들을 보여주는 뷰라고 생각하면 됩니다. 이 view에 미리보기를 뿌려주기 위해 PreviewConfig라는 설정을 이용해서 Preview() 인스턴스를 생성하고, preview의 결과값으로 나오는 output을 TextureView에 뿌려주도록 연결을 하고 있습니다.

이미지 분석을 위한 인스턴스 생성

그 다음은 이미지 분석을 위한 ImageAnalysis 인스턴스 생성입니다. 이 인스턴스를 생성하기 위해 먼저 설정을 위한 빌더 인스턴스에서 해상도, 콜백, 이미지 렌더 모드 등을 설정합니다. 최신 버전인 1.2에는 콜백 함수를 등록하는 메소드는 사라지는 등 변경점이 많아 자세하게는 설명하진 않겠습니다만, 이미지 분석을 위한 백그라운드 처리나 해상도 설정, 가장 최신 이미지를 사용해서 이미지를 분석할 것이다 정도의 의미입니다.

앞서 설정한 설정값으로 ImageAnalysis 인스턴스를 생성하고 나면 이제 이미지 분석을 위한 Analyzer를 설정합니다. 여기에서는 람다식으로 Analyzer의 익명 클래스를 생성해 줍니다. 새로운 이미지를 받자마자 계속해서 분석하는 것을 방지하기 위해 이전에 이미지 분석한 시간에서부터 500ms가 지나고 받은 새로운 이미지를 분석하고, 그 중간에 받은 이미지는 분석하지 않습니다.

final R result = analyzeImage(image, rotationDegrees);
if (result != null) {
mLastAnalysisResultTime = SystemClock.elapsedRealtime();
runOnUiThread(() -> applyToUiAnalyzeImageResult(result));
}

본격적인 분석 메소드인 analyzeImage() 및 applyToUiAnalyzeImageResult()도 추상 메소드입니다. 따라서 이 클래스를 상속받은 자식 클래스에서 정의한 분석 로직을 실행하고, 그 결과를 runOnUiThread()를 통해서 화면을 갱신시켜준다 정도의 뜻이 됩니다.

CameraX.bindToLifecycle(this, preview, imageAnalysis);

이 코드는 ImageAnalysis와 preview를 각 화면의 생명 주기에 맞출 수 있도록 해 줍니다. 이미지 분석, 캡쳐 등의 카메라를 사용해서 수행할 수 있는 다양한 작업을 Use Case라고 하는데, 자세한 사항은 여기를 참고하시면 됩니다. 아무튼 안드로이드에서는 화면 전환이나 화면 변경과 같은 화면의 생명 주기에 카메라 기능을 맞춰서 사용하게 하는 기능도 있다 정도만 알아두시면 됩니다. 실제로 모바일 환경에서는 화면 전환이나 앱 종료 등의 다양한 이벤트가 존재하기에 안드로이드에서는 이런 동작을 자동으로 처리하도록 제공하는 기능들이 많이 있습니다.

추상 클래스인 만큼 세부적인 분석 로직은 자식 클래스에서 알아서 구현할 수 있도록 추상 메소드로 처리를 해 뒀고, camera x의 미리보기 설정 같은 공통 작업은 추상 클래스에서 처리를 할 수 있게 해 뒀습니다. 파이토치를 이용한 이미지 분석 로직과 카메라 사용을 위한 안드로이드 로직을 어느 정도 분리를 잘 시켰다고도 할 수 있습니다. 물론 스트래티지 패턴을 이용해서 분석 로직을 잘 분리시키고 인터페이스로 구현하게 할 수도 있지만, 예제 코드에서는 이 정도로도 충분한 수준입니다.

예제 앱이 어떻게 구성이 되어 있는지 대략적인 밑그림은 설명을 한 것 같군요. 한데 지금까지 파이토치나 YOLO 모델과 관련된 내용은 하나도 나오지 않아 의아하게 여길 수도 있을 겁니다. 하지만 반대로 생각해 보자면, 지금까지 이렇게 안드로이드 관련된 부분을 전부 구현해 두고, 파이토치를 사용하는 코드는 추상 클래스의 추상 메소드 내부에 내용을 채워넣기만 하면 되는 구조로 구현을 했다고도 볼 수 있습니다. 이렇게 구조를 구성해 두면 비단 객체 탐지뿐만 아니라 다른 기능도 camera x에서 읽어들인 이미지를 이용할 수도 있기에 이미지 분석을 위한 다양한 기능을 시험해 볼 수 있는 밑그림으로 사용할 수도 있습니다.

초보자 눈높이에서 설명하려고 했는데 생각보다 내용이 길어졌습니다 .다음 편에서는 이제 파이토치를 사용해서 객체 탐지를 하는 부분을 보도록 하겠습니다.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Responses (1)

Write a response