go: Wordcloud
/*
# 版权所有 2026 ©涂聚文有限公司™ ®
# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
# 描述: Algorithms
# Author : geovindu,Geovin Du 涂聚文.
# IDE : goLang 2024.3.6 go 26.2
# os : windows 10
# database : mysql 9.0 sql server 2019, postgreSQL 17.0 Oracle 21c Neo4j
# Datetime : 2026/7/8 23:07
# User : geovindu
# Product : GoLand
# Project : goalgorithms
# File : main.go
*/
// ============================================================
// 中文词云生成器(Go)
// 对标 Python wordcloud 示例(alice 蒙版词云):
// - 读取中文文本 alice2.txt(UTF-8)
// - Go 中文分词(go-ego/gse,纯 Go,无 CGO,Windows 可直接编译)
// - 停用词过滤(工作/就是/个人/没有/村民委员会/said 等)
// - 蒙版图片 alice_mask.png(深色人像 = 文字填充区,白色背景 = 禁区)
// - 自定义字体:方正小篆体
// - 白色背景 + 钢蓝色轮廓(contour_width=3, contour_color=steelblue)
// - 输出 chinese_wordcloud2.png
//
// go get -u github.com/psykhi/wordclouds
// go get -u github.com/go-ego/gse
// Author : geovindu, Geovin Du 涂聚文
// ============================================================
package main
import (
"fmt"
"image"
"image/color"
"image/draw"
"image/png"
"log"
"math"
"math/rand"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/go-ego/gse"
"github.com/psykhi/wordclouds"
)
// ==================== 路径配置(按需修改) ====================
var (
// 文本文件(对应 Python 的 alice2.txt)
textFile = "alice2.txt"
// 蒙版图片(对应 Python 的 alice_mask.png)
maskFile = "alice_mask.png"
// 停用词文件(可选,一行一个词;不存在则用内置停用词)
stopWordsFile = "stopwords.txt"
// 方正小篆体字体文件(用户级安装字体,Python 已验证可用的路径)
// 可通过环境变量 WORDCLOUD_FONT 覆盖,便于测试/切换字体,无需改代码
fontFile = getEnv("WORDCLOUD_FONT", `C:\Users\geovindu\AppData\Local\Microsoft\Windows\Fonts\方正小篆体.ttf`)
// 输出图片(对应 Python 的 chinese_wordcloud2.png)
outputFile = "chinese_wordcloud2.png"
// 钢蓝色,对应 Python contour_color='steelblue'
steelBlue = color.RGBA{R: 70, G: 130, B: 180, A: 255}
// 白色,蒙版禁区色(白色背景区域不可填字)
maskWhite = color.RGBA{R: 255, G: 255, B: 255, A: 255}
// 只保留中英文/数字,过滤纯标点
validWordRegex = regexp.MustCompile(`[\x{4e00}-\x{9fff}A-Za-z0-9]+`)
)
func getEnv(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
func main() {
// ========== 1. 读取中文文本(对应 Python: open(..., encoding='utf-8').read()) ==========
rawBytes, err := os.ReadFile(textFile)
if err != nil {
log.Fatalf("[错误] 读取文本文件失败:%v", err)
}
rawText := string(rawBytes)
// ========== 2. Go 中文分词(对应 Python: jieba.lcut(text)) ==========
var seg gse.Segmenter
if err := seg.LoadDict(); err != nil {
log.Fatalf("[错误] 加载分词词典失败:%v", err)
}
tokens := seg.Cut(rawText)
// ========== 3. 停用词集合(对应 Python: stopwords = set(STOPWORDS); stopwords.update([...])) ==========
stopwords := loadStopWords()
// ========== 4. 统计词频(对应 Python wordcloud 内部按词频统计) ==========
freq := make(map[string]int)
for _, token := range tokens {
w := strings.TrimSpace(token)
if len(w) == 0 {
continue
}
if !validWordRegex.MatchString(w) {
continue // 纯标点符号
}
if _, ok := stopwords[w]; ok {
continue // 停用词
}
freq[w]++
}
if len(freq) == 0 {
log.Fatalln("[错误] 词频统计为空,请检查文本内容与停用词。")
}
// 只取前 2000 个词(对应 Python: max_words=2000)
freq = topNWords(freq, 2000)
// ========== 5. 读取蒙版尺寸(画布与蒙版同尺寸,对应 Python 行为) ==========
width, height := maskSize(maskFile)
// ========== 6. 构建蒙版禁区(深色人像=填字区,白色背景=禁区) ==========
// Mask(path, w, h, exclude):exclude 颜色区域为禁放区
// 蒙版为“深色人像+白色背景”时 exclude=白色 → 文字填进深色人像内
boxes := wordclouds.Mask(maskFile, width, height, maskWhite)
// ========== 7. 生成词云(对应 Python: WordCloud(...)) ==========
if _, err := os.Stat(fontFile); err != nil {
log.Fatalf("[错误] 找不到字体文件:%s\n请修改 fontFile 为你的方正小篆体真实路径。", fontFile)
}
// 随机彩色文字调色板(对应 Python 默认 hsl 随机彩色)
palette := randomPalette(16)
wc := wordclouds.NewWordcloud(freq,
wordclouds.FontFile(fontFile), // 自定义字体:方正小篆体
wordclouds.FontMaxSize(140), // 最大字号
wordclouds.FontMinSize(12), // 最小字号
wordclouds.BackgroundColor(color.White), // 白色背景
wordclouds.Width(width),
wordclouds.Height(height),
wordclouds.MaskBoxes(boxes), // 蒙版禁区
wordclouds.Colors(palette), // 随机彩色
wordclouds.WordSizeFunction("sqrt"), // 字号随词频非线性缩放,更接近 Python 效果
)
img := wc.Draw()
// ========== 8. 叠加钢蓝色轮廓(对应 Python: contour_width=3, contour_color='steelblue') ==========
rgba := toRGBA(img)
drawContour(rgba, maskFile, steelBlue, 3)
// ========== 9. 保存 PNG(对应 Python: wc.to_file(...)) ==========
f, err := os.Create(outputFile)
if err != nil {
log.Fatalf("[错误] 创建输出文件失败:%v", err)
}
defer f.Close()
if err := png.Encode(f, rgba); err != nil {
log.Fatalf("[错误] 保存 PNG 失败:%v", err)
}
fmt.Printf("[完成] 词云已保存:%s\n", mustAbs(outputFile))
}
// ============================================================
// 工具函数
// ============================================================
// topNWords 取词频最高的前 n 个词
func topNWords(freq map[string]int, n int) map[string]int {
type kv struct {
word string
count int
}
list := make([]kv, 0, len(freq))
for w, c := range freq {
list = append(list, kv{w, c})
}
// 按词频降序
sortSlice(list, func(a, b kv) bool { return a.count > b.count })
if len(list) > n {
list = list[:n]
}
res := make(map[string]int, len(list))
for _, it := range list {
res[it.word] = it.count
}
return res
}
func sortSlice[T any](s []T, less func(a, b T) bool) {
// 简单插入排序(词数有限,足够)
for i := 1; i < len(s); i++ {
for j := i; j > 0 && less(s[j], s[j-1]); j-- {
s[j], s[j-1] = s[j-1], s[j]
}
}
}
// loadStopWords 加载停用词:内置 + stopwords.txt(一行一个词)
func loadStopWords() map[string]struct{} {
stop := make(map[string]struct{})
builtin := []string{
// Python 示例中 stopwords.update([...]) 显式加入的词
"工作", "就是", "个人", "没有", "村民委员会", "said",
// 常用英文停用词(对应 Python: wordcloud.STOPWORDS 的主要子集)
"a", "an", "the", "and", "or", "but", "if", "of", "to", "in", "on", "for",
"with", "as", "at", "by", "from", "up", "about", "into", "over", "after",
"is", "are", "was", "were", "be", "been", "being", "have", "has", "had",
"do", "does", "did", "will", "would", "can", "could", "should", "may",
"not", "no", "so", "too", "very", "just", "only", "then", "there",
"it", "its", "this", "that", "these", "those", "i", "you", "he", "she",
"we", "they", "them", "his", "her", "our", "your", "their", "my", "me",
"said", "say", "one", "two", "get", "got", "like", "make", "new", "now",
// 常用中文停用词
"的", "了", "是", "和", "在", "有", "就", "都", "而", "及", "与",
"一个", "我们", "你们", "他们", "这个", "那个", "什么", "怎么", "可以",
}
for _, w := range builtin {
stop[w] = struct{}{}
}
if data, err := os.ReadFile(stopWordsFile); err == nil {
for _, line := range strings.Split(string(data), "\n") {
w := strings.TrimSpace(line)
if w != "" {
stop[w] = struct{}{}
}
}
}
return stop
}
// maskSize 读取蒙版图片尺寸(画布与蒙版同尺寸)
func maskSize(path string) (int, int) {
f, err := os.Open(path)
if err != nil {
log.Printf("[提示] 找不到蒙版图片 %s,改用矩形画布 800x600。", path)
return 800, 600
}
defer f.Close()
cfg, _, err := image.DecodeConfig(f)
if err != nil {
log.Printf("[提示] 蒙版图片解码失败(%v),改用矩形画布 800x600。", err)
return 800, 600
}
return cfg.Width, cfg.Height
}
// toRGBA 将 image.Image 转为 *image.RGBA(便于后续画轮廓)
func toRGBA(img image.Image) *image.RGBA {
if rgba, ok := img.(*image.RGBA); ok {
return rgba
}
b := img.Bounds()
rgba := image.NewRGBA(b)
draw.Draw(rgba, b, img, b.Min, draw.Src)
return rgba
}
// drawContour 沿蒙版“深色人像”边界绘制钢蓝色轮廓
func drawContour(dst *image.RGBA, maskPath string, contourColor color.Color, contourWidth int) {
f, err := os.Open(maskPath)
if err != nil {
return
}
defer f.Close()
maskImg, _, err := image.Decode(f)
if err != nil {
return
}
b := maskImg.Bounds()
w, h := b.Dx(), b.Dy()
// 计算灰度亮度
lum := make([][]float64, h)
for y := 0; y < h; y++ {
lum[y] = make([]float64, w)
for x := 0; x < w; x++ {
r, g, bl, _ := maskImg.At(b.Min.X+x, b.Min.Y+y).RGBA()
lum[y][x] = 0.299*float64(r>>8) + 0.587*float64(g>>8) + 0.114*float64(bl>>8)
}
}
isLight := func(x, y int) bool {
if x < 0 || y < 0 || x >= w || y >= h {
return false // 越界视为不可绘制(背景)
}
return lum[y][x] > 127.5
}
half := contourWidth / 2
// 深色人像边界像素:本身为暗(人像),四邻接中有亮(背景)像素
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
if isLight(x, y) {
continue
}
if isLight(x-1, y) || isLight(x+1, y) || isLight(x, y-1) || isLight(x, y+1) {
for dy := -half; dy <= half; dy++ {
for dx := -half; dx <= half; dx++ {
nx, ny := x+dx, y+dy
if nx >= 0 && ny >= 0 && nx < w && ny < h {
dst.Set(b.Min.X+nx, b.Min.Y+ny, contourColor)
}
}
}
}
}
}
}
// randomPalette 生成随机彩色调色板(对应 Python: "hsl(随机色相, 80%, 50%)")
func randomPalette(n int) []color.Color {
pal := make([]color.Color, 0, n)
for i := 0; i < n; i++ {
hue := rand.Float64() * 360
pal = append(pal, hslToRGB(hue, 0.8, 0.5))
}
return pal
}
// hslToRGB HSL 转 RGB(S=0.8, L=0.5 时得到鲜艳彩色)
func hslToRGB(h, s, l float64) color.Color {
h = math.Mod(h, 360) / 360
hue2rgb := func(p, q, t float64) float64 {
if t < 0 {
t += 1
}
if t > 1 {
t -= 1
}
if t < 1.0/6.0 {
return p + (q-p)*6*t
}
if t < 0.5 {
return q
}
if t < 2.0/3.0 {
return p + (q-p)*(2.0/3.0-t)*6
}
return p
}
var q float64
if l < 0.5 {
q = l * (1 + s)
} else {
q = l + s - l*s
}
p := 2*l - q
r := uint8(math.Round(hue2rgb(p, q, h+1.0/3.0) * 255))
g := uint8(math.Round(hue2rgb(p, q, h) * 255))
bl := uint8(math.Round(hue2rgb(p, q, h-1.0/3.0) * 255))
return color.RGBA{R: r, G: g, B: bl, A: 255}
}
// mustAbs 返回文件绝对路径
func mustAbs(path string) string {
abs, err := filepath.Abs(path)
if err != nil {
return path
}
return abs
}
输出:

