Android基于OpenCV實(shí)現(xiàn)QR二維碼檢測(cè)
QR二維碼
QR碼(英語(yǔ):Quick Response Code;全稱(chēng)為快速響應(yīng)矩陣圖碼)是二維碼的一種,于1994年由日本DENSO WAVE公司發(fā)明。QR來(lái)自英文Quick Response的縮寫(xiě),即快速反應(yīng),因?yàn)榘l(fā)明者希望QR碼可以快速解碼其內(nèi)容。QR碼使用四種標(biāo)準(zhǔn)化編碼模式(數(shù)字、字母數(shù)字、字節(jié)(二進(jìn)制)和日文(Shift_JIS))來(lái)存儲(chǔ)數(shù)據(jù)。QR碼常見(jiàn)于日本,為目前日本最通用的二維空間條碼,在世界各國(guó)廣泛運(yùn)用于手機(jī)讀碼操作。QR碼比普通一維條碼具有快速讀取和更大的存儲(chǔ)資料容量,也無(wú)需要像一維條碼般在掃描時(shí)需要直線對(duì)準(zhǔn)掃描儀。因此其應(yīng)用范圍已經(jīng)擴(kuò)展到包括產(chǎn)品跟蹤,物品識(shí)別,文檔管理,庫(kù)存營(yíng)銷(xiāo)等方面?!揪S基百科】
QR二維碼格式
QR碼呈正方形,常見(jiàn)的是黑白兩色。在3個(gè)角落,印有較小,像“回”字的正方圖案。這3個(gè)是幫助解碼軟件定位的圖案,用戶不需要對(duì)準(zhǔn),無(wú)論以任何角度掃描,資料仍然可以正確被讀取。日本QR碼的標(biāo)準(zhǔn)JIS X 0510在1999年1月發(fā)布,而其對(duì)應(yīng)的ISO國(guó)際標(biāo)準(zhǔn)ISO/IEC18004,則在2000年6月獲得批準(zhǔn)。根據(jù)Denso Wave公司的網(wǎng)站資料,QR碼是屬于開(kāi)放式的標(biāo)準(zhǔn),QR碼的規(guī)格公開(kāi),雖由Denso Wave公司持有的專(zhuān)利權(quán)益,但不會(huì)被運(yùn)行。除了標(biāo)準(zhǔn)的QR碼之外,也存在一種稱(chēng)為“微型QR碼”的格式,是QR碼標(biāo)準(zhǔn)的縮小版本,主要是為了無(wú)法處理較大型掃描的應(yīng)用而設(shè)計(jì)。微型QR碼同樣有多種標(biāo)準(zhǔn),最高可存儲(chǔ)35個(gè)字符。【維基百科】
QR二維碼結(jié)構(gòu)
QR碼最大特征為其左上,右上,左下三個(gè)大型的如同“回”字的黑白間同心方圖案,為QR碼識(shí)別定位標(biāo)記,失去其中一個(gè)會(huì)影響識(shí)別。而呈棋盤(pán)般分布的有別與大定位標(biāo)記的較小的同心方則為其校正標(biāo)記,用于校正識(shí)別,版本1沒(méi)有校正標(biāo)記,版本2在右下方,其中心點(diǎn)在左下和右上定位標(biāo)記的外邊框的相交點(diǎn),版本10開(kāi)始以每個(gè)等距的方式出現(xiàn)在右下校正點(diǎn)至左下和右上定位標(biāo)記的外邊框的連線、左上與左下定位標(biāo)記的外邊框的連線、左上與右上定位標(biāo)記的外邊框的連線之間、這四邊線上等距點(diǎn)對(duì)邊相連線,版本10等距有1個(gè),版本25為3個(gè),版本40為5個(gè)。【維基百科】
API
QRCodeDetector類(lèi)結(jié)構(gòu)

