Use functional Swift to convert images to characters

Use functional Swift to convert images to characters

Today, I was sorting out the articles to be read in Pocket and saw this article "Creating ASCII art in functional Swift", which explains how to use Swift to convert images into ASCII characters. The specific principle is explained in detail in the article, so I will not repeat it here, but the "in functional Swift" in the title made me very interested. I wanted to know where the functionality is reflected, so I downloaded the swift-ascii-art source code to find out.

Pixel

The image is composed of pixels, which are implemented by the Pixel struct in the code. Each pixel is allocated 4 bytes, which (2^8 = 256) are used to store the RBGA value.

createPixelMatrix

You can create a width * height pixel matrix using the static method createPixelMatrix:

  1. static func createPixelMatrix(width: Int, _ height: Int) -> [[Pixel]] {
  2. return map( 0 .. map( 0 .. let offset = (width * row + col) * Pixel.bytesPerPixel
  3. return Pixel(offset)
  4. }
  5. }
  6. }


Unlike the traditional method of using a for loop to create a multidimensional array, this is achieved through the map function. In Swift 2.0, the map function has been removed and can only be called as a method.

intensityFromPixelPointer

The intensityFromPixelPointer method calculates and returns the brightness value of a pixel. The code is as follows:

  1. func intensityFromPixelPointer(pointer: PixelPointer) -> Double {
  2. let
  3. red = pointer[offset + 0 ],
  4. green = pointer[offset + 1 ],
  5. blue = pointer[offset + 2 ]
  6. return Pixel.calculateIntensity(red, green, blue)
  7. }
  8. private   static func calculateIntensity(r: UInt8, _ g: UInt8, _ b: UInt8) -> Double {
  9. let
  10. redWeight = 0.229 ,
  11. greenWeight = 0.587 ,
  12. blueWeight = 0.114 ,
  13. weightedMax = 255.0 * redWeight +
  14. 255.0 * greenWeight +
  15. 255.0 * blueWeight,
  16. weightedSum = Double(r) * redWeight +
  17. Double(g) * greenWeight +
  18. Double(b) * blueWeight
  19. return weightedSum / weightedMax
  20. }

The calculateIntensity method obtains the intensity of a pixel based on Y'UV encoding:

  1. Y' = 0.299 R' + 0.587 G' + 0.114 B'

YUV is a color encoding method, where Y represents brightness, UV represents color difference, and U and V are the two components that make up color. Its advantage is that it can use the characteristics of the human eye to reduce the storage capacity required for digital color images. The Y we get from this formula is the value of brightness.

Offset

There is actually only one value stored in Pixel: offset. The matrix created by Pixel.createPixelMatrix looks like this:

  1. [[ 0 , 4 , 8 , ...], ...]

It does not store the relevant data of each pixel as you might imagine, but is more like a conversion tool that calculates the grayscale value of PixelPointer.

AsciiArtist

AsciiArtist encapsulates some methods for generating character paintings.

createAsciiArt

The createAsciiArt method is used to create character art:

  1. func createAsciiArt() -> String {
  2. let
  3. // Load image data and get pointer object  
  4. dataProvider = CGImageGetDataProvider(image.CGImage),
  5. pixelData = CGDataProviderCopyData(dataProvider),
  6. pixelPointer = CFDataGetBytePtr(pixelData),
  7. // Convert the image into a brightness matrix  
  8. intensities = intensityMatrixFromPixelPointer(pixelPointer),
  9. // Convert the brightness value into the corresponding character  
  10. symbolMatrix = symbolMatrixFromIntensityMatrix(intensities)
  11. return join( "\n" , symbolMatrix)
  12. }

The CFDataGetBytePtr function returns the pointer to the byte array of the image. Each element in the array is a byte, that is, an integer from 0 to 255. Every 4 bytes form a pixel, corresponding to the RGBA value.

intensityMatrixFromPixelPointer

intensityMatrixFromPixelPointer This method generates the corresponding brightness value matrix through PixelPointer:

  1. private func intensityMatrixFromPixelPointer(pointer: PixelPointer) -> [[Double]]
  2. {
  3. let
  4. width = Int(image.size.width),
  5. height = Int(image.size.height),
  6. matrix = Pixel.createPixelMatrix(width, height)
  7. return matrix.map { pixelRow in
  8. pixelRow.map { pixel in
  9. pixel.intensityFromPixelPointer(pointer)
  10. }
  11. }
  12. }

First, an empty two-dimensional array is created through the Pixel.createPixelMatrix method to store values. Then two maps are nested to traverse all the elements in it and convert pixels into intensity values.

symbolMatrixFromIntensityMatrix

