gozxing库-对图片中多个二维码进行识别的完整示例教程
本文档基于项目中使用的gozxing库,详细介绍如何使用该库对图片中多个二维码进行识别的完整流程,包括图片加载、剪裁、处理、分块解码、保存已解码图片块、遮盖已解码区域等步骤。
完整流程图
graph TD
A[开始] --> B[图片加载]
B --> C[图片剪裁]
C --> D[图片处理]
D --> E[图片分块]
E --> F[多种参数解码]
F --> G{解码成功?}
G -->|是| H[保存解码图片块]
G -->|否| I[尝试其他参数]
I --> F
H --> J[遮盖已解码区域]
J --> K[更新图像继续处理]
K --> L[是否还有未处理区域?]
L -->|是| E
L -->|否| M[保存最终图像]
M --> N[输出识别结果]
N --> O[结束]
graph TD
subgraph 图片处理
D1[对比度增强]
D2[图像二值化]
D3[其他预处理]
end
subgraph 分块解码
E1[确定分块大小]
E2[遍历图像区块]
E3[应用不同参数]
end
subgraph 解码参数
F1[标准参数]
F2[纯条码模式]
F3[不同字符集]
F4[高精度模式]
end
详细步骤说明
1. 图片加载
从文件路径加载图片是二维码识别的第一步。以下是一个完整的图片加载示例:
go
// 导入必要的包
import (
"image" // Go标准库中的图像处理包
"image/jpeg" // JPEG格式支持
"image/png" // PNG格式支持
"os" // 文件操作支持
_ "image/gif" // GIF格式支持(匿名导入)
_ "image/jpeg" // JPEG格式支持(匿名导入)
_ "image/png" // PNG格式支持(匿名导入)
)
// ReadMultipleQRCodes 从图片中识别多个二维码的主函数
func ReadMultipleQRCodes(inputImagePath, outputImagePath string) ([]string, error) {
// 打开输入图片文件,通过os.Open函数打开指定路径的文件
inputFile, err := os.Open(inputImagePath)
if err != nil {
// 如果打开文件出错,返回错误信息
return nil, fmt.Errorf("无法打开输入图片: %v", err)
}
// 函数退出时自动关闭文件,避免资源泄露
defer inputFile.Close()
// 解码图片,image.Decode函数会自动识别图片格式并解码
img, format, err := image.Decode(inputFile)
if err != nil {
// 如果解码失败,返回错误信息
return nil, fmt.Errorf("无法解码图片 (%s): %v", format, err)
}
// ... 后续处理步骤
}
// loadPicture 从指定路径加载图片的简化版本
func loadPicture(path string) (image.Image, error) {
// 打开指定路径的文件,返回文件对象和可能的错误
file, err := os.Open(path)
if err != nil {
// 如果打开文件出错,直接返回空图像和错误信息
return nil, err
}
// 函数退出时自动关闭文件,避免资源泄露
defer file.Close()
// 解码图片文件,返回图像对象、格式信息和可能的错误
img, _, err := image.Decode(file)
if err != nil {
// 如果解码失败,返回空图像和错误信息
return nil, err
}
// 返回成功加载的图像对象和空错误
return img, nil
}
2. 图片剪裁
为了提高识别效率和准确性,通常需要对图像进行剪裁,只保留感兴趣的区域:
go
// 导入绘图相关的包
import (
"image" // Go标准库中的图像基础包
"image/draw" // 图像绘制操作包
)
// cropImageToRightHalf 将图片裁剪为右侧一半
func cropImageToRightHalf(img image.Image) image.Image {
// 获取原始图像的边界信息(包含宽度和高度)
bounds := img.Bounds()
// 计算图像宽度(右边界x坐标减去左边界x坐标)
width := bounds.Dx()
// 计算图像高度(右边界y坐标减去左边界y坐标)
height := bounds.Dy()
// 创建一个新的RGBA图像,宽度为原图的一半,高度不变,起始坐标为(0,0)
croppedImg := image.NewRGBA(image.Rect(0, 0, width/2, height))
// 将原图的右半部分绘制到新创建的图像上
// 参数说明:
// 1. croppedImg:目标图像
// 2. croppedImg.Bounds():目标图像的绘制区域(整个图像)
// 3. img:源图像
// 4. image.Point{width / 2, 0}:源图像的起始点(从宽度的一半处开始,即右半部分)
// 5. draw.Src:绘制模式(Src表示直接替换)
draw.Draw(croppedImg, croppedImg.Bounds(), img, image.Point{width / 2, 0}, draw.Src)
// 返回裁剪后的图像
return croppedImg
}
// cropMiddleThirdByWidth对图片根据宽度进行平分为左中右三个部分,并只取中间部分
func cropMiddleThirdByWidth(img image.Image, i int) image.Image {
// 获取原图的边界,包含图像的尺寸信息
srcBounds := img.Bounds()
// 获取图像宽度(右边界x坐标减去左边界x坐标)
width := srcBounds.Dx()
// 获取图像高度(右边界y坐标减去左边界y坐标)
height := srcBounds.Dy()
// 创建一个新的图像作为裁剪结果,宽度为原图的三分之一,高度相同
dstBounds := image.Rect(0, 0, width/3, height)
// 创建一个新的RGBA图像对象
dstImg := image.NewRGBA(dstBounds)
// 计算中间部分的起始位置,向左偏移100像素
startX := width/3 - 100
// 将原图中指定区域复制到新图像
// 注意这里的坐标映射:新图像的(0,0)对应原图的(startX, srcBounds.Min.Y)
draw.Draw(dstImg, dstBounds, img, image.Point{startX, srcBounds.Min.Y}, draw.Src)
// 返回裁剪后的图像
return dstImg
}
// cropImageToThird 将图片平分为上中下三部分,每部分的高度要相等,宽度与原图相同
func cropImageToThird(img image.Image, partIndex int) image.Image {
// 获取原图的边界信息
srcBounds := img.Bounds()
// 获取图像高度
height := srcBounds.Dy()
// 计算每部分的高度(总高度除以3)
thirdHeight := height / 3
// 声明要裁剪的矩形区域变量
var srcRect image.Rectangle
// 根据索引选择不同的区域
switch partIndex {
case 0:
// 上部分:从顶部开始,高度为三分之一
srcRect = image.Rect(
srcBounds.Min.X, // 左边界x坐标
srcBounds.Min.Y, // 左边界y坐标(顶部)
srcBounds.Max.X, // 右边界x坐标
srcBounds.Min.Y+thirdHeight, // 右边界y坐标(顶部+三分之一高度)
)
case 1:
// 中间部分:从三分之一处开始,到三分之二处结束
srcRect = image.Rect(
srcBounds.Min.X, // 左边界x坐标
srcBounds.Min.Y+thirdHeight, // 左边界y坐标(三分之一处)
srcBounds.Max.X, // 右边界x坐标
srcBounds.Min.Y+2*thirdHeight, // 右边界y坐标(三分之二处)
)
case 2:
// 下部分:从三分之二处开始,到底部结束
srcRect = image.Rect(
srcBounds.Min.X, // 左边界x坐标
srcBounds.Min.Y+2*thirdHeight, // 左边界y坐标(三分之二处)
srcBounds.Max.X, // 右边界x坐标
srcBounds.Max.Y, // 右边界y坐标(底部)
)
default:
// 如果索引超出范围,触发panic
panic("invalid index (must be 0, 1, or 2)")
}
// 创建目标图像的边界(从(0,0)开始,宽高与裁剪区域一致)
dstBounds := image.Rect(0, 0, srcRect.Dx(), srcRect.Dy())
// 创建新的RGBA图像作为裁剪结果
dstImg := image.NewRGBA(dstBounds)
// 将原图中指定区域的内容绘制到新图像上
draw.Draw(dstImg, dstBounds, img, srcRect.Min, draw.Src)
// 返回裁剪后的图像
return dstImg
}
3. 图片处理
为了提高二维码识别的成功率,常常需要对图像进行预处理,例如增强对比度或进行二值化:
go
// 导入数学运算包
import (
"image" // 图像处理基础包
"image/color" // 颜色处理包
"math" // 数学运算包
)
// enhanceContrast 增强图像对比度(直方图均衡化)
func enhanceContrast(img image.Image) image.Image {
// 获取图像边界信息,确定处理范围
bounds := img.Bounds()
// 创建一个新的RGBA图像,用于存储增强后的结果
enhanced := image.NewRGBA(bounds)
// 声明256个桶的直方图数组,用于统计各灰度级别的像素数量
var histogram [256]int
// 遍历图像的所有像素
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
// 获取当前像素的RGBA值(每个值都是16位)
r, g, b, _ := img.At(x, y).RGBA()
// 根据权重计算灰度值(人眼对绿光敏感度最高,红光次之,蓝光最低)
gray := uint8((r*299 + g*587 + b*114) / 1000 >> 8)
// 对应灰度级别计数加一
histogram[gray]++
}
}
// 计算累积分布函数(CDF)
var cdf [256]int
// 第一个元素的CDF就是其自身的直方图值
cdf[0] = histogram[0]
// 计算后续每个灰度级别的CDF值
for i := 1; i < 256; i++ {
cdf[i] = cdf[i-1] + histogram[i]
}
// 查找最小的非零CDF值
cdfMin := 0
for i := 0; i < 256; i++ {
if cdf[i] > 0 {
cdfMin = cdf[i]
break
}
}
// 最大的CDF值就是最后一个元素
cdfMax := cdf[255]
// 创建查找表(LUT),用于映射旧灰度值到新灰度值
var lut [256]uint8
// 遍历所有灰度级别
for i := 0; i < 256; i++ {
if cdfMax == cdfMin {
// 特殊情况处理:如果最大值等于最小值,则所有值都映射为0
lut[i] = 0
} else {
// 使用直方图均衡化公式计算新的灰度值
lut[i] = uint8(math.Round(float64(cdf[i]-cdfMin) / float64(cdfMax-cdfMin) * 255))
}
}
// 应用查找表进行直方图均衡化
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
// 获取当前像素的RGBA值
r, g, b, a := img.At(x, y).RGBA()
// 计算灰度值
gray := uint8((r*299 + g*587 + b*114) / 1000 >> 8)
// 通过查找表获取新的灰度值
newGray := lut[gray]
// 设置增强后图像对应位置的像素值
enhanced.SetRGBA(x, y, color.RGBA{newGray, newGray, newGray, uint8(a >> 8)})
}
}
// 返回对比度增强后的图像
return enhanced
}
// binarizeImage 将图像二值化
func binarizeImage(img image.Image) image.Image {
// 获取图像边界信息
bounds := img.Bounds()
// 创建一个新的RGBA图像用于存储二值化结果
binarized := image.NewRGBA(bounds)
// 计算全局阈值(简单平均法)
var totalGray uint64 // 所有像素灰度值总和
var pixelCount uint64 // 像素总数
// 遍历图像的所有像素
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
// 获取当前像素的RGB值
r, g, b, _ := img.At(x, y).RGBA()
// 计算灰度值
gray := uint8((r*299 + g*587 + b*114) / 1000 >> 8)
// 累加灰度值和像素数
totalGray += uint64(gray)
pixelCount++
}
}
// 计算平均灰度值作为阈值
threshold := uint8(totalGray / pixelCount)
// 应用阈值进行二值化处理
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
// 获取当前像素的RGBA值
r, g, b, a := img.At(x, y).RGBA()
// 计算灰度值
gray := uint8((r*299 + g*587 + b*114) / 1000 >> 8)
// 声明新的灰度值变量
var newGray uint8
// 根据阈值判断新像素值:大于阈值为白(255),小于等于阈值为黑(0)
if gray > threshold {
newGray = 255
} else {
newGray = 0
}
// 设置二值化图像对应位置的像素值
binarized.SetRGBA(x, y, color.RGBA{newGray, newGray, newGray, uint8(a >> 8)})
}
}
// 返回二值化后的图像
return binarized
}
4. 图片分块与多种参数解码
对于复杂的图像,可以通过分块处理和多种解码参数尝试来提高识别成功率:
go
// 导入gozxing库相关包
import (
"github.com/makiuchi-d/gozxing" // gozxing核心包
"github.com/makiuchi-d/gozxing/qrcode" // 二维码专用包
"log" // 日志包
)
// detectQRCodesWithUltraFineGrainedBlocks 使用超精细分块识别二维码
func detectQRCodesWithUltraFineGrainedBlocks(img image.Image, markedImg *image.RGBA, outputImagePath string) []string {
// 声明用于存储识别结果的字符串切片
var results []string
// 获取图像边界信息
bounds := img.Bounds()
// 获取图像宽度
width := bounds.Dx()
// 获取图像高度
height := bounds.Dy()
// 定义多种区块大小进行尝试(从小到大)
blockSizes := []int{413}
// 遍历每种区块大小
for _, blockSize := range blockSizes {
// 如果区块大小超过图像尺寸,则跳过
if blockSize > width || blockSize > height {
continue
}
// 设置步进值(约为区块大小,即约50%重叠率)
step := blockSize
// 遍历整个图像,按设定的步进值移动区块
for y := 0; y <= height-blockSize; y += step {
for x := 0; x <= width-blockSize; x += step {
// 定义子图像边界(当前区块)
subBounds := image.Rect(x, y, x+blockSize, y+2*blockSize)
// 创建新的RGBA图像用于存储子图像
subImg := image.NewRGBA(subBounds)
// 将原图中对应区域的内容绘制到子图像上
draw.Draw(subImg, subBounds, img, image.Point{x, y}, draw.Src)
// 准备多种处理后的图像进行尝试
processedImages := []struct {
img image.Image // 处理后的图像
name string // 处理方法名称
}{
// 原始图像
{subImg, "原始"},
}
// 添加对比度增强的图像
enhancedImg := enhanceContrast(subImg)
processedImages = append(processedImages, struct {
img image.Image
name string
}{enhancedImg, "对比度增强"})
// 添加二值化的图像
binarizedImg := binarizeImage(subImg)
processedImages = append(processedImages, struct {
img image.Image
name string
}{binarizedImg, "二值化"})
// 对每种处理后的图像尝试识别
for _, processedImg := range processedImages {
// 使用多种解码参数尝试识别
texts := tryMultipleDecodeParams(processedImg.img)
// 遍历识别结果
for _, text := range texts {
// 检查结果是否已经在最终结果中
if !contains(results, text) {
// 如果不在,则添加到结果中
results = append(results, text)
// 打印识别结果和位置信息
fmt.Printf("在区块(%d,%d)使用%s处理识别到二维码: %s\n", x, y, processedImg.name, text)
// 在原始标记图像上标记检测到的区域
// 由于我们是在子图像中识别的,需要调整坐标到原始图像
if len(text) > 0 {
// 标记检测区域
markDetectedArea(markedImg, x, y+120, x+2*blockSize-160)
}
}
}
}
}
}
}
// 返回所有识别到的二维码内容
return results
}
// tryMultipleDecodeParams 尝试多种解码参数来识别二维码
func tryMultipleDecodeParams(img image.Image) []string {
// 声明用于存储识别结果的字符串切片
var results []string
// 从图像创建BinaryBitmap,这是gozxing解码所需的格式
bmp, err := gozxing.NewBinaryBitmapFromImage(img)
if err != nil {
// 如果创建失败,记录日志并返回空结果
log.Printf("无法创建BinaryBitmap: %v", err)
return results
}
// 定义多种参数组合以提高识别成功率
paramSets := []struct {
name string // 参数集名称(用于日志输出)
hints map[gozxing.DecodeHintType]interface{} // 解码提示参数映射
}{
{
// 标准参数配置
"标准参数",
map[gozxing.DecodeHintType]interface{}{
// 指定可能的条码格式为二维码
gozxing.DecodeHintType_POSSIBLE_FORMATS: []gozxing.BarcodeFormat{
gozxing.BarcodeFormat_QR_CODE,
},
// 启用更努力的扫描模式
gozxing.DecodeHintType_TRY_HARDER: true,
// 指定字符集为UTF-8
gozxing.DecodeHintType_CHARACTER_SET: "UTF-8",
},
},
{
// 纯条码模式参数配置
"纯条码模式",
map[gozxing.DecodeHintType]interface{}{
// 指定可能的条码格式为二维码
gozxing.DecodeHintType_POSSIBLE_FORMATS: []gozxing.BarcodeFormat{
gozxing.BarcodeFormat_QR_CODE,
},
// 启用纯条码模式(适用于高质量图像)
gozxing.DecodeHintType_PURE_BARCODE: true,
// 指定字符集为UTF-8
gozxing.DecodeHintType_CHARACTER_SET: "UTF-8",
},
},
{
// GBK字符集参数配置
"GBK字符集",
map[gozxing.DecodeHintType]interface{}{
// 指定可能的条码格式为二维码
gozxing.DecodeHintType_POSSIBLE_FORMATS: []gozxing.BarcodeFormat{
gozxing.BarcodeFormat_QR_CODE,
},
// 启用更努力的扫描模式
gozxing.DecodeHintType_TRY_HARDER: true,
// 指定字符集为GBK(适用于中文二维码)
gozxing.DecodeHintType_CHARACTER_SET: "GBK",
},
},
{
// 简化参数配置
"简化参数",
map[gozxing.DecodeHintType]interface{}{
// 只指定可能的条码格式为二维码
gozxing.DecodeHintType_POSSIBLE_FORMATS: []gozxing.BarcodeFormat{
gozxing.BarcodeFormat_QR_CODE,
},
},
},
{
// 高精度模式参数配置
"高精度模式",
map[gozxing.DecodeHintType]interface{}{
// 指定可能的条码格式为二维码
gozxing.DecodeHintType_POSSIBLE_FORMATS: []gozxing.BarcodeFormat{
gozxing.BarcodeFormat_QR_CODE,
},
// 同时启用TRY_HARDER和PURE_BARCODE以获得最高识别精度
gozxing.DecodeHintType_TRY_HARDER: true,
gozxing.DecodeHintType_PURE_BARCODE: true,
// 指定字符集为UTF-8
gozxing.DecodeHintType_CHARACTER_SET: "UTF-8",
},
},
}
// 创建二维码读取器实例
qrReader := qrcode.NewQRCodeReader()
// 尝试每种参数组合进行解码
for _, paramSet := range paramSets {
// 调用读取器的Decode方法进行解码
result, err := qrReader.Decode(bmp, paramSet.hints)
if err == nil {
// 如果解码成功,获取文本内容
text := result.GetText()
// 检查是否已存在相同的结果
if !contains(results, text) {
// 如果不存在,则添加到结果列表中
results = append(results, text)
}
}
}
// 返回识别到的所有二维码内容
return results
}
5. 已解码图片块二维码图片创建jpg
当成功解码一个图片块后,将其保存为JPEG文件以供后续分析或验证:
go
// 导入JPEG编码包
import (
"image/jpeg" // JPEG编码支持
)
// 在detectQRCodesWithUltraFineGrainedBlocks函数中保存已解码的图片块
// 这段代码在成功识别到二维码时执行
// 将subBounds保存为jpg图片
subImgFile, err := os.Create(fmt.Sprintf("%sGetQrCode_%d_%d_%d.jpg", outputImagePath, x, y, blockSize))
if err == nil {
// 使用JPEG编码器将图像保存为JPEG文件,质量设置为90
jpeg.Encode(subImgFile, subImg, &jpeg.Options{Quality: 90})
// 关闭文件
subImgFile.Close()
}
6. 图片中的已解码遮盖区域
识别并保存二维码块后,需要在原图上遮盖已识别的区域,避免重复识别:
go
// markDetectedArea 在原始图像上标记检测到的区域
func markDetectedArea(img *image.RGBA, x, y, size int) {
// 构造一个稍微大一点的矩形,确保完全覆盖二维码
rect := image.Rect(
x, // 左上角x坐标
y, // 左上角y坐标
x+size, // 右下角x坐标
y+size, // 右下角y坐标
)
// 确保矩形在图像边界内
imgBounds := img.Bounds()
rect = rect.Intersect(imgBounds)
// 将矩形区域涂成黑色以遮盖二维码
if rect.Empty() {
fmt.Println("矩形区域为空,无法进行遮盖")
return
}
// 使用draw.Draw将黑色区域绘制到图像上,覆盖二维码
draw.Draw(img, rect, image.NewUniform(color.Black), image.Point{}, draw.Src)
// 使用蓝色边框标记检测区域
blue := color.RGBA{255, 0, 0, 255}
for x := rect.Min.X; x < rect.Max.X; x++ {
img.SetRGBA(x, rect.Min.Y, blue)
img.SetRGBA(x, rect.Max.Y-1, blue)
}
for y := rect.Min.Y; y < rect.Max.Y; y++ {
img.SetRGBA(rect.Min.X, y, blue)
img.SetRGBA(rect.Max.X-1, y, blue)
}
}
7. main.go测试代码
go
// main.go中的测试代码
func main() {
// 记录程序开始时间
startTime := time.Now()
// 检查命令行参数
if len(os.Args) < 2 {
fmt.Println("使用方法: qrreader <图片路径> [输出路径]")
fmt.Println("示例: qrreader example.png marked_output.jpg")
os.Exit(1)
}
// 获取输入图片路径
inputImagePath := os.Args[1]
// 获取输出图片路径(可选)
outputImagePath := ""
if len(os.Args) >= 3 {
outputImagePath = os.Args[2]
}
// 识别二维码
results, err := ReadMultipleQRCodes(inputImagePath, outputImagePath)
if err != nil {
log.Fatal("识别失败:", err)
}
// 打印识别结果
fmt.Println("\n=== 所有识别到的二维码内容 ===")
for i, res := range results {
fmt.Printf("%d: %s\n", i+1, res)
}
// 计算并输出程序执行时间
duration := time.Since(startTime)
fmt.Printf("程序执行时间: %v\n", duration)
}
总结
本教程展示了使用gozxing库进行二维码识别的完整流程,包括:
- 图片加载:使用Go标准库加载各种格式的图像文件
- 图片剪裁:根据需求裁剪图像的特定区域以提高识别效率
- 图片处理:通过对比度增强和二值化等技术优化图像质量
- 图片分块与多参数解码:通过分块处理和多种解码参数提高识别成功率
- 保存已解码的图片块:将成功识别的图像块保存为JPEG文件
- 遮盖已解码区域:在原图上标记并遮盖已识别的区域,避免重复识别
这些技术的组合使用能够大大提高二维码识别的准确性和鲁棒性,特别是在处理复杂或低质量图像时。