Files
AntiCaptcha/internal/captcha/handler.go
T
admin d918fb1654
Build and Deploy / build (push) Successful in 5m43s
Build and Deploy / deploy (push) Successful in 9s
fix: parse charset as JSON array instead of line-by-line
2026-07-17 00:42:42 +00:00

960 lines
22 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package captcha
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"image"
"math"
"net/http"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"anticaptcha/pkg/onnx"
"anticaptcha/pkg/opencv"
"github.com/disintegration/imaging"
)
type Handler struct {
modelPath string
mu sync.RWMutex
}
// 模型配置:本地文件名 -> 远程文件名(可选模型用 optional 标记)
var modelConfigs = map[string]struct {
remote string
optional bool
}{
"CRNN_Math.onnx": {remote: "[AntiCAP]-CRNN_Math.onnx", optional: false},
"OCR.onnx": {remote: "[Dddd]-OCR.onnx", optional: false},
"Rotation-RotNetR.onnx": {remote: "[AntiCAP]-Rotation-RotNetR.onnx", optional: false},
"Siamese-ResNet18.onnx": {remote: "[AntiCAP]-Siamese-ResNet18.onnx", optional: false},
"CharSets.txt": {remote: "[Dddd]-CharSets.txt", optional: false},
"Detection_Icon.onnx": {remote: "Detection_Icon.onnx", optional: false},
"Detection_Text.onnx": {remote: "Detection_Text.onnx", optional: false},
}
// 从 Gitea 仓库下载(公开仓库,无需认证)
const modelBaseURL = "https://git.viaeon.com/admin/anticaptcha/raw/branch/main/models"
func NewHandler(modelPath string) *Handler {
h := &Handler{
modelPath: modelPath,
}
// 确保模型目录存在并下载缺失的模型
h.ensureModels()
// 加载模型
h.loadModels()
return h
}
// loadModels 加载所有模型
func (h *Handler) loadModels() {
// 加载 OCR 模型
ocrPath := filepath.Join(h.modelPath, "OCR.onnx")
if _, err := os.Stat(ocrPath); err == nil {
if err := onnx.LoadModel("ocr", ocrPath); err != nil {
fmt.Printf("警告: 加载 OCR 模型失败: %v\n", err)
} else {
fmt.Println("OCR 模型加载成功")
}
}
// 加载 Math 模型
mathPath := filepath.Join(h.modelPath, "CRNN_Math.onnx")
if _, err := os.Stat(mathPath); err == nil {
if err := onnx.LoadModel("math", mathPath); err != nil {
fmt.Printf("警告: 加载 Math 模型失败: %v\n", err)
} else {
fmt.Println("Math 模型加载成功")
}
}
// 加载 Rotation 模型
rotatePath := filepath.Join(h.modelPath, "Rotation-RotNetR.onnx")
if _, err := os.Stat(rotatePath); err == nil {
if err := onnx.LoadModel("rotate", rotatePath); err != nil {
fmt.Printf("警告: 加载 Rotation 模型失败: %v\n", err)
} else {
fmt.Println("Rotation 模型加载成功")
}
}
// 加载 Siamese 模型
siamesePath := filepath.Join(h.modelPath, "Siamese-ResNet18.onnx")
if _, err := os.Stat(siamesePath); err == nil {
if err := onnx.LoadModel("siamese", siamesePath); err != nil {
fmt.Printf("警告: 加载 Siamese 模型失败: %v\n", err)
} else {
fmt.Println("Siamese 模型加载成功")
}
}
// 加载 YOLO 检测模型
iconPath := filepath.Join(h.modelPath, "Detection_Icon.onnx")
if _, err := os.Stat(iconPath); err == nil {
if err := opencv.LoadYOLO("icon", iconPath, ""); err != nil {
fmt.Printf("警告: 加载 Icon 检测模型失败: %v\n", err)
} else {
fmt.Println("Icon 检测模型加载成功")
}
}
textPath := filepath.Join(h.modelPath, "Detection_Text.onnx")
if _, err := os.Stat(textPath); err == nil {
if err := opencv.LoadYOLO("text", textPath, ""); err != nil {
fmt.Printf("警告: 加载 Text 检测模型失败: %v\n", err)
} else {
fmt.Println("Text 检测模型加载成功")
}
}
}
// ensureModels 检查并下载缺失的模型
func (h *Handler) ensureModels() {
if err := os.MkdirAll(h.modelPath, 0755); err != nil {
fmt.Printf("警告: 创建模型目录失败: %v\n", err)
return
}
for localName, cfg := range modelConfigs {
localPath := filepath.Join(h.modelPath, localName)
if _, err := os.Stat(localPath); os.IsNotExist(err) {
fmt.Printf("下载模型: %s -> %s\n", cfg.remote, localName)
if err := h.downloadModel(cfg.remote, localPath); err != nil {
if cfg.optional {
fmt.Printf("提示: 可选模型 %s 下载失败,相关功能不可用: %v\n", localName, err)
} else {
fmt.Printf("警告: 下载模型 %s 失败: %v\n", cfg.remote, err)
}
} else {
fmt.Printf("模型下载完成: %s\n", localName)
}
}
}
}
// downloadModel 下载模型文件
func (h *Handler) downloadModel(remoteName, localPath string) error {
url := fmt.Sprintf("%s/%s", modelBaseURL, remoteName)
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("HTTP %d", resp.StatusCode)
}
out, err := os.Create(localPath)
if err != nil {
return err
}
defer out.Close()
_, err = out.ReadFrom(resp.Body)
return err
}
// ===================== OCR 文字识别 =====================
func (h *Handler) OCR(imageBase64 string) (string, error) {
sess, ok := onnx.GetSession("ocr")
if !ok {
return "", fmt.Errorf("OCR 模型未加载")
}
img, err := decodeBase64ToImage(imageBase64)
if err != nil {
return "", err
}
charset, err := h.loadCharset()
if err != nil {
return "", err
}
input, width, err := preprocessOCR(img)
if err != nil {
return "", err
}
dims := []int64{1, 1, 64, int64(width)}
output, err := sess.Run(input, dims)
if err != nil {
return "", err
}
return ctcDecode(output, charset), nil
}
func preprocessOCR(img image.Image) ([]float32, int, error) {
bounds := img.Bounds()
width := bounds.Dx()
height := bounds.Dy()
newHeight := 64
newWidth := width * newHeight / height
if newWidth < 1 {
newWidth = 1
}
resized := imaging.Resize(img, newWidth, newHeight, imaging.Lanczos)
gray := imaging.Grayscale(resized)
pixels := make([]float32, newWidth*newHeight)
for y := 0; y < newHeight; y++ {
for x := 0; x < newWidth; x++ {
c := gray.At(x, y)
r, _, _, _ := c.RGBA()
val := float32(r) / 65535.0
pixels[y*newWidth+x] = (val - 0.5) / 0.5
}
}
return pixels, newWidth, nil
}
func ctcDecode(output []float32, charset []string) string {
if len(charset) == 0 {
return ""
}
result := ""
lastIdx := 0
numClasses := len(charset)
timesteps := len(output) / numClasses
for t := 0; t < timesteps; t++ {
maxIdx := 0
maxProb := float32(-math.MaxFloat32)
for c := 0; c < numClasses; c++ {
idx := t * numClasses + c
if idx < len(output) && output[idx] > maxProb {
maxProb = output[idx]
maxIdx = c
}
}
if maxIdx != 0 && maxIdx != lastIdx && maxIdx < len(charset) {
result += charset[maxIdx]
}
lastIdx = maxIdx
}
return result
}
func (h *Handler) loadCharset() ([]string, error) {
charsetPath := filepath.Join(h.modelPath, "CharSets.txt")
// 读取整个文件
data, err := os.ReadFile(charsetPath)
if err != nil {
return nil, err
}
// 解析 JSON 数组格式
var charset []string
if err := json.Unmarshal(data, &charset); err != nil {
return nil, fmt.Errorf("解析字符集失败: %v", err)
}
// 确保第一个元素是空字符串(CTC blank)
if len(charset) == 0 || charset[0] != "" {
charset = append([]string{""}, charset...)
}
return charset, nil
}
// ===================== Math 数学计算 =====================
const mathChars = "0123456789+-*/÷×=?"
func (h *Handler) Math(imageBase64 string) (string, error) {
sess, ok := onnx.GetSession("math")
if !ok {
return "", fmt.Errorf("Math 模型未加载")
}
img, err := decodeBase64ToImage(imageBase64)
if err != nil {
return "", err
}
input, err := preprocessMath(img)
if err != nil {
return "", err
}
dims := []int64{1, 3, 70, 200}
output, err := sess.Run(input, dims)
if err != nil {
return "", err
}
expr := decodeMath(output)
if expr == "" {
return "", fmt.Errorf("无法识别表达式")
}
result, err := evalMathExpression(expr)
if err != nil {
return "", err
}
return fmt.Sprintf("%v", result), nil
}
func preprocessMath(img image.Image) ([]float32, error) {
resized := imaging.Resize(img, 200, 70, imaging.Lanczos)
rgb := imaging.Clone(resized)
pixels := make([]float32, 3*70*200)
for y := 0; y < 70; y++ {
for x := 0; x < 200; x++ {
c := rgb.At(x, y)
r, g, b, _ := c.RGBA()
pixels[0*70*200+y*200+x] = (float32(r)/65535.0 - 0.5) / 0.5
pixels[1*70*200+y*200+x] = (float32(g)/65535.0 - 0.5) / 0.5
pixels[2*70*200+y*200+x] = (float32(b)/65535.0 - 0.5) / 0.5
}
}
return pixels, nil
}
func decodeMath(output []float32) string {
numChars := len(mathChars) + 1
timesteps := len(output) / numChars
result := ""
lastIdx := 0
for t := 0; t < timesteps; t++ {
maxIdx := 0
maxProb := float32(-math.MaxFloat32)
for c := 0; c < numChars; c++ {
idx := t * numChars + c
if idx < len(output) && output[idx] > maxProb {
maxProb = output[idx]
maxIdx = c
}
}
if maxIdx != 0 && maxIdx != lastIdx {
if maxIdx-1 < len(mathChars) {
result += string(mathChars[maxIdx-1])
}
}
lastIdx = maxIdx
}
return result
}
func evalMathExpression(expr string) (interface{}, error) {
expr = strings.ReplaceAll(expr, "×", "*")
expr = strings.ReplaceAll(expr, "÷", "/")
expr = strings.ReplaceAll(expr, "?", "")
expr = strings.ReplaceAll(expr, "=", "")
var result float64
var op byte = '+'
num := 0.0
for i := 0; i < len(expr); i++ {
c := expr[i]
if c >= '0' && c <= '9' {
num = num*10 + float64(c-'0')
} else if c == '+' || c == '-' || c == '*' || c == '/' {
switch op {
case '+':
result += num
case '-':
result -= num
case '*':
result *= num
case '/':
if num != 0 {
result /= num
}
}
op = c
num = 0
}
}
switch op {
case '+':
result += num
case '-':
result -= num
case '*':
result *= num
case '/':
if num != 0 {
result /= num
}
}
if result == float64(int(result)) {
return int(result), nil
}
return result, nil
}
// ===================== 滑块匹配 =====================
type SliderMatchResult struct {
Target []int `json:"target"`
}
func (h *Handler) SliderMatch(targetBase64, backgroundBase64 string) (*SliderMatchResult, error) {
target, err := opencv.DecodeFromBase64(targetBase64)
if err != nil {
return nil, err
}
defer target.Free()
background, err := opencv.DecodeFromBase64(backgroundBase64)
if err != nil {
return nil, err
}
defer background.Free()
x, err := opencv.SliderMatch(target, background)
if err != nil {
return nil, err
}
return &SliderMatchResult{
Target: []int{x, 0, x + target.Width(), target.Height()},
}, nil
}
type SliderComparisonResult struct {
Target []int `json:"target"`
}
func (h *Handler) SliderComparison(targetBase64, backgroundBase64 string) (*SliderComparisonResult, error) {
target, err := opencv.DecodeFromBase64(targetBase64)
if err != nil {
return nil, err
}
defer target.Free()
background, err := opencv.DecodeFromBase64(backgroundBase64)
if err != nil {
return nil, err
}
defer background.Free()
x, y, err := opencv.SliderComparison(target, background)
if err != nil {
return nil, err
}
return &SliderComparisonResult{
Target: []int{x, y},
}, nil
}
// ===================== 图像相似度 =====================
func (h *Handler) CompareSimilarity(img1Base64, img2Base64 string) (float32, error) {
sess, ok := onnx.GetSession("siamese")
if ok {
return h.compareSimilarityONNX(sess, img1Base64, img2Base64)
}
img1, err := opencv.DecodeFromBase64(img1Base64)
if err != nil {
return 0, err
}
defer img1.Free()
img2, err := opencv.DecodeFromBase64(img2Base64)
if err != nil {
return 0, err
}
defer img2.Free()
return opencv.CompareSimilarity(img1, img2)
}
func (h *Handler) compareSimilarityONNX(sess *onnx.Session, img1Base64, img2Base64 string) (float32, error) {
img1, err := decodeBase64ToImage(img1Base64)
if err != nil {
return 0, err
}
img2, err := decodeBase64ToImage(img2Base64)
if err != nil {
return 0, err
}
input1, err := preprocessSiamese(img1)
if err != nil {
return 0, err
}
input2, err := preprocessSiamese(img2)
if err != nil {
return 0, err
}
dims := []int64{1, 3, 105, 105}
output, err := sess.RunDualInput(input1, dims, input2, dims)
if err != nil {
return 0, err
}
if len(output) >= 2 {
emb1 := output[:len(output)/2]
emb2 := output[len(output)/2:]
var dist float32
for i := 0; i < len(emb1); i++ {
d := emb1[i] - emb2[i]
dist += d * d
}
dist = float32(math.Sqrt(float64(dist)))
similarity := 1.0 / (1.0 + dist)
return similarity, nil
}
return 0, fmt.Errorf("输出格式错误")
}
func preprocessSiamese(img image.Image) ([]float32, error) {
resized := imaging.Resize(img, 105, 105, imaging.Lanczos)
rgb := imaging.Clone(resized)
mean := [3]float32{0.485, 0.456, 0.406}
std := [3]float32{0.229, 0.224, 0.225}
pixels := make([]float32, 3*105*105)
for y := 0; y < 105; y++ {
for x := 0; x < 105; x++ {
c := rgb.At(x, y)
r, g, b, _ := c.RGBA()
pixels[0*105*105+y*105+x] = (float32(r)/65535.0 - mean[0]) / std[0]
pixels[1*105*105+y*105+x] = (float32(g)/65535.0 - mean[1]) / std[1]
pixels[2*105*105+y*105+x] = (float32(b)/65535.0 - mean[2]) / std[2]
}
}
return pixels, nil
}
// ===================== 旋转检测 =====================
func (h *Handler) SingleRotate(imageBase64 string) (int, error) {
sess, ok := onnx.GetSession("rotate")
if ok {
return h.singleRotateONNX(sess, imageBase64)
}
img, err := opencv.DecodeFromBase64(imageBase64)
if err != nil {
return 0, err
}
defer img.Free()
angle, err := opencv.DetectRotation(img)
if err != nil {
return 0, err
}
return int(angle), nil
}
func (h *Handler) singleRotateONNX(sess *onnx.Session, imageBase64 string) (int, error) {
img, err := decodeBase64ToImage(imageBase64)
if err != nil {
return 0, err
}
input, err := preprocessRotation(img)
if err != nil {
return 0, err
}
dims := []int64{1, 3, 224, 224}
output, err := sess.Run(input, dims)
if err != nil {
return 0, err
}
maxIdx := 0
maxProb := float32(-math.MaxFloat32)
for i := 0; i < len(output); i++ {
if output[i] > maxProb {
maxProb = output[i]
maxIdx = i
}
}
return maxIdx, nil
}
func preprocessRotation(img image.Image) ([]float32, error) {
bounds := img.Bounds()
w, h := bounds.Dx(), bounds.Dy()
size := w
if h < w {
size = h
}
cropX := (w - size) / 2
cropY := (h - size) / 2
cropped := imaging.Crop(img, image.Rect(cropX, cropY, cropX+size, cropY+size))
sqrt2 := math.Sqrt(2.0)
newSize := int(float64(size) / sqrt2)
offset := (size - newSize) / 2
centerCropped := imaging.Crop(cropped, image.Rect(offset, offset, offset+newSize, offset+newSize))
resized := imaging.Resize(centerCropped, 224, 224, imaging.Lanczos)
rgb := imaging.Clone(resized)
mean := [3]float32{0.485, 0.456, 0.406}
std := [3]float32{0.229, 0.224, 0.225}
pixels := make([]float32, 3*224*224)
for y := 0; y < 224; y++ {
for x := 0; x < 224; x++ {
c := rgb.At(x, y)
r, g, b, _ := c.RGBA()
pixels[0*224*224+y*224+x] = (float32(r)/65535.0 - mean[0]) / std[0]
pixels[1*224*224+y*224+x] = (float32(g)/65535.0 - mean[1]) / std[1]
pixels[2*224*224+y*224+x] = (float32(b)/65535.0 - mean[2]) / std[2]
}
}
return pixels, nil
}
type DoubleRotateResult struct {
Angle int `json:"angle"`
}
func (h *Handler) DoubleRotate(insideBase64, outsideBase64 string, checkPixel int, speedRatio float64, grayscale, anticlockwise bool, cutPixelValue int) (*DoubleRotateResult, error) {
sess, ok := onnx.GetSession("rotate")
if ok {
insideAngle, err := h.singleRotateONNX(sess, insideBase64)
if err != nil {
return nil, err
}
outsideAngle, err := h.singleRotateONNX(sess, outsideBase64)
if err != nil {
return nil, err
}
angle := insideAngle - outsideAngle
if anticlockwise {
angle = -angle
}
if angle < 0 {
angle += 360
}
return &DoubleRotateResult{Angle: angle}, nil
}
inside, err := opencv.DecodeFromBase64(insideBase64)
if err != nil {
return nil, err
}
defer inside.Free()
outside, err := opencv.DecodeFromBase64(outsideBase64)
if err != nil {
return nil, err
}
defer outside.Free()
angleInside, err := opencv.DetectRotation(inside)
if err != nil {
return nil, err
}
angleOutside, err := opencv.DetectRotation(outside)
if err != nil {
return nil, err
}
angle := int(angleInside - angleOutside)
if anticlockwise {
angle = -angle
}
if angle < 0 {
angle += 360
}
return &DoubleRotateResult{Angle: angle}, nil
}
// ===================== 图标/文字检测 (YOLO via OpenCV DNN) =====================
type Detection struct {
Class string `json:"class"`
Box []int `json:"box"`
}
type DetectionResult struct {
Detections []Detection `json:"detections"`
}
func (h *Handler) DetectionIcon(imageBase64 string) (*DetectionResult, error) {
return h.detectYOLO("icon", imageBase64)
}
func (h *Handler) DetectionText(imageBase64 string) (*DetectionResult, error) {
return h.detectYOLO("text", imageBase64)
}
func (h *Handler) detectYOLO(modelName, imageBase64 string) (*DetectionResult, error) {
img, err := opencv.DecodeFromBase64(imageBase64)
if err != nil {
return nil, err
}
defer img.Free()
detections, err := opencv.DetectYOLO(modelName, img)
if err != nil {
return nil, err
}
result := &DetectionResult{
Detections: make([]Detection, len(detections)),
}
for i, det := range detections {
result.Detections[i] = Detection{
Class: det.ClassName,
Box: []int{int(det.X1), int(det.Y1), int(det.X2), int(det.Y2)},
}
}
return result, nil
}
// ===================== 按序点击 (匈牙利算法匹配) =====================
func (h *Handler) DetectionIconOrder(orderImgBase64, targetImgBase64 string) ([]Detection, error) {
return h.detectOrder("icon", orderImgBase64, targetImgBase64)
}
func (h *Handler) DetectionTextOrder(orderImgBase64, targetImgBase64 string) ([]Detection, error) {
return h.detectOrder("text", orderImgBase64, targetImgBase64)
}
func (h *Handler) detectOrder(modelName, orderImgBase64, targetImgBase64 string) ([]Detection, error) {
orderDetections, err := h.detectYOLO(modelName, orderImgBase64)
if err != nil {
return nil, err
}
targetDetections, err := h.detectYOLO(modelName, targetImgBase64)
if err != nil {
return nil, err
}
sort.Slice(orderDetections.Detections, func(i, j int) bool {
return orderDetections.Detections[i].Box[0] < orderDetections.Detections[j].Box[0]
})
return h.hungarianMatch(orderImgBase64, targetImgBase64, orderDetections.Detections, targetDetections.Detections)
}
func (h *Handler) hungarianMatch(orderImgBase64, targetImgBase64 string, orderBoxes, targetBoxes []Detection) ([]Detection, error) {
if len(orderBoxes) == 0 || len(targetBoxes) == 0 {
return make([]Detection, len(orderBoxes)), nil
}
orderImg, err := decodeBase64ToImage(orderImgBase64)
if err != nil {
return nil, err
}
targetImg, err := decodeBase64ToImage(targetImgBase64)
if err != nil {
return nil, err
}
numOrders := len(orderBoxes)
numTargets := len(targetBoxes)
costMatrix := make([][]float64, numOrders)
for i := range costMatrix {
costMatrix[i] = make([]float64, numTargets)
for j := range costMatrix[i] {
costMatrix[i][j] = 1.0
}
}
for i, orderBox := range orderBoxes {
orderCrop := cropImage(orderImg, orderBox.Box)
if orderCrop == nil {
continue
}
for j, targetBox := range targetBoxes {
targetCrop := cropImage(targetImg, targetBox.Box)
if targetCrop == nil {
continue
}
similarity, err := h.computeImageSimilarity(orderCrop, targetCrop)
if err != nil {
continue
}
costMatrix[i][j] = 1.0 - float64(similarity)
}
}
assignments := hungarian(costMatrix)
result := make([]Detection, numOrders)
for i, j := range assignments {
if j >= 0 && j < len(targetBoxes) {
result[i] = targetBoxes[j]
}
}
return result, nil
}
func cropImage(img image.Image, box []int) image.Image {
if len(box) < 4 {
return nil
}
bounds := img.Bounds()
if box[0] < 0 || box[1] < 0 || box[2] > bounds.Dx() || box[3] > bounds.Dy() {
return nil
}
if box[2] <= box[0] || box[3] <= box[1] {
return nil
}
return imaging.Crop(img, image.Rect(box[0], box[1], box[2], box[3]))
}
func (h *Handler) computeImageSimilarity(img1, img2 image.Image) (float32, error) {
sess, ok := onnx.GetSession("siamese")
if ok {
buf1 := new(bytes.Buffer)
imaging.Encode(buf1, img1, imaging.PNG)
b64_1 := base64.StdEncoding.EncodeToString(buf1.Bytes())
buf2 := new(bytes.Buffer)
imaging.Encode(buf2, img2, imaging.PNG)
b64_2 := base64.StdEncoding.EncodeToString(buf2.Bytes())
return h.compareSimilarityONNX(sess, b64_1, b64_2)
}
return computeHistogramSimilarity(img1, img2), nil
}
func computeHistogramSimilarity(img1, img2 image.Image) float32 {
size := 64
resized1 := imaging.Resize(img1, size, size, imaging.Lanczos)
resized2 := imaging.Resize(img2, size, size, imaging.Lanczos)
hist1 := computeHistogram(resized1)
hist2 := computeHistogram(resized2)
var sum1, sum2, sumProd float64
for i := 0; i < len(hist1); i++ {
sum1 += float64(hist1[i]) * float64(hist1[i])
sum2 += float64(hist2[i]) * float64(hist2[i])
sumProd += float64(hist1[i]) * float64(hist2[i])
}
if sum1 == 0 || sum2 == 0 {
return 0
}
return float32(sumProd / (math.Sqrt(sum1) * math.Sqrt(sum2)))
}
func computeHistogram(img image.Image) []int {
hist := make([]int, 256)
bounds := img.Bounds()
for y := 0; y < bounds.Dy(); y++ {
for x := 0; x < bounds.Dx(); x++ {
c := img.At(x, y)
r, g, b, _ := c.RGBA()
gray := int((r + g + b) / 3 / 256)
hist[gray]++
}
}
return hist
}
func hungarian(costMatrix [][]float64) []int {
n := len(costMatrix)
if n == 0 {
return nil
}
m := len(costMatrix[0])
used := make([]bool, m)
result := make([]int, n)
for i := range result {
result[i] = -1
}
for i := 0; i < n; i++ {
bestJ := -1
bestCost := 1.0
for j := 0; j < m; j++ {
if !used[j] && costMatrix[i][j] < bestCost {
bestCost = costMatrix[i][j]
bestJ = j
}
}
if bestJ >= 0 {
result[i] = bestJ
used[bestJ] = true
}
}
return result
}
// ===================== 工具函数 =====================
func decodeBase64ToImage(base64Str string) (image.Image, error) {
if strings.Contains(base64Str, ",") {
parts := strings.SplitN(base64Str, ",", 2)
if len(parts) == 2 {
base64Str = parts[1]
}
}
data, err := base64.StdEncoding.DecodeString(base64Str)
if err != nil {
data, err = base64.RawStdEncoding.DecodeString(base64Str)
if err != nil {
return nil, err
}
}
return imaging.Decode(bytes.NewReader(data))
}