/swift-image

SwiftImage: an image library in Swift with Swifty APIs and value semantics

Primary LanguageSwiftMIT LicenseMIT

SwiftImage

Build Status

SwiftImage is an image library written in Swift, which provides Swifty APIs and image types with value semantics.

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

Introduction

SwiftImage makes it easy to access pixels of images. The Image type in SwiftImage can be used intuitively like 2D 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

We can also access pixels of images using CoreGraphics. However, CoreGraphics requires us to struggle with complicated formats, old C APIs and painful memory management. SwiftImage provides clear and Swifty APIs for images.

Typically Image is used with the RGBA type. RGBA is a simple struct declared as follows.

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

Because RGBA is a generic type, it can represent various formats of pixels. For example, RGBA<UInt8> represents a pixel of 8-bit RGBA image (each channel has a value in 0...255). Similarly, RGBA<UInt16> represents a pixel of 16-bit RGBA image (0...65535). RGBA<Float> can represent a pixel whose channels are Floats, which is often used for machine learning. A pixel of binary images, which have only black or white pixels and are used for fax, can be represented using RGBA<Bool>.

When RGBA is used with Image, type parameters are nested like Image<RGBA<UInt8>> because both Image and RGBA are generic types. On the other hand, grayscale images can be represented without nested parameters: Image<UInt8> for 8-bit grayscale images and Image<UInt16> for 16-bit grayscale images.

Image and RGBA provide powerful APIs to handle images. For example, it is possible to convert a RGBA image to grayscale combining Image.map with RGBA.gray in one line.

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

Another notable feature of SwiftImage is that Image is a struct with value semantics, which is achieved using copy-on-write. Therefore,

  • Image instances never be shared
  • defensive copying is unnecessary
  • there are no wasteful copying of Image instances
  • copying is executed lazily only when it is inevitable
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

Usage

Import

import SwiftImage

Initialization

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

Access to a pixel

// 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")
}

Iteration

for pixel in image {
    ...
}

Rotation

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))

Flip

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

Resizing

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

Crop

Slicing is executed with no copying costs.

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

Conversion

Image can be converted by map in the same way as Array. Followings are the examples.

Grayscale

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

Binarization

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

Binarization (auto threshold)

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

Mean filter

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

Gaussian filter

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)

With UIImage

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

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

With NSImage

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

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

With 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

Requirements

  • Swift 5.0 or later
  • Xcode 10.2 or later

Installation

Swift Package Manager

.package(url: "https://github.com/koher/swift-image.git", from: "0.7.0"),
github "koher/swift-image" ~> 0.7.0

Manually

  1. Put SwiftImage.xcodeproj into your project/workspace in Xcode.
  2. Click your project icon and select the application target and the "General" tab.
  3. Add SwiftImage.framework to "Embedded Binaries".

License

The MIT License