如何在 CameraX 的相机预览中裁剪图像矩形
How to crop image rectangle in camera preview on CameraX
我有一个自定义相机应用程序,它有一个居中的矩形视图,如下所示:
当我拍照时,我想忽略矩形以外的一切。这是我的 XML 布局:
<?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:background="@color/black_50">
<TextureView
android:id="@+id/viewFinder"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:layout_width="match_parent"
android:layout_height="250dp"
android:layout_margin="16dp"
android:background="@drawable/rectangle"
app:layout_constraintBottom_toTopOf="@+id/cameraBottomView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/cameraBottomView"
android:layout_width="match_parent"
android:layout_height="130dp"
android:background="@color/black_50"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<ImageButton
android:id="@+id/cameraCaptureImageButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:src="@drawable/ic_capture_image"
app:layout_constraintBottom_toBottomOf="@id/cameraBottomView"
app:layout_constraintEnd_toEndOf="@id/cameraBottomView"
app:layout_constraintStart_toStartOf="@id/cameraBottomView"
app:layout_constraintTop_toTopOf="@id/cameraBottomView"
tools:ignore="ContentDescription" />
</androidx.constraintlayout.widget.ConstraintLayout>
这是我的 cameraX 预览的 kotlin 代码:
class CameraFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_camera, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewFinder.post { setupCamera() }
}
private fun setupCamera() {
CameraX.unbindAll()
CameraX.bindToLifecycle(
this,
buildPreviewUseCase(),
buildImageCaptureUseCase(),
buildImageAnalysisUseCase()
)
}
private fun buildPreviewUseCase(): Preview {
val preview = Preview(
UseCaseConfigBuilder.buildPreviewConfig(
viewFinder.display
)
)
preview.setOnPreviewOutputUpdateListener { previewOutput ->
updateViewFinderWithPreview(previewOutput)
correctPreviewOutputForDisplay(previewOutput.textureSize)
}
return preview
}
private fun updateViewFinderWithPreview(previewOutput: Preview.PreviewOutput) {
val parent = viewFinder.parent as ViewGroup
parent.removeView(viewFinder)
parent.addView(viewFinder, 0)
viewFinder.surfaceTexture = previewOutput.surfaceTexture
}
/**
* Corrects the camera/preview's output to the display, by scaling
* up/down and/or rotating the camera/preview's output.
*/
private fun correctPreviewOutputForDisplay(textureSize: Size) {
val matrix = Matrix()
val centerX = viewFinder.width / 2f
val centerY = viewFinder.height / 2f
val displayRotation = getDisplayRotation()
val (dx, dy) = getDisplayScalingFactors(textureSize)
matrix.postRotate(displayRotation, centerX, centerY)
matrix.preScale(dx, dy, centerX, centerY)
// Correct preview output to account for display rotation and scaling
viewFinder.setTransform(matrix)
}
private fun getDisplayRotation(): Float {
val rotationDegrees = when (viewFinder.display.rotation) {
Surface.ROTATION_0 -> 0
Surface.ROTATION_90 -> 90
Surface.ROTATION_180 -> 180
Surface.ROTATION_270 -> 270
else -> throw IllegalStateException("Unknown display rotation ${viewFinder.display.rotation}")
}
return -rotationDegrees.toFloat()
}
private fun getDisplayScalingFactors(textureSize: Size): Pair<Float, Float> {
val cameraPreviewRation = textureSize.height / textureSize.width.toFloat()
val scaledWidth: Int
val scaledHeight: Int
if (viewFinder.width > viewFinder.height) {
scaledHeight = viewFinder.width
scaledWidth = (viewFinder.width * cameraPreviewRation).toInt()
} else {
scaledHeight = viewFinder.height
scaledWidth = (viewFinder.height * cameraPreviewRation).toInt()
}
val dx = scaledWidth / viewFinder.width.toFloat()
val dy = scaledHeight / viewFinder.height.toFloat()
return Pair(dx, dy)
}
private fun buildImageCaptureUseCase(): ImageCapture {
val capture = ImageCapture(
UseCaseConfigBuilder.buildImageCaptureConfig(
viewFinder.display
)
)
cameraCaptureImageButton.setOnClickListener {
capture.takePicture(
FileCreator.createTempFile(JPEG_FORMAT),
Executors.newSingleThreadExecutor(),
object : ImageCapture.OnImageSavedListener {
override fun onImageSaved(file: File) {
requireActivity().runOnUiThread {
launchGalleryFragment(file.absolutePath)
}
}
override fun onError(
imageCaptureError: ImageCapture.ImageCaptureError,
message: String,
cause: Throwable?
) {
Toast.makeText(requireContext(), "Error: $message", Toast.LENGTH_LONG)
.show()
Log.e("CameraFragment", "Capture error $imageCaptureError: $message", cause)
}
})
}
return capture
}
private fun buildImageAnalysisUseCase(): ImageAnalysis {
val analysis = ImageAnalysis(
UseCaseConfigBuilder.buildImageAnalysisConfig(
viewFinder.display
)
)
analysis.setAnalyzer(
Executors.newSingleThreadExecutor(),
ImageAnalysis.Analyzer { image, rotationDegrees ->
Log.d(
"CameraFragment",
"Image analysis: $image - Rotation degrees: $rotationDegrees"
)
})
return analysis
}
private fun launchGalleryFragment(path: String) {
val action = CameraFragmentDirections.actionLaunchGalleryFragment(path)
findNavController().navigate(action)
}
}
当我拍照并将其发送到新页面 (GalleryPage) 时,它会显示相机预览中的所有屏幕,如下所示:
这是从 cameraX 预览中获取图片并将其显示到 ImageView 中的 kotlin 代码:
class GalleryFragment : Fragment() {
private lateinit var imageView: ImageView
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_gallery, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
imageView = view.findViewById(R.id.img)
val imageFilePath = GalleryFragmentArgs.fromBundle(arguments!!).data
val bitmap = BitmapFactory.decodeFile(imageFilePath)
val rotatedBitmap = bitmap.rotate(90)
if (imageFilePath.isBlank()) {
Log.i(
"GalleryFragment",
"Image is Null or Empty"
)
} else {
Glide.with(activity!!)
.load(rotatedBitmap)
.into(imageView)
}
}
private fun Bitmap.rotate(degree:Int):Bitmap{
// Initialize a new matrix
val matrix = Matrix()
// Rotate the bitmap
matrix.postRotate(degree.toFloat())
// Resize the bitmap
val scaledBitmap = Bitmap.createScaledBitmap(
this,
width,
height,
true
)
// Create and return the rotated bitmap
return Bitmap.createBitmap(
scaledBitmap,
0,
0,
scaledBitmap.width,
scaledBitmap.height,
matrix,
true
)
}
}
有人可以帮我如何正确裁剪图像吗?因为我已经在搜索和研究如何去做,但仍然感到困惑并且不适合我。
这是我如何裁剪您提到的 cameraX 拍摄的图像的示例。我不知道这是否是最好的方法,我很想知道其他解决方案。
camerax_version = "1.0.0-alpha07"
CameraFragment.java
初始化cameraX :
// Views
private PreviewView previewView;
// CameraX
private ProcessCameraProvider cameraProvider;
private ListenableFuture<ProcessCameraProvider> cameraProviderFuture;
private CameraSelector cameraSelector;
private Executor executor;
private ImageCapture imageCapture;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
cameraProviderFuture = ProcessCameraProvider.getInstance(getContext());
executor = ContextCompat.getMainExecutor(getContext());
cameraSelector = new CameraSelector.Builder().requireLensFacing(LensFacing.BACK).build();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
previewView = view.findViewById(R.id.preview);
ImageButton btnCapture = view.findViewById(R.id.btn_capture);
// Wait for the view to be properly laid out
previewView.post(() ->{
//Initialize CameraX
cameraProviderFuture.addListener(() -> {
if(cameraProvider != null) cameraProvider.unbindAll();
try {
cameraProvider = cameraProviderFuture.get();
// Set up the preview use case to display camera preview
Preview preview = new Preview.Builder()
.setTargetAspectRatio(AspectRatio.RATIO_4_3)
.setTargetRotation(previewView.getDisplay().getRotation())
.build();
preview.setPreviewSurfaceProvider(previewView.getPreviewSurfaceProvider());
// Set up the capture use case to allow users to take photos
imageCapture = new ImageCapture.Builder()
.setCaptureMode(ImageCapture.CaptureMode.MINIMIZE_LATENCY)
.setTargetRotation(previewView.getDisplay().getRotation())
.setTargetAspectRatio(AspectRatio.RATIO_4_3)
.build();
// Apply declared configs to CameraX using the same lifecycle owner
cameraProvider.bindToLifecycle(this, cameraSelector, preview,imageCapture);
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
}, ContextCompat.getMainExecutor(getContext()));
});
btnCapture.setOnClickListener(v -> {
String format = "yyyy-MM-dd-HH-mm-ss-SSS";
SimpleDateFormat fmt = new SimpleDateFormat(format, Locale.US);
String date = fmt.format(System.currentTimeMillis());
File file = new File(getContext().getCacheDir(), date+".jpg");
imageCapture.takePicture(file, executor, imageSavedListener);
});
}
拍完照片后,打开经过照片路径的图库片段:
private ImageCapture.OnImageSavedCallback imageSavedListener = new ImageCapture.OnImageSavedCallback() {
@Override
public void onImageSaved(@NonNull File photoFile) {
// Create new fragment and transaction
Fragment newFragment = new GalleryFragment();
FragmentTransaction transaction = getActivity().getSupportFragmentManager().beginTransaction();
// Set arguments
Bundle args = new Bundle();
args.putString("KEY_PATH", Uri.fromFile(photoFile).toString());
newFragment.setArguments(args);
// Replace whatever is in the fragment_container view with this fragment,
transaction.replace(R.id.fragment_container, newFragment,null);
transaction.addToBackStack(null);
// Commit the transaction
transaction.commit();
}
@Override
public void onError(int imageCaptureError, @NonNull String message, @Nullable Throwable cause) {
if (cause != null) {
cause.printStackTrace();
}
}
};
目前照片还没有裁剪,不知道能不能直接用cameraX裁剪。
GalleryFragment.java
加载传递给片段的参数。
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String path = getArguments().getString("KEY_PATH");
sourceUri = Uri.parse(path);
}
在 ImageView 中使用 glide 加载 Uri,然后裁剪它。
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// Initialize the views
ImageView imageView = view.findViewById(R.id.image_view);
View cropArea = view.findViewById(R.id.crop_area);
// Display the image
Glide.with(this).load(sourceUri).listener(new RequestListener<Drawable>() {
@Override
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
return false;
}
@Override
public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
// Get original bitmap
sourceBitmap = ((BitmapDrawable)resource).getBitmap();
// Create a new bitmap corresponding to the crop area
int[] cropAreaXY = new int[2];
int[] placeHolderXY = new int[2];
Rect rect = new Rect();
imageView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw() {
try {
imageView.getLocationOnScreen(placeHolderXY);
cropArea.getLocationOnScreen(cropAreaXY);
cropArea.getGlobalVisibleRect(rect);
croppedBitmap = Bitmap.createBitmap(sourceBitmap, cropAreaXY[0], cropAreaXY[1] - placeHolderXY[1], rect.width(), rect.height());
// Save the croppedBitmap if you wish
getActivity().runOnUiThread(() -> imageView.setImageBitmap(croppedBitmap));
return true;
}finally {
imageView.getViewTreeObserver().removeOnPreDrawListener(this);
}
}
});
return false;
}
}).into(imageView);
}
fragment_camera.xml
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black">
<androidx.camera.view.PreviewView
android:id="@+id/preview"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="3:4"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/crop_area"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="8dp"
android:background="@drawable/rectangle_round_corners"
app:layout_constraintBottom_toBottomOf="@+id/preview"
app:layout_constraintDimensionRatio="4.5:3"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/cameraBottomView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="@android:color/black"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/preview" />
<ImageButton
android:id="@+id/btn_capture"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:background="@drawable/ic_shutter"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/preview" />
</androidx.constraintlayout.widget.ConstraintLayout>
fragment_gallery.xml
<?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"
android:id="@+id/layout_main"
android:background="@android:color/black"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/image_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/crop_area"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="4.5:3"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
我有一个解决办法,我就是用这个功能在截取图片后裁剪图片:
private fun cropImage(bitmap: Bitmap, frame: View, reference: View): ByteArray {
val heightOriginal = frame.height
val widthOriginal = frame.width
val heightFrame = reference.height
val widthFrame = reference.width
val leftFrame = reference.left
val topFrame = reference.top
val heightReal = bitmap.height
val widthReal = bitmap.width
val widthFinal = widthFrame * widthReal / widthOriginal
val heightFinal = heightFrame * heightReal / heightOriginal
val leftFinal = leftFrame * widthReal / widthOriginal
val topFinal = topFrame * heightReal / heightOriginal
val bitmapFinal = Bitmap.createBitmap(
bitmap,
leftFinal, topFinal, widthFinal, heightFinal
)
val stream = ByteArrayOutputStream()
bitmapFinal.compress(
Bitmap.CompressFormat.JPEG,
100,
stream
) //100 is the best quality possibe
return stream.toByteArray()
}
裁剪图像以参考父视图(如框架)和子视图(如最终参考)
- param
bitmap
要裁剪的图像
- param
frame
图片设置的地方
- param
reference
帧作为裁剪图像的参考
return
图片已裁剪
你可以看到这个例子:https://github.com/rrifafauzikomara/CustomCamera/tree/custom_camerax
我找到了一种使用 camerax 配置执行此操作的简单直接的方法。
从相机预览中获取您需要的预览区域矩形的高度和宽度。
例如
<View
android:background="@drawable/background_drawable"
android:id="@+id/border_view"
android:layout_gravity="center"
android:layout_width="350dp"
android:layout_height="100dp"/>
我的宽度是350dp,高度是100dp
然后用ViewPort得到你需要的区域
val viewPort = ViewPort.Builder(Rational(width, height), rotation).build()
//width = 350, height = 100, rotation = Surface.ROTATION_0
val useCaseGroup = UseCaseGroup.Builder()
.addUseCase(preview) //your preview
.addUseCase(imageAnalysis) //if you are using imageAnalysis
.addUseCase(imageCapture)
.setViewPort(viewPort)
.build()
然后绑定CameraProvider的LifeCycle
cameraProvider.bindToLifecycle(this, cameraSelector, useCaseGroup)
使用此 link CropRect 了解更多信息
如果您需要任何帮助,请在下方评论,我可以为您提供工作源代码。
编辑
如果您希望将图像裁剪为 PreviewView
显示的图像,只需执行以下操作:
val useCaseGroup = UseCaseGroup.Builder()
.addUseCase(preview!!)
.addUseCase(imageCapture!!)
.setViewPort(previewView.viewPort!!)
.build()
camera = cameraProvider.bindToLifecycle(
this, cameraSelector, useCaseGroup)
我有一个自定义相机应用程序,它有一个居中的矩形视图,如下所示:
当我拍照时,我想忽略矩形以外的一切。这是我的 XML 布局:
<?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:background="@color/black_50">
<TextureView
android:id="@+id/viewFinder"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:layout_width="match_parent"
android:layout_height="250dp"
android:layout_margin="16dp"
android:background="@drawable/rectangle"
app:layout_constraintBottom_toTopOf="@+id/cameraBottomView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/cameraBottomView"
android:layout_width="match_parent"
android:layout_height="130dp"
android:background="@color/black_50"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<ImageButton
android:id="@+id/cameraCaptureImageButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:src="@drawable/ic_capture_image"
app:layout_constraintBottom_toBottomOf="@id/cameraBottomView"
app:layout_constraintEnd_toEndOf="@id/cameraBottomView"
app:layout_constraintStart_toStartOf="@id/cameraBottomView"
app:layout_constraintTop_toTopOf="@id/cameraBottomView"
tools:ignore="ContentDescription" />
</androidx.constraintlayout.widget.ConstraintLayout>
这是我的 cameraX 预览的 kotlin 代码:
class CameraFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_camera, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewFinder.post { setupCamera() }
}
private fun setupCamera() {
CameraX.unbindAll()
CameraX.bindToLifecycle(
this,
buildPreviewUseCase(),
buildImageCaptureUseCase(),
buildImageAnalysisUseCase()
)
}
private fun buildPreviewUseCase(): Preview {
val preview = Preview(
UseCaseConfigBuilder.buildPreviewConfig(
viewFinder.display
)
)
preview.setOnPreviewOutputUpdateListener { previewOutput ->
updateViewFinderWithPreview(previewOutput)
correctPreviewOutputForDisplay(previewOutput.textureSize)
}
return preview
}
private fun updateViewFinderWithPreview(previewOutput: Preview.PreviewOutput) {
val parent = viewFinder.parent as ViewGroup
parent.removeView(viewFinder)
parent.addView(viewFinder, 0)
viewFinder.surfaceTexture = previewOutput.surfaceTexture
}
/**
* Corrects the camera/preview's output to the display, by scaling
* up/down and/or rotating the camera/preview's output.
*/
private fun correctPreviewOutputForDisplay(textureSize: Size) {
val matrix = Matrix()
val centerX = viewFinder.width / 2f
val centerY = viewFinder.height / 2f
val displayRotation = getDisplayRotation()
val (dx, dy) = getDisplayScalingFactors(textureSize)
matrix.postRotate(displayRotation, centerX, centerY)
matrix.preScale(dx, dy, centerX, centerY)
// Correct preview output to account for display rotation and scaling
viewFinder.setTransform(matrix)
}
private fun getDisplayRotation(): Float {
val rotationDegrees = when (viewFinder.display.rotation) {
Surface.ROTATION_0 -> 0
Surface.ROTATION_90 -> 90
Surface.ROTATION_180 -> 180
Surface.ROTATION_270 -> 270
else -> throw IllegalStateException("Unknown display rotation ${viewFinder.display.rotation}")
}
return -rotationDegrees.toFloat()
}
private fun getDisplayScalingFactors(textureSize: Size): Pair<Float, Float> {
val cameraPreviewRation = textureSize.height / textureSize.width.toFloat()
val scaledWidth: Int
val scaledHeight: Int
if (viewFinder.width > viewFinder.height) {
scaledHeight = viewFinder.width
scaledWidth = (viewFinder.width * cameraPreviewRation).toInt()
} else {
scaledHeight = viewFinder.height
scaledWidth = (viewFinder.height * cameraPreviewRation).toInt()
}
val dx = scaledWidth / viewFinder.width.toFloat()
val dy = scaledHeight / viewFinder.height.toFloat()
return Pair(dx, dy)
}
private fun buildImageCaptureUseCase(): ImageCapture {
val capture = ImageCapture(
UseCaseConfigBuilder.buildImageCaptureConfig(
viewFinder.display
)
)
cameraCaptureImageButton.setOnClickListener {
capture.takePicture(
FileCreator.createTempFile(JPEG_FORMAT),
Executors.newSingleThreadExecutor(),
object : ImageCapture.OnImageSavedListener {
override fun onImageSaved(file: File) {
requireActivity().runOnUiThread {
launchGalleryFragment(file.absolutePath)
}
}
override fun onError(
imageCaptureError: ImageCapture.ImageCaptureError,
message: String,
cause: Throwable?
) {
Toast.makeText(requireContext(), "Error: $message", Toast.LENGTH_LONG)
.show()
Log.e("CameraFragment", "Capture error $imageCaptureError: $message", cause)
}
})
}
return capture
}
private fun buildImageAnalysisUseCase(): ImageAnalysis {
val analysis = ImageAnalysis(
UseCaseConfigBuilder.buildImageAnalysisConfig(
viewFinder.display
)
)
analysis.setAnalyzer(
Executors.newSingleThreadExecutor(),
ImageAnalysis.Analyzer { image, rotationDegrees ->
Log.d(
"CameraFragment",
"Image analysis: $image - Rotation degrees: $rotationDegrees"
)
})
return analysis
}
private fun launchGalleryFragment(path: String) {
val action = CameraFragmentDirections.actionLaunchGalleryFragment(path)
findNavController().navigate(action)
}
}
当我拍照并将其发送到新页面 (GalleryPage) 时,它会显示相机预览中的所有屏幕,如下所示:
这是从 cameraX 预览中获取图片并将其显示到 ImageView 中的 kotlin 代码:
class GalleryFragment : Fragment() {
private lateinit var imageView: ImageView
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_gallery, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
imageView = view.findViewById(R.id.img)
val imageFilePath = GalleryFragmentArgs.fromBundle(arguments!!).data
val bitmap = BitmapFactory.decodeFile(imageFilePath)
val rotatedBitmap = bitmap.rotate(90)
if (imageFilePath.isBlank()) {
Log.i(
"GalleryFragment",
"Image is Null or Empty"
)
} else {
Glide.with(activity!!)
.load(rotatedBitmap)
.into(imageView)
}
}
private fun Bitmap.rotate(degree:Int):Bitmap{
// Initialize a new matrix
val matrix = Matrix()
// Rotate the bitmap
matrix.postRotate(degree.toFloat())
// Resize the bitmap
val scaledBitmap = Bitmap.createScaledBitmap(
this,
width,
height,
true
)
// Create and return the rotated bitmap
return Bitmap.createBitmap(
scaledBitmap,
0,
0,
scaledBitmap.width,
scaledBitmap.height,
matrix,
true
)
}
}
有人可以帮我如何正确裁剪图像吗?因为我已经在搜索和研究如何去做,但仍然感到困惑并且不适合我。
这是我如何裁剪您提到的 cameraX 拍摄的图像的示例。我不知道这是否是最好的方法,我很想知道其他解决方案。
camerax_version = "1.0.0-alpha07"
CameraFragment.java
初始化cameraX :
// Views
private PreviewView previewView;
// CameraX
private ProcessCameraProvider cameraProvider;
private ListenableFuture<ProcessCameraProvider> cameraProviderFuture;
private CameraSelector cameraSelector;
private Executor executor;
private ImageCapture imageCapture;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
cameraProviderFuture = ProcessCameraProvider.getInstance(getContext());
executor = ContextCompat.getMainExecutor(getContext());
cameraSelector = new CameraSelector.Builder().requireLensFacing(LensFacing.BACK).build();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
previewView = view.findViewById(R.id.preview);
ImageButton btnCapture = view.findViewById(R.id.btn_capture);
// Wait for the view to be properly laid out
previewView.post(() ->{
//Initialize CameraX
cameraProviderFuture.addListener(() -> {
if(cameraProvider != null) cameraProvider.unbindAll();
try {
cameraProvider = cameraProviderFuture.get();
// Set up the preview use case to display camera preview
Preview preview = new Preview.Builder()
.setTargetAspectRatio(AspectRatio.RATIO_4_3)
.setTargetRotation(previewView.getDisplay().getRotation())
.build();
preview.setPreviewSurfaceProvider(previewView.getPreviewSurfaceProvider());
// Set up the capture use case to allow users to take photos
imageCapture = new ImageCapture.Builder()
.setCaptureMode(ImageCapture.CaptureMode.MINIMIZE_LATENCY)
.setTargetRotation(previewView.getDisplay().getRotation())
.setTargetAspectRatio(AspectRatio.RATIO_4_3)
.build();
// Apply declared configs to CameraX using the same lifecycle owner
cameraProvider.bindToLifecycle(this, cameraSelector, preview,imageCapture);
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
}, ContextCompat.getMainExecutor(getContext()));
});
btnCapture.setOnClickListener(v -> {
String format = "yyyy-MM-dd-HH-mm-ss-SSS";
SimpleDateFormat fmt = new SimpleDateFormat(format, Locale.US);
String date = fmt.format(System.currentTimeMillis());
File file = new File(getContext().getCacheDir(), date+".jpg");
imageCapture.takePicture(file, executor, imageSavedListener);
});
}
拍完照片后,打开经过照片路径的图库片段:
private ImageCapture.OnImageSavedCallback imageSavedListener = new ImageCapture.OnImageSavedCallback() {
@Override
public void onImageSaved(@NonNull File photoFile) {
// Create new fragment and transaction
Fragment newFragment = new GalleryFragment();
FragmentTransaction transaction = getActivity().getSupportFragmentManager().beginTransaction();
// Set arguments
Bundle args = new Bundle();
args.putString("KEY_PATH", Uri.fromFile(photoFile).toString());
newFragment.setArguments(args);
// Replace whatever is in the fragment_container view with this fragment,
transaction.replace(R.id.fragment_container, newFragment,null);
transaction.addToBackStack(null);
// Commit the transaction
transaction.commit();
}
@Override
public void onError(int imageCaptureError, @NonNull String message, @Nullable Throwable cause) {
if (cause != null) {
cause.printStackTrace();
}
}
};
目前照片还没有裁剪,不知道能不能直接用cameraX裁剪。
GalleryFragment.java
加载传递给片段的参数。
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String path = getArguments().getString("KEY_PATH");
sourceUri = Uri.parse(path);
}
在 ImageView 中使用 glide 加载 Uri,然后裁剪它。
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// Initialize the views
ImageView imageView = view.findViewById(R.id.image_view);
View cropArea = view.findViewById(R.id.crop_area);
// Display the image
Glide.with(this).load(sourceUri).listener(new RequestListener<Drawable>() {
@Override
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
return false;
}
@Override
public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
// Get original bitmap
sourceBitmap = ((BitmapDrawable)resource).getBitmap();
// Create a new bitmap corresponding to the crop area
int[] cropAreaXY = new int[2];
int[] placeHolderXY = new int[2];
Rect rect = new Rect();
imageView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw() {
try {
imageView.getLocationOnScreen(placeHolderXY);
cropArea.getLocationOnScreen(cropAreaXY);
cropArea.getGlobalVisibleRect(rect);
croppedBitmap = Bitmap.createBitmap(sourceBitmap, cropAreaXY[0], cropAreaXY[1] - placeHolderXY[1], rect.width(), rect.height());
// Save the croppedBitmap if you wish
getActivity().runOnUiThread(() -> imageView.setImageBitmap(croppedBitmap));
return true;
}finally {
imageView.getViewTreeObserver().removeOnPreDrawListener(this);
}
}
});
return false;
}
}).into(imageView);
}
fragment_camera.xml
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black">
<androidx.camera.view.PreviewView
android:id="@+id/preview"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="3:4"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/crop_area"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="8dp"
android:background="@drawable/rectangle_round_corners"
app:layout_constraintBottom_toBottomOf="@+id/preview"
app:layout_constraintDimensionRatio="4.5:3"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/cameraBottomView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="@android:color/black"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/preview" />
<ImageButton
android:id="@+id/btn_capture"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:background="@drawable/ic_shutter"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/preview" />
</androidx.constraintlayout.widget.ConstraintLayout>
fragment_gallery.xml
<?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"
android:id="@+id/layout_main"
android:background="@android:color/black"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/image_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/crop_area"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="4.5:3"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
我有一个解决办法,我就是用这个功能在截取图片后裁剪图片:
private fun cropImage(bitmap: Bitmap, frame: View, reference: View): ByteArray {
val heightOriginal = frame.height
val widthOriginal = frame.width
val heightFrame = reference.height
val widthFrame = reference.width
val leftFrame = reference.left
val topFrame = reference.top
val heightReal = bitmap.height
val widthReal = bitmap.width
val widthFinal = widthFrame * widthReal / widthOriginal
val heightFinal = heightFrame * heightReal / heightOriginal
val leftFinal = leftFrame * widthReal / widthOriginal
val topFinal = topFrame * heightReal / heightOriginal
val bitmapFinal = Bitmap.createBitmap(
bitmap,
leftFinal, topFinal, widthFinal, heightFinal
)
val stream = ByteArrayOutputStream()
bitmapFinal.compress(
Bitmap.CompressFormat.JPEG,
100,
stream
) //100 is the best quality possibe
return stream.toByteArray()
}
裁剪图像以参考父视图(如框架)和子视图(如最终参考)
- param
bitmap
要裁剪的图像 - param
frame
图片设置的地方 - param
reference
帧作为裁剪图像的参考 return
图片已裁剪
你可以看到这个例子:https://github.com/rrifafauzikomara/CustomCamera/tree/custom_camerax
我找到了一种使用 camerax 配置执行此操作的简单直接的方法。
从相机预览中获取您需要的预览区域矩形的高度和宽度。
例如
<View
android:background="@drawable/background_drawable"
android:id="@+id/border_view"
android:layout_gravity="center"
android:layout_width="350dp"
android:layout_height="100dp"/>
我的宽度是350dp,高度是100dp
然后用ViewPort得到你需要的区域
val viewPort = ViewPort.Builder(Rational(width, height), rotation).build()
//width = 350, height = 100, rotation = Surface.ROTATION_0
val useCaseGroup = UseCaseGroup.Builder()
.addUseCase(preview) //your preview
.addUseCase(imageAnalysis) //if you are using imageAnalysis
.addUseCase(imageCapture)
.setViewPort(viewPort)
.build()
然后绑定CameraProvider的LifeCycle
cameraProvider.bindToLifecycle(this, cameraSelector, useCaseGroup)
使用此 link CropRect 了解更多信息
如果您需要任何帮助,请在下方评论,我可以为您提供工作源代码。
编辑
如果您希望将图像裁剪为 PreviewView
显示的图像,只需执行以下操作:
val useCaseGroup = UseCaseGroup.Builder()
.addUseCase(preview!!)
.addUseCase(imageCapture!!)
.setViewPort(previewView.viewPort!!)
.build()
camera = cameraProvider.bindToLifecycle(
this, cameraSelector, useCaseGroup)