Skip to content

Commit

Permalink
Merge pull request #103 from Team-HealthC/feature/#101
Browse files Browse the repository at this point in the history
Feature/#101
  • Loading branch information
jeongjaino committed Nov 17, 2023
2 parents 56dc987 + e031b25 commit bb041b3
Show file tree
Hide file tree
Showing 18 changed files with 457 additions and 250 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.healthc.app.R
import com.healthc.app.databinding.FragmentObjectDetectionBinding
import com.healthc.app.presentation.detection.object_detection.ObjectDetectionViewModel.ObjectDetectionUiState
import com.healthc.app.presentation.detection.object_detection.ObjectDetectionViewModel.ObjectDetectionEvent
import com.healthc.app.presentation.detection.object_detection.adapter.ObjectDetectionAdapter
import com.healthc.app.presentation.widget.NegativeSignDialog
import com.healthc.app.presentation.widget.ObjectDetectionDialog
import com.healthc.app.presentation.widget.PositiveSignDialog
import com.healthc.data.model.local.detection.ObjectDetectionResult
import com.healthc.domain.model.auth.Allergy
Expand All @@ -42,6 +44,7 @@ class ObjectDetectionFragment : Fragment() {

private val viewModel : ObjectDetectionViewModel by viewModels()
private val args : ObjectDetectionFragmentArgs by navArgs()
private lateinit var objectDetectionAdapter: ObjectDetectionAdapter

override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
Expand All @@ -66,8 +69,12 @@ class ObjectDetectionFragment : Fragment() {
navigateToCamera()
}

binding.btODBackCamera.setOnClickListener {
navigateToCamera()
}

// 이미지 크기 측정을 위해, 크기가 정해진 후 이미지 전처리 시작
with(binding.CaptureImageView) {
with(binding.ivODObjectImage) {
viewTreeObserver.addOnGlobalLayoutListener(object: OnGlobalLayoutListener {
override fun onGlobalLayout() {
viewTreeObserver.removeOnGlobalLayoutListener(this)
Expand All @@ -77,12 +84,95 @@ class ObjectDetectionFragment : Fragment() {
}
}

private fun observeData(){
viewModel.objectDetectionEvent.flowWithLifecycle(viewLifecycleOwner.lifecycle)
.onEach {
when (it) {
is ObjectDetectionEvent.Failure -> { // 객체 인식 실패
initViewsIfFailedDetection(it.error)
}
}
}.launchIn(viewLifecycleOwner.lifecycleScope)

viewModel.detectedObjectUiState.flowWithLifecycle(viewLifecycleOwner.lifecycle)
.onEach {
when (it) {
is ObjectDetectionUiState.Init -> {}

is ObjectDetectionUiState.Success -> { // 객체 인식 성공
if(it.objectDetectionResultList.isEmpty()) {
initViewsIfNotDetected()
} else {
initViewsIfDetected(it.objectDetectionResultList)
}
}
}
}.launchIn(viewLifecycleOwner.lifecycleScope)

viewModel.detectedAllergiesUiEvent.flowWithLifecycle(viewLifecycleOwner.lifecycle)
.onEach { detectedAllergies ->
if(detectedAllergies.isEmpty()) {
showPositiveDialog()
} else {
showNegativeDialog(detectedAllergies = detectedAllergies)
}
}.launchIn(viewLifecycleOwner.lifecycleScope)
}

private fun initViewsIfFailedDetection(error: Throwable) {
Toast.makeText(requireContext(), error.message, Toast.LENGTH_SHORT).show()
binding.progressBar.visibility = View.GONE
binding.cdODBottom.visibility = View.GONE
}

private fun initViewsIfNotDetected() {
with(binding) {
progressBar.visibility = View.GONE
tvODResultTitle.text = resources.getString(R.string.failed_object_detection_title)
}
}

private fun initViewsIfDetected(
objectDetectionResultList: List<ObjectDetectionResult>
) {
// Object Detection Rectangle Draw
val classes = getLabelClasses()
with(binding.objectDetectionResultView) {
visibility = View.VISIBLE
setObjectDetectionResult(objectDetectionResultList, classes)
invalidate()
}
binding.progressBar.visibility = View.GONE

// 검출된 음식 리스트 초기화
initAdapter(objectDetectionResultList, classes)
}

private fun initAdapter(
objectDetectionResultList: List<ObjectDetectionResult>,
classes: List<String>
) {
objectDetectionAdapter = ObjectDetectionAdapter(
classes = classes,
onClick = { detectedObject ->
viewModel.checkAllergies(detectedObject)
}
)
objectDetectionAdapter.submitList(objectDetectionResultList)
with(binding.rvODDetectedObject) {
this.layoutManager = LinearLayoutManager(
requireContext(), RecyclerView.HORIZONTAL, false
)
this.adapter = objectDetectionAdapter
}
}

private fun loadPreprocessedImage(){
val imageUri = Uri.parse(args.imageUrl)
val bufferedInputStream = BufferedInputStream(
requireContext()
.contentResolver
.openInputStream(imageUri)
.contentResolver
.openInputStream(imageUri)
)

bufferedInputStream.mark(bufferedInputStream.available())
Expand All @@ -95,9 +185,9 @@ class ObjectDetectionFragment : Fragment() {
)

if(bitmap == null) {
Toast.makeText(
requireActivity(), "이미지를 불러오는데 실패하였습니다.", Toast.LENGTH_SHORT
).show()
Toast.makeText(
requireActivity(), "이미지를 불러오는데 실패하였습니다.", Toast.LENGTH_SHORT
).show()
} else {
preprocessImage(bitmap, imageUri)
}
Expand Down Expand Up @@ -133,8 +223,8 @@ class ObjectDetectionFragment : Fragment() {
bitmap = preprocessedBitmap,
inputImageWidth = rotatedBitmap.width.toFloat(), // 리사이징 전 사진의 너비
inputImageHeight = rotatedBitmap.height.toFloat(), // 리사이징 전 사진의 높이
imageViewWidth = binding.CaptureImageView.width.toFloat(),
imageViewHeight = binding.CaptureImageView.height.toFloat(),
imageViewWidth = binding.ivODObjectImage.width.toFloat(),
imageViewHeight = binding.ivODObjectImage.height.toFloat(),
)
}

Expand All @@ -150,64 +240,7 @@ class ObjectDetectionFragment : Fragment() {
return Bitmap.createScaledBitmap(bitmap, IMAGE_DEFAULT_WIDTH, IMAGE_DEFAULT_HEIGHT, true)
}

private fun observeData(){
viewModel.objectDetectionEvent.flowWithLifecycle(viewLifecycleOwner.lifecycle)
.onEach {
when (it) {
is ObjectDetectionEvent.Failure -> { // 객체 인식 실패
Toast.makeText(requireContext(), it.error.message, Toast.LENGTH_SHORT).show()
eraseProgressBar()
}

/*is ObjectDetectionEvent.Detected -> { // 알러지 성분 검출
if(it.detectedList.isEmpty()){
showPositiveDialog()
}
else{
showNegativeDialog(it.detectedList)
}
}*/
}
}.launchIn(viewLifecycleOwner.lifecycleScope)

viewModel.detectedObjectUiState.flowWithLifecycle(viewLifecycleOwner.lifecycle)
.onEach {
when (it) {
is ObjectDetectionUiState.Init -> {}

is ObjectDetectionUiState.Success -> {
eraseProgressBar()

// Object Detection Result Dialog
showObjectDetectionDialog(it.objectDetectionResultList)

// Object Detection Rectangle Draw
val classes = getLabelClasses()
with(binding.objectDetectionResultView) {
visibility = View.VISIBLE
setObjectDetectionResult(it.objectDetectionResultList, classes)
invalidate()
}
}
}

}.launchIn(viewLifecycleOwner.lifecycleScope)
}

private fun showObjectDetectionDialog(objectDetectionResult: List<ObjectDetectionResult>){
ObjectDetectionDialog(
context = requireContext(),
objectDetectionResult = objectDetectionResult,
onClickNegButton = {
navigateToCamera()
},
onClickPosButton = { detectedObject ->
viewModel.checkAllergies(detectedObject = detectedObject)
}
).show()
}

// 에셋으로부터 라벨 가져오기
// assets 으로부터 라벨 가져오기
private fun getLabelClasses(): List<String> {
val br = BufferedReader(
InputStreamReader(requireContext().assets.open("labels_ko.txt"))
Expand All @@ -222,21 +255,17 @@ class ObjectDetectionFragment : Fragment() {
return classes
}

private fun showNegativeDialog(detectedList: List<Allergy>){
private fun showNegativeDialog(detectedAllergies: List<Allergy>){
NegativeSignDialog(
context = requireContext(),
allergyList = detectedList
detectedAllergies = detectedAllergies
).show()
}

private fun showPositiveDialog(){
PositiveSignDialog(context = requireContext()).show()
}

private fun eraseProgressBar(){
binding.progressBar.visibility = View.GONE
}

private fun navigateToCamera(){
val direction = ObjectDetectionFragmentDirections.actionObjectDetectionFragmentToCameraFragment()
findNavController().navigate(direction)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import androidx.lifecycle.viewModelScope
import com.healthc.data.model.local.detection.InputImage
import com.healthc.data.model.local.detection.ObjectDetectionResult
import com.healthc.data.source.detection.LocalDetectionDataSource
import com.healthc.domain.model.auth.Allergy
import com.healthc.domain.usecase.detection.CheckAllergiesInImageUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
Expand All @@ -29,6 +31,9 @@ class ObjectDetectionViewModel @Inject constructor(
MutableStateFlow(ObjectDetectionUiState.Init)
val detectedObjectUiState: StateFlow<ObjectDetectionUiState> = _detectedObjectUiState.asStateFlow()

private val _detectedAllergiesUiEvent: MutableSharedFlow<List<Allergy>> = MutableSharedFlow()
val detectedAllergiesUiEvent: SharedFlow<List<Allergy>> = _detectedAllergiesUiEvent.asSharedFlow()

private val _objectDetectionEvent = MutableSharedFlow<ObjectDetectionEvent>()
val objectDetectionEvent : SharedFlow<ObjectDetectionEvent> get() = _objectDetectionEvent

Expand Down Expand Up @@ -64,7 +69,7 @@ class ObjectDetectionViewModel @Inject constructor(
viewModelScope.launch {
checkAllergiesInImageUseCase(detectedObject)
.onSuccess { result ->
// _objectDetectionEvent.emit(ObjectDetectionEvent.Detected(result))
_detectedAllergiesUiEvent.emit(result)
}
.onFailure { error ->
_objectDetectionEvent.emit(ObjectDetectionEvent.Failure(error))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.healthc.app.presentation.detection.object_detection.adapter

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.healthc.app.databinding.ItemObjectDetectionBinding
import com.healthc.data.model.local.detection.ObjectDetectionResult

class ObjectDetectionAdapter(
private val classes: List<String>,
private val onClick: (String) -> Unit,
): ListAdapter<ObjectDetectionResult, ObjectDetectionAdapter.ObjectDetectionViewHolder>(diffCallback) {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ObjectDetectionViewHolder {
return ObjectDetectionViewHolder(
ItemObjectDetectionBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
)
}

override fun onBindViewHolder(holder: ObjectDetectionViewHolder, position: Int) {
holder.bind(currentList[position])
}

inner class ObjectDetectionViewHolder(
private val binding: ItemObjectDetectionBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(objectDetectionResult: ObjectDetectionResult) {
val detectedObject = classes[objectDetectionResult.classIndex]
binding.tvODItemObject.text = detectedObject

val score = objectDetectionResult.score * 100
binding.tvODItemScore.text = "${String.format("%.2f", score)}%"

binding.cdODItem.setOnClickListener {
onClick(detectedObject)
}
}
}

companion object {
val diffCallback = object : DiffUtil.ItemCallback<ObjectDetectionResult>() {
override fun areItemsTheSame(oldItem: ObjectDetectionResult, newItem: ObjectDetectionResult): Boolean {
return oldItem == newItem
}

override fun areContentsTheSame(oldItem: ObjectDetectionResult, newItem: ObjectDetectionResult): Boolean {
return oldItem == newItem
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import com.healthc.domain.model.auth.Allergy

class NegativeSignDialog(
context: Context,
private val allergyList : List<Allergy>,
private val detectedAllergies : List<Allergy>,
) : Dialog(context){

private val binding by lazy { DialogNegativeBinding.inflate(layoutInflater) }
Expand All @@ -22,7 +22,7 @@ class NegativeSignDialog(
}

private fun initViews(){
val detectedList: List<String> = allergyList.map{ it.allergy }
val detectedList: List<String> = detectedAllergies.map{ it.allergy }
val content = "해당 음식에는 ${detectedList.joinToString(", ")}이(가) 포함되어 있습니다."
binding.negativeDialogContent.text = content

Expand Down
Loading

0 comments on commit bb041b3

Please sign in to comment.