This answer is derived from @Husayn's answer. I have added relevant sample code part.
Camerax image size for preview and analysis varies for various reasons (example device specific display size/hardware/camera or app specific view and processing)
However there are options to map the processing image size and resulting xy coordinates to preview size and to preview xy coordinates.
Setup layout with DimensionRatio 3:4 for both preview and analysis overlay in layout,
Example:
<androidx.camera.view.PreviewView
android:id="@+id/view_finder"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="3:4"
app:layout_constraintTop_toTopOf="parent"/>
<com.loa.sepanex.scanner.view.GraphicOverlay
android:id="@+id/graphic_overlay"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="3:4"
app:layout_constraintTop_toTopOf="parent"/>
setup preview and analysis use cases wuth AspectRatio.RATIO_4_3
Example:
viewFinder = view.findViewById(R.id.view_finder)
graphicOverlay = view.findViewById(R.id.graphic_overlay)
//...
preview = Preview.Builder()
.setTargetAspectRatio(AspectRatio.RATIO_4_3)
.setTargetRotation(rotation)
.build()
imageAnalyzer = ImageAnalysis.Builder()
.setTargetAspectRatio(AspectRatio.RATIO_4_3)
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.setTargetRotation(rotation)
.build()
.also {
it.setAnalyzer(cameraExecutor, ImageAnalysis.Analyzer {
image ->
//val rotationDegrees = image.imageInfo.rotationDegrees
try {
val mediaImage: Image? = image.image
if (mediaImage != null) {
val imageForFaceDetectionProcess = InputImage.fromMediaImage(mediaImage, image.getImageInfo().getRotationDegrees())
//...
}
}
}
}
Define scale and traslate APIs for getting the mapping of analysis image xy coordinates to preview xy coordinates, as shown below
val preview = viewFinder.getChildAt(0)
var previewWidth = preview.width * preview.scaleX
var previewHeight = preview.height * preview.scaleY
val rotation = preview.display.rotation
if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) {
val temp = previewWidth
previewWidth = previewHeight
previewHeight = temp
}
val isImageFlipped = lensFacing == CameraSelector.LENS_FACING_FRONT
val rotationDegrees: Int = imageProxy.getImageInfo().getRotationDegrees()
if (rotationDegrees == 0 || rotationDegrees == 180) {
graphicOverlay!!.setImageSourceInfo(
imageProxy.getWidth(), imageProxy.getHeight(), isImageFlipped)
} else {
graphicOverlay!!.setImageSourceInfo(
imageProxy.getHeight(), imageProxy.getWidth(), isImageFlipped)
}
:::
:::
float viewAspectRatio = (float) previewWidth / previewHeight;
float imageAspectRatio = (float) imageWidth / imageHeight;
postScaleWidthOffset = 0;
postScaleHeightOffset = 0;
if (viewAspectRatio > imageAspectRatio) {
// The image needs to be vertically cropped to be displayed in this view.
scaleFactor = (float) previewWidth / imageWidth;
postScaleHeightOffset = ((float) previewWidth / imageAspectRatio - previewHeight) / 2;
} else {
// The image needs to be horizontally cropped to be displayed in this view.
scaleFactor = (float) previewHeight / imageHeight;
postScaleWidthOffset = ((float) previewHeight * imageAspectRatio - previewWidth) / 2;
}
transformationMatrix.reset();
transformationMatrix.setScale(scaleFactor, scaleFactor);
transformationMatrix.postTranslate(-postScaleWidthOffset, -postScaleHeightOffset);
if (isImageFlipped) {
transformationMatrix.postScale(-1f, 1f, previewWidth / 2f, previewHeight / 2f);
}
:::
:::
public float scale(float imagePixel) {
return imagePixel * overlay.scaleFactor;
}
public float translateX(float x) {
if (overlay.isImageFlipped) {
return overlay.getWidth() - (scale(x) - overlay.postScaleWidthOffset);
} else {
return scale(x) - overlay.postScaleWidthOffset;
}
}
public float translateY(float y) {
return scale(y) - overlay.postScaleHeightOffset;
}
use translateX and translateY methods for plotting analysis image based data into preview
Example:
for (FaceContour contour : face.getAllContours()) {
for (PointF point : contour.getPoints()) {
canvas.drawCircle(translateX(point.x), translateY(point.y), FACE_POSITION_RADIUS, facePositionPaint);
}
}