背景:前段时间公司有个需求,需要用app提取医院化验单上的有用数据,利用OCR识别技术,当然为了提高识别准确率,对OCR拍照界面实现自定义,去引导用户正确使用拍照,提高精确度。
实现思路:给SurfaceView加背景,取我们需要的部分;拍摄照片分辨率设置,选择最佳;调整自动对焦和闪光灯
1、布局分为三个部分:引导页、预览界面、拍摄界面
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<SurfaceView
android:id="@+id/surfaceView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible" />
//实现拍照后预览
<ImageView
android:id="@+id/current_img"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<View
android:id="@+id/mTopView"
android:layout_width="match_parent"
android:layout_height="54dp"
android:background="#ff000000" />
<FrameLayout
android:id="@+id/layout_camera"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/mTopView"
android:visibility="visible">
//拍照背景图
<RelativeLayout
android:id="@+id/layout_button_btn"
android:layout_width="309.5dp"
android:layout_height="354dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="44dp"
android:background="@mipmap/camera_content" />
</FrameLayout>
//底部工具栏
<LinearLayout
android:id="@+id/bottom_ll"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="#ff000000"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingTop="26dp"
android:paddingBottom="26dp">
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<TextView
android:id="@+id/btn_cancel"
android:layout_width="42dp"
android:layout_height="42dp"
android:background="@drawable/circle_white"
android:gravity="center"
android:text="重拍"
android:textColor="#ffffffff"
android:textSize="9sp"
android:visibility="gone" />
<TextView
android:id="@+id/btn_cancel1"
android:layout_width="42dp"
android:layout_height="42dp"
android:background="@drawable/circle_white"
android:gravity="center"
android:text="取消"
android:textColor="#ffffffff"
android:textSize="9sp"
android:visibility="visible" />
<Button
android:id="@+id/btn_take_photo"
android:layout_width="66dp"
android:layout_height="66dp"
android:layout_marginLeft="50dp"
android:layout_marginRight="50dp"
android:background="@mipmap/take_photo_background" />
<Button
android:id="@+id/btn_finish"
style="@style/text_white_18"
android:layout_width="66dp"
android:layout_height="66dp"
android:layout_marginLeft="50dp"
android:layout_marginRight="50dp"
android:background="@mipmap/camera_save"
android:text="@string/save"
android:visibility="gone" />
<Button
android:id="@+id/flash_light"
android:layout_width="42dp"
android:layout_height="42dp"
android:background="@mipmap/close_led"
android:visibility="visible" />
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
</LinearLayout>
//首次进入提示用户拍照方式
<LinearLayout
android:id="@+id/is_show_hint_ll"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/mTopView"
android:background="#90000000">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginLeft="30dp"
android:layout_marginRight="30dp"
android:layout_marginBottom="38dp"
android:background="@drawable/white_card_bck"
android:gravity="center_horizontal"
android:orientation="vertical"
android:paddingLeft="12dp"
android:paddingRight="12dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:src="@mipmap/guide_ocr" />
<TextView
style="@style/color_blue_20"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="17dp"
android:text="请将蓝色虚线与表头对齐" />
<TextView
android:id="@+id/camera_go_on"
style="@style/text_666_16"
android:layout_width="match_parent"
android:layout_height="44dp"
android:layout_gravity="left"
android:layout_marginLeft="4dp"
android:layout_marginTop="44dp"
android:layout_marginRight="4dp"
android:background="@drawable/gray_btn"
android:gravity="center"
android:text="马上试一试" />
<CheckBox
android:id="@+id/is_continue_hint"
style="@style/color_999_12"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:layout_marginBottom="15dp"
android:text="下次使用不再提醒我" />
</LinearLayout>
</LinearLayout>
</RelativeLayout>
</layout>
2、初始化surfaceView,设置自动对焦、并初始化相机、调整分辨率、闪光等,用完记得释放资源。
private void initCamera() {
surfaceView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
if (!status && camera != null) {
//添加自动对焦回调
camera.autoFocus(autoFocusCallback);
}
return false;
}
});
MySurfaceCallback mySurfaceCallback = new MySurfaceCallback();
holder = surfaceView.getHolder();
holder.setKeepScreenOn(true);// 屏幕常亮
mSensorController = SensorController.getInstance(mActivity);
mSensorController.setCameraFocusListener(newSensorController.CameraFocusListener() {
@Override
public void onFocus() {
if (camera != null ) {
DisplayMetricsdisplayMetrics=mActivity.getApplicationContext().getResources().getDisplayMetrics();
mMScreenWidth = displayMetrics.widthPixels;//获取屏幕参数
if (!mSensorController.isFocusLocked()) {
if (newFocus(mMScreenWidth / 2, mMScreenWidth / 2, camera)) {
mSensorController.lockFocus();//设置自动对焦后锁住
}
}
}
}
});
mSensorController.start();//对焦加速度传感器
holder.addCallback(mySurfaceCallback);//添加拍摄回调
holder.lockCanvas();//锁住画布
}
private boolean newFocus(int x, int y ,Camera mCamera) {
//正在对焦时返回
if (mCamera == null || isFocusing) {
return false;
}
isFocusing = true;
Camera.Parameters mCameraParameters = mCamera.getParameters();
setMeteringRect(x, y,mCameraParameters);
mCameraParameters.setExposureCompensation(15);
mCameraParameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
mCamera.cancelAutoFocus(); // 先要取消掉进程中所有的聚焦功能
try {
mCamera.setParameters(mCameraParameters);
mCamera.autoFocus(autoFocusCallback);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 设置感光区域
* 需要将屏幕坐标映射到Rect对象对应的单元格矩形
*
* @param x
* @param y
*/
private void setMeteringRect(int x, int y, Camera.Parameters mCameraParameters) {
if (mCameraParameters.getMaxNumMeteringAreas() > 0) {
List<Camera.Area> areas = new ArrayList<Camera.Area>();
Rect rect = new Rect(x - 100, y - 100, x + 100, y + 100);
int left = rect.left * 2000 / mMScreenWidth - 1000;
int top = rect.top * 2000 / mMScreenWidth - 1000;
int right = rect.right * 2000 / mMScreenWidth - 1000;
int bottom = rect.bottom * 2000 / mMScreenWidth - 1000;
// 如果超出了(-1000,1000)到(1000, 1000)的范围,则会导致相机崩溃
left = left < -1000 ? -1000 : left;
top = top < -1000 ? -1000 : top;
right = right > 1000 ? 1000 : right;
bottom = bottom > 1000 ? 1000 : bottom;
Rect area1 = new Rect(left, top, right, bottom);
//只有一个感光区,直接设置权重为1000了
areas.add(new Camera.Area(area1, 400));
//在图像的中心指定一个测光区域
Rect areaRect1 = new Rect(-100, -100, 100, 100);
//设置权重为600,最高1000
areas.add(new Camera.Area(areaRect1, 600));
mCameraParameters.setMeteringAreas(areas);
}
}
private void initCameraParamsAndOpen() {
try {
// surfaceview创建之后,就去打开相机
camera = getCameraInstance();
camera.setPreviewDisplay(holder);
updateCameraParameters(camera);
camera.startPreview();
} catch (Exception e) {
if (camera != null) {
camera.release();
}
e.printStackTrace();
}
}
//定义surface回调
class MySurfaceCallback implements SurfaceHolder.Callback
{
@Override
public void surfaceCreated(SurfaceHolder surfaceHolder) {
if(camera !=null) {
camera.release();
camera = null;
}
initCameraParamsAndOpen();//重新初始化相机
}
@Override
public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
//当surface的格式或大小发生改变,这个方法就被调用,或者View被隐藏
status = false;
btnTakePhoto.setVisibility(View.VISIBLE); //拍照按钮显示
btnFinish.setVisibility(View.GONE); //拍照完成按钮隐藏
btnCancel.setVisibility(View.GONE);
if(camera !=null) {
camera.release();
camera = null;
}
initCameraParamsAndOpen(); //重新初始化相机
}
//释放资源
@Override
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
if (camera != null) {
camera.stopPreview();
camera.release();
mSensorController.stop();
camera = null;
}
}
}
Camera.PictureCallback myPictureCallback = new Camera.PictureCallback(){
@Override
public void onPictureTaken(byte[] data, Camera camera) {
Log.e("lenita", " onPictureTaken");
//卡住预览页面,让用户看
camera.stopPreview();
//保存图片
tempBitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
if (tempBitmap != null) {
saveBitmap(tempBitmap);
}
}
};
//保存图片
public void saveBitmap(Bitmap bitmap) {
if(tempFile != null){
int width = bitmap.getWidth();
int height = bitmap.getHeight();
if(width < height) {
bitmap = Bitmap.createBitmap(bitmap,0,0,bitmap.getWidth(),bitmap.getHeight());
}else {
Matrix matrix = new Matrix();
matrix.postRotate(90);//旋转90度,解决在有些机型系统相机拍摄横屏分问题
bitmap = Bitmap.createBitmap(bitmap,0,0,bitmap.getWidth(),bitmap.getHeight(),matrix,true);
}
try {
FileOutputStream out = new FileOutputStream(tempFile);
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);
out.flush();
out.close();
currentImg.setImageBitmap(bitmap);
currentImg.setVisibility(View.VISIBLE);
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
Log.e("lenita","saveBitmap e = "+e);
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
Log.e("lenita","saveBitmap e = "+e);
e.printStackTrace();
}
}else {
Log.e("save", "tempFile == null");
}
}
private Handler mHandler = new Handler();
Camera.AutoFocusCallback autoFocusCallback = new Camera.AutoFocusCallback() {
@Override
public void onAutoFocus(boolean b, Camera camera) {
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
//一秒之后才能再次对焦
isFocusing = false;
mSensorController.unlockFocus();
}
}, 1000);
}
};
public static Camera getCameraInstance() {
Camera c = null;
try {
c = Camera.open(0); // attempt to get a Camera instance
} catch (Exception e) {
// Camera is not available (in use or does not exist)
}
return c;
}
3、做到这一步已经基本可以完成拍照并上传到服务器进行识别了,但是经过多种机型测试,发现在有些手机上会出现一些问题:照片质量很模糊,所以就需要做适配。以下是修改后的适配方案,适配之前的代码已经删掉了(PS:所以一定要养成一个善于记录笔记的良好习惯)
private Point mScreenResolution;//屏幕分辨率
private Point previewSizeOnScreen;//相机预览尺寸
private Point pictureSizeOnScreen;//图片尺寸
//设置相机参数,拍照尺寸、预览尺寸等
private void updateCameraParameters(Camera camera) {
if (camera != null) {
Camera.Parameters parameters = camera.getParameters();
WindowManager windowManager = (WindowManager) EmpApp.getInstance().getSystemService(Context.WINDOW_SERVICE);
Display display = windowManager.getDefaultDisplay();
Point theScreenResolution = new Point();
display.getSize(theScreenResolution);//得到屏幕的尺寸,单位是像素
mScreenResolution = theScreenResolution;
previewSizeOnScreen = CameraUtils.findBestPreviewSizeValue(parameters, theScreenResolution);//通过相机尺寸、屏幕尺寸来得到最好的展示尺寸,此尺寸为相机的
parameters.setPreviewSize(previewSizeOnScreen.x, previewSizeOnScreen.y);
pictureSizeOnScreen = CameraUtils.findBestPictureSizeValue(parameters, theScreenResolution);//通过相机尺寸、屏幕尺寸来得到最好的展示尺寸,此尺寸为相机的
parameters.setPictureSize(pictureSizeOnScreen.x, pictureSizeOnScreen.y);
boolean isScreenPortrait = mScreenResolution.x < mScreenResolution.y;
boolean isPreviewSizePortrait = previewSizeOnScreen.x < previewSizeOnScreen.y;
if (isScreenPortrait != isPreviewSizePortrait) {//相机与屏幕一个方向,则使用相机尺寸
previewSizeOnScreen = new Point(previewSizeOnScreen.y, previewSizeOnScreen.x);//否则翻个
}
// 设置照片的格式
parameters.setPictureFormat(ImageFormat.JPEG);
parameters.setRotation(90);//防止保存的图片旋转,有些手机在设置了这个仍然会横屏,因为有些厂商改动了底层
CameraUtils.setFocus(parameters, true, false, true);//设置相机对焦模式
CameraUtils.setBarcodeSceneMode(parameters, Camera.Parameters.SCENE_MODE_BARCODE);//设置相机场景模式
CameraUtils.setBestPreviewFPS(parameters);//设置相机帧数
camera.setParameters(parameters);
// 系统相机默认是横屏的,我们要旋转90°
camera.setDisplayOrientation(90);
}
}
4、还有个问题是在拍照和预览的时候,有些手机显示有拉伸的情况,所以还需要做屏幕适配
在项目引入:
implementation 'com.github.MrRightChen:ScreenAdapter:v1.0.0'
自定义 AdapterLinearLayout 继承LinearLayout,重写onMeasure()方法,根据屏幕分辨率和UI设计尺寸重新计算每个控件的大小
public class AdapterLinearLayout extends LinearLayout {
private static boolean isFlag = true;
public AdapterLinearLayout(Context context) {
super(context);
}
public AdapterLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public AdapterLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if(isFlag){
int count = this.getChildCount();
float scaleX = UiUtils.getInstance(this.getContext()).getHorizontalScaleValue();
float scaleY = UiUtils.getInstance(this.getContext()).getVerticalScaleValue();
Log.i("","x系数:"+scaleX);
Log.i("","y系数:"+scaleY);
for (int i = 0;i < count;i++){
//获得当前布局的子布局
View child = this.getChildAt(i);
//代表的是当前空间的所有属性列表
LayoutParams layoutParams = ( LayoutParams) child.getLayoutParams();
int matchParent = LayoutParams.MATCH_PARENT;
int wrapContent = LayoutParams.WRAP_CONTENT;
if (layoutParams.width!=LayoutParams.MATCH_PARENT&&layoutParams.width!=LayoutParams.WRAP_CONTENT){
layoutParams.width = (int) (layoutParams.width * scaleX);
}
if (layoutParams.height!=LayoutParams.MATCH_PARENT&&layoutParams.height!=LayoutParams.WRAP_CONTENT){
layoutParams.height = (int) (layoutParams.height * scaleY);
}
layoutParams.rightMargin = (int) (layoutParams.rightMargin * scaleX);
layoutParams.leftMargin = (int) (layoutParams.leftMargin * scaleX);
layoutParams.topMargin = (int) (layoutParams.topMargin * scaleY);
layoutParams.bottomMargin = (int) (layoutParams.bottomMargin * scaleY);
}
isFlag = false;
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
计算缩放比例
/**
* 获得缩放系数
* 假定Ui设计为1920 * 1080
*/
public class UiUtils {
private Context mContext;
private static UiUtils mInstance;
//UI 设计尺寸
public final float STANDARD_WIDTH = 1080f;
public final float STANDARD_HEIGHT = 1920;
private final String DIMEN_CLASS = "com.android.internal.R$dimen";
//当前设备实际宽高
public float mDisplayMetricsWidth ;
public float mDisplayMetricsHeight ;
//单例模式
public static UiUtils getInstance(Context context){
if (mInstance==null){
mInstance = new UiUtils(context);
}
return mInstance;
}
//私有构造方法
private UiUtils(Context mContext) {
this.mContext = mContext;
//拿到窗口管理器
WindowManager windowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
//加载当前界面信息
DisplayMetrics displayMetrics =new DisplayMetrics();
windowManager.getDefaultDisplay().getMetrics(displayMetrics);
if (mDisplayMetricsWidth == 0.0f || mDisplayMetricsHeight == 0.0f){
//获取状态栏的高度
int systemBarHeight = getValue(mContext,"system_bar_height",48);
if(displayMetrics.widthPixels > displayMetrics.heightPixels){
this.mDisplayMetricsWidth = displayMetrics.heightPixels;
this.mDisplayMetricsHeight = displayMetrics.widthPixels - systemBarHeight;
}else{
this.mDisplayMetricsWidth = displayMetrics.widthPixels;
this.mDisplayMetricsHeight = displayMetrics.heightPixels - systemBarHeight;
}
}
}
//获取状态栏高度
public int getValue(Context context,String systemid,int defValue) {
try {
Class<?> clazz = Class.forName(DIMEN_CLASS);
Object r = clazz.newInstance();
Field field = clazz.getField(systemid);
int x = (int) field.get(r);
return context.getResources().getDimensionPixelOffset(x);
} catch (Exception e) {
return defValue;
}
}
//获取水平的缩放系数
public float getHorizontalScaleValue(){
return mDisplayMetricsWidth / STANDARD_WIDTH;
}
//获取垂直缩放系数
public float getVerticalScaleValue(){
Log.i("","displayMetricsHeight:"+mDisplayMetricsHeight);
return mDisplayMetricsHeight / STANDARD_HEIGHT;
}
}
然后将布局文件里所有的LinearLayout、RelativeLayout、FrameLayout替换成相应的Adapter......Layout
上结果截图
我是新手,有什么疑问或建议欢迎评论区指出,共同讨论,一起进步。