Normal view

There are new articles available, click to refresh the page.
Yesterday — 18 February 2025Main stream

Go+deepseek 帮你写 git 提交信息

By: wangyjjt
18 February 2025 at 16:03
wangyjjt:

背景

很早之前就想做一个借助 ai 能力自动生成 git 提交信息的工具,但是当时的 ai 大模型要么和智能不沾边,要么 API 接口价格太贵。现在好用大模型价格也降下来了,甚至的很多平台免费的模型也堪大用了,于是就做了这么个工具。

功能

工具集成在以前写的命令行工具中了,它的gcmsg子命令可以帮助开发人员快速生成 git 提交消息,确认后可以提交并推送到远程仓库。

git 地址 lwe

效果看图:

gcmsg.webp

模型选择

我开发这个功能的时候是,deepseek-V3刚推出不久,我测试后发现效果不错,而且价格也可以接受了,就打算对接一个 deepseek ,再接一个国外的 gemini 模型。但是来了,功能还没开发完(因为懒),R1 模型火出了圈,调试时接口一直超时,后来赠送的 token 也过期并且不让充值了,无奈只能去对接硅基流动部署deepseeek-V3,所以目前这个版本支持了两家的大模型,基本上可以非常低的成本使用了。

经过我对多个模型的测试,硅基上提供的千问Qwen/Qwen2.5-Coder-32B-Instruct模型也完全足够用,价格更低,而且硅基流动注册送的 2000w token 就能用好久好久了。

实现

实现原理也很简单,整个流程就是:利用 git diff 命令,获取代码的变动信息,再调用大模型 API 进行分析,大模型按照prompt要求将结果返回展示,最后完成提交、推送。

问题

目前还有几个问题没有解决很好

  • 不同的模型,“智商”不一样,有的未必能够正确理解你的指令返回数据,甚至返回的格式都是错误的,怎样兜底能力弱的模型?
  • 如何压缩 git diff 的结果,代码改动比较大或者初始化仓库场景,token 消耗爆炸不说,也容易超时
  • git diff 中敏感内容如何处理?目前只能简单的过滤

有经验或者好方案的可以交流一下

[Go]消息发布订阅的代码 单元测试始终不通过 请教一下

By: anviod
18 February 2025 at 11:56
anviod:

单元测试代码

func TestDynamicExpansion(t *testing.T) 测试不通过

package eventBus

import (
"testing"
"time"
)
// 该测试不通过
func TestDynamicExpansion(t *testing.T) {
ps, err := NewPubsub(2) // 初始化缓冲区大小为 2
if err != nil {
t.Fatalf("Failed to create pubsub: %v", err)
}
defer ps.Close()

ch, err := ps.Subscribe("test")
if err != nil {
t.Fatalf("Failed to subscribe to topic: %v", err)
}

// 发布 3 条消息,触发动态扩容
ps.Publish("test", "msg1")
ps.Publish("test", "msg2")
ps.Publish("test", "msg3")

// 读取消息
expectedMessages := map[string]bool{"msg1": false, "msg2": false, "msg3": false}
for i := 0; i < 3; i++ {
msg := <-ch
strMsg := msg.(string) // 类型断言
if _, ok := expectedMessages[strMsg]; !ok {
t.Errorf("Unexpected message: %v", strMsg)
}
expectedMessages[strMsg] = true
}

for msg, received := range expectedMessages {
if !received {
t.Errorf("Expected message %v not received", msg)
}
}

ps.Unsubscribe("test", ch)
ps.Close()
}

func TestBus_SubscribeAndPublish(t *testing.T) {
ps, err := NewPubsub(10)
if err != nil {
t.Fatalf("Failed to create pubsub: %v", err)
}
defer ps.Close()

topic := "test_topic"
ch, err := ps.Subscribe(topic)
if err != nil {
t.Fatalf("Failed to subscribe to topic: %v", err)
}

msg := "test_message"
go func() {
if err := ps.Publish(topic, msg); err != nil {
t.Errorf("Failed to publish message: %v", err)
}
}()

select {
case received := <-ch:
if received != msg {
t.Errorf("Expected message %v, but got %v", msg, received)
}
case <-time.After(time.Second):
t.Errorf("Timeout waiting for message")
}
}