檢測(cè)QR二維碼
public boolean detect(Mat img, Mat points)
- 參數(shù)一:img,待檢測(cè)是否含有QR二維碼的的灰度圖或者彩色(BGR)圖像。
- 參數(shù)二:points,檢測(cè)到的QR二維碼的最小區(qū)域四邊形的4個(gè)頂點(diǎn)坐標(biāo)集合。
- 返回值:布爾類(lèi)型,true,代表檢測(cè)到QR二維碼;false,代表未檢測(cè)到QR二維碼。
public boolean detectMulti(Mat img, Mat points)
- 參數(shù)一:img,待檢測(cè)是否含有QR二維碼的的灰度圖或者彩色(BGR)圖像。
- 參數(shù)二:points,多個(gè)檢測(cè)結(jié)果QR二維碼的最小區(qū)域四邊形的4個(gè)頂點(diǎn)坐標(biāo)集合。
- 返回值:布爾類(lèi)型,true,代表檢測(cè)到QR二維碼;false,代表未檢測(cè)到QR二維碼。
識(shí)別QR二維碼
public String decode(Mat img, Mat points, Mat straight_qrcode)
- 參數(shù)一:img,含有QR二維碼的灰度圖像或者彩色(BGR)圖像。
- 參數(shù)二:points,detect方法得到的points值。數(shù)據(jù)量不可為空。
- 參數(shù)三:straight_qrcode,經(jīng)過(guò)矯正和二值化的QR二維碼?!究蛇x參數(shù)】
- 返回值:字符串類(lèi)型,如果解碼失敗,則為空串。
public boolean decodeMulti(Mat img, Mat points, List<String> decoded_info, List<Mat> straight_qrcode)
- 參數(shù)一:img,含有QR二維碼的灰度圖像或者彩色(BGR)圖像。
- 參數(shù)二:points,detect方法得到的points值。數(shù)據(jù)量不可為空。
- 參數(shù)三:decoded_info,多個(gè)二維碼的解碼信息。
- 參數(shù)四:straight_qrcode,所有檢測(cè)到的二維碼矯正和二值化的后的結(jié)果集合?!究蛇x參數(shù)】
- 返回值:布爾類(lèi)型,true,代表解碼成功,反之,解碼失敗。
檢測(cè)并識(shí)別QR二維碼
public String detectAndDecode(Mat img, Mat points, Mat straight_qrcode)
- 參數(shù)一:img,含有QR二維碼的灰度圖像或者彩色(BGR)圖像。
- 參數(shù)二:points,檢測(cè)到的QR二維碼的最小區(qū)域四邊形的4個(gè)頂點(diǎn)坐標(biāo)。
- 參數(shù)三:straight_qrcode,經(jīng)過(guò)矯正和二值化的QR二維碼?!究蛇x參數(shù)】
- 返回值:字符串類(lèi)型,如果解碼失敗,則為空串。
public boolean detectAndDecodeMulti(Mat img, List<String> decoded_info, Mat points, List<Mat> straight_qrcode)
- 參數(shù)一:img,含有QR二維碼的灰度圖像或者彩色(BGR)圖像。
- 參數(shù)二:decoded_info,多個(gè)二維碼的解碼信息。
- 參數(shù)三:points,檢測(cè)到的多個(gè)QR二維碼的最小區(qū)域四邊形的4個(gè)頂點(diǎn)坐標(biāo)集合?!究蛇x參數(shù)】
- 參數(shù)四:straight_qrcode,所有檢測(cè)到的二維碼矯正和二值化的后的結(jié)果集合?!究蛇x參數(shù)】
- 返回值:字符串類(lèi)型,如果解碼失敗,則為空串。
操作
/**
* QR二維碼檢測(cè)
* author: yidong
* 2020/10/27
*/
class QRDetectActivity : AppCompatActivity() {
private lateinit var mBinding: ActivityQrDetectBinding
private lateinit var mQRCodeDetector: QRCodeDetector
private var mPhotoSavePath = ""
private lateinit var mUri: Uri
private lateinit var mSource: Mat
private lateinit var mGray: Mat
private lateinit var mOperationSheet: BottomSheetDialog
private lateinit var mSheetBinding: LayoutQrDetectOpBinding
private lateinit var mPhotoSheet: BottomSheetDialog
private lateinit var mPhotoOpBinding: LayoutPhotoOpBinding
// 請(qǐng)求相機(jī)權(quán)限
private val requestCameraPermission =
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
if (it) {
mPhotoSavePath =
cacheDir.path + File.separator + "${System.currentTimeMillis()}.png"
mUri = MediaStoreUtils.getIntentUri(this, File(mPhotoSavePath))
requestCamera.launch(mUri)
} else {
Toast.makeText(applicationContext, "無(wú)相機(jī)權(quán)限", Toast.LENGTH_SHORT).show()
}
}
// 請(qǐng)求外部存儲(chǔ)權(quán)限
private val requestStoragePermission =
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
if (it) {
pickImage.launch("image/*")
} else {
Toast.makeText(applicationContext, "無(wú)存儲(chǔ)權(quán)限", Toast.LENGTH_SHORT).show()
}
}
private val requestCamera = registerForActivityResult(ActivityResultContracts.TakePicture()) {
if (it) {
val bgr = Imgcodecs.imread(mPhotoSavePath, Imgcodecs.IMREAD_COLOR)
if (bgr.empty()) {
Toast.makeText(applicationContext, "讀取拍照結(jié)果失敗", Toast.LENGTH_SHORT).show()
return@registerForActivityResult
} else {
Imgproc.cvtColor(bgr, mSource, Imgproc.COLOR_BGR2RGB)
Imgproc.cvtColor(bgr, mGray, Imgproc.COLOR_BGR2GRAY)
mBinding.ivLena.showMat(mSource)
}
} else {
Toast.makeText(applicationContext, "拍照失敗", Toast.LENGTH_SHORT).show()
}
}
private val pickImage = registerForActivityResult(ActivityResultContracts.GetContent()) {
if (it != null) {
val filePath = MediaStoreUtils.getMediaPath(this, it)
if (filePath.isNullOrEmpty()) {
Toast.makeText(applicationContext, "讀取圖片失敗", Toast.LENGTH_SHORT).show()
return@registerForActivityResult
}
val bgr = Imgcodecs.imread(filePath, Imgcodecs.IMREAD_COLOR)
if (bgr.empty()) {
Toast.makeText(applicationContext, "讀取圖片失敗", Toast.LENGTH_SHORT).show()
return@registerForActivityResult
} else {
Imgproc.cvtColor(bgr, mSource, Imgproc.COLOR_BGR2RGB)
Imgproc.cvtColor(bgr, mGray, Imgproc.COLOR_BGR2GRAY)
mBinding.ivLena.showMat(mSource)
}
} else {
Toast.makeText(applicationContext, "選圖失敗", Toast.LENGTH_SHORT).show()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_qr_detect)
mQRCodeDetector = QRCodeDetector()
mSource = Mat()
mGray = Mat()
val bgr = Utils.loadResource(this, R.drawable.qrcode)
Imgproc.cvtColor(bgr, mSource, Imgproc.COLOR_BGR2RGB)
Imgproc.cvtColor(bgr, mGray, Imgproc.COLOR_BGR2GRAY)
mBinding.ivLena.showMat(mSource)
createDialog()
}
private fun createDialog() {
mOperationSheet = BottomSheetDialog(this)
mSheetBinding = LayoutQrDetectOpBinding.inflate(layoutInflater, null, false)
mOperationSheet.setContentView(mSheetBinding.root)
mSheetBinding.tvDetect.setOnClickListener {
mOperationSheet.dismiss()
doDetect()
}
mSheetBinding.tvDecode.setOnClickListener {
mOperationSheet.dismiss()
doDecode()
}
mPhotoSheet = BottomSheetDialog(this)
mPhotoOpBinding = LayoutPhotoOpBinding.inflate(layoutInflater, null, false)
mPhotoSheet.setContentView(mPhotoOpBinding.root)
mPhotoOpBinding.tvCamera.setOnClickListener {
mPhotoSheet.dismiss()
requestCameraPermission.launch(
Manifest.permission.CAMERA
)
}
mPhotoOpBinding.tvPhoto.setOnClickListener {
mPhotoSheet.dismiss()
requestStoragePermission.launch(
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
}
}
private fun doDetect() {
val points = Mat()
val isHasQr = mQRCodeDetector.detect(mSource, points)
if (isHasQr) {
val pointArr = FloatArray(8)
points.get(0, 0, pointArr)
Log.d(App.TAG, pointArr.toList().toString())
val tmp = mSource.clone()
for (i in pointArr.indices step 2) {
val start = Point(pointArr[i % 8].toDouble(), pointArr[(i + 1) % 8].toDouble())
val end = Point(pointArr[(i + 2) % 8].toDouble(), pointArr[(i + 3) % 8].toDouble())
Imgproc.line(tmp, start, end, Scalar(255.0, 0.0, 0.0), 8, Imgproc.LINE_8)
}
mBinding.ivResult.showMat(tmp)
tmp.release()
}
}
private fun doDecode() {
val points = Mat()
val isHasQr = mQRCodeDetector.detect(mGray, points)
if (isHasQr) {
val result = mQRCodeDetector.decode(mGray, points)
if (result.isEmpty()) {
Toast.makeText(applicationContext, "無(wú)法解碼", Toast.LENGTH_SHORT).show()
} else {
Snackbar.make(mBinding.root, "解碼結(jié)果:$result", 3000).show()
}
Log.d(App.TAG, result)
} else {
Toast.makeText(applicationContext, "未檢測(cè)到QRCode", Toast.LENGTH_SHORT).show()
}
}
private fun selectMedia() {
if (this::mPhotoSheet.isInitialized) {
mPhotoSheet.show()
}
}
private fun selectOps() {
if (this::mOperationSheet.isInitialized) {
mOperationSheet.show()
}
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu_qr_detect, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_pick_photo -> selectMedia()
R.id.menu_qr_ops -> selectOps()
}
return true
}
override fun onDestroy() {
mSource.release()
mGray.release()
super.onDestroy()
}
}
結(jié)果

