// Copyright © 2023 OpenIM. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package msg import ( "context" "fmt" "image" "image/color" _ "image/jpeg" _ "image/png" "math" "os" "sync" "github.com/makiuchi-d/gozxing" "github.com/makiuchi-d/gozxing/qrcode" "github.com/openimsdk/tools/log" ) // openImageFile 打开图片文件 func openImageFile(imagePath string) (*os.File, error) { file, err := os.Open(imagePath) if err != nil { return nil, fmt.Errorf("无法打开文件: %v", err) } return file, nil } // QRDecoder 二维码解码器接口 type QRDecoder interface { Name() string Decode(ctx context.Context, imagePath string, logPrefix string) (bool, error) // 返回是否检测到二维码 } // ============================================================================ // QuircDecoder - Quirc解码器包装 // ============================================================================ // QuircDecoder 使用 Quirc 库的解码器 type QuircDecoder struct { detectFunc func([]uint8, int, int) (bool, error) } // NewQuircDecoder 创建Quirc解码器 func NewQuircDecoder(detectFunc func([]uint8, int, int) (bool, error)) *QuircDecoder { return &QuircDecoder{detectFunc: detectFunc} } func (d *QuircDecoder) Name() string { return "quirc" } func (d *QuircDecoder) Decode(ctx context.Context, imagePath string, logPrefix string) (bool, error) { if d.detectFunc == nil { return false, fmt.Errorf("quirc 解码器未启用") } // 打开并解码图片 file, err := openImageFile(imagePath) if err != nil { log.ZError(ctx, "打开图片文件失败", err, "imagePath", imagePath) return false, err } defer file.Close() img, _, err := image.Decode(file) if err != nil { log.ZError(ctx, "解码图片失败", err, "imagePath", imagePath) return false, fmt.Errorf("无法解码图片: %v", err) } bounds := img.Bounds() width := bounds.Dx() height := bounds.Dy() // 转换为灰度图 grayData := convertToGrayscale(img, width, height) // 调用Quirc检测 hasQRCode, err := d.detectFunc(grayData, width, height) if err != nil { log.ZError(ctx, "Quirc检测失败", err, "width", width, "height", height) return false, err } return hasQRCode, nil } // ============================================================================ // CustomQRDecoder - 自定义解码器(兼容圆形角) // ============================================================================ // CustomQRDecoder 自定义二维码解码器,兼容圆形角等特殊格式 type CustomQRDecoder struct{} func (d *CustomQRDecoder) Name() string { return "custom (圆形角兼容)" } // Decode 解码二维码,返回是否检测到二维码 func (d *CustomQRDecoder) Decode(ctx context.Context, imagePath string, logPrefix string) (bool, error) { file, err := openImageFile(imagePath) if err != nil { log.ZError(ctx, "打开图片文件失败", err, "imagePath", imagePath) return false, fmt.Errorf("无法打开文件: %v", err) } defer file.Close() img, _, err := image.Decode(file) if err != nil { log.ZError(ctx, "解码图片失败", err, "imagePath", imagePath) return false, fmt.Errorf("无法解码图片: %v", err) } bounds := img.Bounds() width := bounds.Dx() height := bounds.Dy() reader := qrcode.NewQRCodeReader() hints := make(map[gozxing.DecodeHintType]interface{}) hints[gozxing.DecodeHintType_TRY_HARDER] = true hints[gozxing.DecodeHintType_PURE_BARCODE] = false hints[gozxing.DecodeHintType_CHARACTER_SET] = "UTF-8" // 尝试直接解码 bitmap, err := gozxing.NewBinaryBitmapFromImage(img) if err == nil { if _, err := reader.Decode(bitmap, hints); err == nil { return true, nil } // 尝试不使用PURE_BARCODE delete(hints, gozxing.DecodeHintType_PURE_BARCODE) if _, err := reader.Decode(bitmap, hints); err == nil { return true, nil } hints[gozxing.DecodeHintType_PURE_BARCODE] = false } // 尝试多尺度缩放 scales := []float64{1.0, 1.5, 2.0, 0.75, 0.5} for _, scale := range scales { scaledImg := scaleImage(img, width, height, scale) if scaledImg == nil { continue } scaledBitmap, err := gozxing.NewBinaryBitmapFromImage(scaledImg) if err == nil { if _, err := reader.Decode(scaledBitmap, hints); err == nil { return true, nil } } } // 转换为灰度图进行预处理 grayData := convertToGrayscale(img, width, height) // 尝试多种预处理方法 preprocessMethods := []struct { name string fn func([]byte, int, int) []byte }{ {"Otsu二值化", enhanceImageOtsu}, {"标准增强", enhanceImage}, {"强对比度", enhanceImageStrong}, {"圆形角处理", enhanceImageForRoundedCorners}, {"去噪+锐化", enhanceImageDenoiseSharpen}, {"高斯模糊+锐化", enhanceImageGaussianSharpen}, } scalesForPreprocessed := []float64{1.0, 2.0, 1.5, 1.2, 0.8} for _, method := range preprocessMethods { processed := method.fn(grayData, width, height) // 快速检测定位图案 corners := detectCornersFast(processed, width, height) if len(corners) < 2 { // 如果没有检测到足够的定位图案,仍然尝试解码 } processedImg := createImageFromGrayscale(processed, width, height) bitmap2, err := gozxing.NewBinaryBitmapFromImage(processedImg) if err == nil { if _, err := reader.Decode(bitmap2, hints); err == nil { return true, nil } delete(hints, gozxing.DecodeHintType_PURE_BARCODE) if _, err := reader.Decode(bitmap2, hints); err == nil { return true, nil } hints[gozxing.DecodeHintType_PURE_BARCODE] = false } // 对预处理后的图像进行多尺度缩放 for _, scale := range scalesForPreprocessed { scaledProcessed := scaleGrayscaleImage(processed, width, height, scale) if scaledProcessed == nil { continue } scaledImg := createImageFromGrayscale(scaledProcessed.data, scaledProcessed.width, scaledProcessed.height) scaledBitmap, err := gozxing.NewBinaryBitmapFromImage(scaledImg) if err == nil { if _, err := reader.Decode(scaledBitmap, hints); err == nil { return true, nil } delete(hints, gozxing.DecodeHintType_PURE_BARCODE) if _, err := reader.Decode(scaledBitmap, hints); err == nil { return true, nil } hints[gozxing.DecodeHintType_PURE_BARCODE] = false } } } return false, nil } // ============================================================================ // ParallelQRDecoder - 并行解码器 // ============================================================================ // ParallelQRDecoder 并行解码器,同时运行 quirc 和 custom 解码器 type ParallelQRDecoder struct { quircDecoder QRDecoder customDecoder QRDecoder } // NewParallelQRDecoder 创建并行解码器 func NewParallelQRDecoder(quircDecoder, customDecoder QRDecoder) *ParallelQRDecoder { return &ParallelQRDecoder{ quircDecoder: quircDecoder, customDecoder: customDecoder, } } func (d *ParallelQRDecoder) Name() string { return "parallel (quirc + custom)" } // Decode 并行解码:同时运行 quirc 和 custom,任一成功立即返回 func (d *ParallelQRDecoder) Decode(ctx context.Context, imagePath string, logPrefix string) (bool, error) { type decodeResult struct { hasQRCode bool err error name string } resultChan := make(chan decodeResult, 2) var wg sync.WaitGroup var mu sync.Mutex var quircErr error var customErr error // 启动Quirc解码 if d.quircDecoder != nil { wg.Add(1) go func() { defer wg.Done() hasQRCode, err := d.quircDecoder.Decode(ctx, imagePath, logPrefix) mu.Lock() if err != nil { quircErr = err } mu.Unlock() resultChan <- decodeResult{ hasQRCode: hasQRCode, err: err, name: d.quircDecoder.Name(), } }() } // 启动Custom解码 if d.customDecoder != nil { wg.Add(1) go func() { defer wg.Done() hasQRCode, err := d.customDecoder.Decode(ctx, imagePath, logPrefix) mu.Lock() if err != nil { customErr = err } mu.Unlock() resultChan <- decodeResult{ hasQRCode: hasQRCode, err: err, name: d.customDecoder.Name(), } }() } // 等待第一个结果 var firstResult decodeResult var secondResult decodeResult firstResult = <-resultChan if firstResult.hasQRCode { // 如果检测到二维码,立即返回 go func() { <-resultChan wg.Wait() }() return true, nil } // 等待第二个结果 if d.quircDecoder != nil && d.customDecoder != nil { secondResult = <-resultChan if secondResult.hasQRCode { wg.Wait() return true, nil } } wg.Wait() // 如果都失败,返回错误 if firstResult.err != nil && secondResult.err != nil { log.ZError(ctx, "并行解码失败,两个解码器都失败", fmt.Errorf("quirc错误=%v, custom错误=%v", quircErr, customErr), "quircError", quircErr, "customError", customErr) return false, fmt.Errorf("quirc 和 custom 都解码失败: quirc错误=%v, custom错误=%v", quircErr, customErr) } return false, nil } // ============================================================================ // 辅助函数 // ============================================================================ // convertToGrayscale 转换为灰度图 func convertToGrayscale(img image.Image, width, height int) []byte { grayData := make([]byte, width*height) bounds := img.Bounds() if ycbcr, ok := img.(*image.YCbCr); ok { for y := 0; y < height; y++ { for x := 0; x < width; x++ { yi := ycbcr.YOffset(x+bounds.Min.X, y+bounds.Min.Y) grayData[y*width+x] = ycbcr.Y[yi] } } return grayData } for y := 0; y < height; y++ { for x := 0; x < width; x++ { r, g, b, _ := img.At(x+bounds.Min.X, y+bounds.Min.Y).RGBA() r8 := uint8(r >> 8) g8 := uint8(g >> 8) b8 := uint8(b >> 8) gray := byte((uint16(r8)*299 + uint16(g8)*587 + uint16(b8)*114) / 1000) grayData[y*width+x] = gray } } return grayData } // enhanceImage 图像增强(标准方法) func enhanceImage(data []byte, width, height int) []byte { enhanced := make([]byte, len(data)) copy(enhanced, data) minVal := uint8(255) maxVal := uint8(0) for _, v := range data { if v < minVal { minVal = v } if v > maxVal { maxVal = v } } if maxVal-minVal < 50 { rangeVal := maxVal - minVal if rangeVal == 0 { rangeVal = 1 } for i, v := range data { stretched := uint8((uint16(v-minVal) * 255) / uint16(rangeVal)) enhanced[i] = stretched } } return enhanced } // enhanceImageStrong 强对比度增强 func enhanceImageStrong(data []byte, width, height int) []byte { enhanced := make([]byte, len(data)) histogram := make([]int, 256) for _, v := range data { histogram[v]++ } cdf := make([]int, 256) cdf[0] = histogram[0] for i := 1; i < 256; i++ { cdf[i] = cdf[i-1] + histogram[i] } total := len(data) for i, v := range data { if total > 0 { enhanced[i] = uint8((cdf[v] * 255) / total) } } return enhanced } // enhanceImageForRoundedCorners 针对圆形角的特殊处理 func enhanceImageForRoundedCorners(data []byte, width, height int) []byte { enhanced := make([]byte, len(data)) copy(enhanced, data) minVal := uint8(255) maxVal := uint8(0) for _, v := range data { if v < minVal { minVal = v } if v > maxVal { maxVal = v } } if maxVal-minVal < 100 { rangeVal := maxVal - minVal if rangeVal == 0 { rangeVal = 1 } for i, v := range data { stretched := uint8((uint16(v-minVal) * 255) / uint16(rangeVal)) enhanced[i] = stretched } } // 形态学操作:先腐蚀后膨胀 dilated := make([]byte, len(enhanced)) kernelSize := 3 halfKernel := kernelSize / 2 // 腐蚀(最小值滤波) for y := halfKernel; y < height-halfKernel; y++ { for x := halfKernel; x < width-halfKernel; x++ { minVal := uint8(255) for ky := -halfKernel; ky <= halfKernel; ky++ { for kx := -halfKernel; kx <= halfKernel; kx++ { idx := (y+ky)*width + (x + kx) if enhanced[idx] < minVal { minVal = enhanced[idx] } } } dilated[y*width+x] = minVal } } // 膨胀(最大值滤波) for y := halfKernel; y < height-halfKernel; y++ { for x := halfKernel; x < width-halfKernel; x++ { maxVal := uint8(0) for ky := -halfKernel; ky <= halfKernel; ky++ { for kx := -halfKernel; kx <= halfKernel; kx++ { idx := (y+ky)*width + (x + kx) if dilated[idx] > maxVal { maxVal = dilated[idx] } } } enhanced[y*width+x] = maxVal } } // 边界保持原值 for y := 0; y < height; y++ { for x := 0; x < width; x++ { if y < halfKernel || y >= height-halfKernel || x < halfKernel || x >= width-halfKernel { enhanced[y*width+x] = data[y*width+x] } } } return enhanced } // enhanceImageDenoiseSharpen 去噪+锐化处理 func enhanceImageDenoiseSharpen(data []byte, width, height int) []byte { denoised := medianFilter(data, width, height, 3) sharpened := sharpenImage(denoised, width, height) return sharpened } // medianFilter 中值滤波去噪 func medianFilter(data []byte, width, height, kernelSize int) []byte { filtered := make([]byte, len(data)) halfKernel := kernelSize / 2 kernelArea := kernelSize * kernelSize values := make([]byte, kernelArea) for y := halfKernel; y < height-halfKernel; y++ { for x := halfKernel; x < width-halfKernel; x++ { idx := 0 for ky := -halfKernel; ky <= halfKernel; ky++ { for kx := -halfKernel; kx <= halfKernel; kx++ { values[idx] = data[(y+ky)*width+(x+kx)] idx++ } } filtered[y*width+x] = quickSelectMedian(values) } } // 边界保持原值 for y := 0; y < height; y++ { for x := 0; x < width; x++ { if y < halfKernel || y >= height-halfKernel || x < halfKernel || x >= width-halfKernel { filtered[y*width+x] = data[y*width+x] } } } return filtered } // quickSelectMedian 快速选择中值 func quickSelectMedian(arr []byte) byte { n := len(arr) if n <= 7 { // 小数组使用插入排序 for i := 1; i < n; i++ { key := arr[i] j := i - 1 for j >= 0 && arr[j] > key { arr[j+1] = arr[j] j-- } arr[j+1] = key } return arr[n/2] } return quickSelect(arr, 0, n-1, n/2) } // quickSelect 快速选择第k小的元素 func quickSelect(arr []byte, left, right, k int) byte { if left == right { return arr[left] } pivotIndex := partition(arr, left, right) if k == pivotIndex { return arr[k] } else if k < pivotIndex { return quickSelect(arr, left, pivotIndex-1, k) } return quickSelect(arr, pivotIndex+1, right, k) } func partition(arr []byte, left, right int) int { pivot := arr[right] i := left for j := left; j < right; j++ { if arr[j] <= pivot { arr[i], arr[j] = arr[j], arr[i] i++ } } arr[i], arr[right] = arr[right], arr[i] return i } // sharpenImage 锐化处理 func sharpenImage(data []byte, width, height int) []byte { sharpened := make([]byte, len(data)) kernel := []int{0, -1, 0, -1, 5, -1, 0, -1, 0} for y := 1; y < height-1; y++ { for x := 1; x < width-1; x++ { sum := 0 idx := 0 for ky := -1; ky <= 1; ky++ { for kx := -1; kx <= 1; kx++ { pixelIdx := (y+ky)*width + (x + kx) sum += int(data[pixelIdx]) * kernel[idx] idx++ } } if sum < 0 { sum = 0 } if sum > 255 { sum = 255 } sharpened[y*width+x] = uint8(sum) } } // 边界保持原值 for y := 0; y < height; y++ { for x := 0; x < width; x++ { if y == 0 || y == height-1 || x == 0 || x == width-1 { sharpened[y*width+x] = data[y*width+x] } } } return sharpened } // enhanceImageOtsu Otsu自适应阈值二值化 func enhanceImageOtsu(data []byte, width, height int) []byte { threshold := calculateOtsuThreshold(data) binary := make([]byte, len(data)) for i := range data { if data[i] > threshold { binary[i] = 255 } } return binary } // calculateOtsuThreshold 计算Otsu自适应阈值 func calculateOtsuThreshold(data []byte) uint8 { histogram := make([]int, 256) for _, v := range data { histogram[v]++ } total := len(data) if total == 0 { return 128 } var threshold uint8 var maxVar float64 var sum int for i := 0; i < 256; i++ { sum += i * histogram[i] } var sum1 int var wB int for i := 0; i < 256; i++ { wB += histogram[i] if wB == 0 { continue } wF := total - wB if wF == 0 { break } sum1 += i * histogram[i] mB := float64(sum1) / float64(wB) mF := float64(sum-sum1) / float64(wF) varBetween := float64(wB) * float64(wF) * (mB - mF) * (mB - mF) if varBetween > maxVar { maxVar = varBetween threshold = uint8(i) } } return threshold } // enhanceImageGaussianSharpen 高斯模糊+锐化 func enhanceImageGaussianSharpen(data []byte, width, height int) []byte { blurred := gaussianBlur(data, width, height, 1.0) sharpened := sharpenImage(blurred, width, height) enhanced := enhanceImage(sharpened, width, height) return enhanced } // gaussianBlur 高斯模糊 func gaussianBlur(data []byte, width, height int, sigma float64) []byte { blurred := make([]byte, len(data)) kernelSize := 5 halfKernel := kernelSize / 2 kernel := make([]float64, kernelSize*kernelSize) sum := 0.0 for y := -halfKernel; y <= halfKernel; y++ { for x := -halfKernel; x <= halfKernel; x++ { idx := (y+halfKernel)*kernelSize + (x + halfKernel) val := math.Exp(-(float64(x*x+y*y) / (2 * sigma * sigma))) kernel[idx] = val sum += val } } for i := range kernel { kernel[i] /= sum } for y := halfKernel; y < height-halfKernel; y++ { for x := halfKernel; x < width-halfKernel; x++ { var val float64 idx := 0 for ky := -halfKernel; ky <= halfKernel; ky++ { for kx := -halfKernel; kx <= halfKernel; kx++ { pixelIdx := (y+ky)*width + (x + kx) val += float64(data[pixelIdx]) * kernel[idx] idx++ } } blurred[y*width+x] = uint8(val) } } // 边界保持原值 for y := 0; y < height; y++ { for x := 0; x < width; x++ { if y < halfKernel || y >= height-halfKernel || x < halfKernel || x >= width-halfKernel { blurred[y*width+x] = data[y*width+x] } } } return blurred } // createImageFromGrayscale 从灰度数据创建图像 func createImageFromGrayscale(data []byte, width, height int) image.Image { img := image.NewGray(image.Rect(0, 0, width, height)) for y := 0; y < height; y++ { rowStart := y * width rowEnd := rowStart + width if rowEnd > len(data) { rowEnd = len(data) } copy(img.Pix[y*img.Stride:], data[rowStart:rowEnd]) } return img } // scaledImage 缩放后的图像数据 type scaledImage struct { data []byte width int height int } // scaleImage 缩放图像 func scaleImage(img image.Image, origWidth, origHeight int, scale float64) image.Image { if scale == 1.0 { return img } newWidth := int(float64(origWidth) * scale) newHeight := int(float64(origHeight) * scale) if newWidth < 50 || newHeight < 50 { return nil } if newWidth > 1500 || newHeight > 1500 { return nil } scaled := image.NewRGBA(image.Rect(0, 0, newWidth, newHeight)) bounds := img.Bounds() for y := 0; y < newHeight; y++ { for x := 0; x < newWidth; x++ { srcX := float64(x) / scale srcY := float64(y) / scale x1 := int(srcX) y1 := int(srcY) x2 := x1 + 1 y2 := y1 + 1 if x2 >= bounds.Dx() { x2 = bounds.Dx() - 1 } if y2 >= bounds.Dy() { y2 = bounds.Dy() - 1 } fx := srcX - float64(x1) fy := srcY - float64(y1) c11 := getPixelColor(img, bounds.Min.X+x1, bounds.Min.Y+y1) c12 := getPixelColor(img, bounds.Min.X+x1, bounds.Min.Y+y2) c21 := getPixelColor(img, bounds.Min.X+x2, bounds.Min.Y+y1) c22 := getPixelColor(img, bounds.Min.X+x2, bounds.Min.Y+y2) r := uint8(float64(c11.R)*(1-fx)*(1-fy) + float64(c21.R)*fx*(1-fy) + float64(c12.R)*(1-fx)*fy + float64(c22.R)*fx*fy) g := uint8(float64(c11.G)*(1-fx)*(1-fy) + float64(c21.G)*fx*(1-fy) + float64(c12.G)*(1-fx)*fy + float64(c22.G)*fx*fy) b := uint8(float64(c11.B)*(1-fx)*(1-fy) + float64(c21.B)*fx*(1-fy) + float64(c12.B)*(1-fx)*fy + float64(c22.B)*fx*fy) scaled.Set(x, y, color.RGBA{R: r, G: g, B: b, A: 255}) } } return scaled } // getPixelColor 获取像素颜色 func getPixelColor(img image.Image, x, y int) color.RGBA { r, g, b, _ := img.At(x, y).RGBA() return color.RGBA{ R: uint8(r >> 8), G: uint8(g >> 8), B: uint8(b >> 8), A: 255, } } // scaleGrayscaleImage 缩放灰度图像 func scaleGrayscaleImage(data []byte, origWidth, origHeight int, scale float64) *scaledImage { if scale == 1.0 { return &scaledImage{data: data, width: origWidth, height: origHeight} } newWidth := int(float64(origWidth) * scale) newHeight := int(float64(origHeight) * scale) if newWidth < 21 || newHeight < 21 || newWidth > 2000 || newHeight > 2000 { return nil } scaled := make([]byte, newWidth*newHeight) for y := 0; y < newHeight; y++ { for x := 0; x < newWidth; x++ { srcX := float64(x) / scale srcY := float64(y) / scale x1 := int(srcX) y1 := int(srcY) x2 := x1 + 1 y2 := y1 + 1 if x2 >= origWidth { x2 = origWidth - 1 } if y2 >= origHeight { y2 = origHeight - 1 } fx := srcX - float64(x1) fy := srcY - float64(y1) v11 := float64(data[y1*origWidth+x1]) v12 := float64(data[y2*origWidth+x1]) v21 := float64(data[y1*origWidth+x2]) v22 := float64(data[y2*origWidth+x2]) val := v11*(1-fx)*(1-fy) + v21*fx*(1-fy) + v12*(1-fx)*fy + v22*fx*fy scaled[y*newWidth+x] = uint8(val) } } return &scaledImage{data: scaled, width: newWidth, height: newHeight} } // Point 表示一个点 type Point struct { X, Y int } // Corner 表示检测到的定位图案角点 type Corner struct { Center Point Size int Type int } // detectCornersFast 快速检测定位图案 func detectCornersFast(data []byte, width, height int) []Corner { var corners []Corner scanStep := max(2, min(width, height)/80) if scanStep < 1 { scanStep = 1 } for y := scanStep * 3; y < height-scanStep*3; y += scanStep { for x := scanStep * 3; x < width-scanStep*3; x += scanStep { if isFinderPatternFast(data, width, height, x, y) { corners = append(corners, Corner{ Center: Point{X: x, Y: y}, Size: 20, }) if len(corners) >= 3 { return corners } } } } return corners } // isFinderPatternFast 快速检测定位图案 func isFinderPatternFast(data []byte, width, height, x, y int) bool { centerIdx := y*width + x if centerIdx < 0 || centerIdx >= len(data) { return false } if data[centerIdx] > 180 { return false } radius := min(width, height) / 15 if radius < 3 { radius = 3 } if radius > 30 { radius = 30 } directions := []struct{ dx, dy int }{{radius, 0}, {-radius, 0}, {0, radius}, {0, -radius}} blackCount := 0 whiteCount := 0 for _, dir := range directions { nx := x + dir.dx ny := y + dir.dy if nx >= 0 && nx < width && ny >= 0 && ny < height { idx := ny*width + nx if idx >= 0 && idx < len(data) { if data[idx] < 128 { blackCount++ } else { whiteCount++ } } } } return blackCount >= 2 && whiteCount >= 2 } // 辅助函数 func max(a, b int) int { if a > b { return a } return b } func min(a, b int) int { if a < b { return a } return b }