func TestBus_UnSubscribe(t *testing.T) {
ps, err := NewPubsub(10)
if err != nil {
t.Fatalf("Failed to create pubsub: %v", err)
}
defer ps.Close()

topic := "test_topic"
ch, err := ps.Subscribe(topic)
if err != nil {
t.Fatalf("Failed to subscribe to topic: %v", err)
}

if err := ps.Unsubscribe(topic, ch); err != nil {
t.Fatalf("Failed to unsubscribe from topic: %v", err)
}

msg := "test_message"
go func() {
if err := ps.Publish(topic, msg); err != nil {
t.Errorf("Failed to publish message: %v", err)
}
}()

select {
case <-ch:
t.Errorf("Received message after unsubscribe")
case <-time.After(time.Second):
// Expected timeout
}
}

func BenchmarkBus_Publish(b *testing.B) {
ps, err := NewPubsub(100)
if err != nil {
b.Fatalf("Failed to create pubsub: %v", err)
}
defer ps.Close()

topic := "benchmark_topic"
ch, err := ps.Subscribe(topic)
if err != nil {
b.Fatalf("Failed to subscribe to topic: %v", err)
}

msg := "benchmark_message"
b.ResetTimer()

for i := 0; i < b.N; i++ {
go func() {
if err := ps.Publish(topic, msg); err != nil {
b.Errorf("Failed to publish message: %v", err)
}
}()

select {
case <-ch:
case <-time.After(time.Second):
b.Errorf("Timeout waiting for message")
}
}
}

func BenchmarkBus_ConcurrentPublish(b *testing.B) {
ps, err := NewPubsub(2048)
if err != nil {
b.Fatalf("Failed to create pubsub: %v", err)
}
defer ps.Close()

topic := "benchmark_topic"
ch, err := ps.Subscribe(topic)
if err != nil {
b.Fatalf("Failed to subscribe to topic: %v", err)
}

msg := "benchmark_message"
b.ResetTimer()

for i := 0; i < b.N; i++ {
go func() {
if err := ps.Publish(topic, msg); err != nil {
b.Errorf("Failed to publish message: %v", err)
}
}()

select {
case <-ch:
case <-time.After(time.Second):
b.Errorf("Timeout waiting for message")
}
}
}

func TestPubsub(t *testing.T) {
ps, err := NewPubsub(10)
if err != nil {
t.Fatalf("Failed to create pubsub: %v", err)
}

topic := "testTopic"
msg := "testMessage"

// Subscribe to the topic
ch, err := ps.Subscribe(topic)
if err != nil {
t.Fatalf("Failed to subscribe to topic: %v", err)
}

// Publish a message to the topic
err = ps.Publish(topic, msg)
if err != nil {
t.Fatalf("Failed to publish message: %v", err)
}

// Verify the message is received
select {
case receivedMsg := <-ch:
if receivedMsg != msg {
t.Fatalf("Expected message %v, but got %v", msg, receivedMsg)
}
case <-time.After(time.Second):
t.Fatal("Timeout waiting for message")
}

// Unsubscribe from the topic
err = ps.Unsubscribe(topic, ch)
if err != nil {
t.Fatalf("Failed to unsubscribe from topic: %v", err)
}

// Close the pubsub
err = ps.Close()
if err != nil {
t.Fatalf("Failed to close pubsub: %v", err)
}
}

源代码如下

package eventBus

import (
"errors"
"sync"
"time"
)

const (
pubTimeout = time.Millisecond * 10
)

var (
ErrPubsubTimeout = errors.New("failed to send message to topic because of timeout")
ErrChannelFull   = errors.New("channel is full")
)

type Pubsub interface {
Publish(topic string, msg interface{}) error
Subscribe(topic string) (chan interface{}, error)
Unsubscribe(topic string, ch chan interface{}) error
Close() error
}

type pubsub struct {
size     int
channels map[string]map[chan interface{}]struct{}
mu       sync.RWMutex
}

var channelPool *sync.Pool

// NewPubsub 初始化 pubsub 系统,使用默认的通道大小
func NewPubsub(size int) (Pubsub, error) {
// 初始化通道池,使用给定的大小作为默认缓冲区大小
channelPool = &sync.Pool{
New: func() interface{} {
return make(chan interface{}, size)
},
}

return &pubsub{
size:     size,
channels: make(map[string]map[chan interface{}]struct{}),
}, nil
}

