gozxing库-对图片中多个二维码进行识别的完整示例教程

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库进行二维码识别的完整流程,包括:

  1. 图片加载:使用Go标准库加载各种格式的图像文件
  2. 图片剪裁:根据需求裁剪图像的特定区域以提高识别效率
  3. 图片处理:通过对比度增强和二值化等技术优化图像质量
  4. 图片分块与多参数解码:通过分块处理和多种解码参数提高识别成功率
  5. 保存已解码的图片块:将成功识别的图像块保存为JPEG文件
  6. 遮盖已解码区域:在原图上标记并遮盖已识别的区域,避免重复识别

这些技术的组合使用能够大大提高二维码识别的准确性和鲁棒性,特别是在处理复杂或低质量图像时。

相关推荐
召摇6 小时前
在浏览器中无缝运行Go工具:WebAssembly实战指南
后端·面试·go
王中阳Go1 天前
我发现不管是Java还是Golang,懂AI之后,是真吃香!
后端·go·ai编程
半枫荷1 天前
二、Go语法基础(基本语法)
go
struggle20252 天前
AxonHub 开源程序是一个现代 AI 网关系统,提供统一的 OpenAI、Anthropic 和 AI SDK 兼容 API
css·人工智能·typescript·go·shell·powershell
Mgx2 天前
高性能 Go 语言带 TTL 的内存缓存实现:精确过期、自动刷新、并发安全
go
考虑考虑2 天前
go格式化时间
后端·go
光头闪亮亮3 天前
ZBar 条码/二维码识别工具介绍及golang通过cmd调用ZBar从图片中批量识别二维码
go
东风t西瓜3 天前
golang项目开发环境配置
go
zhuyasen4 天前
在某些 Windows 版本,Go 1.25.x 编译出来的 exe 运行报错:此应用无法在你的电脑上运行
windows·go·编译器