├── icg.jpg ├── tmp-140.jpg ├── tmp-150.jpg ├── logger └── logger.go ├── .gitignore ├── yukkuri └── main.go ├── command └── cmd.go ├── ascii ├── writer.go ├── yukkuri.go ├── grey.go ├── ascii_ykr.go └── ascii.go ├── README.md ├── tmp_ykr.txt ├── LICENSE └── tmp.txt /icg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fx-slayer/yukkuri/HEAD/icg.jpg -------------------------------------------------------------------------------- /tmp-140.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fx-slayer/yukkuri/HEAD/tmp-140.jpg -------------------------------------------------------------------------------- /tmp-150.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fx-slayer/yukkuri/HEAD/tmp-150.jpg -------------------------------------------------------------------------------- /logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import "log" 4 | 5 | func InitLog(){ 6 | log.SetFlags(0) 7 | log.SetPrefix("yukkuri > ") 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | .idea/yukkuri.iml 14 | .idea/workspace.xml 15 | .idea/modules.xml 16 | .idea/misc.xml 17 | .idea/inspectionProfiles/Project_Default.xml 18 | .idea/dictionaries/shiin.xml 19 | 20 | # bin 21 | bin/ 22 | build.bat 23 | -------------------------------------------------------------------------------- /yukkuri/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "ascii" 5 | "command" 6 | "flag" 7 | "log" 8 | "logger" 9 | ) 10 | 11 | func main(){ 12 | logger.InitLog() 13 | c := command.ParseCmd() 14 | if c.Help{ 15 | flag.Usage() 16 | return 17 | } 18 | if c.ImgPath != ""{ 19 | log.Printf("load file [%s]" ,c.ImgPath) 20 | ykr := ascii.NewYukkuri(c) 21 | if gray ,err := ykr.TransImgToGrey();err != nil{ 22 | log.Printf("failed to converted the file into ascii %v\n" ,err) 23 | }else{ 24 | converter := ascii.NewAsc11Converter(gray ,c.Ykr) 25 | ykr.TransImgToAsc(converter) 26 | } 27 | }else{ 28 | log.Println("no file is specified") 29 | return 30 | } 31 | } -------------------------------------------------------------------------------- /command/cmd.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "flag" 5 | ) 6 | 7 | type Cmd struct { 8 | ImgPath string 9 | Help bool 10 | Threshold int 11 | Filename string 12 | TmpImgName string 13 | AscWidth int 14 | AscHeight int 15 | Ykr bool // true -> 使用AsciiYukkuri处理 ,false -> 使用Ascii 16 | } 17 | 18 | 19 | func ParseCmd() Cmd{ 20 | cmd := Cmd{} 21 | flag.StringVar(&cmd.ImgPath ,"f" ,"" ,"specify an image file to transcode") 22 | flag.BoolVar(&cmd.Help ,"h" ,false ,"help") 23 | flag.IntVar(&cmd.Threshold ,"t" ,140 ,"set threshold for image grey processing") 24 | flag.StringVar(&cmd.Filename ,"n" ,"tmp.txt" ,"set output file name") 25 | flag.StringVar(&cmd.TmpImgName ,"m" ,"" ,"save temporary image") 26 | flag.IntVar(&cmd.AscHeight ,"H" ,-1 ,"set ascii max height") 27 | flag.IntVar(&cmd.AscWidth ,"W" ,-1 ,"set ascii max width") 28 | flag.BoolVar(&cmd.Ykr ,"y" ,false ,"make the output ascii more like yukkuri style,but more likely to be unrecognizable." ) 29 | flag.Parse() 30 | flag.Usage = func() { 31 | flag.PrintDefaults() 32 | } 33 | return cmd 34 | } 35 | 36 | -------------------------------------------------------------------------------- /ascii/writer.go: -------------------------------------------------------------------------------- 1 | package ascii 2 | 3 | import ( 4 | "bytes" 5 | "image" 6 | "image/jpeg" 7 | "os" 8 | "sync" 9 | ) 10 | 11 | type ImgWriter struct {} 12 | 13 | var imgWriter ImgWriter 14 | var once sync.Once 15 | 16 | func NewImgWriter() ImgWriter{ 17 | once.Do(func() { 18 | imgWriter = ImgWriter{} 19 | }) 20 | return imgWriter 21 | } 22 | 23 | type ImageType int 24 | 25 | func (w ImgWriter)writeImg(filePath string ,img image.Image) error{ 26 | if f ,err := os.OpenFile(filePath ,os.O_WRONLY | os.O_CREATE ,os.ModePerm);err != nil{ 27 | return err 28 | }else{ 29 | defer f.Close() 30 | 31 | return jpeg.Encode(f ,img ,&jpeg.Options{Quality:100}) 32 | } 33 | } 34 | 35 | func (w ImgWriter)writeAscii(filepath string ,asc [][]string) error{ 36 | if f ,err := os.OpenFile(filepath ,os.O_WRONLY | os.O_CREATE ,os.ModePerm);err != nil{ 37 | return err 38 | }else{ 39 | defer f.Close() 40 | 41 | buf := bytes.Buffer{} 42 | for i:=0 ;i 原图像的构造会很大程度地影响还原效果。线条越简单的图像越容易在较少的输出(<50*50)下被还原。 84 | 85 | #### 输出示例 86 | 87 | - 命令:`yukkuri -f icg.jpg -W 150 -H 150` 88 | 89 | 输出:[tmp.txt](tmp.txt) 90 | 91 | - 启用`-y`参数使用另一套ASCII字符: `yukkuri -f icg.jpg -t 152 -n tmp_ykr.txt -y` 92 | 93 | `-y`会将输入自动压缩3*3倍 94 | 95 | 输出:[tmp_ykr.txt](tmp_ykr.txt) 96 | 97 | > 建议使用 notepad++ / Consolas 打开 98 | -------------------------------------------------------------------------------- /ascii/yukkuri.go: -------------------------------------------------------------------------------- 1 | package ascii 2 | 3 | import ( 4 | "command" 5 | "errors" 6 | "github.com/nfnt/resize" 7 | "image" 8 | "image/jpeg" 9 | "image/png" 10 | "log" 11 | "os" 12 | "strings" 13 | ) 14 | 15 | type Yukkuri struct { 16 | Img image.Image 17 | File *os.File 18 | Greyer GreyFunction 19 | Cmd command.Cmd 20 | } 21 | 22 | func NewYukkuri(cmd command.Cmd) *Yukkuri{ 23 | ykr := &Yukkuri{ 24 | Cmd:cmd, 25 | Greyer:NewGreyHandler(), 26 | } 27 | return ykr 28 | } 29 | 30 | func (ykr *Yukkuri)TransImgToGrey() (*image.Gray ,error){ 31 | if f ,err := os.OpenFile(ykr.Cmd.ImgPath ,os.O_RDONLY ,os.ModeTemporary);err != nil{ 32 | return nil ,err 33 | }else{ 34 | defer f.Close() 35 | 36 | ykr.File = f 37 | tmp := strings.Split(f.Name() ,".") 38 | suffix := strings.ToLower(tmp[len(tmp) - 1]) 39 | var decodeErr error 40 | switch suffix { 41 | case "jpg": 42 | ykr.Img ,decodeErr = jpeg.Decode(f) 43 | case "jpeg": 44 | ykr.Img ,decodeErr = jpeg.Decode(f) 45 | case "png": 46 | ykr.Img ,decodeErr = png.Decode(f) 47 | default: 48 | return nil ,errors.New("unsupported image type") 49 | } 50 | if decodeErr != nil{ 51 | return nil ,decodeErr 52 | } 53 | 54 | rtg := ykr.Img.Bounds() 55 | log.Printf("image size %d*%d" ,ykr.Img.Bounds().Max.X ,ykr.Img.Bounds().Max.Y) 56 | 57 | // resize 58 | 59 | if ykr.Cmd.AscWidth > 0 && ykr.Cmd.AscHeight > 0{ 60 | ykr.Img = resize.Resize(uint(ykr.Cmd.AscWidth) ,uint(ykr.Cmd.AscHeight) ,ykr.Img ,resize.Lanczos3) 61 | log.Printf("resize image to %d*%d\n" ,ykr.Cmd.AscWidth ,ykr.Cmd.AscHeight) 62 | } 63 | 64 | // gray handle 65 | grey := image.NewGray(ykr.Img.Bounds()) 66 | for i:=rtg.Min.X ;i -1 ,means grey == 0(t)255 ,decided by threshold 51 | 52 | return current x ,y and grey value 53 | */ 54 | func (gh GreyHandler)GreyFunc(x int ,y int ,img image.Image ,threshold int)(int, int, color.Gray){ 55 | 56 | var maxRatio float32 = 0 57 | var maxMidRatio float32 = 0 58 | var max = 0x000 59 | var mid = 0x000 60 | var min = 0x000 61 | 62 | rgba := fmt.Sprint(img.At(x ,y)) 63 | tmp := strings.Split(rgba[1:len(rgba) - 1] ," ") 64 | tr ,tg ,tb := tmp[0] ,tmp[1] ,tmp[2] 65 | r ,_ := strconv.Atoi(tr) 66 | g ,_ := strconv.Atoi(tg) 67 | b ,_ := strconv.Atoi(tb) 68 | rgb := []int{r ,g ,b} 69 | sort.Ints(rgb) 70 | 71 | switch rgb[2] { 72 | case r: 73 | max = r 74 | maxRatio = gh.defaultRatio[CRed] 75 | case g: 76 | max = g 77 | maxRatio = gh.defaultRatio[CGreen] 78 | case b: 79 | max = b 80 | maxRatio = gh.defaultRatio[CBlue] 81 | } 82 | 83 | switch rgb[1] { 84 | case r: 85 | mid = r 86 | case g: 87 | mid = g 88 | case b: 89 | mid = b 90 | } 91 | 92 | if b == rgb[0]{ 93 | min = b 94 | maxMidRatio = gh.defaultRatio[CYellow] 95 | } 96 | if r == rgb[0]{ 97 | min = r 98 | maxMidRatio = gh.defaultRatio[CCyan] 99 | } 100 | if g == rgb[0]{ 101 | min = g 102 | maxMidRatio = gh.defaultRatio[CMagenta] 103 | } 104 | 105 | val := uint8((float32(max) - float32(mid)) * maxRatio + (float32(mid) - float32(min)) * maxMidRatio + float32(min)) 106 | if threshold > -1{ 107 | if int(val) < threshold{ 108 | val = 0x00 109 | }else{ 110 | val = 0xff 111 | } 112 | } 113 | 114 | grey := color.Gray{ 115 | Y:val, 116 | } 117 | return x ,y ,grey 118 | } 119 | 120 | 121 | -------------------------------------------------------------------------------- /ascii/ascii_ykr.go: -------------------------------------------------------------------------------- 1 | package ascii 2 | 3 | import ( 4 | "image" 5 | ) 6 | 7 | // 还原风格应当更趋近于油库里风,现在会默认将输入图像压缩3*3倍 8 | // 输出更可能会变得不可辨认. 9 | // 将会使用与Ascii不同的字符集. 10 | type AsciiYukkuri struct { 11 | X int 12 | Y int 13 | GreyImg *image.Gray 14 | AsciiMap [][]string 15 | 16 | Ch0369 string 17 | 18 | Ch903 string 19 | Ch036 string 20 | Ch369 string 21 | Ch690 string 22 | 23 | Ch93 string 24 | Ch06 string 25 | Ch03 string 26 | Ch36 string 27 | Ch69 string 28 | Ch90 string 29 | } 30 | 31 | func NewAsciiYukkuri(grey *image.Gray) *AsciiYukkuri{ 32 | a := &AsciiYukkuri{} 33 | a.GreyImg = grey 34 | rtg := grey.Bounds() 35 | a.X = rtg.Max.X / 3 36 | a.Y = rtg.Max.Y / 3 37 | a.Ch03 = "ノ" 38 | a.Ch06 = "│" 39 | a.Ch36 = "ノ" 40 | a.Ch90 = "╰" 41 | a.Ch69 = "╰" 42 | a.Ch93 = "─" 43 | a.Ch036 = "ノ" 44 | a.Ch369 = "─" 45 | a.Ch690 = "╰" 46 | a.Ch903 = "─" 47 | a.Ch0369 = "." 48 | var m [][]string 49 | for i:=0 ;i 更倾向油库里风格,输出矩阵更小,但更容易失真 15 | func NewAsc11Converter(grey *image.Gray ,yukkuriVer bool) Asc11Converter{ 16 | if yukkuriVer{ 17 | return NewAsciiYukkuri(grey) 18 | }else{ 19 | return NewAscii(grey) 20 | } 21 | } 22 | 23 | // 转出来的ascii字符画基本上是一个像素点对应一个字符。 24 | // 因此完整还原一幅图像需要较大的空间 25 | type Ascii struct { 26 | X int 27 | Y int 28 | GreyImg *image.Gray 29 | AsciiMap [][]string 30 | 31 | Ch0369 string 32 | 33 | Ch903 string 34 | Ch036 string 35 | Ch369 string 36 | Ch690 string 37 | 38 | Ch93 string 39 | Ch06 string 40 | Ch03 string 41 | Ch36 string 42 | Ch69 string 43 | Ch90 string 44 | } 45 | 46 | func NewAscii(grey *image.Gray) *Ascii{ 47 | x := grey.Bounds().Max.X - grey.Bounds().Min.X 48 | y := grey.Bounds().Max.Y - grey.Bounds().Min.Y 49 | a := &Ascii{ 50 | X:x, 51 | Y:y, 52 | GreyImg:grey, 53 | Ch0369:"┼", 54 | Ch903:"┴", 55 | Ch690:"┤", 56 | Ch036:"├", 57 | Ch369:"┬", 58 | Ch06:"│", 59 | Ch93:"─", 60 | Ch03:"└", 61 | Ch36:"┌", 62 | Ch69:"┐", 63 | Ch90:"┘", 64 | } 65 | var m [][]string 66 | for i:=0 ;i