/flow

Primary LanguageC++

몰입캠프 1주차 과제

목차

프로젝트 개요

목표

  • 탭 구조를 활용한 안드로이드 앱 제작

목적

  • 서로 함께 공통의 과제를 함으로써 개발에 빠르게 익숙해지기

사용 언어, 툴

  • JAVA
  • Kotlin
  • android studio
  • tensorflow lite
  • ARCore

결과물

  • 세개의 탭이 존재하는 안드로이드 앱
  • 탭1 : 나만의 이미지 갤러리 구축
  • 탭2 : 나의 연락처 구축, 휴대폰의 연락처 데이터 활용
  • 탭3 : 자유 주제(스톱워치 구현)
  • 탭4 : 텐서플로우 Lite를 활용한 사물인식 탭
    • 해당 탭은 탭1을 추가 기능 구현중 코드가 복잡해져서 분리함
  • 탭5 : ARCore를 활용한 얼굴인식 및 가면 스티커 탭
    • 해당 탭은 탭1을 추가 기능 구현중 코드가 복잡해져서 분리함

탭별 주요코드 설명

Gallery

  • 앨범

    • 갤러리의 모든 리소스에 대한 uri(Uniform Resource Identifier : 일종으 자원 식별자)를 구해 앨범 그리드뷰 리스트에 추가
    private fun getAllShownImagesPath() {
          //contentResolver의 데이터타입과 가져오는 주소(갤러리) 정의
          val uriExternal: Uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
          
          //이미지 인덱스 번호 및 아이디
          var columnIndexID: Int
          var imageId: Long
          
          //contentResolver 정의 및 생성
          val cursor = contentResolver(
              MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
              null,
              null,
              null,
              MediaStore.Images.ImageColumns.DATE_TAKEN + " DESC"
          )
          if (cursor != null) {
              // 매 루프마다 contentResolver에 쿼리를 하여 모든 이미지 리소스를 순회
              while (cursor.moveToNext()) {
              
                  // 사진 경로 Uri 가져오기
                  columnIndexID = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
                  imageId = cursor.getLong(columnIndexID)
                  val uriImage = Uri.withAppendedPath(uriExternal, "" + imageId)
                  
                  // 그리드뷰 어댑터에 uri를 추가하여 앨범에 해당 리소스 표시
                  pictureAdapter.addItem(PictureItem(uriImage))
              }
              cursor.close()
          }
      }
  • 카메라

    • Preview
    private fun openCamera() {
    
        //카메라 프로바이더 객체
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
        
        //카메라 프로바이더 객체에 리스너를 등록하여 프리뷰에 객체의 화면을 출력
        cameraProviderFuture.addListener({
            val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
    
            val preview = Preview.Builder()
                .build()
                .also {
                    it.setSurfaceProvider(previewView.surfaceProvider)
                }
            //카메라 캡쳐 UseCase 등록
            imageCapture = ImageCapture.Builder()
                .build()
    
            //후면 카메라 선택
            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
    
            //화며 바인딩
            try {
    
                cameraProvider.unbindAll()
                cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture)
                Log.d("TAG", "바인딩 성공")
    
            } catch (e: Exception) {
                Log.d("TAG", "바인딩 실패 $e")
            }
        }, ContextCompat.getMainExecutor(this))
    }
    • Camera Capture 및 갤러리 저장
    private fun CameraCapture() {
        이미지 캡쳐
        imageCapture = imageCapture ?: return
        val fileName:String = "CS496_" + System.currentTimeMillis().toString() + ".png"
        
        //사진 파일 생성, 일단 write 권한이 있는 cache 디렉토리에 저장
        val photoFile = File(cacheDir,fileName)
        
        Log.d(TAG,"photoFile : ${photoFile.toString()}")
        val outputOption = ImageCapture.OutputFileOptions.Builder(photoFile).build()
        imageCapture?.takePicture(
            outputOption,
            ContextCompat.getMainExecutor(this),
            object : ImageCapture.OnImageSavedCallback {
                override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
                    savedUri = Uri.fromFile(photoFile)
    
                    //contentResolver를 통해 savedUri에 해당하는 이미지를 byte 포맷으로 변환 후 FileOutputStream을 통해 캐시 디렉토리에 저장한 카메라 캡쳐 이미지를 외부저장소(갤러리)에 복사하는 함수 
                    saveFile()
                    
                    //사진 추가 복사 후 새로 찍은 사진을 gridview에 추가후 새로고침
                    pictureAdapter.addItem(PictureItem(savedUri))
                    gridView.invalidateViews()
                }
    
                override fun onError(exception: ImageCaptureException) {
                    Log.d(TAG,"실패")
                    exception.printStackTrace()
                    onBackPressed()
                }
            })
    
        }
    • 셔터 애니메이션

      • 셔터 애니메이션 리스너
      private fun setCameraAnimationListener() {
            cameraAnimationListener = object : Animation.AnimationListener {
                override fun onAnimationStart(animation: Animation?) { }
                
                //애니메이션이 끝날때 셔터 애니메이션 출려 뷰를 비활성화 하고, 촬영 사진을 보여준다
                override fun onAnimationEnd(animation: Animation?) {
                    frameLayoutShutter.visibility = View.GONE
                    showCaptureImage()
                }
                override fun onAnimationRepeat(animation: Animation?) { }
            }
        }
      • 셔터 애니메이션 호출 파트 (CameraCapture함수 내부에서 호출)
      val animation = AnimationUtils.loadAnimation(this@galleryActivity, R.anim.camera_shutter)
      animation.setAnimationListener(cameraAnimationListener)
      frameLayoutShutter.animation = animation
      frameLayoutShutter.visibility = View.VISIBLE
      frameLayoutShutter.startAnimation(animation)

