-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.go
298 lines (248 loc) · 9.44 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
package main
import (
chars "./chars"
"fmt"
"github.com/nfnt/resize"
"image"
jpeg "image/jpeg"
"math/bits"
"os"
)
// Defining a custom class here for Color to find the average color,
// converting to 8-bit space, and so on
type color struct {
r, g, b uint32
}
func (a *color) Add(v *color) *color {
a.r += v.r
a.g += v.g
a.b += v.b
return a
}
// Converts R,G,B values back to 8 bits.
func (a *color) retrofy() *color {
a.Div(0x101)
return a
}
func (a *color) Div(number uint32) *color {
a.r /= number
a.g /= number
a.b /= number
return a
}
func (a *color) RGB() (uint32, uint32, uint32) {
return a.r, a.g, a.b
}
// Refer: https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit
func getCharWithColor(bestChar string, c *color) string {
c.retrofy()
return fmt.Sprintf("\x1b[38;2;%d;%d;%dm%s\x1b[0m", c.r, c.g, c.b, bestChar)
}
// Here <pattern> represents a 8x8 window of the image compressed into a 64 bit number
// and c represents the dominant color in that window, i.e. the color of the ASCII char
func getClosestChar(pattern uint64, c *color) string {
maxDistance := 100
var bestLetter string
// Go through each character Mapping we have in chars.CharMap
for k, v := range chars.CharMap {
// Count the number of bits which are different in the pattern and the character
// This count represents dissimilar these 2 8x8 images are.
// Remember both are actually 8x8 images/patterns packed in 64 bit numbers.
// Here we take the XOR of these two numbers, which gives us count of bits which are different.
distance := bits.OnesCount64(v ^ pattern)
// We need to store the character which is the most similar.
// i.e. having the least number of different bits between it and the pattern.
if distance < maxDistance {
bestLetter = k
maxDistance = distance
}
}
return getCharWithColor(bestLetter, c)
}
// TODO: Move this inside windowProcessor
func getPackedFormOfWindow(img image.Image, winX, winY, w, h int, threshold uint32) uint64 {
// <pattern> will eventually be the packed form of the current 8 x 8 window
// The packing will be similar to the one done in chars.CharMap
// Refer the comment there for more details.
var pattern uint64 = 0
// Start assigning values in <pattern> from the MSB.
// <cnt> indicates the bit to currently set/unset
cnt := 63
for y := winY; y < winY+8 && y < h; y++ {
for x := winX; x < winX+8 && x < w; x++ {
r, g, b, _ := img.At(x, y).RGBA()
// We need to somewhow represent this RGB value as 0/1.
// This is known as 'binarization', There can be multiple ways to do this.
// Overall, it depends on some value (<threshold> here), which governs whether this pixel/bit
// will be a 0 or a 1.
if r+g+b >= threshold {
pattern |= 1 << uint(cnt) // Set the <cnt>th bit in pattern as this pixel is above the threshold.
}
cnt-- // Move towards LSB
}
}
return pattern
}
// TODO: Move this inside windowProcessor
func getMeanColorForWindow(img image.Image, winX, winY, w, h int) *color {
colorAccum := &color{0, 0, 0}
// Just go through all the pixels in the current window
// While ensuring that we don't cross the image bounds.
//
// Again, the order of scanning is important as we we want to store
// the top most line on the most significant 8 bits.
for y := winY; y < winY+8 && y < h; y++ {
for x := winX; x < winX+8 && x < w; x++ {
// Read the R,G,B values of the image at pixel <x>,<y>
r, g, b, _ := img.At(x, y).RGBA()
colorAccum.Add(&color{r, g, b})
}
}
return colorAccum.Div(64)
}
// windowProcessor is the object which holds required information for
// binarization and coloring.
type windowProcessor struct {
img image.Image
winX, winY, w, h, buffI, buffJ int
}
// Results to be pushed on the channel.
// The go routine cannot directly write to the buffer because of race issues.
type windowProcessorResult struct {
c string
buffI, buffJ int
}
// Runs runs a pipeline of different operations on the image (window of size 8x8 currently)
// Once the operations complete, it determines the ASCII character which should represent
// this particular window and pushes that on the <inform> channel.
func (p windowProcessor) Run(inform chan windowProcessorResult) {
//
// First, we figure out the color is dominant in this 8x8 window.
// Therefore, we can draw the character with that color in order to convey the color information.
// These can be done in multiple ways: (mean/mode/median/maximum) value of R,G,Bs in the window
//
// Here, we are going to try out the mean color.
avgColor := getMeanColorForWindow(p.img, p.winX, p.winY, p.w, p.h)
// Not really 'intensity' in the proper sense. But some kind of value to indicate the "brightness"
avgIntensity := uint32(avgColor.r + avgColor.g + avgColor.b/3)
// Pack the current window into a 64 bit integer by performing binarization.
// Details in the function definition.
packedWindow := getPackedFormOfWindow(p.img, p.winX, p.winY, p.w, p.h, avgIntensity)
// Figure out and print the character whose 8x8 representation is most similar to the current 8x8 window
char := getClosestChar(packedWindow, avgColor)
inform <- windowProcessorResult{char, p.buffI, p.buffJ}
}
func displayBuffer(buffer [][]string) {
for _, v := range buffer {
for _, s := range v {
fmt.Printf("%s", s)
}
fmt.Printf("\n")
}
}
func printImage(path string, ascii_width uint) {
// Open the image present at <path>
f, err := os.Open(path)
if err != nil {
fmt.Printf("Some Error occured while opening %s: Erro: %v", path, err)
return
}
// Try to read as JPEG
img_big, err := jpeg.Decode(f)
bounds := img_big.Bounds()
aspect_ratio := float64(bounds.Max.X) / float64(bounds.Max.Y)
// Resize according to width
// The scripts converts each 8 x 8 block of image to 1 character.
// Therefore, in order to write X characters per line, the image should be resized to 8*X.
// Which maybe bigger/smaller than the original image.
width := ascii_width * 8
// There interesting bit here is that, we are not preserving the aspect ratio of the image while
// resizing. Specifically, we make the height about half the value it is supposed to be wrt to the width.
// This is done because, if we don't rescale the image, it will show up as squished in ASCII.
//
// The generated ASCII image has somewhat similar aspect ratio (visually) to that of the source image.
height := uint(float64(width) * 0.45 / aspect_ratio)
img := resize.Resize(width, height, img_big, resize.Lanczos3)
bounds = img.Bounds()
w, h := bounds.Max.X, bounds.Max.Y
// Create a 2D buffer of ASCII chars.
// This is required the goroutines responsible for processing window can/will finish in a random
// sequence.
// Therefore, we can't just draw the chars on the screen at the end of each goroutine's execution.
// We need a way to set characters in arbitrary location on the final image.
// So, we use a buffer here.
// Instead of drawing on the screen directly, the go routines will set the appropriate characters
// in this buffer, and once all go routines are done processing the image, we can finally draw the
// buffer on the screen in a single go.
buffer := make([][]string, h/8+1)
for i := range buffer {
buffer[i] = make([]string, w/8+1)
}
// Each go routine must know what coordinate in the buffer is it respondible for.
// These variables are used to track that.
buffI, buffJ := 0, 0
// Common pattern to invoke multiple workers is to create 2 channels, one on which work is published,
// and another one where the main go routine waits for all of the work which was generated to be completed.
inform := make(chan windowProcessorResult)
done := make(chan bool)
numProcessors := 0
// We need to scan (and draw) the image from left to right (and top to bottom)
// Here winX, winY represents the top-left corner of the 8x8 window of the image, which will be
// mapped to a single character.
//
// We move the window by 8 units, since we don't want to read the same pixels again.
// Also, we move alone X axis first and then Y axis, because of the reasons stated earlier.
for winY := 0; winY < h; winY += 8 {
buffJ = 0
for winX := 0; winX < w; winX += 8 {
// Create a processor responsible for processing this 8x8 window
processor := windowProcessor{img, winX, winY, w, h, buffI, buffJ}
// Run the processor as a goroutine \m/
//
// <inform> is a channel where the processor will inform back with the appropriate ASCII
// representation for this window.
go processor.Run(inform)
// Store count of processors for closing the channel
numProcessors++
buffJ++
}
buffI++
}
// Start a go routine which collects the results from all of the processor go routines
// and writes to the buffer.
// This ensures that there are no race-conditions while accessing the buffer.
//
// Refer:
// https://gobyexample.com/closing-channels
go func() {
resultsReceived := 0
for {
result, more := <-inform
if more {
resultsReceived++
buffer[result.buffI][result.buffJ] = result.c
// Close channel once all information is received.
// On the next call to `<- inform`, we will exit the loop, as <more> will not be true.
if resultsReceived == numProcessors {
close(inform)
}
} else {
break
}
}
// Display the buffer contents on the screen
displayBuffer(buffer)
// Push to done channel
done <- true
}()
<-done
}
func main() {
// Number of characters per line
var width uint = 150
imgs := os.Args[1:]
for _, img_path := range imgs {
// Display Image
printImage(img_path, width)
}
}