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. 遮盖已解码区域:在原图上标记并遮盖已识别的区域,避免重复识别

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

相关推荐
梦想很大很大20 小时前
使用 Go + Gin + Fx 构建工程化后端服务模板(gin-app 实践)
前端·后端·go
lekami_兰1 天前
MySQL 长事务:藏在业务里的性能 “隐形杀手”
数据库·mysql·go·长事务
却尘1 天前
一篇小白也能看懂的 Go 字符串拼接 & Builder & cap 全家桶
后端·go
ん贤1 天前
一次批量删除引发的死锁,最终我选择不加锁
数据库·安全·go·死锁
mtngt112 天前
AI DDD重构实践
go
Grassto3 天前
12 go.sum 是如何保证依赖安全的?校验机制源码解析
安全·golang·go·哈希算法·go module
Grassto5 天前
11 Go Module 缓存机制详解
开发语言·缓存·golang·go·go module
程序设计实验室6 天前
2025年的最后一天,分享我使用go语言开发的电子书转换工具网站
go
我的golang之路果然有问题6 天前
使用 Hugo + GitHub Pages + PaperMod 主题 + Obsidian 搭建开发博客
golang·go·github·博客·个人开发·个人博客·hugo
啊汉8 天前
古文观芷App搜索方案深度解析:打造极致性能的古文搜索引擎
go·软件随想