歡迎加入QQ討論群258996829
黑暗掠奪者 頭像
蘋果2袋
2
黑暗掠奪者

用函數(shù)式的 Swift 實現(xiàn)圖片轉(zhuǎn)字符畫的功能

發(fā)布時間:2015-06-30 21:22  回復:0  查看:2959   最后回復:2015-06-30 21:22  

1.jpg

今天整理 Pocket 中待看的文章,看到這篇《Creating ASCII art in functional Swift》,講解如何用 Swift 將圖片轉(zhuǎn)成 ASCII 字符。具體原理文中講解的很詳細,不再贅述,但是標題中的 in functional Swift 讓我很感興趣,想知道 functional 到底體現(xiàn)在哪里,于是下載 swift-ascii-art 源碼一探究竟。


Pixel

圖片是由各個像素點組成的,在代碼中像素通過 Pixel 這個 struct 實現(xiàn)。每個像素分配了4個字節(jié),這4個字節(jié) (2^8 = 256) 分別用來存儲 RBGA 的值。

createPixelMatrix

可以通過 createPixelMatrix 這個靜態(tài)方法創(chuàng)建一個 width * height 像素矩陣:

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

和傳統(tǒng)方法中使用 for 循環(huán)來創(chuàng)建多維數(shù)組有所不同的是,這里是通過 map 函數(shù)實現(xiàn)的。在 Swift 2.0 中, map 函數(shù)已經(jīng)被干掉了,只能作為方法調(diào)用。

intensityFromPixelPointer

intensityFromPixelPointer 方法計算并返回像素點的亮度值,代碼如下:

func intensityFromPixelPointer(pointer: PixelPointer) -> Double {
    let
    red   = pointer[offset + 0],
    green = pointer[offset + 1],
    blue  = pointer[offset + 2]
    return Pixel.calculateIntensity(red, green, blue)
}
private static func calculateIntensity(r: UInt8, _ g: UInt8, _ b: UInt8) -> Double {
    let
    redWeight   = 0.229,
    greenWeight = 0.587,
    blueWeight  = 0.114,
    weightedMax = 255.0 * redWeight   +
                  255.0 * greenWeight +
                  255.0 * blueWeight,
    weightedSum = Double(r) * redWeight   +
                  Double(g) * greenWeight +
                  Double(b) * blueWeight
    return weightedSum / weightedMax
}
calculateIntensity 方法基于  Y’UV  編碼獲取某個像素的亮度 (intensity) :
Y’ = 0.299 R’ + 0.587 G’ + 0.114 B’

YUV 是一種顏色編碼方法,Y 表示亮度, UV 用來表示色差, U 和 V 是構(gòu)成彩色的兩個分量。它的優(yōu)點是可以利用人眼的特性來降低數(shù)字彩色圖像所需要的存儲容量。我們通過這個公式獲取到的 Y 就是亮度的值。

Offset

Pixel 中其實只存了一個值: offset 。 Pixel.createPixelMatrix 創(chuàng)建出來的矩陣是這樣的:

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

并沒有像想象中那樣存儲了每個像素相關(guān)數(shù)據(jù),而更像是一個轉(zhuǎn)換工具,計算 PixelPointer 的灰度值。

AsciiArtist

AsciiArtist 里封裝了一些生成字符畫的方法。

createAsciiArt

createAsciiArt 方法就是創(chuàng)建字符畫:

func createAsciiArt() -> String {
    let
    // 加載圖片數(shù)據(jù),獲取指針對象
    dataProvider = CGImageGetDataProvider(image.CGImage),
    pixelData    = CGDataProviderCopyData(dataProvider),
    pixelPointer = CFDataGetBytePtr(pixelData),
    // 將圖片轉(zhuǎn)成亮度值矩陣
    intensities  = intensityMatrixFromPixelPointer(pixelPointer),
    // 將亮度值轉(zhuǎn)成對應(yīng)字符
    symbolMatrix = symbolMatrixFromIntensityMatrix(intensities)
    return join("\n", symbolMatrix)
}

其中 CFDataGetBytePtr 函數(shù)返回了圖像的字節(jié)數(shù)組指針,數(shù)組里每個元素都是一個字節(jié),即 0~255 的整數(shù)。每4個字節(jié)組成了一個 Pixel ,分別對應(yīng)著 RGBA 的值。

intensityMatrixFromPixelPointer

intensityMatrixFromPixelPointer 這個方法是通過 PixelPointer 生成對應(yīng)的亮度值矩陣:

