复制项目
This commit is contained in:
971
internal/rpc/msg/qrcode_decoder.go
Normal file
971
internal/rpc/msg/qrcode_decoder.go
Normal file
@@ -0,0 +1,971 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user