diff --git a/api/graphql.go b/api/graphql.go index 20561be..fd97f61 100644 --- a/api/graphql.go +++ b/api/graphql.go @@ -2,11 +2,14 @@ package api import ( "fmt" + "image" "log" + "os" "reflect" "regexp" "strconv" "strings" + "time" "git.satori.love/gameui/webp/models" "github.com/doug-martin/goqu/v9" @@ -104,18 +107,49 @@ func NewSchema(config Config) (graphql.Schema, error) { log.Fatalln("连接数据库失败", err) } - //// 定时检查补全颜色字段 - //checkColorNullRows := func() { - // for { - // var ids struct { - // ID int - // } - // if err := db.Table("web_images").Where("color IS NULL").Scan(ids).Error; err != nil { - // fmt.Println("定时检查补全颜色字段查询失败", err) - // } - // fmt.Println("定时检查补全颜色字段查询成功", ids) - // } - //} + // 定时检查补全颜色字段 + checkColorNullRows := func() { + for { + time.Sleep(10 * time.Second) + var list []struct { + ID int + Content string + } + if err := db.Table("web_images").Select("id", "content").Where("article_category_top_id = 22").Where("color_0_r IS NULL").Limit(10).Scan(&list).Error; err != nil { + fmt.Println("定时检查补全颜色字段查询失败", err) + continue + } + fmt.Println("定时检查补全颜色字段查询成功", list) + for index, item := range list { + matches := regexp.MustCompile(`^https?://image\.gameuiux\.cn/(.*)`).FindStringSubmatch(item.Content) + if len(matches) < 2 { + fmt.Println("转换路径失败", index, item.ID, item.Content) + continue + } + // 打开图像文件 + file, err := os.Open("oss/" + matches[1]) + if err != nil { + fmt.Println("打开文件失败", index, item.ID, item.Content, err) + continue + } + defer file.Close() + + // 解码 JPEG 图像 + img, _, err := image.Decode(file) + if err != nil { + fmt.Println("解码图像失败", index, item.ID, item.Content, err) + } + + centers, _ := KMeans(extractColors(img), 8) + fmt.Println(centers) + } + } + } + + if config.Oss.Local { + fmt.Println("开启图像色调计算") + go checkColorNullRows() + } // 用户的可选字段 user := graphql.NewObject(graphql.ObjectConfig{ diff --git a/api/kMeans.go b/api/kMeans.go new file mode 100644 index 0000000..83a6ea2 --- /dev/null +++ b/api/kMeans.go @@ -0,0 +1,131 @@ +package api + +import ( + "image" + "image/color" + "math" + "math/rand" + "os" +) + +// 定义一个结构来表示 RGB 颜色 +type RGB struct { + R, G, B int +} + +// 计算两个 RGB 颜色的欧氏距离 +func distance(c1, c2 RGB) float64 { + return math.Sqrt(float64((c1.R-c2.R)*(c1.R-c2.R) + (c1.G-c2.G)*(c1.G-c2.G) + (c1.B-c2.B)*(c1.B-c2.B))) +} + +// 对图像颜色进行 KMeans 聚类 +func KMeans(colors []RGB, k int) ([]RGB, []int) { + // 随机选择初始中心 + var centers []RGB + for i := 0; i < k; i++ { + centers = append(centers, colors[rand.Intn(len(colors))]) + } + + // 聚类算法 + labels := make([]int, len(colors)) + for iter := 0; iter < 10; iter++ { // 设置最大迭代次数为 10 + // 为每个颜色分配最近的聚类中心 + for i, color := range colors { + minDist := distance(color, centers[0]) + labels[i] = 0 + for j := 1; j < k; j++ { + dist := distance(color, centers[j]) + if dist < minDist { + minDist = dist + labels[i] = j + } + } + } + + // 计算新的聚类中心 + newCenters := make([]RGB, k) + counts := make([]int, k) + for i, label := range labels { + newCenters[label].R += colors[i].R + newCenters[label].G += colors[i].G + newCenters[label].B += colors[i].B + counts[label]++ + } + + // 计算平均值作为新的中心 + for i := 0; i < k; i++ { + if counts[i] > 0 { + newCenters[i].R /= counts[i] + newCenters[i].G /= counts[i] + newCenters[i].B /= counts[i] + } + } + + // 如果中心没有变化,停止迭代 + changed := false + for i := 0; i < k; i++ { + if newCenters[i] != centers[i] { + changed = true + break + } + } + if !changed { + break + } + centers = newCenters + } + + return centers, labels +} + +// 加载图像并提取颜色 +func extractColors(img image.Image) (colors []RGB) { + bounds := img.Bounds() + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + for x := bounds.Min.X; x < bounds.Max.X; x++ { + r, g, b, _ := img.At(x, y).RGBA() + colors = append(colors, RGB{int(r >> 8), int(g >> 8), int(b >> 8)}) + } + } + return colors +} + +// 将聚类后的颜色应用到图像上 +func recolorImage(img image.Image, centers []RGB, labels []int) image.Image { + bounds := img.Bounds() + newImage := image.NewRGBA(bounds) + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + for x := bounds.Min.X; x < bounds.Max.X; x++ { + label := labels[(y-bounds.Min.Y)*bounds.Max.X+(x-bounds.Min.X)] + // 将 RGB 颜色转换为 RGBA 类型 + newColor := color.RGBA{ + R: uint8(centers[label].R), + G: uint8(centers[label].G), + B: uint8(centers[label].B), + A: 255, // 不透明 + } + // 设置新图像像素颜色 + newImage.Set(x, y, newColor) + } + } + return newImage +} + +func ImageToColors(str string) (colors []RGB, err error) { + // 打开图像文件 + file, err := os.Open(str) + if err != nil { + return nil, err + } + defer file.Close() + + // 解码 JPEG 图像 + img, _, err := image.Decode(file) + if err != nil { + return nil, err + } + + // 提取图像颜色 + centers, _ := KMeans(extractColors(img), 8) + return centers, nil +} diff --git a/api/struct.go b/api/struct.go index ef97e48..c5eac71 100644 --- a/api/struct.go +++ b/api/struct.go @@ -155,6 +155,11 @@ type ConfigMysql struct { Password string } +type Oss struct { + Local bool +} + type Config struct { Mysql ConfigMysql + Oss } diff --git a/bin/main.go b/bin/main.go index 0dca1a3..d382b1d 100644 --- a/bin/main.go +++ b/bin/main.go @@ -247,6 +247,9 @@ func main() { UserName: config.GetString("mysql.user"), Password: config.GetString("mysql.password"), }, + Oss: api.Oss{ + Local: config.GetBool("oss.local"), + }, }) if err != nil { log.Fatalf("failed to create new schema, error: %v", err)