如何在 Android 中提高 Camera 2 的图像质量?
Posted
技术标签:
【中文标题】如何在 Android 中提高 Camera 2 的图像质量?【英文标题】:How to increase Camera 2 image quality in Android? 【发布时间】:2017-06-14 02:58:51 【问题描述】:我使用谷歌示例创建 android 相机应用程序。 在几天内我无法解决输出图像质量的问题。这是用于捕获照片/视频的基本片段。
public class Camera2BaseFragment extends Fragment implements View.OnTouchListener
protected static final SparseIntArray DEFAULT_ORIENTATIONS = new SparseIntArray();
protected final String FRAGMENT_DIALOG = "dialog";
protected Integer[] mExposureCompensation = ;
protected String mCameraId = "0";
protected float mFingerSpacing = 0f;
protected double mZoomLevel = 1;
protected int mCurrentProgress = 0;
protected int mSensorOrientation;
protected ImageView mToolbarBackIcon;
protected TextView mTvZoomLevel;
protected Rect mZoomRect;
protected View mView;
protected Camera2FitTextureView mTextureView;
protected CameraCaptureSession mCaptureSession;
protected CameraDevice mCameraDevice;
protected Size mPreviewSize;
protected HandlerThread mBackgroundThread;
protected Handler mBackgroundHandler;
protected CaptureRequest.Builder mPreviewRequestBuilder;
protected Semaphore mCameraOpenCloseLock = new Semaphore(1);
private CameraCharacteristics mCharacteristics;
private float mRatio = 1.0f;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState)
return inflater.inflate(R.layout.fragment_camera2, container, false);
@Override
public boolean onTouch(View v, MotionEvent event)
try
mCharacteristics = MainFragment.getCameraCharacteristics(getActivity());
if (mCharacteristics == null) return true;
if (mCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM) == null)
return true;
float maxZoom = mCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM) * 10;
Rect m = mCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
if (m == null) return true;
float current_finger_spacing;
if (event.getPointerCount() > 1)
// Multi touch logic
current_finger_spacing = getFingerSpacing(event);
if (mFingerSpacing != 0)
if (current_finger_spacing > mFingerSpacing && maxZoom > mZoomLevel)
//mZoomLevel++;
mZoomLevel = mZoomLevel + .5;
else if (current_finger_spacing < mFingerSpacing && mZoomLevel > 1)
//mZoomLevel--;
mZoomLevel = mZoomLevel - .5;
int minW = (int) (m.width() / maxZoom);
int minH = (int) (m.height() / maxZoom);
int difW = m.width() - minW;
int difH = m.height() - minH;
int cropW = difW / 100 * (int)mZoomLevel;
int cropH = difH / 100 * (int)mZoomLevel;
cropW -= cropW & 3;
cropH -= cropH & 3;
mZoomRect = new Rect(cropW, cropH, m.width() - cropW, m.height() - cropH);
mPreviewRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, mZoomRect);
mRatio = (int)mZoomLevel >= 1 ? ((float)mZoomLevel / 10) : (float) mZoomLevel;
setZoomLevelText(mRatio);
mFingerSpacing = current_finger_spacing;
mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, null);
else
// Single touch logic
catch (CameraAccessException | NullPointerException e)
//..
return true;
private void setZoomLevelText(float ratio)
if(ratio <= 1.0f) ratio = 1.0f;
mTvZoomLevel.setText(String.format(Locale.US, "%.1fX", ratio));
private float getFingerSpacing(MotionEvent event)
float x = event.getX(0) - event.getX(1);
float y = event.getY(0) - event.getY(1);
return (float) Math.sqrt(x * x + y * y);
static class CompareSizesByArea implements Comparator<Size>
@Override
public int compare(Size lhs, Size rhs)
return Long.signum((long) lhs.getWidth() * lhs.getHeight() -
(long) rhs.getWidth() * rhs.getHeight());
用于拍摄照片的我的片段
public class Camera2PhotoFragment extends Camera2BaseFragment
implements View.OnClickListener, ActivityCompat.OnRequestPermissionsResultCallback
private static final int STATE_PREVIEW = 0;
private static final int STATE_WAITING_LOCK = 1;
private static final int STATE_WAITING_PRECAPTURE = 2;
private static final int STATE_WAITING_NON_PRECAPTURE = 3;
private static final int STATE_PICTURE_TAKEN = 4;
private static final int MAX_PREVIEW_WIDTH = 1920;
private static final int MAX_PREVIEW_HEIGHT = 1080;
private ImageReader mImageReader;
private File mFile = new File("ImagePath");
private final ImageReader.OnImageAvailableListener mOnImageAvailableListener
= new ImageReader.OnImageAvailableListener()
@Override
public void onImageAvailable(ImageReader reader)
mBackgroundHandler.post(new ImageSaver(reader.acquireLatestImage(), mFile,
mOnImageSavedListener));
;
private int mState = STATE_PREVIEW;
private boolean isEnabledCameraImg = true;
private final OnImageSavedListener mOnImageSavedListener = new OnImageSavedListener()
@Override
public void onImageSavedSuccessfully()
// Do something with saved image
isEnabledCameraImg = true;
;
private CameraCaptureSession.CaptureCallback mCaptureCallback
= new CameraCaptureSession.CaptureCallback()
private void process(CaptureResult result)
switch (mState)
case STATE_PREVIEW:
break;
case STATE_WAITING_LOCK:
Integer afState = result.get(CaptureResult.CONTROL_AF_STATE);
if (afState == null)
captureStillPicture();
else if (CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED == afState ||
CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED == afState)
Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
if (aeState == null ||
aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED)
mState = STATE_PICTURE_TAKEN;
captureStillPicture();
else
runPrecaptureSequence();
else
mState = STATE_PICTURE_TAKEN;
captureStillPicture();
break;
case STATE_WAITING_PRECAPTURE:
Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
if (aeState == null ||
aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE ||
aeState == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED)
mState = STATE_WAITING_NON_PRECAPTURE;
break;
case STATE_WAITING_NON_PRECAPTURE:
Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE)
mState = STATE_PICTURE_TAKEN;
captureStillPicture();
break;
@Override
public void onCaptureProgressed(@NonNull CameraCaptureSession session,
@NonNull CaptureRequest request,
@NonNull CaptureResult partialResult)
process(partialResult);
@Override
public void onCaptureCompleted(@NonNull CameraCaptureSession session,
@NonNull CaptureRequest request,
@NonNull TotalCaptureResult result)
process(result);
;
private final CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback()
@Override
public void onOpened(@NonNull CameraDevice cameraDevice)
mCameraOpenCloseLock.release();
mCameraDevice = cameraDevice;
createCameraPreviewSession();
;
private final TextureView.SurfaceTextureListener mSurfaceTextureListener
= new TextureView.SurfaceTextureListener()
@Override
public void onSurfaceTextureAvailable(SurfaceTexture texture, int width, int height)
openCamera(width, height);
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture texture, int width, int height)
configureTransform(width, height);
;
public static Camera2PhotoFragment newInstance()
return new Camera2PhotoFragment();
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState)
super.onCreateView(inflater, container, savedInstanceState);
return mView;
@Override
public void onResume()
super.onResume();
isEnabledCameraImg = true;
if (mTextureView.isAvailable())
openCamera(mTextureView.getWidth(), mTextureView.getHeight());
else
mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
@Override
public void onClick(View view)
switch (view.getId())
case R.id.imgCameraTakePicture:
takePicture();
break;
private static Size chooseOptimalSize(Size[] choices, int textureViewWidth,
int textureViewHeight, int maxWidth, int maxHeight, Size aspectRatio)
List<Size> bigEnough = new ArrayList<>();
List<Size> notBigEnough = new ArrayList<>();
int w = aspectRatio.getWidth();
int h = aspectRatio.getHeight();
for (Size option : choices)
if (option.getWidth() <= maxWidth && option.getHeight() <= maxHeight &&
option.getHeight() == option.getWidth() * h / w)
if (option.getWidth() >= textureViewWidth &&
option.getHeight() >= textureViewHeight)
bigEnough.add(option);
else
notBigEnough.add(option);
if (bigEnough.size() > 0)
return Collections.min(bigEnough, new CompareSizesByArea());
else if (notBigEnough.size() > 0)
return Collections.max(notBigEnough, new CompareSizesByArea());
else
return choices[0];
private void setUpCameraOutputs(int width, int height)
Activity activity = getActivity();
CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
try
for (String cameraId : manager.getCameraIdList())
CameraCharacteristics characteristics
= manager.getCameraCharacteristics(cameraId);
Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);
if (facing != null && facing == CameraCharacteristics.LENS_FACING_FRONT)
continue;
StreamConfigurationMap map = characteristics.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
if (map == null)
continue;
Size largest = Collections.max(
Arrays.asList(map.getOutputSizes(ImageFormat.JPEG)),
new CompareSizesByArea());
mImageReader = ImageReader.newInstance(largest.getWidth(), largest.getHeight(),
ImageFormat.JPEG, /*maxImages*/2);
mImageReader.setOnImageAvailableListener(
mOnImageAvailableListener, mBackgroundHandler);
int displayRotation = activity.getWindowManager().getDefaultDisplay().getRotation();
mSensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
boolean swappedDimensions = false;
switch (displayRotation)
case Surface.ROTATION_0:
// do nothing
case Surface.ROTATION_180:
if (mSensorOrientation == 90 || mSensorOrientation == 270)
swappedDimensions = true;
break;
case Surface.ROTATION_90:
// do nothing
case Surface.ROTATION_270:
if (mSensorOrientation == 0 || mSensorOrientation == 180)
swappedDimensions = true;
break;
Point displaySize = new Point();
activity.getWindowManager().getDefaultDisplay().getSize(displaySize);
int rotatedPreviewWidth = width;
int rotatedPreviewHeight = height;
int maxPreviewWidth = displaySize.x;
int maxPreviewHeight = displaySize.y;
if (swappedDimensions)
rotatedPreviewWidth = height;
rotatedPreviewHeight = width;
maxPreviewWidth = displaySize.y;
maxPreviewHeight = displaySize.x;
if (maxPreviewWidth > MAX_PREVIEW_WIDTH)
maxPreviewWidth = MAX_PREVIEW_WIDTH;
if (maxPreviewHeight > MAX_PREVIEW_HEIGHT)
maxPreviewHeight = MAX_PREVIEW_HEIGHT;
mPreviewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class),
rotatedPreviewWidth, rotatedPreviewHeight, maxPreviewWidth,
maxPreviewHeight, largest);
int orientation = getResources().getConfiguration().orientation;
if (orientation == Configuration.ORIENTATION_LANDSCAPE)
mTextureView.setAspectRatio(
mPreviewSize.getWidth(), mPreviewSize.getHeight());
else
mTextureView.setAspectRatio(
mPreviewSize.getHeight(), mPreviewSize.getWidth());
mCameraId = cameraId;
return;
catch (CameraAccessException e)
//..
private void openCamera(int width, int height)
setUpCameraOutputs(width, height);
configureTransform(width, height);
Activity activity = getActivity();
CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
try
if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS))
throw new RuntimeException("Time out waiting to lock camera opening.");
if (ActivityCompat.checkSelfPermission(getContext(), Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED)
ActivityCompat.requestPermissions(getActivity(), new String[]Manifest.permission.CAMERA, REQUEST_OPEN_CAMERA);
return;
manager.openCamera(mCameraId, mStateCallback, mBackgroundHandler);
catch (CameraAccessException | InterruptedException e)
e.printStackTrace();
private void closeCamera()
//...
private void createCameraPreviewSession()
try
SurfaceTexture texture = mTextureView.getSurfaceTexture();
assert texture != null;
texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
Surface surface = new Surface(texture);
mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
mPreviewRequestBuilder.addTarget(surface);
if (mZoomRect != null)
mPreviewRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, mZoomRect);
mCameraDevice.createCaptureSession(Arrays.asList(surface, mImageReader.getSurface()),
new CameraCaptureSession.StateCallback()
@Override
public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession)
if (null == mCameraDevice)
return;
mCaptureSession = cameraCaptureSession;
try
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_AUTO);
mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(),
mCaptureCallback, mBackgroundHandler);
catch (CameraAccessException e)
e.printStackTrace();
@Override
public void onConfigureFailed(
@NonNull CameraCaptureSession cameraCaptureSession)
//...
, null
);
catch (CameraAccessException e)
//..
private void configureTransform(int viewWidth, int viewHeight)
Activity activity = getActivity();
if (null == mTextureView || null == mPreviewSize || null == activity)
return;
int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
Matrix matrix = new Matrix();
RectF viewRect = new RectF(0, 0, viewWidth, viewHeight);
RectF bufferRect = new RectF(0, 0, mPreviewSize.getHeight(), mPreviewSize.getWidth());
float centerX = viewRect.centerX();
float centerY = viewRect.centerY();
if (Surface.ROTATION_90 == rotation || Surface.ROTATION_270 == rotation)
bufferRect.offset(centerX - bufferRect.centerX(), centerY - bufferRect.centerY());
matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL);
float scale = Math.max(
(float) viewHeight / mPreviewSize.getHeight(),
(float) viewWidth / mPreviewSize.getWidth());
matrix.postScale(scale, scale, centerX, centerY);
matrix.postRotate(90 * (rotation - 2), centerX, centerY);
else if (Surface.ROTATION_180 == rotation)
matrix.postRotate(180, centerX, centerY);
mTextureView.setTransform(matrix);
private void takePicture()
lockFocus();
private void lockFocus()
try
mState = STATE_WAITING_LOCK;
mCaptureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler);
catch (CameraAccessException e)
//..
private void runPrecaptureSequence()
try
mState = STATE_WAITING_PRECAPTURE;
mCaptureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback,
mBackgroundHandler);
catch (CameraAccessException | NullPointerException e)
//..
private void captureStillPicture()
try
final Activity activity = getActivity();
if (null == activity || null == mCameraDevice)
return;
final CaptureRequest.Builder captureBuilder =
mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
captureBuilder.addTarget(mImageReader.getSurface());
if (mZoomRect != null) captureBuilder.set(CaptureRequest.SCALER_CROP_REGION, mZoomRect);
if (mCurrentProgress != 0)
captureBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, mExposureCompensation[mCurrentProgress]);
captureBuilder.set(CaptureRequest.CONTROL_AF_MODE, CameraMetadata.CONTROL_AF_MODE_AUTO);
// Orientation
int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation));
captureBuilder.set(CaptureRequest.JPEG_QUALITY, (byte) 100);
CameraCaptureSession.CaptureCallback CaptureCallback
= new CameraCaptureSession.CaptureCallback()
@Override
public void onCaptureCompleted(@NonNull CameraCaptureSession session,
@NonNull CaptureRequest request,
@NonNull TotalCaptureResult result)
isEnabledCameraImg = true;
;
mCaptureSession.stopRepeating();
mCaptureSession.capture(captureBuilder.build(), CaptureCallback, null);
unlockFocus();
if (mZoomRect != null)
mPreviewRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, mZoomRect);
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_AUTO);
mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(),
mCaptureCallback, mBackgroundHandler);
catch (Throwable e)
//..
private int getOrientation(int rotation)
return (DEFAULT_ORIENTATIONS.get(rotation) + mSensorOrientation + 270) % 360;
private void unlockFocus()
try
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
CameraMetadata.CONTROL_AF_TRIGGER_IDLE);
mState = STATE_PREVIEW;
mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), mCaptureCallback,
mBackgroundHandler);
catch (Throwable e)
//..
interface OnImageSavedListener
void onImageSavedSuccessfully();
private static class ImageSaver implements Runnable
private final Image mImage;
private final File mFile;
private final OnImageSavedListener mOnImageSavedListener;
public ImageSaver(Image image, File file, OnImageSavedListener onImageSavedListener)
mImage = image;
mFile = file;
mOnImageSavedListener = onImageSavedListener;
@Override
public void run()
ByteBuffer buffer = mImage.getPlanes()[0].getBuffer();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
FileOutputStream output = null;
try
output = new FileOutputStream(mFile);
output.write(bytes);
mOnImageSavedListener.onImageSavedSuccessfully();
catch (IOException e)
//..
finally
mImage.close();
if (null != output)
try
output.close();
catch (IOException e)
//..
相机预览和保存的图像之间存在图像质量差异。 左侧:相机预览截图(带缩放) 右侧:捕获/保存的图像。左侧的图像更好。
如何为输出图像实现良好的图像质量?可能是缩放的问题吗?
我在相机 2 文档中发现 CaptureRequest 的以下常量:
captureBuilder.set(CaptureRequest.EDGE_MODE, CameraMetadata.EDGE_MODE_HIGH_QUALITY);
captureBuilder.set(CaptureRequest.SHADING_MODE, CameraMetadata.SHADING_MODE_HIGH_QUALITY);
captureBuilder.set(CaptureRequest.TONEMAP_MODE, CameraMetadata.TONEMAP_MODE_HIGH_QUALITY);
captureBuilder.set(CaptureRequest.COLOR_CORRECTION_ABERRATION_MODE, CameraMetadata.COLOR_CORRECTION_ABERRATION_MODE_HIGH_QUALITY);
captureBuilder.set(CaptureRequest.COLOR_CORRECTION_MODE, CameraMetadata.COLOR_CORRECTION_ABERRATION_MODE_HIGH_QUALITY);
captureBuilder.set(CaptureRequest.HOT_PIXEL_MODE, CameraMetadata.HOT_PIXEL_MODE_HIGH_QUALITY);
captureBuilder.set(CaptureRequest.NOISE_REDUCTION_MODE, CameraMetadata.NOISE_REDUCTION_MODE_HIGH_QUALITY);
captureBuilder.set(CaptureRequest.LENS_OPTICAL_STABILIZATION_MODE, CameraMetadata.LENS_OPTICAL_STABILIZATION_MODE_ON);
为了测试,我将所有这些常量一起添加到我的代码中的代码行下方: captureBuilder.set(CaptureRequest.JPEG_QUALITY, (byte) 100);
但这并没有解决我的问题。
有人在相机 2 上遇到过类似的图像质量问题,或者有人知道这个问题的原因吗?
感谢您的回复!
如果你需要,我可以将我的代码上传到 github。
【问题讨论】:
你找到解决办法了吗? 我也遇到了同样的问题。 【参考方案1】:这可能是一个 hack,但我通过设置 CameraDevice.TEMPLATE_PREVIEW
而不是
CameraDevice.TEMPLATE_STILL_CAPTURE
在captureStillPicture()
.
【讨论】:
不清楚您所说的“更好”是什么意思。这个绿色形状只是一个随机镜头,还是真的代表了您希望在这个项目中处理的输入类型?如果是后者,那么您的“黑客”是相当合法的。 @AlexCohn 我不确定他是否满意,但在我的情况下图像不那么模糊。以上是关于如何在 Android 中提高 Camera 2 的图像质量?的主要内容,如果未能解决你的问题,请参考以下文章
如何使用 Android CameraX ImageAnalysis 提高帧速率?
Android Camera API 1.0,2.0,3.0 知多少
Android 实时视频采集—Camera预览采集与显示(平台系统camera功能理解分享)