目錄
相關(guān)文章
Android NDK開發(fā):實(shí)戰(zhàn)案例-電動車牌號識別(介紹)
利用PorterDuffXfermode繪制圖片文字
自定義相機(jī)
代碼展示
public class ScanningCameraView extends SurfaceView implements SurfaceHolder.Callback,Camera.PreviewCallback{
private Camera mCamera;//相機(jī)
private boolean isSupportAutoFocus;//是否支持自動對焦
private int screenHeight;//屏幕的高度
private int screenWidth;//屏幕的寬度
private boolean isPreviewing;//是否在預(yù)覽
private IdentifyCallBack identifyCallBack;//掃描成功的回調(diào)函數(shù)
private boolean isScanning =false;
Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.what == 1){
if(identifyCallBack!=null){
identifyCallBack.onIdentifyImage((Bitmap) msg.obj);
}
}
}
};
public ScanningCameraView(Context context) {
super(context);
init();
}
public ScanningCameraView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public ScanningCameraView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
//獲取屏幕分辨率
DisplayMetrics dm = getContext().getResources().getDisplayMetrics();
screenWidth = dm.heightPixels;
screenHeight = dm.widthPixels;
isSupportAutoFocus = getContext().getPackageManager().hasSystemFeature(
PackageManager.FEATURE_CAMERA_AUTOFOCUS);
getHolder().addCallback(this);
}
public void setIdentifyCallBack(IdentifyCallBack identifyCallBack) {
this.identifyCallBack = identifyCallBack;
}
/**
* 開燈
*/
public void openLight(){
Camera.Parameters parameters = mCamera.getParameters();
//打開閃光燈
parameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);//開啟
mCamera.setParameters(parameters);
}
/**
* 關(guān)燈
*/
public void closeLight(){
Camera.Parameters parameters = mCamera.getParameters();
//打開閃光燈
parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);//開啟
mCamera.setParameters(parameters);
}
/**
* Camera幀數(shù)據(jù)回調(diào)用
*/
@Override
public void onPreviewFrame(final byte[] data, final Camera camera) {
camera.addCallbackBuffer(data);
new Thread(new Runnable() {
@Override
public void run() {
//識別中不處理其他幀數(shù)據(jù)
if (!isScanning) {
isScanning = true;
try {
//獲取Camera預(yù)覽尺寸
Camera.Size size = camera.getParameters().getPreviewSize();
//將幀數(shù)據(jù)轉(zhuǎn)為bitmap
YuvImage image = new YuvImage(data, ImageFormat.NV21, size.width, size.height, null);
if (image != null) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
image.compressToJpeg(new Rect(0, 0, size.width, size.height), 80, stream);
Bitmap bmp = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size());
Bitmap bitmap = cutImage(bmp);//獲取遮罩處圖像
Message message = handler.obtainMessage();
message.what = 1;
message.obj = bitmap;
handler.sendMessage(message);
isScanning = false;
}
} catch (Exception ex) {
isScanning = false;
}
}
}
}
).start();
}
/**
* 攝像頭自動聚焦
*/
Camera.AutoFocusCallback autoFocusCB = new Camera.AutoFocusCallback() {
public void onAutoFocus(boolean success, Camera camera) {
postDelayed(doAutoFocus, 500);
}
};
private Runnable doAutoFocus = new Runnable() {
public void run() {
if (mCamera != null) {
try {
mCamera.autoFocus(autoFocusCB);
} catch (Exception e) {
}
}
}
};
/**
* 裁剪照片
*
* @return
*/
private Bitmap cutImage(Bitmap bitmap) {
int h = bitmap.getWidth();
int w = bitmap.getHeight();
int clipw = w/5*3;//這里根據(jù)遮罩的比例進(jìn)行裁剪
int cliph = (int) (clipw*1.93f);
int x = (w - clipw) / 2;
int y = (h - cliph) / 2;
return Bitmap.createBitmap(bitmap, y, x,cliph, clipw);
}
/**
* 打開指定攝像頭
*/
public void openCamera() {
Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
for (int cameraId = 0; cameraId < Camera.getNumberOfCameras(); cameraId++) {
Camera.getCameraInfo(cameraId, cameraInfo);
if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
try {
mCamera = Camera.open(cameraId);
} catch (Exception e) {
if (mCamera != null) {
mCamera.release();
mCamera = null;
}
}
break;
}
}
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
try {
releaseCamera();
openCamera();
initCamera();
}catch (Exception e){
mCamera = null;
}
}
/**
* 加載相機(jī)配置
*/
private void initCamera() {
try {
mCamera.setPreviewDisplay(getHolder());//當(dāng)前控件顯示相機(jī)數(shù)據(jù)
mCamera.setDisplayOrientation(90);//調(diào)整預(yù)覽角度
setCameraParameters();
startPreview();//打開相機(jī)
}catch (Exception e){
releaseCamera();
}
}
/**
* 配置相機(jī)參數(shù)
*/
private void setCameraParameters() {
Camera.Parameters parameters = mCamera.getParameters();
List<Camera.Size> sizes = parameters.getSupportedPreviewSizes();
double scale = (double) getWidth() / getHeight();
Camera.Size cameraSize = SortCameraSizeUtil.getCameraSize(sizes, scale);
if(cameraSize != null){
screenWidth = cameraSize.width;
screenHeight = cameraSize.height;
}else {
//確定前面定義的預(yù)覽寬高是camera支持的,不支持取就更大的
for (int i = 0; i < sizes.size(); i++) {
if ((sizes.get(i).width >= screenWidth && sizes.get(i).height >= screenHeight) || i == sizes.size() - 1) {
screenWidth = sizes.get(i).width;
screenHeight = sizes.get(i).height;
break;
}
}
}
//設(shè)置最終確定的預(yù)覽大小
parameters.setPreviewSize(screenWidth, screenHeight);//設(shè)置預(yù)覽分辨率
parameters.setPictureSize(screenWidth, screenHeight);//設(shè)置拍照圖片的分辨率
mCamera.setParameters(parameters);
}
/**
* 釋放相機(jī)
*/
private void releaseCamera() {
if(mCamera!=null){
stopPreview();
mCamera.setPreviewCallback(null);
mCamera.release();
mCamera=null;
}
}
/**
* 停止預(yù)覽
*/
private void stopPreview() {
if (mCamera != null && isPreviewing) {
mCamera.stopPreview();
isPreviewing = false;
}
}
/**
* 開始預(yù)覽
*/
public void startPreview() {
if (mCamera != null) {
mCamera.addCallbackBuffer(new byte[((screenWidth * screenHeight) * ImageFormat.getBitsPerPixel(ImageFormat.NV21)) / 8]);
mCamera.setPreviewCallbackWithBuffer(this);
mCamera.startPreview();
if(isSupportAutoFocus) {
mCamera.autoFocus(autoFocusCB);
}
isPreviewing = true;
}
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
stopPreview();
initCamera();
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
releaseCamera();
}
public interface IdentifyCallBack{
/**
* 回調(diào)掃描的車牌區(qū)域
*/
void onIdentifyImage(Bitmap bitmap);
}
}
重點(diǎn)講解
●調(diào)整預(yù)覽角度
如果不進(jìn)行預(yù)覽角度的調(diào)整,自定義相機(jī)展現(xiàn)的畫面會是橫著的琐脏,因此需要將角度旋轉(zhuǎn)90度杨名。
mCamera.setDisplayOrientation(90);//調(diào)整預(yù)覽角度
●獲取最佳比例的分辨率
這里是根據(jù)這個(gè)自定義相機(jī)控件的寬高比與相機(jī)支持的各個(gè)分辨率的寬高比對比來進(jìn)行篩選的香罐,找到那個(gè)差值最小的分辨率即可科雳,代碼如下:
/**
* 配置相機(jī)參數(shù)
*/
private void setCameraParameters() {
Camera.Parameters parameters = mCamera.getParameters();
//獲取相機(jī)支持的分辨率
List<Camera.Size> sizes = parameters.getSupportedPreviewSizes();
//自定義相機(jī)控件的寬高比
double scale = (double) getWidth() / getHeight();
//通過對比取出差值最小的那個(gè)分辨率
Camera.Size cameraSize = SortCameraSizeUtil.getCameraSize(sizes, scale);
if(cameraSize != null){
screenWidth = cameraSize.width;
screenHeight = cameraSize.height;
}else {
//如果找不到就取一個(gè)最清晰的
//確定前面定義的預(yù)覽寬高是camera支持的,不支持取就更大的
for (int i = 0; i < sizes.size(); i++) {
if ((sizes.get(i).width >= screenWidth && sizes.get(i).height >= screenHeight) || i == sizes.size() - 1) {
screenWidth = sizes.get(i).width;
screenHeight = sizes.get(i).height;
break;
}
}
}
//設(shè)置最終確定的預(yù)覽大小
parameters.setPreviewSize(screenWidth, screenHeight);//設(shè)置預(yù)覽分辨率
parameters.setPictureSize(screenWidth, screenHeight);//設(shè)置拍照圖片的分辨率
mCamera.setParameters(parameters);
}
/**
* 返回寬高比差值最小的Size
* @param sizes 系統(tǒng)支持的Camera的Size
* @param showViewRatio 當(dāng)前自定義的相機(jī)控件展示的寬高的比值
* @return
*/
public static Camera.Size getCameraSize(List<Camera.Size> sizes,double showViewRatio){
ArrayList<CameraSizeBean> sortSizeBeans = new ArrayList<>();
for (Camera.Size size : sizes) {
if(size != null){
//獲取當(dāng)前遍歷到的分辨率的寬高比(注意由于相機(jī)的角度問題衬鱼,這里是高比寬)
double scale = (double) size.height / size.width;
//獲取到兩個(gè)比值的差值的絕對值并存儲起來
sortSizeBeans.add(new CameraSizeBean(size,Math.abs(showViewRatio - scale)));
}
}
//對集合進(jìn)行排序买喧,差值最小的排上面
Collections.sort(sortSizeBeans);
if(sortSizeBeans.size() > 0){
//將差值最小的返回
return sortSizeBeans.get(0).getSize();
}else {
return null;
}
}
●自定義相機(jī)幀數(shù)據(jù)的回調(diào)
由于我們實(shí)現(xiàn)的是掃描識別并不是拍照識別捻悯,因此我們需要獲取相機(jī)的每一幀圖像,并進(jìn)行處理淤毛,這就需要我們進(jìn)行相應(yīng)的配置今缚,代碼如下:
mCamera.addCallbackBuffer(new byte[((screenWidth * screenHeight) * ImageFormat.getBitsPerPixel(ImageFormat.NV21)) / 8]);
mCamera.setPreviewCallbackWithBuffer(this);
回調(diào)函數(shù)如下:
/**
* Camera幀數(shù)據(jù)回調(diào)用
*/
@Override
public void onPreviewFrame(final byte[] data, final Camera camera) {
camera.addCallbackBuffer(data);
new Thread(new Runnable() {
@Override
public void run() {
//識別中不處理其他幀數(shù)據(jù)
if (!isScanning) {
isScanning = true;
try {
//獲取Camera預(yù)覽尺寸
Camera.Size size = camera.getParameters().getPreviewSize();
//將幀數(shù)據(jù)轉(zhuǎn)為bitmap
YuvImage image = new YuvImage(data, ImageFormat.NV21, size.width, size.height, null);
if (image != null) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
image.compressToJpeg(new Rect(0, 0, size.width, size.height), 80, stream);
Bitmap bmp = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size());
Bitmap bitmap = cutImage(bmp);//獲取遮罩處圖像
Message message = handler.obtainMessage();
message.what = 1;
message.obj = bitmap;
handler.sendMessage(message);
isScanning = false;
}
} catch (Exception ex) {
isScanning = false;
}
}
}
}
).start();
}
自定義遮罩
代碼展示
public class ScanningMaskView extends View {
private float mMaskWidth;//中間透明部分的寬度
private float mMaskHeight;//中間透明部分的高度
private Paint mPaintMask;//遮罩畫筆
private Paint mPaintText;//文字畫筆
private Paint mPaintMaskStrok;//遮罩描邊畫筆
private float mTextSize = 30;//文字大小
private Path mMaskPath;//遮罩透明部分路徑
private String mTopTripStr = "請掃描電動車牌號";
private String mBottomTripStr = "請保持光線充足";
public ScanningMaskView(Context context) {
super(context);
init();
}
public ScanningMaskView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public ScanningMaskView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
/**
* 當(dāng)控件大小改變的時(shí)候動態(tài)調(diào)整遮罩的大小
* @param w
* @param h
* @param oldw
* @param oldh
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mMaskWidth = w/5*3;//遮罩透明部分的寬度為控件寬度的3/5
mMaskHeight = mMaskWidth*1.93f;//遮罩透明部分的高度根據(jù)車牌比例算出
mMaskPath.reset();
mTextSize = w/20;
mPaintText.setTextSize(mTextSize);//設(shè)置文字大小
float left = (w-mMaskWidth)/2;
float top = (h-mMaskHeight)/2;
float right = left + mMaskWidth;
float bottom = top + mMaskHeight;
mMaskPath.addRoundRect(new RectF(left,top,right,bottom),10,10, Path.Direction.CW);
invalidate();
}
private void init(){
//關(guān)閉硬件加速
setLayerType(LAYER_TYPE_SOFTWARE,null);
mPaintMask = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaintMask.setStyle(Paint.Style.FILL);
mPaintMask.setColor(Color.BLACK);
mPaintMask.setAlpha(160);//設(shè)置半透明
mPaintText = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaintText.setStrokeWidth(3);
mPaintText.setColor(Color.WHITE);//設(shè)置文字顏色
mPaintText.setTextAlign(Paint.Align.CENTER);//文字水平居中
mPaintMaskStrok = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaintMaskStrok.setColor(Color.WHITE);
mPaintMaskStrok.setStyle(Paint.Style.STROKE);
mPaintMaskStrok.setStrokeWidth(3);
mMaskPath = new Path();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.save();//離屏繪制
canvas.drawRect(0,0,getWidth(),getHeight(),mPaintMask);//繪制整個(gè)控件大小遮罩
mPaintMask.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));//將透明部分摳出來
canvas.drawPath(mMaskPath,mPaintMask);
mPaintMask.setXfermode(null);//清除混合模式
canvas.restore();
canvas.drawPath(mMaskPath,mPaintMaskStrok);//繪制遮罩描邊
canvas.save();
canvas.translate(getWidth()/2,getHeight()/2);
canvas.rotate(90);//旋轉(zhuǎn)90度繪制文字
canvas.drawText(mTopTripStr,0,-((mMaskWidth/2)+((getWidth()-mMaskWidth)/4)),mPaintText);
canvas.drawText(mBottomTripStr,0,(mMaskWidth/2)+((getWidth()-mMaskWidth)/4),mPaintText);
canvas.restore();
}
}
重點(diǎn)講解
●遮罩透明部分
遮罩透明部分主要是用了混合模式來實(shí)現(xiàn)的,具體就不多做解釋了低淡,這里我有一個(gè)小案例:利用PorterDuffXfermode繪制圖片文字姓言,想了解的同學(xué)可以點(diǎn)一下看看,或是自己百度了解下也可以蔗蹋。
●透明部分比例
透明部分的比例何荚,以豎著的方式來看,寬度為屏幕寬度的3/5纸颜,高度為寬度的1.93倍兽泣,這個(gè)1.93并不是隨便寫的數(shù)據(jù)绎橘,而是因?yàn)?.93差不多也是電動車牌的寬高比胁孙,對應(yīng)代碼如下:
mMaskWidth = w/5*3;//遮罩透明部分的寬度為控件寬度的3/5
mMaskHeight = mMaskWidth*1.93f;//遮罩透明部分的高度根據(jù)車牌比例算出
●車牌號區(qū)域裁剪
車牌號部分的裁剪其實(shí)就是遮罩透明區(qū)域的裁剪,既然我們知道了透明部分區(qū)域的計(jì)算方式那么裁剪就很簡單了称鳞,這部分代碼在自定義相機(jī)里涮较,代碼如下:
/**
* 裁剪照片
*
* @return
*/
private Bitmap cutImage(Bitmap bitmap) {
int h = bitmap.getWidth();
int w = bitmap.getHeight();
int clipw = w/5*3;//這里根據(jù)遮罩的比例進(jìn)行裁剪
int cliph = (int) (clipw*1.93f);
int x = (w - clipw) / 2;
int y = (h - cliph) / 2;
return Bitmap.createBitmap(bitmap, y, x,cliph, clipw);
}
掃描動畫實(shí)現(xiàn)
掃描動畫我是使用TranslateAnimation來實(shí)現(xiàn)的,就是利用TranslateAnimation使掃描線從豎著的方向來看從右向左移動冈止,到底之后就反轉(zhuǎn)執(zhí)行動畫狂票,而那條線就是背景為橢圓shape的View拉長之后看著就像中間粗兩邊細(xì)的線,代碼如下:
/**
* 動畫設(shè)置
*/
void setAnimation() {
TranslateAnimation mAnimation = new TranslateAnimation(TranslateAnimation.RELATIVE_TO_PARENT, 0.99f,TranslateAnimation.RELATIVE_TO_PARENT, 0.01f,TranslateAnimation.ABSOLUTE, 0f,TranslateAnimation.ABSOLUTE,0f);
mAnimation.setDuration(5000);
mAnimation.setRepeatMode(Animation.REVERSE);// 設(shè)置反方向執(zhí)行
mAnimation.setRepeatCount(Animation.INFINITE);
viewScanningline.setAnimation(mAnimation);
mAnimation.start();
}
而執(zhí)行動畫的區(qū)域是使用了百分百布局來進(jìn)行固定的熙暴,使其移動的軌跡剛好在遮罩透明區(qū)域闺属,代碼如下:
<?xml version="1.0" encoding="utf-8"?>
<android.support.percent.PercentRelativeLayout
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">
<com.itfitness.licenseocrdemo.widget.camera.ScanningCameraView
android:id="@+id/camera2"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
<com.itfitness.licenseocrdemo.widget.mask.ScanningMaskView
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
<!--掃描線的區(qū)域-->
<RelativeLayout
android:id="@+id/layout_scanning"
app:layout_aspectRatio="51.90%"
app:layout_widthPercent="60%"
android:layout_centerInParent="true">
<!--掃描線-->
<View
android:id="@+id/view_scanningline"
android:layout_alignParentLeft="true"
android:background="@drawable/shape_scanningline"
android:layout_width="2dp"
android:layout_height="match_parent"/>
</RelativeLayout>
<ImageView
android:id="@+id/img"
android:scaleType="centerInside"
android:layout_width="wrap_content"
android:layout_height="90dp"
/>
<ImageView
android:id="@+id/img_light"
android:layout_centerHorizontal="true"
app:layout_marginTopPercent="86%"
android:rotation="90"
android:background="@drawable/selector_light"
app:layout_aspectRatio="120%"
app:layout_widthPercent="15%"
/>
</android.support.percent.PercentRelativeLayout>
實(shí)現(xiàn)效果
注意事項(xiàng)
●權(quán)限問題
別忘了權(quán)限的添加:
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
另外如果是Android6.0及以上別忘了動態(tài)申請權(quán)限,這里我為了省事所以沒進(jìn)行動態(tài)申請而是直接手動開啟的權(quán)限周霉。
●項(xiàng)目類型
我這里創(chuàng)建的是Native C++項(xiàng)目