// getChannelFromPool 从池中获取一个通道
func getChannelFromPool() chan interface{} {
return channelPool.Get().(chan interface{})
}

// putChannelToPool 将通道放回池中,并清空其内容
func putChannelToPool(ch chan interface{}) {
for len(ch) > 0 {
<-ch
}
channelPool.Put(ch)
}

// Publish 向订阅者发送消息
func (m *pubsub) Publish(topic string, msg interface{}) error {
m.mu.RLock() // 读锁,允许并发读取
defer m.mu.RUnlock()

if chs, ok := m.channels[topic]; ok {
for ch := range chs {
if err := m.publish(topic, ch, msg); err != nil {
return err
}
}
}
return nil
}

// publish 尝试向单个通道发送消息,并处理通道扩容
func (m *pubsub) publish(topic string, ch chan interface{}, msg interface{}) error {
// 尝试向现有通道发送消息
select {
case ch <- msg: // 尝试发送消息
return nil
default:
// 通道已满,需要动态扩容
newCh := make(chan interface{}, cap(ch)*2) // 扩容为原来的两倍

// 使用锁确保旧通道被正确替换为新通道
m.mu.Lock()
defer m.mu.Unlock()

// 将旧通道中的消息移动到新通道
go func() {
for v := range ch {
newCh <- v
}
}()

// 更新通道映射,指向新通道
if chs, ok := m.channels[topic]; ok {
delete(chs, ch)         // 删除旧通道
chs[newCh] = struct{}{} // 添加新扩容的通道
}

// 尝试向新扩容的通道发送消息
select {
case newCh <- msg:
return nil
case <-time.After(pubTimeout):
return ErrPubsubTimeout
}
}
}

// Subscribe 为给定主题创建一个新的订阅者通道
func (m *pubsub) Subscribe(topic string) (chan interface{}, error) {
ch := getChannelFromPool() // 从池中获取一个通道

m.mu.Lock()
defer m.mu.Unlock()

if _, ok := m.channels[topic]; !ok {
m.channels[topic] = make(map[chan interface{}]struct{})
}
m.channels[topic][ch] = struct{}{} // 存储通道到主题的映射中
return ch, nil
}

// Unsubscribe 从给定主题中移除订阅者通道
func (m *pubsub) Unsubscribe(topic string, ch chan interface{}) error {
m.mu.Lock()
defer m.mu.Unlock()

if chs, ok := m.channels[topic]; ok {
delete(chs, ch)      // 从主题的订阅者列表中移除通道
putChannelToPool(ch) // 将通道放回池中
}
return nil
}

// Close 关闭 pubsub 系统,关闭所有通道并清理资源
func (m *pubsub) Close() error {
m.mu.Lock()
defer m.mu.Unlock()

for topic, chs := range m.channels {
for ch := range chs {
close(ch)
}
delete(m.channels, topic)
}
return nil
}

测试结果

Running tool: C:\Program Files\Go\bin\go.exe test -timeout 30s -run ^TestDynamicExpansion$ codex/src/eventBus

panic: test timed out after 30s
running tests:
TestDynamicExpansion (30s)

goroutine 7 [running]:
testing.(*M).startAlarm.func1()
C:/Program Files/Go/src/testing/testing.go:2484 +0x394
created by time.goFunc
C:/Program Files/Go/src/time/sleep.go:215 +0x2d

goroutine 1 [chan receive]:
testing.(*T).Run(0xc000003340, {0xef7e20?, 0x7ffef7500e50?}, 0xf02878)
C:/Program Files/Go/src/testing/testing.go:1859 +0x414
testing.runTests.func1(0xc000003340)
C:/Program Files/Go/src/testing/testing.go:2279 +0x37
testing.tRunner(0xc000003340, 0xc00002bc70)
C:/Program Files/Go/src/testing/testing.go:1792 +0xcb
testing.runTests(0xc0000080d8, {0x102fde0, 0x4, 0x4}, {0x1035d00?, 0x7?, 0x1034b80?})
C:/Program Files/Go/src/testing/testing.go:2277 +0x4b4
testing.(*M).Run(0xc00007a320)
C:/Program Files/Go/src/testing/testing.go:2142 +0x64a
main.main()
_testmain.go:55 +0x9b