PhoneBook

  • 검색

    • 검색창 변경 리스너
    callText.addTextChangedListener(new TextWatcher() {
        @Override
        public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { }
    
        @Override
        public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { }
    
        //입력 이벤트가 발생할때 마다 새로고침 할 수 있도록 검색함수를 호출해준다
        @Override
        public void afterTextChanged(Editable editable) {
            String text = callText.getText().toString();
            search(text);
        }
    });
    • 검색 함수
    public void search(String charText) {
    
        // 문자 입력시마다 리스트를 지우고 새로 뿌려준다.
        callList.clear();
    
        // 문자 입력이 없을때는 모든 데이터를 보여준다.
        if (charText.length() == 0) {
            callList.addAll(callList2);
        }
        // 문자 입력을 할때..
        else
        {
            // 리스트의 모든 데이터를 검색한다.
            for(int i = 0;i < callList2.size(); i++)
            {
                // arraylist의 모든 데이터에 입력받은 단어(charText)가 포함되어 있으면 true를 반환한다.
                if (callList2.get(i).getName().toLowerCase().contains(charText))
                {
                    // 검색된 데이터를 리스트에 추가한다.
                    callList.add(callList2.get(i));
                }
            }
        }
        // 리스트 데이터가 변경되었으므로 아답터를 갱신하여 검색된 데이터를 화면에 보여준다.
        callAdapter.notifyDataSetChanged();
    }
  • 상세 정보창

    list.setOnItemClickListener(new AdapterView.OnItemClickListener(){
        @Override
        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
            
            //개별 항목 마다 클릭시 상세정보창 activity로 이동
            Intent intent = new Intent(PhoneActivity.this, CallActivity.class);
            intent.putExtra("POSITION", position);
            startActivity(intent);
        }
    });

StopWatch

  • 타이머 핸들러
Handler myTimer = new Handler(){
    public void handleMessage(Message msg){
        //해당 핸들러가 호출될때 마다 타이머 값을 수정해준다.
        myOutput.setText(getTimeOut());

        //해당 핸들러를 delay없이 호출
        myTimer.sendEmptyMessage(0);
    }
};
//현재시각 반환 함수
String getTimeOut(){
    long now = SystemClock.elapsedRealtime();
    long outTime = now - myBaseTime;

    String easy_outTime = String.format("%02d:%02d:%02d", outTime/1000 / 60, (outTime/1000)%60,(outTime%1000)/10);
    return easy_outTime;
}

Object Recognition

  • 영상분석 리스너
imageAnalysis.setAnalyzer(executor, ImageAnalysis.Analyzer { image ->

    //카메라가 회전한 경우 원래대로 변환
    if (!::bitmapBuffer.isInitialized) {
        imageRotationDegrees = image.imageInfo.rotationDegrees
        bitmapBuffer = Bitmap.createBitmap(image.width, image.height, Bitmap.Config.ARGB_8888)
    }

    //이미지를 RGB로 변환후 bitmapBuffer(텐서플로우 분석함수의 매개변수)에 저장
    mage.use { converter.yuvToRgb(image.image!!, bitmapBuffer) }

    // Tensorflow를 통해 이미지 처리
    val tfImage =  tfImageProcessor.process(tfImageBuffer.apply { load(bitmapBuffer) })
    // 이미지 처리 예측 결고 반환
    val predictions = detector.predict(tfImage)

    // 예측 결과중 가장 확률이 높은 1개를 선택하여 예측 결과와 해당 물체 바운더리 박스 출력
    reportPrediction(predictions.maxByOrNull { it.score })

    // 이미지 처리 analyzer 파이프라인 속도 계산
    val frameCount = 10
    if (++frameCounter % frameCount == 0) {
        frameCounter = 0
        val now = System.currentTimeMillis()
        val delta = now - lastFpsTimestamp
        val fps = 1000 * frameCount.toFloat() / delta
        Log.d(TAG, "FPS: ${"%.02f".format(fps)}")
        lastFpsTimestamp = now
    }
})

카메라 프리뷰 부분은 위 코드와 동일

MetaMong

  • 영상 인식 후 3D 오브젝트 배치 코드
//얼굴 랜더링 한 augmented face redering 불러오기
@Override
public void onWindowFocusChanged(boolean hasFocus) {
    super.onWindowFocusChanged(hasFocus);
    FullScreenHelper.setFullScreenOnWindowFocusChanged(this, hasFocus);
}

@Override
//얼굴 표면 인식 및 좌표 감지
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
    GLES20.glClearColor(0.1f, 0.1f, 0.1f, 1.0f);

    try {
        //backgroudRenderer = 얼굴 전체 rendering 
        backgroundRenderer.createOnGlThread(/*context=*/ this);
        //얼굴을 obj파일로 3D모델을 만들어서 형상화
        //3D Object(메타몽) 배치
        augmentedFaceRenderer.createOnGlThread(this, "models/metamonghi.png");
        augmentedFaceRenderer.setMaterialProperties(0.0f, 1.0f, 0.1f, 6.0f);

        //꽃도 마찬가지로 렌더링
        rightEarObject.createOnGlThread(this, "models/forehead_right.obj", "models/flower.png");
        rightEarObject.setMaterialProperties(0.0f, 1.0f, 0.1f, 6.0f);
        rightEarObject.setBlendMode(ObjectRenderer.BlendMode.AlphaBlending);


    } catch (IOException e) {
        Log.e(TAG, "Failed to read an asset file", e);
    }
}

구현 결과

1. 갤러리

사물인식

2. 연락처

연락처

3. 스톱워치

스톱워치

4. 사물인식

original

5. 메타몽

메타몽