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

A romantic space trip

The cost of holding a wedding is getting higher a...

Don't take the hot weather lightly, heat stroke is not a trivial matter

Author: Wang Changyuan, Chief Physician, Xuanwu H...

Toutiao account operation and promotion, do you know all these?

In recent years, Toutiao has developed rapidly, a...

APP operation indicators and promotion and marketing suggestions!

How to do a good job in App operation and promoti...

The Hunger Games of Internet TV: LeTV’s Suspension

Once any business model touches upon policy restr...

The Ultimate Method for Strategic Planning

In the process of formulating marketing plans or ...

How much does it cost to join a marriage and love mini program in Turpan?

Is it easy to join the Turpan Marriage and Love M...

Basic interface - Get the WeChat server IP address

If the official account needs to know the IP addr...

Tips for acquiring customers through Weibo Fans Advertising!

It’s time to share with you our nearly 10 years o...