Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.simprints.face.capture.models

import android.content.res.Configuration
import android.content.res.Resources

enum class ScreenOrientation {
Comment thread
alexandr-simprints marked this conversation as resolved.
Landscape, Portrait;

companion object {
fun getCurrentOrientation(resources: Resources) =
when (resources.configuration.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> Landscape
else -> Portrait
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import android.graphics.RectF
import android.util.Size
import androidx.camera.core.ImageProxy
import androidx.core.graphics.toRect
import com.simprints.face.capture.models.ScreenOrientation
import com.simprints.face.capture.screens.livefeedback.views.CameraTargetOverlay
import com.simprints.face.capture.usecases.ImageProxyToBitmapUseCase
import java.lang.Float.min
Expand All @@ -20,7 +21,8 @@ internal class FrameProcessor @Inject constructor(
private var previewViewHeight: Int = 0

private lateinit var boxOnTheScreen: RectF
private lateinit var cropRect: Rect
var cropRect: Rect? = null
private set

/**
* Init the frame processor
Expand All @@ -35,22 +37,24 @@ internal class FrameProcessor @Inject constructor(
this.boxOnTheScreen = boxOnTheScreen
}

fun clear() {
cropRect = null
}

/**
* Extracts part of the image that lays inside
* the cropRect
*
* @param image
* @return Bitmap
*/
fun cropRotateFrame(image: ImageProxy): Bitmap {
if (!this::cropRect.isInitialized) {
// The cropRect should be calculated once as its value will be the same for all images.
calcRotatedCropRect(image)
}
fun cropRotateFrame(image: ImageProxy, screenOrientation: ScreenOrientation): Bitmap {
val cropRect = this.cropRect
?: calcRotatedCropRect(image, screenOrientation).also { this.cropRect = it }
return imageProxyToBitmap(image, cropRect)
}

private fun calcRotatedCropRect(image: ImageProxy) {
private fun calcRotatedCropRect(image: ImageProxy, screenOrientation: ScreenOrientation): Rect {
val cameraWidth = image.width
val cameraHeight = image.height

Expand All @@ -64,10 +68,14 @@ internal class FrameProcessor @Inject constructor(
boxOnTheScreen
)

val newBoundingBox =
CameraTargetOverlay.rectForPlane(rotatedCameraWidth, rotatedCameraHeight, newRectSize)
val newBoundingBox = CameraTargetOverlay.rectForPlane(
width = rotatedCameraWidth,
height = rotatedCameraHeight,
rectSize = newRectSize,
screenOrientation = screenOrientation
)

cropRect = getRotatedBoundingBox(
return getRotatedBoundingBox(
image.imageInfo.rotationDegrees,
newBoundingBox,
cameraWidth,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import com.simprints.core.tools.extentions.hasPermission
import com.simprints.face.capture.R
import com.simprints.face.capture.databinding.FragmentLiveFeedbackBinding
import com.simprints.face.capture.models.FaceDetection
import com.simprints.face.capture.models.ScreenOrientation
import com.simprints.face.capture.screens.FaceCaptureViewModel
import com.simprints.infra.logging.Simber
import com.simprints.infra.uibase.navigation.navigateSafely
Expand Down Expand Up @@ -76,6 +77,11 @@ internal class LiveFeedbackFragment : Fragment(R.layout.fragment_live_feedback)
initFragment()
}

override fun onDestroyView() {
vm.clearFrameProcessor()
super.onDestroyView()
}

private fun initFragment() {
screenSize = with(resources.displayMetrics) { Size(widthPixels, widthPixels) }

Expand Down Expand Up @@ -133,7 +139,7 @@ internal class LiveFeedbackFragment : Fragment(R.layout.fragment_live_feedback)

override fun onStop() {
// Shut down our background executor
if(::cameraExecutor.isInitialized) {
if (::cameraExecutor.isInitialized) {
cameraExecutor.shutdown()
}
super.onStop()
Expand Down Expand Up @@ -161,7 +167,10 @@ internal class LiveFeedbackFragment : Fragment(R.layout.fragment_live_feedback)

private fun analyze(image: ImageProxy) {
try {
vm.process(image)
vm.process(
image = image,
screenOrientation = ScreenOrientation.getCurrentOrientation(resources)
)
} catch (t: Throwable) {
Simber.e(t)
// Image analysis is running in bg thread
Expand All @@ -185,7 +194,9 @@ internal class LiveFeedbackFragment : Fragment(R.layout.fragment_live_feedback)

private fun renderCapturingStateColors() {
with(binding) {
captureOverlay.drawWhiteTarget()
captureOverlay.drawWhiteTarget(
screenOrientation = ScreenOrientation.getCurrentOrientation(resources)
)

captureTitle.setTextColor(
ContextCompat.getColor(requireContext(), IDR.color.simprints_blue_grey)
Expand All @@ -198,7 +209,9 @@ internal class LiveFeedbackFragment : Fragment(R.layout.fragment_live_feedback)

private fun renderCapturingNotStarted() {
binding.apply {
captureOverlay.drawSemiTransparentTarget()
captureOverlay.drawSemiTransparentTarget(
screenOrientation = ScreenOrientation.getCurrentOrientation(resources)
)
captureTitle.text = getString(IDR.string.face_capture_preparation_title)
captureFeedbackTxtTitle.text = getString(IDR.string.face_capture_title_previewing)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.simprints.core.tools.extentions.area
import com.simprints.core.tools.time.TimeHelper
import com.simprints.face.capture.models.FaceDetection
import com.simprints.face.capture.models.FaceTarget
import com.simprints.face.capture.models.ScreenOrientation
import com.simprints.face.capture.models.SymmetricTarget
import com.simprints.face.capture.usecases.SimpleCaptureEventReporter
import com.simprints.infra.config.store.ConfigRepository
Expand Down Expand Up @@ -55,9 +56,9 @@ internal class LiveFeedbackFragmentViewModel @Inject constructor(
*
* @param image is the camera frame
*/
fun process(image: ImageProxy) {
fun process(image: ImageProxy, screenOrientation: ScreenOrientation) {
val captureStartTime = timeHelper.now()
val croppedBitmap = frameProcessor.cropRotateFrame(image)
val croppedBitmap = frameProcessor.cropRotateFrame(image, screenOrientation)
val potentialFace = faceDetector.analyze(croppedBitmap)

val faceDetection = getFaceDetectionFromPotentialFace(croppedBitmap, potentialFace)
Expand Down Expand Up @@ -92,6 +93,8 @@ internal class LiveFeedbackFragmentViewModel @Inject constructor(
frameProcessor.init(previewSize, cropRect)
}

fun clearFrameProcessor() = frameProcessor.clear()

fun startCapture() {
capturingState.value = CapturingState.CAPTURING
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
package com.simprints.face.capture.screens.livefeedback.views

import android.content.Context
import android.graphics.*
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.graphics.RectF
import android.util.AttributeSet
import android.view.View
import androidx.appcompat.widget.AppCompatImageView
import com.simprints.core.tools.extentions.dpToPx
import com.simprints.face.capture.models.ScreenOrientation
import com.simprints.infra.uibase.annotations.ExcludedFromGeneratedTestCoverageReports

@ExcludedFromGeneratedTestCoverageReports("UI code")
Expand All @@ -16,9 +22,29 @@ internal class CameraTargetOverlay(
companion object {
private val SEMI_TRANSPARENT_OVERLAY = Color.argb(102, 0, 0, 0)
private val WHITE_OVERLAY = Color.argb(242, 255, 255, 255)
private const val percentFromTop = 0.3f

fun rectForPlane(width: Int, height: Int, rectSize: Float): RectF {
/**
* Reference to the guideline's percentage of margin from the top of the screen. Used when
* the screen is in the portrait (vertical) mode
*/
private const val percentFromTopPortrait = 0.3f

/**
* Reference to the guideline's percentage of margin from the top of the screen. Used when
* the screen is in the landscape (horizontal) mode
*/
private const val percentFromTopLandscape = 0.5f

fun rectForPlane(
width: Int,
height: Int,
rectSize: Float,
screenOrientation: ScreenOrientation
): RectF {
val percentFromTop = when (screenOrientation) {
ScreenOrientation.Landscape -> percentFromTopLandscape
ScreenOrientation.Portrait -> percentFromTopPortrait
}
val top = (height * percentFromTop) - (rectSize / 2)
val bottom = top + rectSize

Expand Down Expand Up @@ -59,24 +85,24 @@ internal class CameraTargetOverlay(
drawingFunc?.invoke(canvas)
}

fun drawSemiTransparentTarget() {
fun drawSemiTransparentTarget(screenOrientation: ScreenOrientation) {
drawingFunc = {
drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
drawColor(SEMI_TRANSPARENT_OVERLAY, PorterDuff.Mode.SRC_OVER)
drawTarget()
drawTarget(screenOrientation)
}
}

fun drawWhiteTarget() {
fun drawWhiteTarget(screenOrientation: ScreenOrientation) {
drawingFunc = {
drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
drawColor(WHITE_OVERLAY, PorterDuff.Mode.SRC_OVER)
drawTarget()
drawTarget(screenOrientation)
}
}

private fun Canvas.drawTarget() {
rectInCanvas = rectForPlane(width, height, rectSize)
private fun Canvas.drawTarget(screenOrientation: ScreenOrientation) {
rectInCanvas = rectForPlane(width, height, rectSize, screenOrientation)

drawOval(rectInCanvas, circlePaint)
drawOval(rectInCanvas, circleBorderPaint)
Expand Down
91 changes: 91 additions & 0 deletions face/capture/src/main/res/layout-land/fragment_live_feedback.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
tools:background="#66000000"
tools:ignore="ContentDescription">

<androidx.camera.view.PreviewView
android:id="@+id/face_capture_camera"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true" />

<!--
Make sure to change the CameraTargetOverlay::percentFromTopPortrait value if any change is
made to the layout_constraintGuide_percent value
-->
<androidx.constraintlayout.widget.Guideline
android:id="@+id/capture_guide_frame"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.5" />

<com.simprints.face.capture.screens.livefeedback.views.CameraTargetOverlay
android:id="@+id/capture_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent" />

<TextView
android:id="@+id/capture_title"
style="@style/Text.Headline5.White"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_default"
android:text="@string/face_capture_preparation_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<com.simprints.face.capture.screens.livefeedback.views.DashedCircularProgress
android:id="@+id/capture_progress"
android:layout_width="242dp"
android:layout_height="242dp"
android:indeterminate="false"
android:visibility="gone"
app:dcp_color="@color/simprints_green_light"
app:dcp_max="10"
app:layout_constraintBottom_toBottomOf="@id/capture_guide_frame"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/capture_guide_frame" />

<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:paddingBottom="@dimen/margin_default"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">

<CheckedTextView
android:id="@+id/capture_feedback_txt_title"
style="@style/Text.Headline6.Bold"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:background="@drawable/feedback_chip_white"
android:drawablePadding="4dp"
android:gravity="center"
android:paddingHorizontal="12dp"
android:paddingVertical="4dp"
android:text="@string/face_capture_title_previewing"
android:textColor="@color/feedback_chip_text" />

<TextView
android:id="@+id/capture_feedback_txt_explanation"
style="@style/Text.Headline5.White"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_default"
android:gravity="center"
tools:text="Move closer" />
</LinearLayout>


</androidx.constraintlayout.widget.ConstraintLayout>
4 changes: 4 additions & 0 deletions face/capture/src/main/res/layout/fragment_live_feedback.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
android:layout_height="match_parent"
android:keepScreenOn="true" />

<!--
Make sure to change the CameraTargetOverlay::percentFromTopPortrait value if any change is
made to the layout_constraintGuide_percent value
-->
<androidx.constraintlayout.widget.Guideline
android:id="@+id/capture_guide_frame"
android:layout_width="wrap_content"
Expand Down
Loading