I am using CameraX Analyzer use-case to detect circles in an image using OpenCV Hough Circles, but even though I believe I am using a separate thread to do this analysis, the camera preview gets really slow sometimes when it gets too many detected circles.
I get that my implementation of the detector isn't the most efficient, but shouldn't this processing time be reflected only in the analysis rate, and not on the preview?
Got great part of the code from crysxd CameraX-Object-Tracking.
This is my MainActivity:
class MainActivity : AppCompatActivity() {
private lateinit var overlayTextureView: DetectionOverlayView
private val camera
get() = supportFragmentManager.findFragmentById(R.id.cameraFragment) as CameraFragment
override fun onCreate(savedInstanceState: Bundle?) {
OpenCVLoader.initDebug()
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (Timber.treeCount() == 0) {
Timber.plant(Timber.DebugTree())
}
overlayTextureView = findViewById(R.id.detectionOverlayView)
camera.imageAnalyzer = ViewModelProviders.of(this).get(HoughPupilDetector::class.java)
}
}
This is my CameraFragment initialization:
open class CameraFragment : Fragment() {
var cameraRunning = false
private set
var imageAnalyzer: ThreadedImageAnalyzer? = null
set(value) {
field = value
if (cameraRunning) {
startCamera()
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
inflater.inflate(R.layout.fragment_camera, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
CameraPermissionHelper().requestCameraPermission(childFragmentManager) {
if (it) {
startCamera()
} else {
activity?.finish()
}
}
}
override fun onDestroyView() {
super.onDestroyView()
if (cameraRunning) {
CameraX.unbindAll()
cameraRunning = false
Timber.i("Stopping camera")
}
}
private fun startCamera() {
preview.post {
try {
val usesCases = mutableListOf<UseCase>()
// Make sure that there are no other use cases bound to CameraX
CameraX.unbindAll()
// Create configuration object for the viewfinder use case
val previewConfig = onCreatePreivewConfigBuilder().build()
usesCases.add(AutoFitPreviewBuilder.build(previewConfig, preview))
// Setup image analysis pipeline that computes average pixel luminance in real time
if (imageAnalyzer != null) {
val analyzerConfig = onCreateAnalyzerConfigBuilder().build()
usesCases.add(ImageAnalysis(analyzerConfig).apply {
analyzer = imageAnalyzer
})
}
// Bind use cases to lifecycle
CameraX.bindToLifecycle(this, *usesCases.toTypedArray())
cameraRunning = true
Timber.i("Started camera with useCases=$usesCases")
} catch (e: Exception) {
Timber.e(e)
AlertDialog.Builder(context)
.setMessage(getString(R.string.camera_error))
.setPositiveButton(android.R.string.ok) { _, _ ->
activity?.finish()
}
.create()
}
}
}
#Suppress("MemberVisibilityCanBePrivate")
protected open fun onCreateAnalyzerConfigBuilder() = ImageAnalysisConfig.Builder().apply {
// Use a worker thread for image analysis to prevent preview glitches
setCallbackHandler(imageAnalyzer!!.getHandler())
// In our analysis, we care more about the latest image than analyzing *every* image
setImageReaderMode(ImageAnalysis.ImageReaderMode.ACQUIRE_LATEST_IMAGE)
setTargetAspectRatio(Rational(1, 1))
setTargetResolution(Size(preview.width, preview.height))
}
#Suppress("MemberVisibilityCanBePrivate")
protected open fun onCreatePreivewConfigBuilder() = PreviewConfig.Builder().apply {
setTargetAspectRatio(Rational(1, 1))
setTargetResolution(Size(preview.width, preview.height))
}
}
This is my analyzer interface initialization:
abstract class PupilDetector(listener: PupilDetectionListener? = null) : ViewModel(), ThreadedImageAnalyzer {
private val listeners = ArrayList<PupilDetectionListener>().apply { listener?.let { add(it) } }
private val isBusy = AtomicBoolean(false)
private val handlerThread = HandlerThread("PupilDetector").apply { start() }
fun addListener(listener: PupilDetectionListener) = listeners.add(listener)
override fun analyze(image: ImageProxy, rotationDegrees: Int) {
if (isBusy.compareAndSet(false, true)) {
Timber.d("Running analysis...")
val pupil = detect(image, rotationDegrees)
Timber.d("Analysis done.")
isBusy.set(false)
// listeners.forEach { it(pupil) }
}
}
override fun getHandler() = Handler(handlerThread.looper)
abstract fun detect(image: ImageProxy, rotationDegrees: Int): Pupil?
}
And this is my Hough Circles analyzer:
class HoughPupilDetector(listener: PupilDetectionListener? = null): PupilDetector(listener) {
val maxCircles = 5
override fun detect(image: ImageProxy, rotationDegrees: Int): Pupil? {
val bitmap = image.toBitmap(rotationDegrees)
val circles = detectCircles(bitmap)
if(circles.isNotEmpty()) {
return Pupil(circles[0].point, circles[0].r)
} else {
return null
}
}
private fun detectCircles(bitmap: Bitmap): List<Circle> {
// Generate Mat object
val img = Mat()
Utils.bitmapToMat(bitmap, img)
// Detect circles
val cannyUpperThreshold = 100.0
val minRadius = 10
val maxRadius = 400
val accumulator = 100.0
val circles = Mat()
Imgproc.cvtColor(img, img, Imgproc.COLOR_RGB2GRAY)
Imgproc.GaussianBlur(img, img, org.opencv.core.Size(3.0, 3.0), 1.0)
Imgproc.HoughCircles(img, circles, Imgproc.CV_HOUGH_GRADIENT,
2.0, 2.0 / 8.0, cannyUpperThreshold, accumulator,
minRadius, maxRadius)
Imgproc.cvtColor(img, img, Imgproc.COLOR_GRAY2BGR)
// Convert Mat to list of circles
val result = toCircles(circles)
// Return detection
return result
}
private fun toCircles(circles: Mat): List<Circle>{
if (circles.cols() > 0){
return (0 until circles.cols().coerceAtMost(maxCircles)).map {
val vCircle = circles.get(0, it)
val pt = Point(vCircle[0].toInt(), vCircle[1].toInt())
val radius = Math.round(vCircle[2]).toInt()
// return circle
Circle(pt, radius)
}
} else {
return emptyList()
}
}
}
I updated CameraX dependencies from alpha01 to alpha05 and glitches stopped happening.
Related
I have following code in place
GraphicOverlay.kt
open class GraphicOverlay(context: Context?, attrs: AttributeSet?) :
View(context, attrs) {
private val lock = Any()
private val graphics: MutableList<Graphic> = ArrayList()
var mScale: Float? = null
var mOffsetX: Float? = null
var mOffsetY: Float? = null
var cameraSelector: Int = CameraSelector.LENS_FACING_FRONT
abstract class Graphic(private val overlay: GraphicOverlay) {
abstract fun draw(canvas: Canvas?)
fun calculateRect(height: Float, width: Float, boundingBoxT: Rect): RectF {
// for land scape
fun isLandScapeMode(): Boolean {
return overlay.context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
}
fun whenLandScapeModeWidth(): Float {
return when (isLandScapeMode()) {
true -> width
false -> height
}
}
fun whenLandScapeModeHeight(): Float {
return when (isLandScapeMode()) {
true -> height
false -> width
}
}
val scaleX = overlay.width.toFloat() / whenLandScapeModeWidth()
val scaleY = overlay.height.toFloat() / whenLandScapeModeHeight()
val scale = scaleX.coerceAtLeast(scaleY)
overlay.mScale = scale
// Calculate offset (we need to center the overlay on the target)
val offsetX = (overlay.width.toFloat() - ceil(whenLandScapeModeWidth() * scale)) / 2.0f
val offsetY =
(overlay.height.toFloat() - ceil(whenLandScapeModeHeight() * scale)) / 2.0f
overlay.mOffsetX = offsetX
overlay.mOffsetY = offsetY
val mappedBox = RectF().apply {
left = boundingBoxT.right * scale + offsetX
top = boundingBoxT.top * scale + offsetY
right = boundingBoxT.left * scale + offsetX
bottom = boundingBoxT.bottom * scale + offsetY
}
// for front mode
if (overlay.isFrontMode()) {
val centerX = overlay.width.toFloat() / 2
mappedBox.apply {
left = centerX + (centerX - left)
right = centerX - (right - centerX)
}
}
return mappedBox
}
}
fun isFrontMode() = cameraSelector == CameraSelector.LENS_FACING_FRONT
fun toggleSelector() {
cameraSelector =
if (cameraSelector == CameraSelector.LENS_FACING_BACK) CameraSelector.LENS_FACING_FRONT
else CameraSelector.LENS_FACING_BACK
}
fun clear() {
synchronized(lock) { graphics.clear() }
postInvalidate()
}
fun add(graphic: Graphic) {
synchronized(lock) { graphics.add(graphic) }
}
fun remove(graphic: Graphic) {
synchronized(lock) { graphics.remove(graphic) }
postInvalidate()
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
synchronized(lock) {
for (graphic in graphics) {
graphic.draw(canvas)
}
}
}
}
FaceContourGraphic.kt
class FaceContourGraphic(
overlay: GraphicOverlay,
private val face: Face,
private val imageRect: Rect
) : GraphicOverlay.Graphic(overlay) {
private val facePositionPaint: Paint
private val idPaint: Paint
private val boxPaint: Paint
init {
val selectedColor = Color.WHITE
facePositionPaint = Paint()
facePositionPaint.color = selectedColor
idPaint = Paint()
idPaint.color = selectedColor
boxPaint = Paint()
boxPaint.color = selectedColor
boxPaint.style = Paint.Style.STROKE
boxPaint.strokeWidth = BOX_STROKE_WIDTH
}
override fun draw(canvas: Canvas?) {
val rect = calculateRect(
imageRect.height().toFloat(),
imageRect.width().toFloat(),
face.boundingBox
)
canvas?.drawRect(rect, boxPaint)
}
companion object {
private const val BOX_STROKE_WIDTH = 5.0f
}
}
SelfieAnalyzer.kt
abstract class SelfieAnalyzer<T> : ImageAnalysis.Analyzer {
abstract val graphicOverlay: GraphicOverlay
#SuppressLint("UnsafeExperimentalUsageError")
override fun analyze(imageProxy: ImageProxy) {
val mediaImage = imageProxy.image
mediaImage?.let { image ->
detectInImage(InputImage.fromMediaImage(image, imageProxy.imageInfo.rotationDegrees))
.addOnSuccessListener { results ->
onSuccess(
results,
graphicOverlay,
image.cropRect
)
imageProxy.close()
}
.addOnFailureListener {
onFailure(it)
imageProxy.close()
}
}
}
protected abstract fun detectInImage(image: InputImage): Task<T>
abstract fun stop()
protected abstract fun onSuccess(
results: T,
graphicOverlay: GraphicOverlay,
rect: Rect
)
protected abstract fun onFailure(e: Exception)
companion object {
const val TAG: String = "SelfieAnalyzer"
}
}
FaceContourDetectionProcessor.kt
class FaceContourDetectionProcessor(private val view: GraphicOverlay) :
SelfieAnalyzer<List<Face>>() {
private val realTimeOpts = FaceDetectorOptions.Builder()
.setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST)
.setContourMode(FaceDetectorOptions.CONTOUR_MODE_NONE)
.build()
private val detector = FaceDetection.getClient(realTimeOpts)
override val graphicOverlay: GraphicOverlay
get() = view
override fun detectInImage(image: InputImage): Task<List<Face>> {
return detector.process(image)
}
override fun stop() {
try {
detector.close()
} catch (e: IOException) {
Log.e(TAG, "Exception thrown while trying to close Face Detector: $e")
}
}
override fun onSuccess(
results: List<Face>,
graphicOverlay: GraphicOverlay,
rect: Rect
) {
graphicOverlay.clear()
results.forEach { face ->
val faceGraphic = FaceContourGraphic(graphicOverlay, face, rect)
graphicOverlay.add(faceGraphic)
}
graphicOverlay.postInvalidate()
}
override fun onFailure(e: Exception) {
Log.w(TAG, "Face Detector failed.$e")
}
companion object {
private const val TAG = "FaceDetectionProcessor"
}
}
SelfieFragment.kt
class SelfieFragment : Fragment() {
// runs on camera permission grant success
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
cameraProviderFuture.addListener({
// Used to bind the lifecycle of cameras to the lifecycle owner
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
// Preview
val preview = Preview.Builder().build()
preview.setSurfaceProvider(binding.viewFinder.surfaceProvider)
imageCapture = ImageCapture.Builder().build()
val imageAnalyzer = ImageAnalysis.Builder()
.setBackpressureStrategy(STRATEGY_KEEP_ONLY_LATEST)
.build()
.also {
it.setAnalyzer(
cameraExecutor,
selectAnalyzer()
)
}
// Select front camera as a default
val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA
try {
// Unbind use cases before rebinding
cameraProvider.unbindAll()
// Bind use cases to camera
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture, imageAnalyzer
)
} catch (exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(context))
}
}
}
fragment_selfie.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".ui.fragments.SelfieFragment">
<androidx.camera.view.PreviewView
android:id="#+id/viewFinder"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.example.appname.customviews.GraphicOverlay
android:id="#+id/graphic_overlay"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="#id/viewFinder"
app:layout_constraintLeft_toLeftOf="#id/viewFinder"
app:layout_constraintRight_toRightOf="#id/viewFinder"
app:layout_constraintTop_toTopOf="#id/viewFinder" />
<com.google.android.material.button.MaterialButton
android:id="#+id/btnCamera"
style="#style/Widget.MaterialComponents.Button.OutlinedButton.Icon"
android:layout_width="wrap_content"
android:layout_height="56dp"
android:layout_marginBottom="16dp"
android:background="#android:color/transparent"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
app:icon="#drawable/ic_photo_camera_48"
app:iconGravity="textStart"
app:iconPadding="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
I want to create bitmap of detected faces but I do not know how, because Face api does not provide an way.
GraphicOverlay extends View and View can be rendered to Bitmap... here's an example. When wanting to render individual FaceContourGraphic, one may have to use a temporary View - so that one can add one FaceContourGraphic, render the output to Bitmap and then proceed with the next one FaceContourGraphic.
I am using surfaceview inside a fragment, however the methods of the surfaceHolder.Callback(surfaceCreated,surfaceChanged, and surfaceDestroyed) seems to overrides nothing.and I also get the following error " object is not abstract and does not implement abstract member".
Below is the error in the image.
e: /Users/malorimorow/AndroidStudioProjects/CapstoneProject/app/src/main/java/com/example/capstoneproject/main/ScannerFragment.kt: (158, 32): Object is not abstract and does not implement abstract member public abstract fun surfaceChanged(p0: SurfaceHolder, p1: Int, p2: Int, p3: Int): Unit defined in android.view.SurfaceHolder.Callback
Below is the source code and you can find the error in the overlay.apply method.I am new to kotlin and would greatly appreciate the help.
class ScannerFragment : Fragment() {
companion object {
fun newInstance() = ScannerFragment()
// We only need to analyze the part of the image that has text, so we set crop percentages
// to avoid analyze the entire image from the live camera feed.
const val DESIRED_WIDTH_CROP_PERCENT = 8
const val DESIRED_HEIGHT_CROP_PERCENT = 74
// This is an arbitrary number we are using to keep tab of the permission
// request. Where an app has multiple context for requesting permission,
// this can help differentiate the different contexts
private const val REQUEST_CODE_PERMISSIONS = 10
// This is an array of all the permission specified in the manifest
private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
private const val RATIO_4_3_VALUE = 4.0 / 3.0
private const val RATIO_16_9_VALUE = 16.0 / 9.0
private const val TAG = "ScannerFragment"
}
private var displayId: Int = -1
private val viewModel: MainViewModel by viewModels()
private var cameraProvider: ProcessCameraProvider? = null
private var camera: Camera? = null
private var imageAnalyzer: ImageAnalysis? = null
private lateinit var container: ConstraintLayout
private lateinit var viewFinder: PreviewView
/** Blocking camera operations are performed using this executor */
private lateinit var cameraExecutor: ExecutorService
private lateinit var scopedExecutor: ScopedExecutor
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return inflater.inflate(R.layout.fragment_scanner, container, false)
}
override fun onDestroyView() {
super.onDestroyView()
// Shut down our background executor
cameraExecutor.shutdown()
scopedExecutor.shutdown()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
container = view as ConstraintLayout
viewFinder = container.findViewById(R.id.viewfinder)
// Initialize our background executor
cameraExecutor = Executors.newSingleThreadExecutor()
scopedExecutor = ScopedExecutor(cameraExecutor)
// Request camera permissions
if (allPermissionsGranted()) {
// Wait for the views to be properly laid out
viewFinder.post {
// Keep track of the display in which this view is attached
displayId = viewFinder.display.displayId
// Set up the camera and its use cases
setUpCamera()
}
} else {
requestPermissions(
REQUIRED_PERMISSIONS,
REQUEST_CODE_PERMISSIONS
)
}
// Get available language list and set up the target language spinner
// with default selections.
val adapter = ArrayAdapter(
requireContext(),
android.R.layout.simple_spinner_dropdown_item, viewModel.availableLanguages
)
targetLangSelector.adapter = adapter
targetLangSelector.setSelection(adapter.getPosition(Language("en")))
targetLangSelector.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>,
view: View?,
position: Int,
id: Long
) {
viewModel.targetLang.value = adapter.getItem(position)
}
override fun onNothingSelected(parent: AdapterView<*>) {}
}
viewModel.sourceLang.observe(viewLifecycleOwner, Observer { srcLang.text = it.displayName })
viewModel.translatedText.observe(viewLifecycleOwner, Observer { resultOrError ->
resultOrError?.let {
if (it.error != null) {
translatedText.error = resultOrError.error?.localizedMessage
} else {
translatedText.text = resultOrError.result
}
}
})
viewModel.modelDownloading.observe(viewLifecycleOwner, Observer { isDownloading ->
progressBar.visibility = if (isDownloading) {
View.VISIBLE
} else {
View.INVISIBLE
}
progressText.visibility = progressBar.visibility
})
overlay.apply {
setZOrderOnTop(true)
holder.setFormat(PixelFormat.TRANSPARENT)
holder.addCallback(object : SurfaceHolder.Callback {
override fun surfaceChanged(
holder: SurfaceHolder?,
format: Int,
width: Int,
height: Int
) {
}
override fun surfaceDestroyed(holder: SurfaceHolder?) {
}
override fun surfaceCreated(holder: SurfaceHolder?) {
holder?.let { drawOverlay(it,
DESIRED_HEIGHT_CROP_PERCENT,
DESIRED_WIDTH_CROP_PERCENT
) }
}
})
}
}
/** Initialize CameraX, and prepare to bind the camera use cases */
private fun setUpCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
cameraProviderFuture.addListener(Runnable {
// CameraProvider
cameraProvider = cameraProviderFuture.get()
// Build and bind the camera use cases
bindCameraUseCases()
}, ContextCompat.getMainExecutor(requireContext()))
}
private fun bindCameraUseCases() {
val cameraProvider = cameraProvider
?: throw IllegalStateException("Camera initialization failed.")
// Get screen metrics used to setup camera for full screen resolution
val metrics = DisplayMetrics().also { viewFinder.display.getRealMetrics(it) }
Log.d(TAG, "Screen metrics: ${metrics.widthPixels} x ${metrics.heightPixels}")
val screenAspectRatio = aspectRatio(metrics.widthPixels, metrics.heightPixels)
Log.d(TAG, "Preview aspect ratio: $screenAspectRatio")
val rotation = viewFinder.display.rotation
val preview = Preview.Builder()
.setTargetAspectRatio(screenAspectRatio)
.setTargetRotation(rotation)
.build()
// Build the image analysis use case and instantiate our analyzer
imageAnalyzer = ImageAnalysis.Builder()
// We request aspect ratio but no resolution
.setTargetAspectRatio(screenAspectRatio)
.setTargetRotation(rotation)
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
.also {
it.setAnalyzer(
cameraExecutor
, TextAnalyzer(
requireContext(),
lifecycle,
viewModel.sourceText,
viewModel.imageCropPercentages
)
)
}
viewModel.sourceText.observe(viewLifecycleOwner, Observer { srcText.text = it })
viewModel.imageCropPercentages.observe(viewLifecycleOwner,
Observer { drawOverlay(overlay.holder, it.first, it.second) })
// Select back camera since text detection does not work with front camera
val cameraSelector =
CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build()
try {
// Unbind use cases before rebinding
cameraProvider.unbindAll()
// Bind use cases to camera
camera = cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageAnalyzer
)
preview.setSurfaceProvider(viewFinder.createSurfaceProvider())
} catch (exc: IllegalStateException) {
Log.e(TAG, "Use case binding failed. This must be running on main thread.", exc)
}
}
private fun drawOverlay(
holder: SurfaceHolder,
heightCropPercent: Int,
widthCropPercent: Int
) {
val canvas = holder.lockCanvas()
val bgPaint = Paint().apply {
alpha = 140
}
canvas.drawPaint(bgPaint)
val rectPaint = Paint()
rectPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
rectPaint.style = Paint.Style.FILL
rectPaint.color = Color.WHITE
val outlinePaint = Paint()
outlinePaint.style = Paint.Style.STROKE
outlinePaint.color = Color.WHITE
outlinePaint.strokeWidth = 4f
val surfaceWidth = holder.surfaceFrame.width()
val surfaceHeight = holder.surfaceFrame.height()
val cornerRadius = 25f
// Set rect centered in frame
val rectTop = surfaceHeight * heightCropPercent / 2 / 100f
val rectLeft = surfaceWidth * widthCropPercent / 2 / 100f
val rectRight = surfaceWidth * (1 - widthCropPercent / 2 / 100f)
val rectBottom = surfaceHeight * (1 - heightCropPercent / 2 / 100f)
val rect = RectF(rectLeft, rectTop, rectRight, rectBottom)
canvas.drawRoundRect(
rect, cornerRadius, cornerRadius, rectPaint
)
canvas.drawRoundRect(
rect, cornerRadius, cornerRadius, outlinePaint
)
val textPaint = Paint()
textPaint.color = Color.WHITE
textPaint.textSize = 50F
val overlayText = getString(R.string.overlay_help)
val textBounds = Rect()
textPaint.getTextBounds(overlayText, 0, overlayText.length, textBounds)
val textX = (surfaceWidth - textBounds.width()) / 2f
val textY = rectBottom + textBounds.height() + 15f // put text below rect and 15f padding
canvas.drawText(getString(R.string.overlay_help), textX, textY, textPaint)
holder.unlockCanvasAndPost(canvas)
}
/**
* [androidx.camera.core.ImageAnalysisConfig] requires enum value of
* [androidx.camera.core.AspectRatio]. Currently it has values of 4:3 & 16:9.
*
* Detecting the most suitable ratio for dimensions provided in #params by comparing absolute
* of preview ratio to one of the provided values.
*
* #param width - preview width
* #param height - preview height
* #return suitable aspect ratio
*/
private fun aspectRatio(width: Int, height: Int): Int {
val previewRatio = ln(max(width, height).toDouble() / min(width, height))
if (abs(previewRatio - ln(RATIO_4_3_VALUE))
<= abs(previewRatio - ln(RATIO_16_9_VALUE))
) {
return AspectRatio.RATIO_4_3
}
return AspectRatio.RATIO_16_9
}
/**
* Process result from permission request dialog box, has the request
* been granted? If yes, start Camera. Otherwise display a toast
*/
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<String>, grantResults: IntArray
) {
if (requestCode == REQUEST_CODE_PERMISSIONS) {
if (allPermissionsGranted()) {
viewFinder.post {
// Keep track of the display in which this view is attached
displayId = viewFinder.display.displayId
// Set up the camera and its use cases
setUpCamera()
}
} else {
Toast.makeText(
context,
"Permissions not granted by the user.",
Toast.LENGTH_SHORT
).show()
}
}
}
/**
* Check if all permission specified in the manifest have been granted
*/
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(
requireContext(), it
) == PackageManager.PERMISSION_GRANTED
}
}
Its probably because of Version confict . Just Remove all 3 methods and implement them with ALT+Enter . again .
As the error says surfaceChanged defined as follows
fun surfaceChanged(p0: SurfaceHolder, p1: Int, p2: Int, p3: Int): Unit
But when you are implementing it you are making SurfaceHolder parameter null-able.
holder.addCallback(object : SurfaceHolder.Callback {
override fun surfaceChanged(
holder: SurfaceHolder,
format: Int,
width: Int,
height: Int
) {
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
}
override fun surfaceCreated(holder: SurfaceHolder) {
holder?.let { drawOverlay(it,
DESIRED_HEIGHT_CROP_PERCENT,
DESIRED_WIDTH_CROP_PERCENT
) }
}
})
I have a viewPager with CubeTransformer, which is transforming every fragment. And inside every fragment is Image or Video view (with Exoplayer). And when you are trying to change a framgnet with transformation, exoplayer losses preview (I've got a black screen), even it's not playing. But after you changing condition to normal, preview is coming back
Ohterwise, if you will remove pageTransformer, review is not dissapears. How to keep preview always on screen?
CubeTransformer
class CubeTransformer : ViewPager.PageTransformer {
override fun transformPage(view: View, position: Float) {
if (view.visibility != View.VISIBLE) return
view.apply {
cameraDistance = (view.width * distanceMultiplier).toFloat()
pivotX = if (position < 0f) view.width.toFloat() else 0f
pivotY = view.height * 0.5f
rotationY = 90f * position
if (position < -1 || position > 1) {
alpha = 0f // item not visible
} else {
alpha = 1f
}
}
}
private companion object {
private const val distanceMultiplier: Int = 20
}
}
VideoView
class VideoView(context: Context) : ConstraintLayout(context, null) {
private val player = ExoPlayerFactory.newSimpleInstance(context, DefaultTrackSelector(), DefaultLoadControl())
private val dataSourceFactory = DefaultDataSourceFactory(context, "android")
private lateinit var model: Model
init {
inflate(context, R.layout.story_item_video, this)
video_view.player = player
video_view.keepScreenOn = true
video_view.setBackgroundColor(Color.TRANSPARENT)
video_view.setShutterBackgroundColor(Color.TRANSPARENT)
}
fun setData(model: Model?) {
if (model== null) return
this.model = model
val mediaSource = HlsMediaSource
.Factory(dataSourceFactory)
.setExtractorFactory(DefaultHlsExtractorFactory())
.createMediaSource(Uri.parse(model.streamLink))
player.playWhenReady = true
player.prepare(mediaSource)
player.addListener(object: Player.EventListener {
override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters?) {
}
override fun onSeekProcessed() {}
override fun onTracksChanged(trackGroups: TrackGroupArray?, trackSelections: TrackSelectionArray?) {
}
override fun onPlayerError(error: ExoPlaybackException?) {
}
override fun onLoadingChanged(isLoading: Boolean) {
}
override fun onPositionDiscontinuity(reason: Int) {
}
override fun onRepeatModeChanged(repeatMode: Int) {
}
override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {
}
override fun onTimelineChanged(timeline: Timeline?, manifest: Any?, reason: Int) {
}
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
}
})
}
}
After a day of searching, I've found an answer to my question. You just need to add app:surface_type="texture_view" to your PlayerView
After update from
androidx.camera:camera-core:1.0.0-alpha03
to
androidx.camera:camera-core:1.0.0-alpha06
signatures of methods setTargetAspectRatio (in ImageCaptureConfig.Builder) and takePicture (in ImageCapture) have been changed.
Official documentation and info in web doesn't show how to use new methods (how to specify executor).
Code which broken after update:
...
val captureConfig = ImageCaptureConfig.Builder()
.setTargetAspectRatioCustom(Rational(1, 1)) //this method changed
.setFlashMode(flashMode)
.setLensFacing(lensFacing)
.build()
val capture = ImageCapture(captureConfig)
binding.takeAPhoto.setOnClickListener {
...
val imageFile = createTempFile(System.currentTimeMillis().toString(), ".jpg")
capture.takePicture(imageFile, object : ImageCapture.OnImageSavedListener { //this method also changed
override fun onImageSaved(file: File) {
...
}
override fun onError(useCaseError: ImageCapture.UseCaseError, message: String, cause: Throwable?) {
...
})
}
}
Does anyone have (or know where to find) example of how to use new methods?
Thanks in advance
The official Google Codelabs which obviously have been updated recently use: Executors.newSingleThreadExecutor()
Reference: https://codelabs.developers.google.com/codelabs/camerax-getting-started/#4
Edit: Since #kos's response also makes sense to me, I've added these two official Android docs references:
https://developer.android.com/reference/java/util/concurrent/Executors.html#newSingleThreadExecutor()
https://developer.android.com/reference/java/util/concurrent/Executors.html#newCachedThreadPool()
This way every reader of this topic can make up his/her own mind with respect to executors.
FURTHER EDIT: There are crucial API changes since 1.0.0-alpha07 so I studied some of the docs. There's a GitHub sample showing executor retrieval like so mainExecutor = ContextCompat.getMainExecutor(requireContext())(Source)
If some of you already implemented CameraX and it works fine, I'd definitely wait for the beta release as recommended by Android's release notes
I faced same thing as you are facing. I resolved it from my side.
class MainActivity : AppCompatActivity(), Executor {
private var right: Int = 0
private var bottom: Int = 0
private var left: Int = 0
private var top: Int = 0
private lateinit var preview: Preview
private val REQUEST_CODE_PERMISSIONS = 10
private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
private lateinit var imageCapture: ImageCapture
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (allPermissionsGranted()) {
viewFinder.post { startCamera() }
} else {
ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
}
viewFinder.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
updateTransform()
}
buttonPlus.setOnClickListener {
if (right < 100) {
right += 100
bottom += 100
left += 100
top += 100
val my = Rect(left, top, right, bottom)
preview.zoom(my)
}
}
buttonMinus.setOnClickListener {
if (right > 0) {
right -= 100
bottom -= 100
left -= 100
top -= 100
val my = Rect(left, top, right, bottom)
preview.zoom(my)
}
}
}
#SuppressLint("RestrictedApi")
private fun startCamera() {
val metrics = DisplayMetrics().also { viewFinder.display.getRealMetrics(it) }
val screenAspectRatio = Rational(metrics.widthPixels, metrics.heightPixels)
val previewConfig = PreviewConfig.Builder().apply {
setTargetAspectRatioCustom(screenAspectRatio)
setTargetRotation(viewFinder.display.rotation)
}.build()
preview = Preview(previewConfig)
preview.setOnPreviewOutputUpdateListener {
val parent = viewFinder.parent as ViewGroup
parent.removeView(viewFinder)
parent.addView(viewFinder, 0)
viewFinder.surfaceTexture = it.surfaceTexture
updateTransform()
}
CameraX.bindToLifecycle(this, preview)
captureImage()
}
#SuppressLint("RestrictedApi")
private fun captureImage() {
val imageCaptureConfig = ImageCaptureConfig.Builder()
.apply {
setTargetAspectRatioCustom(Rational(1, 1))
setCaptureMode(ImageCapture.CaptureMode.MIN_LATENCY)
}.build()
imageCapture = ImageCapture(imageCaptureConfig)
CameraX.bindToLifecycle(this, imageCapture)
capture_button.setOnClickListener {
val file = File(this.externalMediaDirs.first(), "${System.currentTimeMillis()}.jpg")
imageCapture.takePicture(file, this, object : ImageCapture.OnImageSavedListener {
override fun onImageSaved(file: File) {
val msg = "Photo capture succeeded: ${file.absolutePath}"
Log.d("CameraXApp", msg)
}
override fun onError(imageCaptureError: ImageCapture.ImageCaptureError, message: String, cause: Throwable?) {
val msg = "Photo capture failed: $message"
Log.e("CameraXApp", msg)
cause?.printStackTrace()
}
})
}
}
override fun execute(command: Runnable) {
command.run()
}
private fun updateTransform() {
val matrix = Matrix()
val centerX = viewFinder.width / 2f
val centerY = viewFinder.height / 2f
val rotationDegrees = when (viewFinder.display.rotation) {
Surface.ROTATION_0 -> 0
Surface.ROTATION_90 -> 90
Surface.ROTATION_180 -> 180
Surface.ROTATION_270 -> 270
else -> return
}
matrix.postRotate(-rotationDegrees.toFloat(), centerX, centerY)
viewFinder.setTransform(matrix)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
if (requestCode == REQUEST_CODE_PERMISSIONS) {
if (allPermissionsGranted()) {
viewFinder.post { startCamera() }
} else {
Toast.makeText(this, "Permissions not granted by the user.", Toast.LENGTH_SHORT).show()
finish()
}
}
}
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(baseContext, it) == PackageManager.PERMISSION_GRANTED
}
override fun onDestroy() {
super.onDestroy()
imageCapture.let {
CameraX.unbind(imageCapture)
}
}
}
And the output is (As I print log in onImageSaved method)
Photo capture succeeded: /storage/emulated/0/Android/media/com.akshay.cameraxzoominoutdemo/1571052301192.jpg
It's working fine for me, try out this.
You can do it like this.
imageCapture.takePicture(file, { it.run() }, object : ImageCapture.OnImageSavedListener {
override fun onImageSaved(file: File) {}
override fun onError(useCaseError: ImageCapture.ImageCaptureError, message: String, cause: Throwable?) {}
})
Here is a change log for the changes in alpha06 : https://developer.android.com/jetpack/androidx/releases/camera
setTargetAspectRatio() method now takes AspectRatio enum with 4_3 or 16_9 value.
takePicture() method takes (file, metadata, executor, imageSavedListener) // could use executor as per your case/need. example is val executor = Executors.newSingleThreadExecutor()
instead of useCase.onPreviewOutputUpdateListener = use useCase.setOnPreviewOutputUpdateListener()
FYI : CameraX will be in Beta in Dec 2019
Inside your click listener call this function/method :
private fun saveImage(){
val file = File(this.externalMediaDirs.first(), "${System.currentTimeMillis()}.jpg")
val fileB = ImageCapture.OutputFileOptions.Builder(file).build()
imageCapture.takePicture(fileB, ContextCompat.getMainExecutor(this), object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(fileB: ImageCapture.OutputFileResults) {
val msg = "${fileB.savedUri} - ${file.absolutePath} - ${file.toURI()}"
}
override fun onError(imageCaptureError: ImageCaptureException) {
val msg = "Photo capture failed: ${imageCaptureError.toString()}"
}
})
}
And the msg in onImageSaved will contain something like this :
null - /storage/emulated/0/Android/media/com.mua.camx/1607589430984.jpg - file:/storage/emulated/0/Android/media/com.mua.camx/1607589430984.jpg
You have to only run the command as below.
#Override
public void execute(Runnable command) {
command.run(); // <-- THIS IS NEEDED
}
CameraX provides with built-in executors and take picture can be implemented as below:
imgCaptureButton.setOnClickListener(new View.OnClickListener() {
#Override
#SuppressLint("RestrictedApi")
public void onClick(View v) {
imgCap.takePicture(CameraXExecutors.mainThreadExecutor(),new ImageCapture.OnImageCapturedListener() {
#Override
public void onCaptureSuccess(ImageProxy image, int rotationDegrees) {
super.onCaptureSuccess(image, rotationDegrees);
// Play with the Image here.
}
});
}
});
It does not use File to save the image, instead saves the image as a buffer in the memory.
So I use kotlin for android, and when inflating views, I tend to do the following:
private val recyclerView by lazy { find<RecyclerView>(R.id.recyclerView) }
This method will work. However, there is a case in which it will bug the app. If this is a fragment, and the fragment goes to the backstack, onCreateView will be called again, and the view hierarchy of the fragment will recreated. Which means, the lazy initiated recyclerView will point out to an old view no longer existent.
A solution is like this:
private lateinit var recyclerView: RecyclerView
And initialise all the properties inside onCreateView.
My question is, is there any way to reset lazy properties so they can be initialised again? I like the fact initialisations are all done at the top of a class, helps to keep the code organised. The specific problem is found in this question: kotlin android fragment empty recycler view after back
Here is a quick version of a resettable lazy, it could be more elegant and needs double checked for thread safety, but this is basically the idea. You need something to manage (keep track) of the lazy delegates so you can call for reset, and then things that can be managed and reset. This wraps lazy() in these management classes.
Here is what your final class will look like, as an example:
class Something {
val lazyMgr = resettableManager()
val prop1: String by resettableLazy(lazyMgr) { ... }
val prop2: String by resettableLazy(lazyMgr) { ... }
val prop3: String by resettableLazy(lazyMgr) { ... }
}
Then to make the lazy's all go back to new values on next time they are accessed:
lazyMgr.reset() // prop1, prop2, and prop3 all will do new lazy values on next access
The implementation of the resettable lazy:
class ResettableLazyManager {
// we synchronize to make sure the timing of a reset() call and new inits do not collide
val managedDelegates = LinkedList<Resettable>()
fun register(managed: Resettable) {
synchronized (managedDelegates) {
managedDelegates.add(managed)
}
}
fun reset() {
synchronized (managedDelegates) {
managedDelegates.forEach { it.reset() }
managedDelegates.clear()
}
}
}
interface Resettable {
fun reset()
}
class ResettableLazy<PROPTYPE>(val manager: ResettableLazyManager, val init: ()->PROPTYPE): Resettable {
#Volatile var lazyHolder = makeInitBlock()
operator fun getValue(thisRef: Any?, property: KProperty<*>): PROPTYPE {
return lazyHolder.value
}
override fun reset() {
lazyHolder = makeInitBlock()
}
fun makeInitBlock(): Lazy<PROPTYPE> {
return lazy {
manager.register(this)
init()
}
}
}
fun <PROPTYPE> resettableLazy(manager: ResettableLazyManager, init: ()->PROPTYPE): ResettableLazy<PROPTYPE> {
return ResettableLazy(manager, init)
}
fun resettableManager(): ResettableLazyManager = ResettableLazyManager()
And some unit tests to be sure:
class Tester {
#Test fun testResetableLazy() {
class Something {
var seed = 1
val lazyMgr = resettableManager()
val x: String by resettableLazy(lazyMgr) { "x ${seed}" }
val y: String by resettableLazy(lazyMgr) { "y ${seed}" }
val z: String by resettableLazy(lazyMgr) { "z $x $y"}
}
val s = Something()
val x1 = s.x
val y1 = s.y
val z1 = s.z
assertEquals(x1, s.x)
assertEquals(y1, s.y)
assertEquals(z1, s.z)
s.seed++ // without reset nothing should change
assertTrue(x1 === s.x)
assertTrue(y1 === s.y)
assertTrue(z1 === s.z)
s.lazyMgr.reset()
s.seed++ // because of reset the values should change
val x2 = s.x
val y2 = s.y
val z2 = s.z
assertEquals(x2, s.x)
assertEquals(y2, s.y)
assertEquals(z2, s.z)
assertNotEquals(x1, x2)
assertNotEquals(y1, y2)
assertNotEquals(z1, z2)
s.seed++ // but without reset, nothing should change
assertTrue(x2 === s.x)
assertTrue(y2 === s.y)
assertTrue(z2 === s.z)
}
}
I find a convenient method:
import java.util.concurrent.atomic.AtomicReference
import kotlin.reflect.KProperty
fun <T> resetableLazy(initializer: () -> T) = ResetableDelegate(initializer)
class ResetableDelegate<T>(private val initializer: () -> T) {
private val lazyRef: AtomicReference<Lazy<T>> = AtomicReference(
lazy(
initializer
)
)
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
return lazyRef.get().getValue(thisRef, property)
}
fun reset() {
lazyRef.set(lazy(initializer))
}
}
test:
import org.junit.Assert
import org.junit.Test
class ResetableLazyData {
var changedData = 0
val delegate = resetableLazy { changedData }
val readOnlyData by delegate
}
class ResetableLazyTest {
#Test
fun testResetableLazy() {
val data = ResetableLazyData()
data.changedData = 1
Assert.assertEquals(data.changedData, data.readOnlyData)
data.changedData = 2
Assert.assertNotEquals(data.changedData, data.readOnlyData)
data.delegate.reset()
Assert.assertEquals(data.changedData, data.readOnlyData)
data.changedData = 3
Assert.assertNotEquals(data.changedData, data.readOnlyData)
}
}
I had the same task, and this is what I used:
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
class SingletonLazy<T : Any>(val initBlock: () -> T, val clazz: Class<T>) {
operator fun <R> provideDelegate(ref: R, prop: KProperty<*>): ReadOnlyProperty<R, T> = delegate()
#Suppress("UNCHECKED_CAST")
private fun <R> delegate(): ReadOnlyProperty<R, T> = object : ReadOnlyProperty<R, T> {
override fun getValue(thisRef: R, property: KProperty<*>): T {
val hash = clazz.hashCode()
val cached = singletonsCache[hash]
if (cached != null && cached.javaClass == clazz) return cached as T
return initBlock().apply { singletonsCache[hash] = this }
}
}
}
private val singletonsCache = HashMap<Int, Any>()
fun <T> clearSingleton(clazz: Class<T>) : Boolean {
val hash = clazz.hashCode()
val result = singletonsCache[hash]
if (result?.javaClass != clazz) return false
singletonsCache.remove(hash)
return true
}
inline fun <reified T : Any> singletonLazy(noinline block: () -> T): SingletonLazy<T>
= SingletonLazy(block, T::class.java)
usage:
val cat: Cat by singletonLazy { Cat() }
fun main(args: Array<String>) {
cat
println(clearSingleton(Cat::class.java))
cat // cat will be created one more time
println(singletonsCache.size)
}
class Cat {
init { println("creating cat") }
}
Of course, you may have you own caching strategies.
If you want something very simple, extends Lazy<T> and yet efficient in few lines of code, you could use this
class MutableLazy<T>(private val initializer: () -> T) : Lazy<T> {
private var cached: T? = null
override val value: T
get() {
if (cached.isNull()) {
cached = initializer()
}
#Suppress("UNCHECKED_CAST")
return cached as T
}
fun reset() {
cached = null
}
override fun isInitialized(): Boolean = cached != null
companion object {
fun <T> resettableLazy(value: () -> T) = MutableLazy(value)
}
}
Use it like this:
class MainActivity() {
val recyclerViewLazy = MutableLazy.resettable {
findViewById<RecyclerView>(R.id.recyclerView)
}
val recyclerView by recyclerViewLazy
// And later on
override onCreate(savedInstanceState: Bundle?) {
recyclerViewLazy.reset() /** On next get of the recyclerView, it would be updated*/
}
}
Borrowed partly from
lazy(LazyThreadSafetyMode.NONE) { }
provided in the stlib
you can try this
fun <P, T> renewableLazy(initializer: (P) -> T): ReadWriteProperty<P, T> =
RenewableSynchronizedLazyWithThisImpl({ t, _ ->
initializer.invoke(t)
})
fun <P, T> renewableLazy(initializer: (P, KProperty<*>) -> T): ReadWriteProperty<P, T> =
RenewableSynchronizedLazyWithThisImpl(initializer)
class RenewableSynchronizedLazyWithThisImpl<in T, V>(
val initializer: (T, KProperty<*>) -> V,
private val lock: Any = {}
) : ReadWriteProperty<T, V> {
#Volatile
private var _value: Any? = null
override fun getValue(thisRef: T, property: KProperty<*>): V {
val _v1 = _value
if (_v1 !== null) {
#Suppress("UNCHECKED_CAST")
return _v1 as V
}
return synchronized(lock) {
val _v2 = _value
if (_v2 !== null) {
#Suppress("UNCHECKED_CAST") (_v2 as V)
} else {
val typedValue = initializer(thisRef, property)
_value = typedValue
typedValue
}
}
}
override fun setValue(thisRef: T, property: KProperty<*>, value: V) {
// 不论设置何值,都会被重置为空
synchronized(lock) {
_value = null
}
}
}