The symbolMatrixFromIntensityMatrix function converts an array of brightness values ​​into an array of glyphs:

  1. private func symbolMatrixFromIntensityMatrix(matrix: [[Double]]) -> [String]
  2. {
  3. return matrix.map { intensityRow in
  4. intensityRow.reduce( "" ) {
  5. $ 0 + self.symbolFromIntensity($ 1 )
  6. }
  7. }
  8. }

Map + reduce successfully implements string accumulation. Each reduce operation uses the symbolFromIntensity method to obtain the character corresponding to the brightness value. The symbolFromIntensity method is as follows:

  1. private func symbolFromIntensity(intensity: Double) -> String
  2. {
  3. assert ( 0.0 <= intensity && intensity <= 1.0 )
  4. let
  5. factor = palette.symbols.count - 1 ,
  6. value = round(intensity * Double(factor)),
  7. index = Int(value)
  8. return palette.symbols[index]
  9. }

Pass intensity, make sure the value is between 0 and 1, convert it to the corresponding character through AsciiPalette, and output sumbol.

AsciiPalette

AsciiPalette is a tool used to convert numerical values ​​into characters, just like a palette in a character painting, generating characters based on different colors.

loadSymbols

loadSymbols loads all symbols:

  1. private func loadSymbols() -> [String]
  2. {
  3. return symbolsSortedByIntensityForAsciiCodes( 32 ... 126 ) // from ' ' to '~'  
  4. }

As you can see, the character range we selected is 32 ~ 126 characters. The next step is to sort these characters by brightness through the symbolsSortedByIntensityForAsciiCodes method. For example, the & symbol definitely represents an area darker than ., so how is it compared? Please see the sorting method.

symbolsSortedByIntensityForAsciiCodes

The symbolsSortedByIntensityForAsciiCodes method implements string generation and sorting:

  1. private func symbolsSortedByIntensityForAsciiCodes(codes: Range) -> [String]
  2. {
  3. let
  4. // Generate character array through Ascii code for backup  
  5. symbols = codes.map { self.symbolFromAsciiCode($ 0 ) },
  6. // Draw the characters and convert the character array into a picture array for brightness comparison  
  7. symbolImages = symbols.map { UIImage.imageOfSymbol($ 0 , self.font) },
  8. // Convert the image array into a brightness value array. The brightness value is expressed as the number of white pixels in the image.  
  9. whitePixelCounts = symbolImages.map { self.countWhitePixelsInImage($ 0 ) },
  10. // Sort the character array by brightness value  
  11. sortedSymbols = sortByIntensity(symbols, whitePixelCounts)
  12. return sortedSymbols
  13. }

Among them, the sortByIntensity sorting method is as follows:

  1. private func sortByIntensity(symbols: [String], _ whitePixelCounts: [Int]) -> [String]
  2. {
  3. let
  4. // Use the dictionary to establish the relationship between the number of white pixels and the character  
  5. mappings = NSDictionary(objects: symbols, forKeys: whitePixelCounts),
  6. // Remove duplicates from the array of white pixel numbers  
  7. uniqueCounts = Set(whitePixelCounts),
  8. // Sort the array of white pixel numbers  
  9. sortedCounts = sorted(uniqueCounts),
  10. // Using the previous dictionary mapping, convert the sorted number of white pixels into corresponding characters, thereby outputting an ordered array  
  11. sortedSymbols = sortedCounts.map { mappings[$ 0 ] as! String }
  12. return sortedSymbols
  13. }

summary

After a brief review of the project, I can vaguely feel some functional style, which is mainly reflected in the following aspects:

The application of functions such as map reduce is just right, and can easily handle the conversion and concatenation of arrays.

Data processing is performed through input and output, such as the sortByIntensity method and the symbolFromIntensity method.

There are few states and attributes, but more direct function conversions. Function logic does not depend on external variables, but only on the parameters passed in.

The code feels simple and light. Through this simple example, we can verify what we have learned from the functional features.

It feels great!

<<:  Why is it difficult for domestic apps to dominate overseas markets?

>>:  How to Become a Great JavaScript Programmer

Recommend

"Only working in the fields is the happiest!" Today, we miss you...

The yield of double-season rice exceeded 1,600 ki...

Using OpenGL to generate transition effects in mobile applications

Author | jzg, a senior front-end development engi...

Why do I see this strange phenomenon when I go to Finland?

In Finland, you can always see some very strange ...

Community operation: 5 elements and 7 key points of high-quality communities!

Social media has the advantages of low cost and h...

How to use Baidu experience for online promotion?

We all know that there are many ways of online pr...

How do mobile phone users become crazy fans?

Can a mobile phone be used to express faith? The ...

Princess Elsa's freezing magic is so powerful!

How powerful is Princess Elsa's freezing magi...

Xiaomi Note: A touchstone for Xiaomi’s entry into the mid-to-high-end market

Although I had a hunch that Xiaomi would not rele...

These Motos, those models.

Editor's note: Every change of MOTO in those ...