goroutine 6 [sync.RWMutex.Lock]:
sync.runtime_SemacquireRWMutex(0xd75301?, 0x40?, 0xec97c0?)
C:/Program Files/Go/src/runtime/sema.go:105 +0x25
sync.(*RWMutex).Lock(0x8080808080808074?)
C:/Program Files/Go/src/sync/rwmutex.go:155 +0x6a
codex/src/eventBus.(*pubsub).publish(0xc000020a80, {0xef3abd, 0x4}, 0xc00012a2a0, {0xec3120, 0xf2f220})
d:/code/X/codex/src/eventBus/bus.go:87 +0xc5
codex/src/eventBus.(*pubsub).Publish(0xc000020a80, {0xef3abd, 0x4}, {0xec3120, 0xf2f220})
d:/code/X/codex/src/eventBus/bus.go:68 +0x14b
codex/src/eventBus.TestDynamicExpansion(0xc000003500)
d:/code/X/codex/src/eventBus/bus_test.go:23 +0x1fb
testing.tRunner(0xc000003500, 0xf02878)
C:/Program Files/Go/src/testing/testing.go:1792 +0xcb
created by testing.(*T).Run in goroutine 1
C:/Program Files/Go/src/testing/testing.go:1851 +0x3f6
FAILcodex/src/eventBus30.025s
FAIL
Before yesterdayMain stream

镜头的变幻就是故事|Midjourney V5.2 Zoomout 测试

By: Steven
26 June 2023 at 00:18

➡阅读更多 AIGC 相关内容

最近一直都非常忙,所以连续 20 来天都没有碰过 Midjourney 了。前两天在社交媒体上看到,新推出的 V5.2 中有一个向外扩写的功能,因为此前已经在 PS+SD 的组合中见过这类拓展画面的应用思路,所以很想看看 MJ 的 Zoomout 能做出什么样的东西来。趁着端午假期这个空档,我集中跑了几波测试,有一些小小的心得,在此记录一下。

总体结论有三个:

1、Zoomout 可以无限次数地向外扩展,但随着镜头的拉远,Midjourney 自身的联想能力并不足以做出任何有意思的画面,不刻意控制地放大出来的画面,到了第 3~5 步之后,就会明显变得乏味和缺乏美感。

2、通过刻意地控制画幅比例、扩张倍数,以及针对性地调整 prompt 的描述,可以利用这个功能讲出有意思的故事。关键在于,使用者对于「镜头语言」的理解,以及对运镜和故事之间联系的掌控程度。

3、对工业设计的辅助甚微,做点「花活儿」可以,一旦涉及到逻辑,依旧不行。

Zoomout 功能的主交互界面

测试内容目录:

1、通过默认的 Zoomout X2 按钮连续放大 3 次

2、通过默认的 Zoomout X2 按钮连续放大 15 次

3、通过自定义 Zoomout 微调构图

4、通过自定义 Zoomout 构建人物画像

5、通过自定义 Zoomout 构建人物性格

6、通过自定义 Zoomout 完善场景氛围

7、在 niji 中应用自定义 Zoomout 构建人物和场景

8、自定义 Zoomout 构建情绪与故事

9、通过焦点变化构建故事的场景

10、通过镜头变化,构建故事的起承转合

以下为部分测试过程记录:

test case no.1:通过默认的 Zoomout X2 按钮连续放大 3 次

⬆ 点击以全屏查看图片 Click to view the image in full screen

操作方式:连续 3 次放大图像两倍,不对 prompt 进行修改,也不对画幅做设置。

输出成果:在奔跑的场景中增加了后方的人,有一点点故事性,但继续放大后会明显失焦,花面焦点始终在最开始的小女孩身上,继续放大生成的场景和人物都是模糊的。

test case no.2:通过默认的 Zoomout X2 按钮连续放大 15 次

⬇ 点击以全屏查看图片 Click to view the image in full screen

操作方式:连续 15 次放大图像两倍,不对 prompt 进行修改,也不对画幅做设置。

