972 lines
23 KiB
Go
972 lines
23 KiB
Go
// 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
|
||
}
|