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

What will be the hottest thing in 2016?

[[161131]] 2016 has arrived. I wonder if you have...

How do web game ads achieve conversions?

Web games, which have always given people the imp...

Cyanogen's plot to steal Android from Google

If you were planning a sophisticated heist on one...

The big-screen phone bet is right! iOS has reversed its global market share

[[125989]] Last year, Apple released two large-sc...

Zhihu’s road to commercial monetization!

From launching the live streaming function to the...

How are those people who got eyeliner tattoos doing now?

The pursuit of beauty by beauty lovers is endless...

What the hell is this? How to choose the online promotion method?

Recently, many companies may be promoting the &qu...

How to apply for a 400 telephone number? How to apply for a 400 number?

How to apply for a 400 telephone number? How to a...