本文已參與好文召集令活動,點擊查看:後端、大前端雙賽道投稿,2萬元獎池等你挑戰!前端
年初寫了一篇CameraX的使用文章,幫到了一些朋友,也收到了一些建議。正值最近了解到華爲
ScanKit
在掃碼場景下的優秀表現,決定集成該方案,並進行一些功能改進。java
以前作的Demo略顯簡陋,本次改進也對UI進行了調整。主要是給頂部操做欄添加了半透明背景,同時給切換按鈕添加了半透明邊框以提升對比度。另外對拍攝和錄製場景的一些配色作了改動。android
ScanKit能夠提供便捷的二維碼與條形碼掃描、解析、生成能力,幫助您快速構建應用內的掃碼功能。git
它擁有諸多優點,包括支持多達13種碼格式,在反光、污損、畸變、模糊等複雜場景下亦能良好識別,在遠距離掃碼的狀況下能自適應放大碼體,還支持多碼識別功能等等。github
ScanKit給開發者提供了四種集成模式,包括固定掃碼界面的Default View Mode
,自定義掃碼界面的Customized View Mode
,以及徹底由開發者自定義畫面和掃碼流程的Bitmap Mode
和MultiProcessor Mode
。後端
前兩種模式的掃碼流程均由ScanKit控制,其內部採用Camera1
實現。若是要集成到CameraX
上的話,只能選擇後兩種模式。MultiProcessor Mode
適用於多碼識別的場景,本次先集成單碼識別的Bitmap Mode
。數組
華爲ScanKit更加詳細的資料可查閱官網:安全
developer.huawei.com/consumer/cn…微信
以及易冬大神的完整演示:markdown
以前的掃碼方案採用的是Zxing
,本次集成ScanKit
以後,爲了對比學習將Zxing
也進行了保留。在點擊掃碼按鈕以後,底部會彈出掃碼方案的選擇Fragment,選擇以後經過ViewModel將對應的方案告知CameraX的ImageAnalysis。
※ Google ML Kit是一個更爲強大的OCR解決方案,後面也將集成進來
你們可能比較關心ScanKit相較於Zxing的優點,能夠參考以下這篇測評文章: developer.huawei.com/consumer/cn…
這篇文章裏提到ScanKit在遠距離掃碼、碼體傾斜、模糊掃碼等場景下的識別速度和成功率都要優於Zxing。你們也可使用本文的Demo,分別選擇Zxing和ScanKit兩個方案,實際對比一下掃碼體驗。
在project的gradle文件裏添加ScanKit的倉庫地址,app的gradle文件裏添加依賴,便可快速集成。※Demo依賴了識別能力更爲出色的scanplus依賴包
// build.gradle
buildscript {
repositories {
...
mavenCentral()
maven {url 'https://developer.huawei.com/repo/'}
}
}
allprojects {
repositories {
...
mavenCentral()
maven {url 'https://developer.huawei.com/repo/'}
}
}
複製代碼
// app/build.gradle
dependencies {
...
// Huawei scan kit
implementation 'com.huawei.hms:scanplus:1.3.2.300'
}
複製代碼
CameraX圖像分析ImageAnalysis
回傳的圖像實例ImageProxy
是YUV格式的,須要先經過YuvImage
將其轉換爲Bitmap,以後再調用ScanKit的Bitmap掃碼模式。
private fun proxyToBitmap(image: ImageProxy): Bitmap {
val planes: Array<ImageProxy.PlaneProxy> = image.planes
val yBuffer: ByteBuffer = planes[0].buffer
val uBuffer: ByteBuffer = planes[1].buffer
val vBuffer: ByteBuffer = planes[2].buffer
val ySize: Int = yBuffer.remaining()
val uSize: Int = uBuffer.remaining()
val vSize: Int = vBuffer.remaining()
val nv21 = ByteArray(ySize + uSize + vSize)
yBuffer.get(nv21, 0, ySize)
vBuffer.get(nv21, ySize, vSize)
uBuffer.get(nv21, ySize + vSize, uSize)
val yuvImage = YuvImage(nv21, ImageFormat.NV21, image.width, image.height, null)
val out = ByteArrayOutputStream()
yuvImage.compressToJpeg(Rect(0, 0, yuvImage.width, yuvImage.height), 75, out)
val imageBytes = out.toByteArray()
val opt = BitmapFactory.Options()
opt.inPreferredConfig = Bitmap.Config.ARGB_8888
var bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size, opt)
return bitmap
}
複製代碼
建立下ScanKit專用的掃碼參數,並將轉換獲得的Bitmap實例傳遞給ScanUtil
,便可開始識別。返回的識別結果包括內容、座標、四角位置等信息,被封裝到HmsScan
對象裏。ScanUtil識別完成後實際返回的是HmsScan數組,其第一個元素即爲單碼的識別結果。HmsScan對象的originalValue
屬性則是解析出來的內容。
class HuaweiScanAnalysis: RealTimeAnalysis {
override fun analyzeContent(imageProxy: ImageProxy, context: Context): AnalysisResult {
val bitmap = proxyToBitmap(imageProxy)
imageProxy.close()
// 建立ScanKit掃碼的參數
val options = HmsScanAnalyzerOptions.Creator()
.setHmsScanTypes(HmsScan.ALL_SCAN_TYPE)
.setPhotoMode(false)
.create()
// 獲得掃碼結果
val result = ScanUtil.decodeWithBitmap(
context,
bitmap,
options
)
val content = if (result != null && result.isNotEmpty() && result[0].originalValue != null)
result[0].originalValue else ""
...
// 將掃碼結果封裝爲咱們自定義的實例
return AnalysisResult(content, scale, rect)
}
}
複製代碼
當遠距離掃碼或碼體太小,ScanKit會計算獲得適合的放大倍率,並賦值到HmsScan對象的zoomValue
屬性裏。能夠利用該數值及時通知CameraX調整圖像採集的倍率,進而提高後續的識別率。實現思路很是簡單,使用CameraControl
提供的setZoomRatio
放大圖像預覽和分析的倍率便可。爲不影響下次的掃碼體驗,在掃碼完成後須將倍率置。
class MyAnalyzer(...): Analyzer {
override fun analyze(image: ImageProxy) {
viewModel.analysePicture(image).also {
if (Constants.DEFAULT_ZOOM_SCALE != it.zoomScale
&& Constants.MIN_ZOOM_SCALE != it.zoomScale
) {
callback.onZoomPreview(it.zoomScale)
} else {
callback.onAnalyzeResult(it)
}
}
}
}
fun onAnalyzeGo(view: View?) {
if (mAnalyzer == null) {
mAnalyzer = MyAnalyzer(viewModel, object : AnalyzeCallback {
override fun onZoomPreview(scale: Double) {
mCamera.cameraControl.setZoomRatio(scale.toFloat())
}
...
})
}
}
複製代碼
爲提升用戶體驗,能夠在掃碼成功的同時播放預設的提示音或震動反饋,能夠利用開源的BeepManager
工具類來實現。
fun onAnalyzeGo(view: View?) {
if (mAnalyzer == null) {
mAnalyzer = MyAnalyzer(viewModel, object : AnalyzeCallback {
override fun onAnalyzeResult(result: AnalysisResult) {
synchronized(isAnalyzing) {
showQRCodeResult(result.content)
...
}
}
})
}
}
private fun showQRCodeResult(result: String) {
stopAnalysis()
beepManager.playBeepSoundAndVibrate()
...
}
複製代碼
掃碼成功的瞬間,微信和支付寶App會在二維碼上展現一個圓點,這樣的提示設計比較好。HmsScan類的borderRect
屬性表明碼體的矩形框位置,經過計算獲得的centerX和centerY能夠幫忙獲取碼體的中心,在該位置能夠展現一個指示View。
須要留意的是,豎屏模式下Analyse的圖片會有90度的誤差,因此須要額外轉換下位置座標。固然若是Bitmap實例已經作過了90度旋轉的處理的話,borderRect數值就不須要額外轉換了。有些遺憾的是,座標計算會有些偏差,很難保證每次都將指示位置繪製在中心。
override fun onAnalyzeResult(result: AnalysisResult) {
synchronized(isAnalyzing) {
showQRCodeResult(result.content)
val centerPoint = Utils.convertRectToPoint(result.rect, binding.previewView)
showPointView(centerPoint)
}
}
private fun showPointView(point: Point) {
val popupWindow = PopupWindow(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
val imageView = ImageView(this)
runOnUiThread {
popupWindow.contentView = imageView
imageView.setImageResource(R.drawable.ic_point_view)
popupWindow.showAsDropDown(binding.previewView, point.x, point.y)
binding.previewView.postDelayed({ popupWindow.dismiss() }, 1000)
}
}
複製代碼
在識別過程或成功的時候也能夠展現二維碼的邊框輔助提示。儘管這樣的設計並不十分必要,咱們能夠試着實現看看。
borderRect屬性的原始數值就是框體的寬高,再依據上面的中心位置就能夠在上面繪製一個矩形框。事實上除了borderRect,cornerPoints
屬性能夠拿到碼體四角的確切位置,也能夠做爲繪製框體的數據來源。
override fun onAnalyzeResult(result: AnalysisResult) {
synchronized(isAnalyzing) {
showQRCodeResult(result.content)
val centerPoint = Utils.convertRectToPoint(result.rect, binding.previewView)
showRectView(centerPoint, result.rect)
}
}
private fun showRectView(point: Point, rect: Rect) {
val popupWindow = PopupWindow(
rect.height(),
rect.width()
)
val imageView = ImageView(this)
runOnUiThread {
popupWindow.contentView = imageView
imageView.setImageResource(R.drawable.ic_rect_view)
imageView.scaleType = ImageView.ScaleType.FIT_XY
try {
popupWindow.showAsDropDown(binding.previewView,
point.x - (rect.width() / 2), point.y)
} catch (e: Exception) {}
binding.previewView.postDelayed({ popupWindow.dismiss() }, 1000)
}
}
複製代碼
※ 不知道拍攝角度的問題仍是ScanKit的識別存在偏差,框體的繪製位置總有些誤差,官方Demo繪製的框體位置也不許確
以前的Demo主要集中在CameraX的API使用上,忽略了支持必要的手勢,本次一併加入經常使用的手勢支持。
CameraControl提供的setLinearZoom() API能夠將拍攝的視野線性地縮放,比較適合雙擊或者滑動縮放視圖的場景。它接受的參數數值介於0~1之間,具體以下:
經過監聽雙擊手勢,讓拍攝的畫面在原始比例0f和0.5F中間比例之間切換。
private fun listenGesture() {
binding.previewView.setOnTouchListener { view, event ->
...
// Zoom when double click.
doubleClickZoom(event)
true
}
}
private fun doubleClickZoom(event: MotionEvent) {
if (doubleClickDetector == null) {
doubleClickDetector = GestureDetector(this@NewCameraXActivity,
object : GestureDetector.SimpleOnGestureListener() {
override fun onDoubleTap(e: MotionEvent?): Boolean {
cameraZoomState.value?.let {
val zoomRatio = it.zoomRatio
val minRatio = it.minZoomRatio
// Ratio parameter from 0f to 1f.
if (zoomRatio > minRatio) {
mCamera.cameraControl.setLinearZoom(Constants.MIN_ZOOM_SCALE.toFloat())
} else {
mCamera.cameraControl.setLinearZoom(Constants.MIDDLE_ZOOM_SCALE.toFloat())
}
}
return true
}
})
}
doubleClickDetector?.onTouchEvent(event)
}
複製代碼
CameraControl提供的setZoomRatio API在線性縮放的基礎之上提供了更爲準確的縮放比率,能夠實現捏合手勢的縮放場景。
private fun listenGesture() {
binding.previewView.setOnTouchListener { view, event ->
...
// Listen to zoom gesture.
scalePreview(event)
true
}
}
private fun scalePreview(event: MotionEvent) {
if (scaleDetector == null) {
scaleDetector = ScaleGestureDetector(this@NewCameraXActivity,
object : SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean {
cameraZoomState.value?.let {
val zoomRatio = it.zoomRatio
mCamera.cameraControl.setZoomRatio(zoomRatio * detector.scaleFactor)
}
return true
}
})
}
scaleDetector?.onTouchEvent(event)
}
複製代碼
以前是在Touch(ACITON_DOWN)的時候依據座標進行手動聚焦,引入縮放手勢的支持以後,縮放的過程當中會誤觸對焦操做。改善方法在於將對焦的時機限制在SingleTap
手勢,即只有單擊操做纔會觸發對焦。
private fun listenGesture() {
binding.previewView.setOnTouchListener { view, event ->
...
// Singe tap for focus.
singleTapForFocus(event)
true
}
}
private fun singleTapForFocus(event: MotionEvent) {
if (singleTapDetector == null) {
singleTapDetector = GestureDetector(this@NewCameraXActivity,
object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
focusOnPosition(event.x, event.y, true)
return super.onSingleTapConfirmed(e)
}
})
}
singleTapDetector?.onTouchEvent(event)
}
複製代碼
改進前不少邏輯都堆在了Activity裏,現將各個UseCase的實現拆分出去,減輕Activity的負擔。同時對CameraX使用的一些問題進行了改進。
展現相機預覽的控件PreviewView還沒有添加到視圖Tree的時候,若是執行CameraX的綁定操做的話,會發生問題。現象上表現爲拍攝畫面結束後再次打開的時候會發生Crash。解決思路很簡單:監聽PreviewView控件的attach時機,在attach成功的回調裏才執行CameraX的綁定操做。
override fun onCreate(savedInstanceState: Bundle?) {
...
setContentView(binding.root)
startCameraWhenAttached()
}
private fun startCameraWhenAttached() {
binding.previewView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener{
override fun onViewAttachedToWindow(v: View?) {
ensureCameraPermission()
}
})
}
private fun ensureCameraPermission() {
...
setupCamera(binding.previewView)
}
複製代碼
記得在畫面不可見的時候結束圖像分析的調用,節省內存。
override fun onStop() {
super.onStop()
mImageAnalysis?.clearAnalyzer()
}
複製代碼
快速點擊視頻錄製和中止的狀況下偶爾會發生以下的crash。
java.lang.IllegalStateException: Failed to stop the muxer
看了CameraX的源碼,錄製開始和結束時Audio實例的請求和釋放發生了錯亂。本此改進加入了錄製視頻的狀態控制,在錄製開始的500ms內禁止終止錄製,以緩解這種現象。
但在極快的錄製和中止的反覆操做下,錄製的部分文件可能會發生損壞。因爲CameraX的視頻錄製API仍處在實驗性階段,因此耐心等待CameraX的解決吧。
private fun videoRecordingPrepared() {
isCameraXHandling = false
// Keep disabled status for a while to avoid fast click error with "Muxer stop failed!".
binding.capture.postDelayed({ binding.capture.isEnabled = true }, 500)
}
複製代碼
CameraX拍攝的照片默認是鏡像的,在拍攝前告知CameraX作下鏡像反轉,作到所見即所得。
private fun takenPictureInternal(isExternal: Boolean) {
...
// Mirror image
ImageCapture.Metadata().apply {
isReversedHorizontal = true
}
mImageCapture?.takePicture(outputFileOptions, lightExecutor, MyCaptureCallback(picCount, this))
}
複製代碼
不少設備的先後並不止一個鏡頭,好比疫情期間很是流行的安全碼和體溫一體化檢測設備。因此有時候鏡頭切換不能是簡單地先後切換,而須要按鏡頭的ID指定切換。
private fun bindPreview(...) {
// Select specified camera.
val cameraSelector = CameraSelector.Builder().addCameraFilter(AllCameraFilter()).build()
...
}
class AllCameraFilter: CameraFilter {
override fun filter(cameraInfos: MutableList<CameraInfo>): MutableList<CameraInfo> {
val result: MutableList<CameraInfo> = mutableListOf()
for (cameraInfo in cameraInfos) {
val id = (cameraInfo as CameraInfoInternal).cameraId
// Specify the camera id that U need, such as front camera which id is 0.
if (CameraSelector.LENS_FACING_FRONT.equals(id)) {
result.add(cameraInfo)
}
}
return result
}
}
複製代碼
實際上CameraX最新版提供了新API(CameraInfo#getCameraSelector()),可返回某鏡頭對應的選擇器實例。
整理一下CameraX使用的主要API,供你們快速查閱。
管理相機實例的接口或實現 | 做用 |
---|---|
CameraController | 獲取和管理相機實例的接口 |
LifecycleCameraController | 經過LifecycleOwner實現生命週期管理Camera實例的接口 |
ProcessCameraProvider | LifecycleOwner的實現類,用以單例模式管理Camera實例 |
訪問鏡頭功能和屬性的API | 做用 |
---|---|
Camera | 提供鏡頭操做的主要接口 |
CameraControl | 用以執行鏡頭縮放、聚焦等操做的接口,經過Camera接口獲取實例 |
CameraInfo | 用以獲取鏡頭參數的IF,好比縮放比率、是否有閃光燈等,其實例一樣由Camera接口提供 |
CameraConfig | 用以獲取Camera使用配置信息的接口,也經過Camera接口獲取實例 |
CameraSelector | 過濾並匹配對應鏡頭的類,在CameraController執行的時候傳入實例以初始化對應的鏡頭 |
場景UseCase類的實現類 | 做用 |
---|---|
Preview | 預覽場景 |
ImageAnalysis | 圖像分析 |
ImageCapture | 圖像拍攝 |
VideoCapture | 視頻錄製 |
相機效果的擴展類 | 做用 |
---|---|
PreviewExtender | 展現預覽擴展效果,實現類有美顏的BeautyPreviewExtender、夜拍的NightPreviewExtender等 |
ImageCaptureExtender | 展現拍攝擴展效果,一樣有美顏等效果的實現類 |
以及ScanKit的部分API:
API | 做用 |
---|---|
ScanUtil | Bitmap掃描碼模式、壓縮Bitmap等功能支持的工具類 |
HmsScanAnalyzerOptions | 指定掃碼格式等參數類 |
HmsScan | 掃碼結果封裝類,包括內容、碼體座標、四角位置等信息 |
華爲ScanKit
的集成仍是很是簡單流暢的,在掃碼技術選型的時候能夠大膽嘗試一下。對於識別率或速度擔憂的朋友能夠下載ScanKit
和Zxing
的官方Apk進行體驗和對比。
Scankit官方Sample下載地址:
developer.huawei.com/consumer/en…
Zxing官方Sample下載地址:
但願針對CameraX的掃碼集成和實用的改進,對你們有所幫助。