输出成果:外围拓展的场景越宏大,有效信息和故事性就越低,除了在阴影中无意间冒出的人影,没有任何惊喜和意料之外,拓展的画面也很单调乏味。

test case no.3:通过自定义 Zoomout 微调构图

⬇ 点击以全屏查看图片 Click to view the image in full screen

操作方式:不对 prompt 进行修改,按 1.1 和 1.2 的拓展比例小幅度调整画幅。

输出成果: 初始图像是近景特写,根据图像本身的特点,对画幅进行小幅度地微调来获得完整的全景镜头,以及合适的构图比例。

test case no.4:通过自定义 Zoomout 构建人物画像

⬇ 点击以全屏查看图片 Click to view the image in full screen

操作方式:先生成一个黄色漩涡图案,然后拓展时改写 prompt 为一只眼睛,进而生成一个带特征的面部局部画面,再次拓展时修改描述词为一个洞穴中的原始部落男性。

输出成果: 成功构建了一个有目标特征「黄色漩涡瞳孔」的男性角色,通过控制拓展比例以达到最终效果—-人物整体和局部特征均得以完整呈现的画面。

test case no.5:通过自定义 Zoomout 构建人物性格

⬇ 点击以全屏查看图片 Click to view the image in full screen

操作方式:先生成一个红色皮夹克的女性胸像,再改写 prompt 获得其坐在摩托车上的局部画面,再改写画幅比例获得完整的人物与车辆的全景照。

输出成果: 成功构建了一个有目标特征「红色皮衣+摩托车」的女性角色,通过控制拓展比例以达到最终效果—-人物细节和整体氛均衡的画面。

test case no.6:通过自定义 Zoomout 完善场景氛围

⬇ 点击以全屏查看图片 Click to view the image in full screen

操作方式:在初次生成的几批图像中,选择合适的画风和画面主体,再根据已有画面特征修改画幅比例。

输出成果: 在选定风格和主体后,将竖幅主体拓展为气势更足的全景影像。关键是拓展比例并非默认的 2 倍或 1.5 倍,而是根据实际需求来控制比例,同时也需要关注怎样的画幅比例可以传达对应的氛围。最终图像画幅比例是 3:1,适合展现有足够细节的宽幅场景。

test case no.7:在 niji 中应用自定义 Zoomout 构建人物和场景

⬇ 点击以全屏查看图片 Click to view the image in full screen

操作方式:

step 1、使用 niji 5 的 style original 生成一个细节丰富的初始人物;

step 2、以 1.2 的 Zoomout 比例纵向拓展出人物的半身画像,画幅比例是 1:2;

step 3、以 1.1 的 Zoomout 比例和 2:1 的画幅比例重构画面,得到外围场景;

step 4、以 1.2 的 Zoomout 比例和 3:4 的画幅比例重构画面,生成人物全身像;

step 5、改写 prompt 添加「宫殿」关键词,以 1.65 的 Zoomout 比例和 3:2 的画幅比例重构画面,生成人物在场景中的全景画面。

输出成果: 虽然人物细节和场景氛围的融合程度还不错,但因为漫画角色的细节较多,在多次 Zoomout 的过程中,场景的丰富会逐渐抢掉中心人物的视觉焦点。因此在每一次修改画幅比例与关键词的时候,需要多加注意对视觉元素的控制。

test case no.8:自定义 Zoomout 构建情绪与故事

⬇ 点击以全屏查看图片 Click to view the image in full screen

操作方式:

step 1、生成一个情绪和神情符合目标的初始人物;

step 2、改写 prompt 同时添加「马」关键词,以 2 的 Zoomout 比例和 3:4 的画幅比例重构画面,生成后续画面的基础,此时需要注意人物与马的位置关系,否则后续生成的画面会非常扭曲怪异;

step 3、以 1.05 的 Zoomout 比例和 2:1 的画幅比例重构画面,生成完整的马匹造型与部份环境信息;

step 4、对比改写 prompt 产生的变化,黑发组不改描述词,以 1.1 的 Zoomout 比例和 3:4 的画幅比例重构画面;白发组添加「巨大镜子」关键词,以 1.6 的 Zoomout 比例和 3:4 的画幅比例重构画面。