/*
# 版权所有 2026 ©涂聚文有限公司™ ®
# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
# 描述: Algorithms
# Author : geovindu,Geovin Du 涂聚文.
# IDE : goLang 2024.3.6 go 26.2
# os : windows 10
# database : mysql 9.0 sql server 2019, postgreSQL 17.0 Oracle 21c Neo4j
# Datetime : 2026/7/8 23:07
# User : geovindu
# Product : GoLand
# Project : goalgorithms
# File : main.go
*/
// ============================================================
// 中文词云生成器(Go)
// 对标 Python wordcloud 示例(alice 蒙版词云):
// - 读取中文文本 alice2.txt(UTF-8)
// - Go 中文分词(go-ego/gse,纯 Go,无 CGO,Windows 可直接编译)
// - 停用词过滤(工作/就是/个人/没有/村民委员会/said 等)
// - 蒙版图片 alice_mask.png(深色人像 = 文字填充区,白色背景 = 禁区)
// - 自研像素级词云布局(对标 Python wordcloud:
// 中心区域优先、字号按词频缩放、放不下自动减小字号重试)
// - 自定义字体:方正小篆体
// - 白色背景 + 钢蓝色轮廓(contour_width=3, contour_color=steelblue)
// - 输出 chinese_wordcloud2.png
// Author : geovindu, Geovin Du 涂聚文
// ============================================================
package main
import (
"fmt"
"image"
"image/color"
"image/png"
"log"
"math"
"math/rand"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"github.com/go-ego/gse"
"golang.org/x/image/font"
"golang.org/x/image/font/opentype"
"golang.org/x/image/math/fixed"
)
// ==================== 路径配置(按需修改) ====================
var (
// 文本文件(对应 Python 的 alice2.txt)
textFile = "alice2.txt"
// 蒙版图片(对应 Python 的 alice_mask.png)
maskFile = "alice_mask.png"
// 停用词文件(可选,一行一个词;不存在则用内置停用词)
stopWordsFile = "stopwords.txt"
// 方正小篆体字体文件(用户级安装字体,Python 已验证可用的路径)
// 可通过环境变量 WORDCLOUD_FONT 覆盖,便于测试/切换字体,无需改代码
fontFile = getEnv("WORDCLOUD_FONT", `C:\Users\geovindu\AppData\Local\Microsoft\Windows\Fonts\方正小篆体.ttf`)
// 输出图片(对应 Python 的 chinese_wordcloud2.png)
outputFile = "chinese_wordcloud2.png"
// 钢蓝色,对应 Python contour_color='steelblue'
steelBlue = color.RGBA{R: 70, G: 130, B: 180, A: 255}
// 布局参数(对应 Python wordcloud 的关键参数)
maxWords = 2000 // max_words=2000
preferHorizon = 0.9 // prefer_horizontal=0.9:约 10% 文字竖向
minFontSize = 10.0 // 最小字号
maxFontRatio = 0.25 // 最大字号 = 画布短边 * 该比例
tryPerRound = 80 // 每个字号尝试的随机落点数量
fontDecay = 0.93 // 放不下时字号衰减系数(对标 python 逐像素减小)
// 只保留中英文/数字,过滤纯标点
validWordRegex = regexp.MustCompile(`[\x{4e00}-\x{9fff}A-Za-z0-9]+`)
)
func getEnv(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
// ============================================================
// 主流程
// ============================================================
func main() {
// ========== 1. 读取中文文本(对应 Python: open(..., encoding='utf-8').read()) ==========
rawBytes, err := os.ReadFile(textFile)
if err != nil {
log.Fatalf("[错误] 读取文本文件失败:%v", err)
}
rawText := string(rawBytes)
// ========== 2. Go 中文分词(对应 Python: jieba.lcut(text)) ==========
var seg gse.Segmenter
if err := seg.LoadDict(); err != nil {
log.Fatalf("[错误] 加载分词词典失败:%v", err)
}
tokens := seg.Cut(rawText)
// ========== 3. 停用词集合 ==========
stopwords := loadStopWords()
// ========== 4. 统计词频 ==========
freq := make(map[string]int)
for _, token := range tokens {
w := strings.TrimSpace(token)
if len(w) == 0 {
continue
}
if !validWordRegex.MatchString(w) {
continue // 纯标点符号
}
if _, ok := stopwords[w]; ok {
continue // 停用词
}
freq[w]++
}
if len(freq) == 0 {
log.Fatalln("[错误] 词频统计为空,请检查文本内容与停用词。")
}
// 词频降序排列,取前 maxWords
words := sortedWords(freq, maxWords)
// ========== 5. 加载蒙版 → 可绘制网格(深色人像=可绘制,白色背景=禁区) ==========
drawable, width, height := loadMaskGrid(maskFile)
if width == 0 || height == 0 {
log.Fatalln("[错误] 蒙版图片尺寸无效。")
}
// ========== 6. 加载字体(方正小篆体) ==========
if _, err := os.Stat(fontFile); err != nil {
log.Fatalf("[错误] 找不到字体文件:%s\n请修改 fontFile 为你的方正小篆体真实路径。", fontFile)
}
fontData, err := os.ReadFile(fontFile)
if err != nil {
log.Fatalf("[错误] 读取字体文件失败:%v", err)
}
// ========== 7. 生成词云(自研布局,对标 Python wordcloud) ==========
img := drawWordCloud(words, drawable, width, height, fontData)
// ========== 8. 叠加钢蓝色轮廓 ==========
drawContour(img, maskFile, steelBlue, 3)
// ========== 9. 保存 PNG ==========
f, err := os.Create(outputFile)
if err != nil {
log.Fatalf("[错误] 创建输出文件失败:%v", err)
}
defer f.Close()
if err := png.Encode(f, img); err != nil {
log.Fatalf("[错误] 保存 PNG 失败:%v", err)
}
abs, _ := filepath.Abs(outputFile)
fmt.Printf("[完成] 词云已保存:%s(共放置 %d 个词)\n", abs, placedCount)
}
// ============================================================
// 自研词云布局(对标 Python wordcloud 算法)
// ============================================================
type wordFreq struct {
word string
count int
}
var placedCount int
// sortedWords 按词频降序返回前 n 个词
func sortedWords(freq map[string]int, n int) []wordFreq {
list := make([]wordFreq, 0, len(freq))
for w, c := range freq {
list = append(list, wordFreq{w, c})
}
sort.Slice(list, func(i, j int) bool { return list[i].count > list[j].count })
if len(list) > n {
list = list[:n]
}
return list
}
// drawWordCloud 在可绘制区域内按词频布局文字,返回白底画布
func drawWordCloud(words []wordFreq, drawable [][]bool, width, height int, fontData []byte) *image.RGBA {
// 解析字体一次,按字号缓存 face
fontParsed, err := opentype.Parse(fontData)
if err != nil {
log.Fatalf("[错误] 解析字体失败:%v", err)
}
faceCache := make(map[int]font.Face)
getFace := func(size int) font.Face {
if f, ok := faceCache[size]; ok {
return f
}
face, err := opentype.NewFace(fontParsed, &opentype.FaceOptions{
Size: float64(size),
DPI: 72,
Hinting: font.HintingFull,
})
if err != nil {
log.Fatalf("[错误] 创建字体 face 失败:%v", err)
}
faceCache[size] = face
return face
}
// 画布:白色背景
canvas := image.NewRGBA(image.Rect(0, 0, width, height))
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
canvas.Set(x, y, color.White)
}
}
// 占用网格
occupied := make([][]bool, height)
for y := 0; y < height; y++ {
occupied[y] = make([]bool, width)
}
// 可绘制区域(人像)的包围盒与质心,用于随机落点采样
minX, minY, maxX, maxY := width, height, 0, 0
cxSum, cySum, cnt := 0, 0, 0
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
if drawable[y][x] {
if x < minX {
minX = x
}
if x > maxX {
maxX = x
}
if y < minY {
minY = y
}
if y > maxY {
maxY = y
}
cxSum += x
cySum += y
cnt++
}
}
}
if cnt == 0 {
log.Fatalln("[错误] 蒙版可绘制区域为空,请检查蒙版图(深色人像)。")
}
centroidX := float64(cxSum) / float64(cnt)
centroidY := float64(cySum) / float64(cnt)
bboxW := float64(maxX - minX)
bboxH := float64(maxY - minY)
rng := rand.New(rand.NewSource(42)) // 固定随机种子,可复现(对应 Python random_state)
maxFontSize := float64(minInt(width, height)) * maxFontRatio
// 逐词布局
missStreak := 0
for _, wf := range words {
if wf.count <= 0 {
continue
}
// 字号 = 线性映射(对标 python: min + (max-min)*count/maxCount)
size := minFontSize + (maxFontSize-minFontSize)*float64(wf.count)/float64(words[0].count)
placed := false
for size >= minFontSize && !placed {
face := getFace(int(math.Round(size)))
offsets, bw, bh := rasterizeWord(face, wf.word)
if len(offsets) == 0 {
break
}
// 对标 prefer_horizontal=0.9:约 10% 的词语旋转 90° 竖排
if rng.Float64() > preferHorizon {
offsets, bw, bh = rotateOffsets(offsets, bw, bh)
}
placed = tryPlace(rng, canvas, occupied, drawable,
offsets, bw, bh, width, height,
centroidX, centroidY, bboxW, bboxH,
randomColor(rng))
if !placed {
size *= fontDecay
}
}
if placed {
placedCount++
missStreak = 0
} else {
missStreak++
// 连续多个词都放不下 → 画布已满,提前结束(避免无谓耗时)
if missStreak > 60 {
break
}
}
}
return canvas
}
// rasterizeWord 把文字按当前字体渲染成小图,返回非透明像素偏移列表与宽高
func rasterizeWord(face font.Face, word string) (offsets [][2]int, bw, bh int) {
m := face.Metrics()
ascent := m.Ascent.Ceil()
descent := m.Descent.Ceil()
bw = font.MeasureString(face, word).Ceil()
bh = ascent + descent
if bw <= 0 || bh <= 0 {
return nil, 0, 0
}
img := image.NewRGBA(image.Rect(0, 0, bw, bh))
d := &font.Drawer{
Dst: img,
Src: image.NewUniform(color.Black),
Face: face,
Dot: fixed.P(0, ascent),
}
d.DrawString(word)
for y := 0; y < bh; y++ {
for x := 0; x < bw; x++ {
if img.RGBAAt(x, y).A > 0 {
offsets = append(offsets, [2]int{x, y})
}
}
}
return offsets, bw, bh
}
// rotateOffsets 将文字像素偏移旋转 90°(顺时针)实现竖排
func rotateOffsets(offsets [][2]int, bw, bh int) ([][2]int, int, int) {
ro := make([][2]int, len(offsets))
for i, o := range offsets {
ro[i] = [2]int{bh - 1 - o[1], o[0]}
}
return ro, bh, bw
}
// tryPlace 尝试在随机落点放置词;成功则画到画布并标记占用
func tryPlace(rng *rand.Rand, canvas *image.RGBA, occupied, drawable [][]bool,
offsets [][2]int, bw, bh, width, height int,
centroidX, centroidY, bboxW, bboxH float64,
col color.Color) bool {
if bw > width || bh > height {
return false
}
// 以人像质心为中心的高斯采样,让大词优先落在中间(对标 python 中心螺旋)
sigmaX := math.Max(bboxW/4.0, 8)
sigmaY := math.Max(bboxH/4.0, 8)
for attempt := 0; attempt < tryPerRound; attempt++ {
cx := int(math.Round(centroidX + rng.NormFloat64()*sigmaX))
cy := int(math.Round(centroidY + rng.NormFloat64()*sigmaY))
cx = clamp(cx-bw/2, 0, width-bw)
cy = clamp(cy-bh/2, 0, height-bh)
// 碰撞检测:所有文字像素必须在可绘制区且未占用
ok := true
for _, o := range offsets {
px, py := cx+o[0], cy+o[1]
if px < 0 || py < 0 || px >= width || py >= height {
ok = false
break
}
if !drawable[py][px] || occupied[py][px] {
ok = false
break
}
}
if !ok {
continue
}
// 放置:标记占用 + 上色
for _, o := range offsets {
px, py := cx+o[0], cy+o[1]
occupied[py][px] = true
canvas.Set(px, py, col)
}
return true
}
return false
}
func clamp(v, lo, hi int) int {
if v < lo {
return lo
}
if v > hi {
return hi
}
return v
}
func minInt(a, b int) int {
if a < b {
return a
}
return b
}
// randomColor 生成随机彩色(对应 Python: "hsl(随机色相, 80%, 50%)")
func randomColor(rng *rand.Rand) color.Color {
hue := rng.Float64() * 360
return hslToRGB(hue, 0.8, 0.5)
}
// ============================================================
// 蒙版、轮廓、颜色、停用词
// ============================================================
// loadMaskGrid 读取蒙版,返回可绘制网格(深色人像=可绘制)
func loadMaskGrid(path string) ([][]bool, int, int) {
f, err := os.Open(path)
if err != nil {
log.Fatalf("[错误] 找不到蒙版图片:%s", path)
}
defer f.Close()
img, _, err := image.Decode(f)
if err != nil {
log.Fatalf("[错误] 蒙版图片解码失败:%v", err)
}
b := img.Bounds()
w, h := b.Dx(), b.Dy()
grid := make([][]bool, h)
for y := 0; y < h; y++ {
grid[y] = make([]bool, w)
for x := 0; x < w; x++ {
r, g, bl, _ := img.At(b.Min.X+x, b.Min.Y+y).RGBA()
lum := 0.299*float64(r>>8) + 0.587*float64(g>>8) + 0.114*float64(bl>>8)
grid[y][x] = lum < 127.5 // 深色 = 人像 = 可绘制
}
}
return grid, w, h
}
// drawContour 沿蒙版“深色人像”边界绘制钢蓝色轮廓
func drawContour(dst *image.RGBA, maskPath string, contourColor color.Color, contourWidth int) {
f, err := os.Open(maskPath)
if err != nil {
return
}
defer f.Close()
maskImg, _, err := image.Decode(f)
if err != nil {
return
}
b := maskImg.Bounds()
w, h := b.Dx(), b.Dy()
lum := make([][]float64, h)
for y := 0; y < h; y++ {
lum[y] = make([]float64, w)
for x := 0; x < w; x++ {
r, g, bl, _ := maskImg.At(b.Min.X+x, b.Min.Y+y).RGBA()
lum[y][x] = 0.299*float64(r>>8) + 0.587*float64(g>>8) + 0.114*float64(bl>>8)
}
}
isLight := func(x, y int) bool {
if x < 0 || y < 0 || x >= w || y >= h {
return false
}
return lum[y][x] > 127.5
}
half := contourWidth / 2
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
if isLight(x, y) {
continue
}
if isLight(x-1, y) || isLight(x+1, y) || isLight(x, y-1) || isLight(x, y+1) {
for dy := -half; dy <= half; dy++ {
for dx := -half; dx <= half; dx++ {
nx, ny := x+dx, y+dy
if nx >= 0 && ny >= 0 && nx < w && ny < h {
dst.Set(b.Min.X+nx, b.Min.Y+ny, contourColor)
}
}
}
}
}
}
}
// loadStopWords 加载停用词:内置 + stopwords.txt(一行一个词)
func loadStopWords() map[string]struct{} {
stop := make(map[string]struct{})
builtin := []string{
// Python 示例中 stopwords.update([...]) 显式加入的词
"工作", "就是", "个人", "没有", "村民委员会", "said",
// 常用英文停用词(对应 Python: wordcloud.STOPWORDS 的主要子集)
"a", "an", "the", "and", "or", "but", "if", "of", "to", "in", "on", "for",
"with", "as", "at", "by", "from", "up", "about", "into", "over", "after",
"is", "are", "was", "were", "be", "been", "being", "have", "has", "had",
"do", "does", "did", "will", "would", "can", "could", "should", "may",
"not", "no", "so", "too", "very", "just", "only", "then", "there",
"it", "its", "this", "that", "these", "those", "i", "you", "he", "she",
"we", "they", "them", "his", "her", "our", "your", "their", "my", "me",
"said", "say", "one", "two", "get", "got", "like", "make", "new", "now",
// 常用中文停用词
"的", "了", "是", "和", "在", "有", "就", "都", "而", "及", "与",
"一个", "我们", "你们", "他们", "这个", "那个", "什么", "怎么", "可以",
}
for _, w := range builtin {
stop[w] = struct{}{}
}
if data, err := os.ReadFile(stopWordsFile); err == nil {
for _, line := range strings.Split(string(data), "\n") {
w := strings.TrimSpace(line)
if w != "" {
stop[w] = struct{}{}
}
}
}
return stop
}
// hslToRGB HSL 转 RGB(S=0.8, L=0.5 时得到鲜艳彩色)
func hslToRGB(h, s, l float64) color.Color {
h = math.Mod(h, 360) / 360
hue2rgb := func(p, q, t float64) float64 {
if t < 0 {
t += 1
}
if t > 1 {
t -= 1
}
if t < 1.0/6.0 {
return p + (q-p)*6*t
}
if t < 0.5 {
return q
}
if t < 2.0/3.0 {
return p + (q-p)*(2.0/3.0-t)*6
}
return p
}
var q float64
if l < 0.5 {
q = l * (1 + s)
} else {
q = l + s - l*s
}
p := 2*l - q
r := uint8(math.Round(hue2rgb(p, q, h+1.0/3.0) * 255))
g := uint8(math.Round(hue2rgb(p, q, h) * 255))
bl := uint8(math.Round(hue2rgb(p, q, h-1.0/3.0) * 255))
return color.RGBA{R: r, G: g, B: bl, A: 255}
}

哲学管理(学)人生, 文学艺术生活, 自动(计算机学)物理(学)工作, 生物(学)化学逆境, 历史(学)测绘(学)时间, 经济(学)数学金钱(理财), 心理(学)医学情绪, 诗词美容情感, 美学建筑(学)家园, 解构建构(分析)整合学习, 智商情商(IQ、EQ)运筹(学)生存.---Geovin Du(涂聚文)
浙公网安备 33010602011771号