I have an array with many millions of elements (7201 x 7201 data points) where I am converting the data to a greyscale image.
var imagePixels = heights.map { terrainToColorScale($0.height, gradient: .Linear) }
let _ = writeImageToFile(imagePixels, height: latStrides, width: lonStrides, imgColorSpace: .BW, to: imageURL, as: .png)
This snippet of code takes about 11 seconds to complete (CPU=2.3Ghz 8-Core i9), but I would like to get better performance if possible. Code is currently running in a single thread.
Would simply splitting my heights
array into chunks (say 100 chunks) and running a TaskGroup with a task for each chunk get a decent improvement?
Or am I looking at getting into Metal and shaders (I know zero about Metal!!) to get a better result?
Just for interest, the typical image generated is... (Image downsampled as would be too big for upload here.)
Update: Adding code associated with terrainToColorScale
Basically, for a linear conversion it will take the terrain height (typically 0...9000) and scale it to return a value between 0...255
I also have non-linear implementations (not shown below) that will show greater detail for datasets that are mostly low/high terrain elevations.
let terrainLowerCutOff: Double = 0.0 // Mean Sea Level
let terrainUpperCutOff: Double = 9000.0 // Value in meters, just higher that Everest
func terrainToColorScale(_ elev: Double, lowerCutOff: Double = terrainLowerCutOff, upperCutOff: Double = terrainUpperCutOff, gradient: ImageColorGradient = .Linear) -> UInt8 {
switch gradient {
case .Linear:
return linearColorScale(elev, lowerCutOff: lowerCutOff, upperCutOff: upperCutOff)
case .LinearInverse:
return linearInverseColorScale(elev, lowerCutOff: lowerCutOff, upperCutOff: upperCutOff)
case .CurveEmphasiseLows:
return reciprocalPowerColorScale(elev, lowerCutOff: lowerCutOff, upperCutOff: upperCutOff)
case .CurveEmphasiseLowsInverse:
return reciprocalInversePowerColorScale(elev, lowerCutOff: lowerCutOff, upperCutOff: upperCutOff)
case .CurveEmphasiseHighs:
return powerColorScale(elev, lowerCutOff: lowerCutOff, upperCutOff: upperCutOff)
case .CurveEmphasiseHighsInverse:
return powerInverseColorScale(elev, lowerCutOff: lowerCutOff, upperCutOff: upperCutOff)
}
}
fileprivate func linearColorScale(_ value: Double, lowerCutOff: Double, upperCutOff: Double) -> UInt8 {
return UInt8( 255 * normaliseColorScale(value, lowerCutOff: lowerCutOff, upperCutOff: upperCutOff) )
}
fileprivate func normaliseColorScale(_ value: Double, lowerCutOff: Double, upperCutOff: Double) -> Double {
switch value {
case _ where value <= lowerCutOff :
return 0.0
case _ where value >= upperCutOff :
return 1.0
default :
return (value - lowerCutOff) / (upperCutOff - lowerCutOff)
}
}
This is not a complete answer to your question, but I think it should give you a start on where to go. vDSP is part of Accelerate
, and it's built to speed up mathematical operations on arrays. This code uses multiple steps, so probably could be more optimised, and it's not taking any other filters than linear into account, but I don't have enough knowledge to make the steps more effective. However, on my machine, vDSP is 4x faster than map for the following processing:
import Foundation
import Accelerate
let count = 7200 * 7200
var date = Date()
print("Generating")
let test: [CGPoint] = (0..<count).map {
CGPoint(x: $0, y: Int.random(in: -2000...10000))
}
print("Generating took \(Date().timeIntervalSince(date))")
date = Date()
print("Mapping")
let heights: [Float] = test.map { Float($0.y) }
print("Mapping took \(Date().timeIntervalSince(date))")
date = Date()
print("Converting via vDSP")
let clipped = vDSP.clip(heights, to: 0...9000)
let scaled = vDSP.divide(clipped, 9000)
let multiplied = vDSP.multiply(255, scaled)
let integers = vDSP.floatingPointToInteger(multiplied, integerType: UInt8.self, rounding: .towardNearestInteger)
print("Converting via DSP took \(Date().timeIntervalSince(date))")
date = Date()
print("Converting via map")
let mappedIntegers = heights.map { height -> UInt8 in
let clipped: Float
if height < 0 {
clipped = 0
} else if height > 9000 {
clipped = 9000
} else {
clipped = height
}
let scaled = clipped / 9000
let multiplied = scaled * 255
return UInt8(multiplied.rounded())
}
print("Converting via map took \(Date().timeIntervalSince(date))")