diff --git a/bobibo.go b/bobibo.go index d576e17..5519f75 100644 --- a/bobibo.go +++ b/bobibo.go @@ -2,7 +2,9 @@ package bobibo import ( "errors" + "image" "io" + "runtime" "github.com/orzation/bobibo/img" u "github.com/orzation/bobibo/util" @@ -55,20 +57,31 @@ func BoBiBo(ima io.Reader, isGif, isInverse bool, opts ...Option) (<-chan Art, e } } - inStream := make(chan img.Img) + var maxCpu = runtime.NumCPU() + + // clone ResizeAndGray fn then sort it. + ragFns := func(in <-chan img.Img) <-chan img.Gray { + num := 1 + if cap(in) > 1 { + // test + num = maxCpu - 3 + } + return u.SortChan( + u.CloneChanFn(img.ResizeAndGray(params.Scale), num, in)) + } mix := u.Multiply(img.ArtotBin(params.Inverse), u.Multiply(img.BinotImg(params.Threshold), - u.Multiply(img.TurnGray, - img.Resize(params.Scale), - ))) + ragFns, + )) - outStream := mix(inStream) - delays, err := putStream(inStream, params) - wrap := wrapOut(delays) + inStream, delays, err := analyzeImage(params) if err != nil { return nil, err } + + outStream := mix(inStream) + wrap := wrapOut(delays) return wrap(outStream), nil } @@ -77,41 +90,45 @@ var wrapOut = func(delays []int) func(<-chan []string) <-chan Art { if delays == nil || len(delays) == 0 { flag = false } - return u.GenChanFunc(func(out <-chan []string, wrapOut chan<- Art) { + return u.GenChanFn(func(in <-chan []string, out chan<- Art) { cnt := 0 - for o := range out { + for i := range in { if flag { - wrapOut <- Art{Content: o, Delay: delays[cnt]} + out <- Art{Content: i, Delay: delays[cnt]} + cnt++ } else { - wrapOut <- Art{Content: o, Delay: 0} + out <- Art{Content: i, Delay: 0} } - cnt++ } }) } -func putStream(in chan<- img.Img, params *Params) ([]int, error) { +func analyzeImage(params *Params) (<-chan img.Img, []int, error) { var delays []int + var inChan <-chan img.Img if params.Gif { p, dls, err := img.LoadAGif(params.Image) if err != nil { - return nil, err + return nil, nil, err } delays = dls - go inStream(in, p...) + inChan = newInStream(p...) } else { i, err := img.LoadAImage(params.Image) if err != nil { - return nil, err + return nil, nil, err } - go inStream(in, i) + inChan = newInStream(i) } - return delays, nil + return inChan, delays, nil } -func inStream[T img.Img](in chan<- img.Img, ims ...T) { +func newInStream[T image.Image](ims ...T) <-chan img.Img { + in := make(chan img.Img, len(ims)) defer close(in) - for _, v := range ims { - in <- v + for i, v := range ims { + imgV := img.NewImg(i, v) + in <- imgV } + return in } diff --git a/cli/makefile_cross b/cli/makefile_cross index f0056d6..9d6abbb 100644 --- a/cli/makefile_cross +++ b/cli/makefile_cross @@ -1,5 +1,5 @@ -OS = windows -ARCH = amd64 +OS = darwin +ARCH = arm64 EXE = bobibo_$(OS)_$(ARCH) IS_STATIC = 0 VERSION=V1.2.0 diff --git a/go.mod b/go.mod index 849ef6a..b0711ab 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,7 @@ module github.com/orzation/bobibo -go 1.20 +go 1.18 -require ( - golang.org/x/image v0.6.0 - golang.org/x/term v0.5.0 -) +require golang.org/x/term v0.5.0 require golang.org/x/sys v0.5.0 // indirect diff --git a/go.sum b/go.sum index 33e5c36..90a2592 100644 --- a/go.sum +++ b/go.sum @@ -1,35 +1,4 @@ -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/image v0.6.0 h1:bR8b5okrPI3g/gyZakLZHeWxAR8Dn5CyxXv1hLH5g/4= -golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/img/img.go b/img/img.go index c1d3de9..6d133d2 100644 --- a/img/img.go +++ b/img/img.go @@ -2,32 +2,29 @@ package img import ( "image" - "image/draw" "image/gif" _ "image/gif" _ "image/jpeg" _ "image/png" "io" - "math" + "strings" u "github.com/orzation/bobibo/util" - xdraw "golang.org/x/image/draw" ) -type Img = image.Image -type Pale = *image.Paletted - // use braille chars to draw arts. -var brailleMap = []rune("⠀⢀⡀⣀⠠⢠⡠⣠⠄⢄⡄⣄⠤⢤⡤⣤" + +const braille = "⠀⢀⡀⣀⠠⢠⡠⣠⠄⢄⡄⣄⠤⢤⡤⣤" + "⠐⢐⡐⣐⠰⢰⡰⣰⠔⢔⡔⣔⠴⢴⡴⣴⠂⢂⡂⣂⠢⢢⡢⣢⠆⢆⡆⣆⠦⢦⡦⣦⠒⢒⡒⣒⠲⢲⡲⣲⠖⢖⡖⣖⠶⢶⡶⣶" + "⠈⢈⡈⣈⠨⢨⡨⣨⠌⢌⡌⣌⠬⢬⡬⣬⠘⢘⡘⣘⠸⢸⡸⣸⠜⢜⡜⣜⠼⢼⡼⣼⠊⢊⡊⣊⠪⢪⡪⣪⠎⢎⡎⣎⠮⢮⡮⣮⠚⢚⡚⣚⠺⢺⡺⣺⠞⢞⡞⣞⠾⢾⡾⣾" + "⠁⢁⡁⣁⠡⢡⡡⣡⠅⢅⡅⣅⠥⢥⡥⣥⠑⢑⡑⣑⠱⢱⡱⣱⠕⢕⡕⣕⠵⢵⡵⣵⠃⢃⡃⣃⠣⢣⡣⣣⠇⢇⡇⣇⠧⢧⡧⣧⠓⢓⡓⣓⠳⢳⡳⣳⠗⢗⡗⣗⠷⢷⡷⣷" + - "⠉⢉⡉⣉⠩⢩⡩⣩⠍⢍⡍⣍⠭⢭⡭⣭⠙⢙⡙⣙⠹⢹⡹⣹⠝⢝⡝⣝⠽⢽⡽⣽⠋⢋⡋⣋⠫⢫⡫⣫⠏⢏⡏⣏⠯⢯⡯⣯⠛⢛⡛⣛⠻⢻⡻⣻⠟⢟⡟⣟⠿⢿⡿⣿") + "⠉⢉⡉⣉⠩⢩⡩⣩⠍⢍⡍⣍⠭⢭⡭⣭⠙⢙⡙⣙⠹⢹⡹⣹⠝⢝⡝⣝⠽⢽⡽⣽⠋⢋⡋⣋⠫⢫⡫⣫⠏⢏⡏⣏⠯⢯⡯⣯⠛⢛⡛⣛⠻⢻⡻⣻⠟⢟⡟⣟⠿⢿⡿⣿" + +var brailleMap = []rune(braille) // loading an image, only support png and jpeg. // if pass a gif, return the first embedded image. // if there is any thing wrong, panic. -func LoadAImage(f io.Reader) (Img, error) { +func LoadAImage(f io.Reader) (image.Image, error) { i, _, err := image.Decode(f) if err != nil { return nil, err @@ -44,93 +41,104 @@ func LoadAGif(f io.Reader) ([]Pale, []int, error) { return g.Image, g.Delay, nil } -// resizing the image with scale value, it won't change the ratio. +// resize the image with scale value, using nearestNeighbor. // return a stream chan function. -var Resize = func(scale float64) func(<-chan Img) <-chan Img { - return u.GenChanFunc(func(in <-chan Img, out chan<- Img) { +var ResizeAndGray = func(scale float64) func(<-chan Img) <-chan Gray { + return u.GenChanFn(func(in <-chan Img, out chan<- Gray) { for i := range in { - dx := int(math.Floor(scale * float64(i.Bounds().Dx()))) - dy := int(math.Floor(scale * float64(i.Bounds().Dy()))) - dst := image.NewRGBA(image.Rect(0, 0, dx, dy)) - xdraw.NearestNeighbor.Scale(dst, dst.Rect, i, i.Bounds(), xdraw.Over, nil) - out <- dst + out <- grayNearestNeighbor(scale, i) } }) } -// turning gray. -var TurnGray = u.GenChanFunc(func(in <-chan Img, out chan<- Img) { - for i := range in { - dx, dy := i.Bounds().Dx(), i.Bounds().Dy() - dst := image.NewGray(image.Rect(0, 0, dx, dy)) - draw.Draw(dst, dst.Bounds(), i, i.Bounds().Min, draw.Src) - out <- dst +func grayNearestNeighbor(scale float64, src Img) Gray { + DY, DX := src.value.Bounds().Dy(), src.value.Bounds().Dx() + dy := int(scale * float64(DY)) + dx := int(scale * float64(DX)) + tgt := make([][]uint8, dy) + + for i := 0; i < dy; i++ { + tgt[i] = make([]uint8, dx) + for j := 0; j < dx; j++ { + x, y := int((float64(j) / scale)), int((float64(i) / scale)) + r, g, b, _ := src.value.At(x, y).RGBA() + grayColor := (299*r + 587*g + 114*b) / 1000 + tgt[i][j] = uint8(grayColor >> 8) + } } -}) + return Gray{id: src.id, value: tgt} +} // turning image to 2d binary matrix. // use threshold to adjust the binarization. -var BinotImg = func(threshold int) func(<-chan Img) <-chan [][]bool { - return u.GenChanFunc(func(in <-chan Img, out chan<- [][]bool) { +var BinotImg = func(threshold int) func(<-chan Gray) <-chan [][]bool { + return u.GenChanFn(func(in <-chan Gray, out chan<- [][]bool) { for im := range in { - out <- img2bin(im, threshold) + out <- img2bin(im, &threshold) } }) } -func img2bin(im Img, th int) [][]bool { - if th < 0 || th > 255 { - th = int(otsu(im)) +func img2bin(im Gray, th *int) [][]bool { + if *th < 0 || *th > 255 { + *th = int(otsu(im)) } - dx, dy := im.Bounds().Dx(), im.Bounds().Dy() + dy, dx := im.size() reB := make([][]bool, dy) for i := range reB { reB[i] = make([]bool, dx) for j := range reB[i] { - r, _, _, _ := im.At(j, i).RGBA() - reB[i][j] = uint8(r>>8) >= uint8(th) + grayValue := im.value[i][j] + reB[i][j] = grayValue >= uint8(*th) } } return reB } // return the best threshold to binarize. -func otsu(im Img) uint8 { - var threshold uint8 = 0 +func otsu(im Gray) uint8 { + var threshold int = 0 const grayScale = 256 var u float32 - dx, dy := im.Bounds().Dx(), im.Bounds().Dy() - grayPro := make([]float32, grayScale) - pixelSum := dx * dy + var w0, u0 float32 + + dy, dx := im.size() + hist := make([]float32, grayScale) + sumPixel := dy * dx + for i := 0; i < dy; i++ { for j := 0; j < dx; j++ { - r, _, _, _ := im.At(j, i).RGBA() - grayPro[uint8(r>>8)]++ + grayValue := im.value[i][j] + hist[grayValue]++ } } - for i := 0; i < grayScale; i++ { - grayPro[i] *= 1.0 / float32(pixelSum) - u += float32(i) * grayPro[i] + + for i := range [grayScale]struct{}{} { + hist[i] *= 1.0 / float32(sumPixel) + u += float32(i) * hist[i] } - var w1, u1, gmax float32 - for i := 0; i < grayScale; i++ { - w1 += grayPro[i] - u1 += float32(i) * grayPro[i] - - tmp := u1 - u*w1 - sigma := tmp * tmp / (w1 * (1 - w1)) - if sigma >= gmax { - threshold = uint8(i) - gmax = sigma + var sigma float32 + for t := range [grayScale]struct{}{} { + w0 += hist[t] + u0 += float32(t) * hist[t] + if w0 == 0 || 1-w0 == 0 { + continue + } + + tmp := u0 - u*w0 + tmp = tmp * tmp / (w0 * (1 - w0)) + if tmp >= sigma { + sigma = tmp + threshold = t } } - return threshold + return uint8(threshold) } // turning 2d binary matrix to string array. // whether reverse color. var ArtotBin = func(w bool) func(<-chan [][]bool) <-chan []string { - return u.GenChanFunc(func(in <-chan [][]bool, out chan<- []string) { + return u.GenChanFn(func(in <-chan [][]bool, out chan<- []string) { for e := range in { out <- bin2art(e, w) } @@ -139,16 +147,18 @@ var ArtotBin = func(w bool) func(<-chan [][]bool) <-chan []string { func bin2art(bin [][]bool, isWhite bool) []string { dy, dx := len(bin)/4, len(bin[0])/2 - reStr := make([]string, dy) + bufStr := make([]strings.Builder, dy) + resStr := make([]string, dy) for i := 0; i < dy; i++ { for j := 0; j < dx; j++ { - reStr[i] += cell(i, j, bin, isWhite) + bufStr[i].WriteRune(cell(i, j, bin, isWhite)) } + resStr[i] = bufStr[i].String() } - return reStr + return resStr } -func cell(y, x int, bin [][]bool, isWhite bool) string { +func cell(y, x int, bin [][]bool, isWhite bool) rune { var reByte uint8 = 0 for i := 0; i < 4; i++ { for j := 0; j < 2; j++ { @@ -163,5 +173,5 @@ func cell(y, x int, bin [][]bool, isWhite bool) string { if isWhite { reByte = ^reByte } - return string(brailleMap[reByte]) + return brailleMap[reByte] } diff --git a/img/img_h.go b/img/img_h.go new file mode 100644 index 0000000..ac6e71d --- /dev/null +++ b/img/img_h.go @@ -0,0 +1,29 @@ +package img + +import ( + "image" +) + +type Pale = *image.Paletted + +type Img struct { + id int + value image.Image +} + +func NewImg(id int, value image.Image) Img { + return Img{id: id, value: value} +} + +type Gray struct { + id int + value [][]uint8 +} + +func (g Gray) Id() int { + return g.id +} + +func (g Gray) size() (int, int) { + return len(g.value), len(g.value[0]) +} diff --git a/img/img_test.go b/img/img_test.go deleted file mode 100644 index b119894..0000000 --- a/img/img_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package img - -import ( - "fmt" - "os" - "testing" - - u "github.com/orzation/bobibo/util" -) - -func TestXxx(t *testing.T) { - in := make(chan Img) - mix := u.Multiply(ArtotBin(false), - u.Multiply(BinotImg(128), - u.Multiply(TurnGray, Resize(0.50)))) - out := mix(in) - f, _ := os.Open("../w.gif") - i, err := LoadAGif(f) - if err != nil { - t.Error(err.Error()) - } - f.Close() - go func() { - defer close(in) - for _, p := range i { - in <- p - } - // in <- i - }() - for e := range out { - for _, v := range e { - fmt.Println(v) - } - } -} diff --git a/makefile b/makefile new file mode 100644 index 0000000..030e823 --- /dev/null +++ b/makefile @@ -0,0 +1,11 @@ +.PHONY: build block cpu mem + +test: + go test -bench=. -cpu=4 -blockprofile=block.pprof -cpuprofile=cpu.pprof -memprofile=mem.pprof +block: block.pprof + go tool pprof -http=:9999 block.pprof +cpu: cpu.pprof + go tool pprof -http=:9999 cpu.pprof +mem: mem.pprof + go tool pprof -http=:9999 mem.pprof + diff --git a/util/fp.go b/util/fp.go index 3fec6a5..f6520cc 100644 --- a/util/fp.go +++ b/util/fp.go @@ -1,12 +1,16 @@ package util -// to make a stream chan function -func GenChanFunc[T any, E any](logic func(in <-chan T, out chan<- E)) func(<-chan T) <-chan E { +import ( + "sync" +) + +// to make a stream channel function. +func GenChanFn[T any, E any](logic func(in <-chan T, out chan<- E)) func(<-chan T) <-chan E { return func(inChan <-chan T) <-chan E { - outChan := make(chan E) + outChan := make(chan E, cap(inChan)) go func() { - defer close(outChan) logic(inChan, outChan) + close(outChan) }() return outChan } @@ -18,3 +22,49 @@ func Multiply[T, E, R any](f func(E) R, g func(T) E) func(T) R { return f(g(v)) } } + +// cloning stream channel function means that more goroutines will work for it. +// and finally all stream will be faned in one channel. +func CloneChanFn[T, E any](fn func(<-chan T) <-chan E, num int, in <-chan T) <-chan E { + out := make(chan E, cap(in)) + + wg := sync.WaitGroup{} + wg.Add(len(in)) + + go func() { + wg.Wait() + close(out) + }() + + for i := 0; i < num; i++ { + go func() { + for v := range fn(in) { + out <- v + wg.Done() + } + }() + } + + return out +} + +// use the id to sort. +type sorter interface { + // start from zero will be eazier. + Id() int +} + +func SortChan[T sorter](in <-chan T) <-chan T { + out := make(chan T, cap(in)) + go func() { + defer close(out) + order := make([]T, cap(in)) + for s := range in { + order[s.Id()] = s + } + for _, v := range order { + out <- v + } + }() + return out +}