输出成果:通过控制 Zoomout 的幅度、画幅比例和 prompt 的调整,可以生成指定场景的画面,且人物的神态到位、情绪饱满,整体画面焦点清晰。但美中不足是,构图不够自由。

test case no.9:通过焦点变化构建故事的场景

⬇ 点击以全屏查看图片 Click to view the image in full screen

操作方式:

step 1、生成一个在河岸上的粽子;

step 2、修改 prompt 为「熊宝宝正准备吃粽子」,以 2 的 Zoomout 比例和 3:4 的画幅比例重构画面;

step 3、修改 prompt 为「小熊一家在野餐」,以 1.2 的 Zoomout 比例和 4:3 的画幅比例重构画面。

输出成果:通过对 prompt 的修改,控制 Zoomout 的幅度、画幅比例,可以改变画面中的焦点和表达主题,适合不同文化元素之间的混搭。

test case no.10:通过镜头变化,构建故事的起承转合

⬇ 点击以全屏查看图片 Click to view the image in full screen

操作方式:

step 1、生成一幅鲜花山谷的画面,人物要明显;

step 2、修改 prompt 为「一面巨大的镜子在草地上」,以 2 的 Zoomout 比例和 3:4 的画幅比例重构画面,此处竖构图是为了生成较高的全身落地镜;

step 3、修改 prompt 为「少女站在镜子前」,以 1.5 的 Zoomout 比例和 3:2 的画幅比例重构画面,改为横构图是为了囊括少女全身以及环境信息。

输出成果:通过改变画面中的焦点和增加元素,在镜头逐渐拉远的过程中,故事缓缓托出。

➡阅读更多 AIGC 相关内容


我的整体感受是:

通过 Midjourney V5.2 的 Zoomout 无限拓展,一次次修改画幅比例、提示词内容,可以用镜头语言的变化来讲故事了,也可以基于一些初始的「点子」延展成有意思的融合作品。但越是这样,越发显得对话式、指令式的交互界面( SD 那种也不算图形交互 )的局限太大了,我很希望今年之内能发展出图形交互界面。

没错,今年 AI 的爆发指向了一个新的趋势:对话式交互界面。但人类之所以发明绘画,开始通过设计图来制作各式各样的新工具,恰恰就是因为语言本身的效率太低。这个逻辑其实也可以从媒体形态上找到端倪:文字–> 图像–> 视频。仅仅依靠对话,我们无法构建出一个一把剪刀;仅仅通过语言表达的播客,也无法传达任何需要视觉才可以精准理解的信息。对话指令的交互界面与图形交互界面之间的关系,并非只是 dos 和 windows 之间的差异,更重要的点在于,后者可以更直观地完成交互,以及精准地进行创作行为。AIGC 的重点不仅仅只是 AI,而是我们如何使用 AI 进行「Generative Content」。

我说一句话,AI 给我一个东西,这不是创作。

创作是一个生命在主观意志的驱使下,刻意的、有目的地表达其心中所想。

因为 GPT 的爆发而说对话式交互是未来,这样的断言是过于冲动的。只要是一个严肃的创作者,就会立刻意识到,真正的创作一定需要多纬度的交互界面。这其中不仅仅包含对话指令,同样更需要图形界面以及在数字虚拟空间中的三维交互。AIGC 工具与 PS、表格、PPT、思维导图等已有工具的结合,就是这类多维交互的雏形。

那一刻,我们不会等太久。

➡阅读更多 AIGC 相关内容

表达的精度就是人类外延的尺度|Midjourney 
V6 Alpha 自然语言生图测试

By: Steven
26 December 2023 at 01:14

Midjourney V6 的质感和细节,真的是飞跃式的成长!

和今年三月相比,已经完全脱胎换骨了。对自然语言的理解和再表达,也已经在渐渐脱离「咒语」的局限,结合 ChatGPT 的语言转译,一个人能够用母语把尚不明确的观念表达清晰,愈发显得重要。

点击图片,可查看原始尺寸高清大图:

当 AI 越来越擅长理解人类的自然语言,我们就愈发迫切地要掌握「用语言表达思想」这件事情。

因为语言的精度和颗粒度,将会在人类与 AI 的相处、合作中,展现出人类智力的上限所在,以及外延的纵深能够得着多远。

