- YOLO v3 를 이용한 민물 어종 인식 어플리케이션
- Team: GalaxyS3
- 서정우
- 김신동
- 송현석
1. 프로젝트 소개
2. 시스템 설계
3. 사용된 기술 스택
4. 기능 소개
5. 주요 소스코드
6. 시연 영상
- 실시간 객체 탐지 알고리즘인 YOLO v3를 이용하여 직접 어종 학습을 진행하고 이를 바탕으로 안드로이드와 연동 시켜 어종인식과 더불어 커뮤니티 기능을 제공하는 앱이다.
- 민물어종 22종에 대해서 크롤링을 통해 이미지를 확보해 딥러닝 학습을 진행했다.
- 어종인식 기능과 커뮤니티 기능이 제공된다.
- 글을 직접 써서 다른 유저들과 소통할 수 있으며 주요 타겟층은 낚시를 즐기는 사람이다.
- 웹서버를 APM으로 직접 구축하여 통신을 했다.
- 유저들의 정보나 게시판의 글 정보는 DB에 저장된다.
- 서버os는 VMware를 이용하여 centos7버전을 사용했다.
- MYSQL 5.7 버전을 사용했다.
- 안드로이드에서 서버 통신은 http 통신이 기본이며 Volley 라이브러리를 이용하여 웹서버와 통신을 했다.
- 추가적으로 DB 설계는 다음 그림과 같다.
- 유저정보를 저장하는 테이블, 게시판을 정보를 저장하는 테이블, 댓글정보를 저장하는 테이블 총 3개로 DB설계를 했다.
- 각 기본키는 시리얼넘버로 지정했다.
- 웹 서버
- PHP
- Centos 7.x
- MYSQL 5.7
- Apache 2.x
- 안드로이드
- Java
- OpenCV 3.x
- Glide
- Volley+
- 딥 러닝
- YOLO v3
- Python
![]() |
![]() |
- 메인화면에서 회원가입을 시도하면 2번째 사진처럼 화면이 넘어가 회원가입을 진행한다.
![]() |
![]() |
- TextUtils코드를 이용하여 만약 공백인 채로 회원가입을 진행하게 된다면 회원가입이 불가능하게 구현하였다.
![]() |
![]() |
- 웹서버에 작성한 php코드를 통해서 중복된 닉네임을 검사하여 회원가입을 진행한다.
- 성공적으로 회원가입이 완료되면 토스트 메시지와 함께 로그인 화면으로 이동하게 된다.
- 자동 로그인은 SharedPreference를 이용하여 기능을 구현했다.
![]() |
![]() |
![]() |
![]() |
- 로그인에 성공하게 되면 다음 첫번째 그림과 같은 선택화면으로 넘어가게 된다.
- 선택화면에서 글을 쓸지 어종을 인식할지 선택하게 된다.
- 게시판 목록 화면은 카드뷰로 나타냈으면 사진이 있다면 썸네일 사진으로 나타내게 하였다.
- 연필 모양의 플로팅 버튼을 왼쪽 하단에 설정하여 버튼을 클릭하면 글을 쓸 수 있는 화면으로 넘어가게 된다.
- 글을 쓸수 있는 화면에서는 갤러리 버튼을 눌러서 사진을 추가할 수도 있다.
- 글을 쓰는 화면에서 사진을 추가하게 되면 미리보기 사진이 뜨게 하였다.
![]() |
![]() |
- 선택화면에서 어종인식을 선택하고 들어가게 되면 카메라로 어종을 인식하게 된다.
- DETECT 버튼을 누르게되면 탐지가 시작이 되며, 실시간으로 탐지가 시작이 된다.
- 탐지가 시작이되면 물고기 사진에 주위에 테두리 박스가 생기면서 물고기의 이름을 나타내며 몇%의 정확도를 가졌는지 나타낸다.
![]() |
![]() |
- 사진 순서대로 회원가입, 로그인 관련 php 소스코드 이다.
- 안드로이드에서 요청하면 쿼리를 통해 회원가입과 로그인을 진행한다.
![]() |
![]() |
- 순서대로 게시판 리스트를 불러오는 코드, 게시글 작성 php 코드를 나타낸 사진이다.
- 각각 게시판 테이블, 유저 테이블을 나타낸 그림이다.
private void login(final String id, final String pw) {
final ProgressDialog progressDialog = new ProgressDialog(MainActivity.this);
progressDialog.setCancelable(false);
progressDialog.setIndeterminate(false);
progressDialog.setTitle("로그인중 입니다.");
progressDialog.show();
String url = "http://211.232.201.35/login.php";
SimpleMultiPartRequest smpr= new SimpleMultiPartRequest(Request.Method.POST, url, new Response.Listener<String>() {
@Override
public void onResponse(String response) {
if (response.equals("Login Success")) {
name = id;
progressDialog.dismiss();
Toast.makeText(MainActivity.this, response, Toast.LENGTH_SHORT).show();
SharedPreferences.Editor editor = sharedPreferences.edit();
if (loginstate.isChecked()) {
editor.putString(getResources().getString(R.string.prefLoginstate), "loggedin");
} else {
editor.putString(getResources().getString(R.string.prefLoginstate), "loggedout");
}
editor.apply();
Intent intent = new Intent(MainActivity.this, Choice_Activity.class);
startActivity(intent);
finish();
} else {
progressDialog.dismiss();
Toast.makeText(MainActivity.this, response, Toast.LENGTH_SHORT).show();
}
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Toast.makeText(MainActivity.this, "ERROR", Toast.LENGTH_SHORT).show();
}
});
smpr.addStringParam("id", id);
smpr.addStringParam("password", pw);
RequestQueue requestQueue= Volley.newRequestQueue(this);
requestQueue.add(smpr);
}
- 사용자가 입력한 id와 pw를 서버에 요청하여 로그인을 하는 코드 부분이다.
String url = "http://211.232.201.35/register.php"; //서버 IP주소
SimpleMultiPartRequest smpr= new SimpleMultiPartRequest(Request.Method.POST, url, new Response.Listener<String>() {
@Override
public void onResponse(String response) {
if (response.equals("Successfully Registered")) {
Toast.makeText(Join_Activity.this, "회원가입을 완료했습니다.", Toast.LENGTH_SHORT).show();
startActivity(new Intent(Join_Activity.this, MainActivity.class));
finish();
} else {
Toast.makeText(Join_Activity.this, response, Toast.LENGTH_SHORT).show();
}
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Toast.makeText(Join_Activity.this, "ERROR", Toast.LENGTH_SHORT).show();
}
});
smpr.addStringParam("nickname", Alias);
smpr.addStringParam("id", ID);
smpr.addStringParam("password", PW);
RequestQueue requestQueue= Volley.newRequestQueue(this);
requestQueue.add(smpr);
- 서버에 회원가입을 하는 닉네임,아이디,비밀번호를 서버에 요청하여 회원가입을 진행한다.
- 이때 닉네임,아이디 중복체크는 웹 서버에서 진행하여 회원가입 성공여부는 response를 통해 알려준다.
- 웹 서버와 통신하기위해 volley plus 라이브러리를 이용해 웹서버와 통신했다.
gallery.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(Intent.ACTION_PICK);
intent.setType("image/*");
startActivityForResult(intent, 101);
}
});
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 101) {
if (resultCode == RESULT_OK) {
//사진의 경로 객체 얻어오기
Uri fileUri = data.getData();
if(fileUri != null){
imgview.setImageURI(fileUri);
imgpath = getImgPath(fileUri);
new AlertDialog.Builder(this).setMessage(fileUri.toString()+"\n"+imgpath).create().show();
}else{
Toast.makeText(getApplicationContext(), "사진을 선택하지 않았습니다!", Toast.LENGTH_SHORT).show();
}
}
}
}
//uri를 절대경로로 바꿔서 알려주는 메소드
private String getImgPath(Uri fileUri) {
String[] proj = {MediaStore.Images.Media.DATA};
CursorLoader loader = new CursorLoader(this, fileUri, proj, null, null,null);
Cursor cursor = loader.loadInBackground();
int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
cursor.moveToFirst();
String realpath = cursor.getString(column_index);
cursor.close();
return realpath;
}
- 게시글을 쓸 때 사진을 넣어서 등록할 경우 사진을 등록하는 코드이다.
- DB의 성능을 위해서 사진 파일은 웹서버에 직접 저장하고 DB에는 사진의 경로만을 저장한다.
- 스마트폰의 갤러리에 있는 사진의 경로 데이터를 얻어오고, getImgPath라는 메소드를 통해서 절대경로로 바꿔줘서 서버에 보낼 큐에 저장한다.
private void RegisterContent(String title, String content, String writer) {
String url = "http://211.232.201.35/board.php";
SimpleMultiPartRequest smpr= new SimpleMultiPartRequest(Request.Method.POST, url, new Response.Listener<String>() {
@Override
public void onResponse(String response) {
new AlertDialog.Builder(Write_Activity.this).setMessage("응답:"+response).create().show();
finish();
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Toast.makeText(Write_Activity.this, "ERROR", Toast.LENGTH_SHORT).show();
}
});
//요청 객체에 보낼 데이터를 추가
smpr.addStringParam("writer", writer);
smpr.addStringParam("title", title);
smpr.addStringParam("content", content);
//이미지 파일 추가
smpr.addFile("img_path", imgpath);
RequestQueue requestQueue= Volley.newRequestQueue(this);
requestQueue.add(smpr);
}
- 사진의 경로와 파일, 작성자, 글 제목, 글 내용을 서버에 요청하여 DB에 반영되도록 한다.
Glide.with(this).load(intent.getExtras().getString("img_path")).into(board_img_view);
- 그 후에 게시판에서 보일때는 Glide 라이브러리를 통해서 이미지 경로를 가져와 이미지를 나타낸다.
static {
System.loadLibrary("opencv_java3");
}
- openCV 라이브러리를 설치하고 인식하고자 하는 클래스에 load해준다.
![]() |
- 학습된 딥러닝 모델을 assets폴더에 넣어서 해당모델을 이용해 어종 인식을 진행한다.
public void YOLO(View Button){
if (startYolo == false){
startYolo = true;
if (firstTimeYolo == false){
firstTimeYolo = true;
String tinyYoloCfg = getPath("yolov3-tiny.cfg", this) ;
String tinyYoloWeights = getPath("yolov3-tiny_final.weights", this) ;
tinyYolo = Dnn.readNetFromDarknet(tinyYoloCfg, tinyYoloWeights);
}
}
else{
startYolo = false;
}
}
- DETECT 버튼을 누르게 되면 실행되는 메소드로, assets 폴더에 넣어둔 모델을 이용하여 인식을 시작한다.
- 어종인식 코드 부분은 여기를 참고하였다.
- 코드가 많으므로 전체 코드는 여기에서 볼 수 있다.
- 약 1분 30초 시연 영상은 링크에서 볼 수 있다.