private func intensityMatrixFromPixelPointer(pointer: PixelPointer) -> [[Double]]
{
    let
    width  = Int(image.size.width),
    height = Int(image.size.height),
    matrix = Pixel.createPixelMatrix(width, height)
    return matrix.map { pixelRow in
        pixelRow.map { pixel in
            pixel.intensityFromPixelPointer(pointer)
        }
    }
}

首先通過 Pixel.createPixelMatrix 方法創(chuàng)建了一個空的二維數(shù)組,用來存放數(shù)值。然后用兩個 map 嵌套遍歷里面的所有元素,將像素 (pixel) 轉(zhuǎn)換成亮度 (intensity) 的值。

symbolMatrixFromIntensityMatrix

symbolMatrixFromIntensityMatrix 函數(shù)將亮度值數(shù)組轉(zhuǎn)換成字符畫數(shù)組:

private func symbolMatrixFromIntensityMatrix(matrix: [[Double]]) -> [String]
{
    return matrix.map { intensityRow in
        intensityRow.reduce("") {
            $0 + self.symbolFromIntensity($1)
        }
    }
}
map + reduce 成功實現(xiàn)了字符串的累加,每次 reduce 都是通過 symbolFromIntensity 方法獲取到亮度值對應(yīng)的字符。 symbolFromIntensity 方法如下:


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

傳入 intensity ,在確保了值的范圍是 0 ~ 1 之后,通過 AsciiPalette 將它轉(zhuǎn)換成對應(yīng)的字符,輸出 sumbol 。

AsciiPalette

AsciiPalette 是用來將數(shù)值轉(zhuǎn)換成字符的工具,像是一個字符畫里的調(diào)色板一樣,根據(jù)不同的顏色生成字符。

loadSymbols

loadSymbols 加載了所有的字符:

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

可以看到,我們選用的字符范圍是 32 ~ 126 的字符,接下來就是通過 symbolsSortedByIntensityForAsciiCodes 方法將這些字符按照亮度進行排序。比如 & 符號肯定代表著比 . 暗的區(qū)域,那么它是如何比較的呢?請看排序方法。

symbolsSortedByIntensityForAsciiCodes

symbolsSortedByIntensityForAsciiCodes 方法實現(xiàn)了字符串的生成和排序:

private func symbolsSortedByIntensityForAsciiCodes(codes: Range) -> [String]
{
    let
    // 通過 Ascii 碼生成字符數(shù)組備用
    symbols          = codes.map { self.symbolFromAsciiCode($0) },
    // 將字符繪制出來,把字符數(shù)組轉(zhuǎn)換成圖片數(shù)組,用于比較亮度
    symbolImages     = symbols.map { UIImage.imageOfSymbol($0, self.font) },
    // 將圖片數(shù)組轉(zhuǎn)換成亮度值數(shù)組,亮度值的表現(xiàn)形式是圖片中白色像素的個數(shù)
    whitePixelCounts = symbolImages.map { self.countWhitePixelsInImage($0) },
    // 將字符數(shù)組通過亮度值就行排序
    sortedSymbols    = sortByIntensity(symbols, whitePixelCounts)
    return sortedSymbols
}
其中, sortByIntensity 這個排序方法如下:
private func sortByIntensity(symbols: [String], _ whitePixelCounts: [Int]) -> [String]
{
    let
    // 用字典建立 白色像素數(shù)目 和 字符 之間的關(guān)系
    mappings      = NSDictionary(objects: symbols, forKeys: whitePixelCounts),
    // 白色像素數(shù)目數(shù)組去重
    uniqueCounts  = Set(whitePixelCounts),
    // 白色像素數(shù)目數(shù)組排序
    sortedCounts  = sorted(uniqueCounts),
    // 利用前面的字典映射,將排序后的白色像素數(shù)目轉(zhuǎn)換成對應(yīng)的字符,從而輸出有序數(shù)組
    sortedSymbols = sortedCounts.map { mappings[$0] as! String }
    return sortedSymbols
}

小結(jié)

簡單了過了一下項目,可以隱約感覺到一些函數(shù)式風格的氣息,主要體現(xiàn)在一下幾個方面:

  • map reduce 等函數(shù)的應(yīng)用恰到好處,自如處理數(shù)組的轉(zhuǎn)換和拼接。

  • 通過 input 和 output 進行數(shù)據(jù)處理,比如 sortByIntensity 方法和 symbolFromIntensity 方法。

  • 很少有狀態(tài)和屬性,更多的是直接的函數(shù)轉(zhuǎn)換,函數(shù)邏輯不依賴外部變量,只依賴于傳入的參數(shù)

代碼感覺簡單輕快。通過這個簡單的小例子,驗證了前面在 函數(shù)式的特性 中學習到的東西。

感覺很贊!

參考文獻:

您還未登錄,請先登錄

熱門帖子

最新帖子

?