Upscale from Variations
Upscale(Subtle)
Upscale from Variations
Upscale(Subtle)
Upscaled (Subtle)
Upscaled (Creative)
Upscaled (Subtle)
Upscaled (Creative)
–Style 50
–Style 100
–Style 250
–Style 750
–Style 1000

大津算法(OTSU)

31 December 2017 at 13:55

在图像处理领域,我们会遇到如下需求:把图像中的目标物体和背景分开。比如背景用白色表示,目标物体用黑色表示。此时我们知道目标物体的灰度值相互接近,背景灰度值相互接近,那么用大津算法能很好把目标从背景当中区分开来。

算法思想

目标

比如对于下面这张灰度图片

origin

目标物体是中间黑色的几何物体,我们想让这些物体和背景区分更明显一些,比如让物体为纯黑,背景全白。那么我们就需要找到一个合适的阈值,使图片上灰度值大于这个阈值的像素点为255(白色),灰度值小于阈值的像素点为0(黑色)。也就是变成下面这幅图:

output

那么大津算法需要处理的就是找到最佳的阈值,让目标和物体尽可能分离开。

灰度直方图

为了找到合适的阈值,我们首先观察原图的灰度直方图📊:

histogram

这是用 Matlab 对原图形成的灰度直方图,灰度直方图的含义是统计图像中不同灰度值的分布情况。由图我们可以看出两个尖峰,在灰度值为0~20的地方存在一个尖峰,代表原图中有大量像素点灰度值为0~20,经观察我们可以认为这部分对应于目标物体。同理我们可以看出背景的灰度值大多集中于100~140之间,为了让目标和背景区分更加明显,我们想让目标物体的灰度值全为0,背景的灰度值全为255,这种处理手法也称为二值化法。

那么阈值取多少合适呢?从图上看似乎取50~100中的任意一点都可以,但是实际情况并不想参考图那样明显,有些图片背景和目标物体较为接近,我们需要一个算法来找到最优阈值才行。

聚类

首先我们思考什么样的东西才能成为一类,而我们又是怎么分类的。对于参考图来说,我们可以认为灰度值接近的为一类,灰度值不接近的不是同一类。那我们又是如何衡量灰度值接近的程度呢?这里面就需要用到方差的概念。
方差相比大家都了解,同一类的物体方差小,不同类的物体方差大。所以对于此图我们希望分类的结果是对于灰度值相近的同一类物体,它的方差最小,称为类内方差最小。灰度值不接近的不同类物体,它的方差大,称为类间方差最大。

步骤

所以步骤总结如下:
首先我们要形成参考图的灰度直方图,这样方便我们找到最佳阈值。
接下来我们通过穷举每一个灰度值,计算以此为阈值的类内和类间方差。
找到能形成类内方差最小的或类间方差最大的阈值,这个就是我们要找的最佳阈值。

算法

下面以两类分割讲解下具体的算法,实际上大津算法可以分割多类出来。

因为 Medium 不支持显示 MathJax 语法的公式,所以对这部分感兴趣的直接移步到《大津算法(OTSU)》查看吧。

代码实现

C语言实现

/* OTSU 算法
* *src 存储灰度图像,width 图像宽,height 图像长
* 返回最佳阈值
*/
int otsu(const int *src, int width, int height)
{
int histogram[256]; //存储灰度直方图,这里为256色灰度
int t,thred;
float wf,wb,ub,uf,curVal,maxVal;
int sumb=0,sumf=0,sumW=0,sumPixel=width*height;
wb=wf=maxVal=0.0f;

//求灰度直方图
memset(histogram,0,sizeof(histogram));
for(i=0;i<width*height;i++)
{
histogram[src[i]]++;
}

for (i=0;i<256;i++)
sumW+=i*histogram[i];

//枚举每个灰度
for(t=0;t<256;t++)
{
//求两类类概率密度
wb+=histogram[t];
wf=sumPixel-wb;
if (wb==0||wf==0)
continue;

//求类均值
sumb+=i*histogram[t];
sumf=sumW-sumb;
ub=sumb/wb;
uf=sumf/wf;

//求当前类间方差
curVal=wb*wf*(ub-uf)*(ub-uf);
if(curVal>maxVal)
{
thred=t;
maxVal=curVal;
}
}
return thred;
}
❌
❌