SwiftImage CI

SwiftImage

SwiftImage 是一个用 Swift 编写的图像库,它提供了 Swifty API 和具有值语义的图像类型。

var image = Image<RGBA<UInt8>>(named: "ImageName")!

let pixel: RGBA<UInt8> = image[x, y]
image[x, y] = RGBA(red: 255, green: 0, blue: 0, alpha: 127)
image[x, y] = RGBA(0xFF00007F) // red: 255, green: 0, blue: 0, alpha: 127

// Iterates over all pixels
for pixel in image {
    // ...
}

// Image processing (e.g. binarizations)
let binarized: Image<Bool> = image.map { $0.gray >= 127 }

// From/to `UIImage`
image = Image<RGBA<UInt8>>(uiImage: imageView.image!)
imageView.image = image.uiImage

简介

SwiftImage 使访问图像的像素变得容易。SwiftImage 中的 Image 类型可以像二维 Array 一样直观地使用。

var image: Image<UInt8> = Image(width: 640, height: 480, pixels: [255, 248, /* ... */])

let pixel: UInt8 = image[x, y]
image[x, y] = 255

let width: Int = image.width // 640
let height: Int = image.height // 480

我们也可以使用 CoreGraphics 访问图像的像素。然而,CoreGraphics 需要我们费力处理复杂的格式、旧的 C API 和痛苦的内存管理。SwiftImage 为图像提供了清晰且 Swifty 的 API。

通常 ImageRGBA 类型一起使用。RGBA 是一个简单的 struct,声明如下。

struct RGBA<Channel> {
    var red: Channel
    var green: Channel
    var blue: Channel
    var alpha: Channel
}

由于 RGBA 是一个泛型类型,它可以表示各种像素格式。例如,RGBA<UInt8> 表示 8 位 RGBA 图像的像素(每个通道的值在 0...255 范围内)。类似地,RGBA<UInt16> 表示 16 位 RGBA 图像的像素 (0...65535)。RGBA<Float> 可以表示通道为 Float 的像素,这通常用于机器学习。二值图像(只有黑色或白色像素,用于传真)的像素可以使用 RGBA<Bool> 表示。

RGBAImage 一起使用时,类型参数像 Image<RGBA<UInt8>> 这样嵌套,因为 ImageRGBA 都是泛型类型。另一方面,灰度图像可以用不嵌套的参数表示:Image<UInt8> 用于 8 位灰度图像,Image<UInt16> 用于 16 位灰度图像。

ImageRGBA 提供了强大的 API 来处理图像。例如,可以将 RGBA 图像转换为灰度图像,只需一行代码即可结合 Image.mapRGBA.gray

let image: Image<RGBA<UInt8>> = // ...
let grayscale: Image<UInt8> = image.map { $0.gray }

SwiftImage 的另一个显著特点是 Image 是一个具有值语义struct,这是通过写时复制来实现的。因此,

var another: Image<UInt8> = image // Not copied here because of copy-on-write
another[x, y] = 255               // Copied here lazily
another[x, y] == image[x, y]      // false: Instances are never shared

用法

导入

import SwiftImage

初始化

let image = Image<RGBA<UInt8>>(named: "ImageName")!
let image = Image<RGBA<UInt8>>(contentsOfFile: "path/to/file")!
let image = Image<RGBA<UInt8>>(data: Data(/* ... */))!
let image = Image<RGBA<UInt8>>(uiImage: imageView.image!) // from a UIImage
let image = Image<RGBA<UInt8>>(nsImage: imageView.image!) // from a NSImage
let image = Image<RGBA<UInt8>>(cgImage: cgImage) // from a CGImage
let image = Image<RGBA<UInt8>>(width: 640, height: 480, pixels: pixels) // from a pixel array
let image = Image<RGBA<UInt8>>(width: 640, height: 480, pixel: .black) // a black RGBA image
let image = Image<UInt8>(width: 640, height: 480, pixel: 0) // a black grayscale image
let image = Image<Bool>(width: 640, height: 480, pixel: false) // a black binary image

访问像素

// Gets a pixel by subscripts
let pixel = image[x, y]
// Sets a pixel by subscripts
image[x, y] = RGBA(0xFF0000FF)
image[x, y].alpha = 127
// Safe get for a pixel
if let pixel = image.pixelAt(x: x, y: y) {
    print(pixel.red)
    print(pixel.green)
    print(pixel.blue)
    print(pixel.alpha)
    
    print(pixel.gray) // (red + green + blue) / 3
    print(pixel) // formatted like "#FF0000FF"
} else {
    // `pixel` is safe: `nil` is returned when out of bounds
    print("Out of bounds")
}

迭代

for pixel in image {
    ...
}

旋转

let result = image.rotated(by: .pi) // Rotated clockwise by π
let result = image.rotated(byDegrees: 180) // Rotated clockwise by 180 degrees
// Rotated clockwise by π / 4 and fill the background with red
let result = image.rotated(by: .pi / 4, extrapolatedBy: .filling(.red))

翻转

let result = image.xReversed() // Flip Horizontally
let result = image.yReversed() // Flip Vertically

调整大小

let result = image.resizedTo(width: 320, height: 240)
let result = image.resizedTo(width: 320, height: 240,
    interpolatedBy: .nearestNeighbor) // Nearest neighbor

裁剪

切片以零复制成本执行。

let slice: ImageSlice<RGBA<UInt8>> = image[32..<64, 32..<64] // No copying costs
let cropped = Image<RGBA<UInt8>>(slice) // Copying is executed here

转换

Image 可以像 Array 一样通过 map 进行转换。以下是一些示例。

灰度

let result: Image<UInt8> = image.map { (pixel: RGBA<UInt8>) -> UInt8 in
    pixel.gray
}
// Shortened form
let result = image.map { $0.gray }

二值化

let result: Image<Bool> = image.map { (pixel: RGBA<UInt8>) -> Bool in
    pixel.gray >= 128
}
// Shortened form
let result = image.map { $0.gray >= 128 }

二值化(自动阈值)

let threshold = UInt8(image.reduce(0) { $0 + $1.grayInt } / image.count)
let result = image.map { $0.gray >= threshold }

均值滤波

let kernel = Image<Float>(width: 3, height: 3, pixel: 1.0 / 9.0)
let result = image.convoluted(kernel)

高斯滤波

let kernel = Image<Int>(width: 5, height: 5, pixels: [
    1,  4,  6,  4, 1,
    4, 16, 24, 16, 4,
    6, 24, 36, 24, 6,
    4, 16, 24, 16, 4,
    1,  4,  6,  4, 1,
]).map { Float($0) / 256.0 }
let result = image.convoluted(kernel)

UIImage 一起使用

// From `UIImage`
let image = Image<RGBA<UInt8>>(uiImage: imageView.image!)

// To `UIImage`
imageView.image = image.uiImage

NSImage 一起使用

// From `NSImage`
let image = Image<RGBA<UInt8>>(nsImage: imageView.image!)

// To `NSImage`
imageView.image = image.nsImage

与 CoreGraphics 一起使用

// Drawing on images with CoreGraphics
var image = Image<PremultipliedRGBA<UInt8>>(uiImage: imageView.image!)
image.withCGContext { context in
    context.setLineWidth(1)
    context.setStrokeColor(UIColor.red.cgColor)
    context.move(to: CGPoint(x: -1, y: -1))
    context.addLine(to: CGPoint(x: 640, y: 480))
    context.strokePath()
}
imageView.image = image.uiImage

要求

许可证

MIT 许可证