源碼
以上就是Android基于OpenCV實(shí)現(xiàn)QR二維碼檢測(cè)的詳細(xì)內(nèi)容,更多關(guān)于Android OpenCV實(shí)現(xiàn)QR二維碼檢測(cè)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android開(kāi)發(fā)X Y軸Board的繪制教程示例
這篇文章主要為大家介紹了Android開(kāi)發(fā)X Y軸Board的繪制教程示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12
Android學(xué)習(xí)筆記之應(yīng)用單元測(cè)試實(shí)例分析
這篇文章主要介紹了Android學(xué)習(xí)筆記之應(yīng)用單元測(cè)試,結(jié)合實(shí)例形式較為詳細(xì)的分析了Android單元測(cè)試的實(shí)現(xiàn)原理與具體步驟,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-11-11
android的消息處理機(jī)制(圖文+源碼分析)—Looper/Handler/Message
這篇文章寫(xiě)的非常好,深入淺出;android的消息處理機(jī)制(圖+源碼分析)—Looper,Handler,Message是一位大三學(xué)生自己剖析的心得,感興趣的朋友可以了解下哦,希望對(duì)你有所幫助2013-01-01
Android網(wǎng)絡(luò)技術(shù)HttpURLConnection詳解
這篇文章主要為大家詳細(xì)介紹了Android網(wǎng)絡(luò)技術(shù)HttpURLConnection的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-07-07
Android自定義控件實(shí)現(xiàn)帶數(shù)值和動(dòng)畫(huà)的圓形進(jìn)度條
這篇文章主要為大家詳細(xì)介紹了Android自定義控件實(shí)現(xiàn)帶數(shù)值和動(dòng)畫(huà)的圓形進(jìn)度條,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-12-12
Android實(shí)戰(zhàn)教程第六篇之一鍵鎖屏應(yīng)用問(wèn)題解決
這篇文章主要為大家詳細(xì)介紹了Android一鍵鎖屏應(yīng)用開(kāi)發(fā)過(guò)程中出現(xiàn)問(wèn)題的解決方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-11-11
Android自定義控件實(shí)現(xiàn)球賽比分條效果
這篇文章主要為大家詳細(xì)介紹了Android自定義控件實(shí)現(xiàn)球賽比分條效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-12-12

