Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

get rid of randomRange(min:max), canvasBoundsProvider,setup(), mutate(), rasterize() #56

Merged
merged 3 commits into from
Oct 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions Sources/geometrize-cli/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ struct GeometrizeOptions: ParsableArguments {
@Option(name: .shortAndLong, help: "Input file pathname.") var inputPath: String
@Option(name: .shortAndLong, help: "Output file pathname.") var outputPath: String
@Option(name: [.customShort("t"), .long], help: "The types of shapes to generate.") var shapeTypes: String = "rectangle"
@Option(name: .shortAndLong, help: "The number of shapes to generate for the final output.") var shapeCount: UInt?
@Option(name: [.customShort("c"), .long], help: "The number of shapes to generate for the final output.") var shapeCount: UInt?
@Flag(name: .shortAndLong, help: "Verbose output.") var verbose: Bool = false
}

//print("Available shaped: \(ShapeType.allCases.map(\.rawValueCapitalized).joined(by: ", ")).")

let options = GeometrizeOptions.parseOrExit()

let inputUrl = URL(fileURLWithPath: options.inputPath)
Expand Down Expand Up @@ -62,6 +64,7 @@ guard outputUrl.pathExtension.caseInsensitiveCompare("svg") == .orderedSame else
exit(1)
}

// TODO: use ExpressibleByArgument?
let shapeTypes = options.shapeTypes.components(separatedBy: .whitespacesAndNewlines)
let shapesOrNil = shapeTypes.map(ShapeType.init)
let indexOfNil = shapesOrNil.firstIndex(of: nil)
Expand All @@ -82,10 +85,10 @@ let shapeCount: Int = Int(options.shapeCount ?? 100)
let runnerOptions = ImageRunnerOptions(
shapeTypes: shapes,
alpha: 128,
shapeCount: 500,
shapeCount: 100,
maxShapeMutations: 100,
seed: 9001,
maxThreads: 1,
maxThreads: 5,
shapeBounds: ImageRunnerShapeBoundsOptions(
enabled: false,
xMinPercent: 0, yMinPercent: 0, xMaxPercent: 100, yMaxPercent: 100
Expand All @@ -97,10 +100,7 @@ var runner = ImageRunner(targetBitmap: targetBitmap)
var shapeData: [ShapeResult] = []

// Hack to add a single background rectangle as the initial shape
let rect = Rectangle(
canvasBoundsProvider: { Bounds(xMin: 0, xMax: targetBitmap.width, yMin: 0, yMax: targetBitmap.height) },
x1: 0, y1: 0, x2: Double(targetBitmap.width), y2: Double(targetBitmap.height)
)
let rect = Rectangle(x1: 0, y1: 0, x2: Double(targetBitmap.width), y2: Double(targetBitmap.height))
shapeData.append(ShapeResult(score: 0, color: targetBitmap.averageColor(), shape: rect))

var counter = 0
Expand Down
56 changes: 43 additions & 13 deletions Sources/geometrize/Core.swift
Original file line number Diff line number Diff line change
Expand Up @@ -198,15 +198,16 @@ func differencePartial(
/// - energyFunction: A function to calculate the energy.
/// - Returns: The best state acquired from hill climbing i.e. the one with the lowest energy.
func bestHillClimbState( // swiftlint:disable:this function_parameter_count
shapeCreator: () -> any Shape,
shapeCreator: ShapeCreator,
alpha: UInt8,
n: Int,
age: Int,
target: Bitmap,
current: Bitmap,
buffer: inout Bitmap,
lastScore: Double,
energyFunction: EnergyFunction = defaultEnergyFunction
energyFunction: EnergyFunction = defaultEnergyFunction,
using generator: inout SplitMix64
) -> State {
let state: State = bestRandomState(
shapeCreator: shapeCreator,
Expand All @@ -216,7 +217,8 @@ func bestHillClimbState( // swiftlint:disable:this function_parameter_count
current: current,
buffer: &buffer,
lastScore: lastScore,
energyFunction: energyFunction
energyFunction: energyFunction,
using: &generator
)
return hillClimb(
state: state,
Expand All @@ -225,7 +227,8 @@ func bestHillClimbState( // swiftlint:disable:this function_parameter_count
current: current,
buffer: &buffer,
lastScore: lastScore,
energyFunction: energyFunction
energyFunction: energyFunction,
using: &generator
)
}

Expand All @@ -247,15 +250,23 @@ func hillClimb( // swiftlint:disable:this function_parameter_count
current: Bitmap,
buffer: inout Bitmap,
lastScore: Double,
energyFunction: EnergyFunction
energyFunction: EnergyFunction,
using generator: inout SplitMix64
) -> State {
var s: State = state.copy()
var bestState: State = state.copy()
var bestEnergy: Double = bestState.score
var age: Int = 0
while age < maxAge {
let undo: State = s.mutate()
s.score = energyFunction(s.shape.rasterize(), s.alpha, target, current, &buffer, lastScore)
let undo: State = s.mutate(xMin: 0, yMin: 0, xMax: target.width - 1, yMax: target.height - 1, using: &generator)
s.score = energyFunction(
s.shape.rasterize(xMin: 0, yMin: 0, xMax: target.width - 1, yMax: target.height - 1),
s.alpha,
target,
current,
&buffer,
lastScore
)
let energy: Double = s.score
if energy >= bestEnergy {
s = undo.copy()
Expand Down Expand Up @@ -285,21 +296,40 @@ func hillClimb( // swiftlint:disable:this function_parameter_count
/// - energyFunction: An energy function to be used.
/// - Returns: The best random state i.e. the one with the lowest energy.
private func bestRandomState( // swiftlint:disable:this function_parameter_count
shapeCreator: () -> any Shape,
shapeCreator: ShapeCreator,
alpha: UInt8,
n: Int,
target: Bitmap,
current: Bitmap,
buffer: inout Bitmap,
lastScore: Double,
energyFunction: EnergyFunction
energyFunction: EnergyFunction,
using generator: inout SplitMix64
) -> State {
var bestState: State = State(shape: shapeCreator(), alpha: alpha)
bestState.score = energyFunction(bestState.shape.rasterize(), bestState.alpha, target, current, &buffer, lastScore)
let shape = shapeCreator(&generator)
shape.setup(xMin: 0, yMin: 0, xMax: target.width, yMax: target.height, using: &generator)
var bestState: State = State(shape: shape, alpha: alpha)
bestState.score = energyFunction(
bestState.shape.rasterize(xMin: 0, yMin: 0, xMax: target.width, yMax: target.height),
bestState.alpha,
target,
current,
&buffer,
lastScore
)
var bestEnergy: Double = bestState.score
for i in 0...n {
var state: State = State(shape: shapeCreator(), alpha: alpha)
state.score = energyFunction(state.shape.rasterize(), state.alpha, target, current, &buffer, lastScore)
let shape = shapeCreator(&generator)
shape.setup(xMin: 0, yMin: 0, xMax: target.width, yMax: target.height, using: &generator)
var state: State = State(shape: shape, alpha: alpha)
state.score = energyFunction(
state.shape.rasterize(xMin: 0, yMin: 0, xMax: target.width, yMax: target.height),
state.alpha,
target,
current,
&buffer,
lastScore
)
let energy: Double = state.score
if i == 0 || energy < bestEnergy {
bestEnergy = energy
Expand Down
2 changes: 1 addition & 1 deletion Sources/geometrize/GeometrizeModelBase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class GeometrizeModelBase {
/// - color: The color (including alpha) of the shape.
/// - Returns: Data about the shape drawn on the model.
func draw(shape: any Shape, color: Rgba) -> ShapeResult {
let lines: [Scanline] = shape.rasterize()
let lines: [Scanline] = shape.rasterize(xMin: 0, yMin: 0, xMax: width, yMax: height)
let before: Bitmap = currentBitmap
currentBitmap.draw(lines: lines, color: color)
lastScore = differencePartial(target: targetBitmap, before: before, after: currentBitmap, score: lastScore, lines: lines)
Expand Down
19 changes: 11 additions & 8 deletions Sources/geometrize/GeometrizeModelHillClimb.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class GeometrizeModelHillClimb: GeometrizeModelBase {
// Therefore it does make sense decrease shapeCount proportionally when increasing maxThreads
// to achieve same effectiveness.
private func getHillClimbState( // swiftlint:disable:this function_parameter_count
shapeCreator: () -> any Shape,
shapeCreator: @escaping ShapeCreator,
alpha: UInt8,
shapeCount: Int,
maxShapeMutations: Int,
Expand All @@ -20,7 +20,9 @@ class GeometrizeModelHillClimb: GeometrizeModelBase {
// Note this implementation requires maxThreads to be the same between tasks for each task to produce the same results.
let seed = baseRandomSeed + randomSeedOffset
randomSeedOffset += 1
seedRandomGenerator(UInt64(seed))
//seedRandomGenerator(UInt64(seed))

var generator = SplitMix64(seed: UInt64(seed))

let lastScore = lastScore

Expand All @@ -34,7 +36,8 @@ class GeometrizeModelHillClimb: GeometrizeModelBase {
current: currentBitmap,
buffer: &buffer,
lastScore: lastScore,
energyFunction: energyFunction
energyFunction: energyFunction,
using: &generator
)

return [state]
Expand All @@ -52,7 +55,7 @@ class GeometrizeModelHillClimb: GeometrizeModelBase {
/// - addShapePrecondition: A function to determine whether to accept a shape.
/// - Returns: Returns `ShapeResult` representing a shape added to improve image or nil if improvement wasn't found.
func step( // swiftlint:disable:this function_parameter_count
shapeCreator: () -> any Shape,
shapeCreator: @escaping ShapeCreator,
alpha: UInt8,
shapeCount: Int,
maxShapeMutations: Int,
Expand Down Expand Up @@ -81,7 +84,7 @@ class GeometrizeModelHillClimb: GeometrizeModelBase {

// Draw the shape onto the image
let shape = it.shape.copy()
let lines: [Scanline] = shape.rasterize()
let lines: [Scanline] = shape.rasterize(xMin: 0, yMin: 0, xMax: width, yMax: height)
let color: Rgba = computeColor(target: targetBitmap, current: currentBitmap, lines: lines, alpha: alpha)
let before: Bitmap = currentBitmap
currentBitmap.draw(lines: lines, color: color)
Expand All @@ -106,13 +109,13 @@ class GeometrizeModelHillClimb: GeometrizeModelBase {
baseRandomSeed = seed
}

private static let defaultMaxThreads: Int = 4
private static let defaultMaxThreads: Int = 8

/// The base value used for seeding the random number generator (the one the user has control over)
var baseRandomSeed: Int = 0 // TODO: atomic
var baseRandomSeed: Int = 0

/// Seed used for random number generation.
/// Note: incremented by each std::async call used for model stepping.
var randomSeedOffset: Int = 0 // TODO: atomic
var randomSeedOffset: Int = 0

}
5 changes: 2 additions & 3 deletions Sources/geometrize/ImageRunner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,13 @@ public struct ImageRunner {
/// geometrization wasn't found.
public mutating func step(
options: ImageRunnerOptions,
shapeCreator: (() -> any Shape)? = nil,
shapeCreator: ShapeCreator? = nil,
energyFunction: @escaping EnergyFunction,
addShapePrecondition: @escaping ShapeAcceptancePreconditionFunction
) -> ShapeResult? {
let (xMin, yMin, xMax, yMax) = mapShapeBoundsToImage(options: options.shapeBounds, image: model.getTarget())
let types = options.shapeTypes

let shapeCreator: () -> any Shape = shapeCreator ?? createDefaultShapeCreator(types: types, canvasBounds: Bounds(xMin: xMin, xMax: xMax, yMin: yMin, yMax: yMax))
let shapeCreator: ShapeCreator = shapeCreator ?? makeDefaultShapeCreator(types: types)

model.setSeed(options.seed)

Expand Down
16 changes: 16 additions & 0 deletions Sources/geometrize/Int_random.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Foundation

var _randomImplementationReference = _randomImplementation // swiftlint:disable:this identifier_name

private func _randomImplementation(in range: ClosedRange<Int>, using generator: inout SplitMix64) -> Int {
Int.random(in: range, using: &generator)
}

extension Int {

// swiftlint:disable:next identifier_name
static func _random(in range: ClosedRange<Int>, using generator: inout SplitMix64) -> Int {
_randomImplementationReference(range, &generator)
}

}
29 changes: 29 additions & 0 deletions Sources/geometrize/ShapeCreator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Foundation

public typealias ShapeCreator = (inout SplitMix64) -> any Shape

/// Creates a function for creating instances of Shape. Returned instances should be set up!
/// - Parameters:
/// - types: The types of shapes to create.
/// - xMin: The minimum x coordinate of the shapes created.
/// - yMin: The minimum y coordinate of the shapes created.
/// - xMax: The maximum x coordinate of the shapes created.
/// - yMax: The maximum y coordinate of the shapes created.
/// - Returns: The default shape creator.
public func makeDefaultShapeCreator(types: Set<ShapeType>) -> ShapeCreator {
return { generator in
let shape: Shape
switch types[types.index(types.startIndex, offsetBy: Int._random(in: 0...types.count - 1, using: &generator))] {
case .rectangle: shape = Rectangle()
case .rotatedRectangle: shape = RotatedRectangle()
case .rotatedEllipse: shape = RotatedEllipse()
case .triangle: shape = Triangle()
case .circle: shape = Circle()
case .ellipse: shape = Ellipse()
case .line: shape = Line()
case .polyline: shape = Polyline()
case .quadraticBezier: shape = QuadraticBezier()
}
return shape
}
}
26 changes: 0 additions & 26 deletions Sources/geometrize/ShapeFactory.swift

This file was deleted.

29 changes: 13 additions & 16 deletions Sources/geometrize/Shapes/Circle.swift
Original file line number Diff line number Diff line change
@@ -1,43 +1,40 @@
import Foundation

public final class Circle: Shape {
public var canvasBoundsProvider: CanvasBoundsProvider

public var x: Double // x-coordinate.
public var y: Double // y-coordinate.
public var r: Double // Radius.

public init(canvasBoundsProvider: @escaping CanvasBoundsProvider) {
self.canvasBoundsProvider = canvasBoundsProvider
public init() {
x = 0.0
y = 0.0
r = 0.0
}

public init(canvasBoundsProvider: @escaping CanvasBoundsProvider, x: Double, y: Double, r: Double) {
self.canvasBoundsProvider = canvasBoundsProvider
public init(x: Double, y: Double, r: Double) {
self.x = x
self.y = y
self.r = r
}

public func copy() -> Circle {
Circle(canvasBoundsProvider: canvasBoundsProvider, x: x, y: y, r: r)
Circle(x: x, y: y, r: r)
}

public func setup(xMin: Int, yMin: Int, xMax: Int, yMax: Int) {
x = Double(randomRange(min: xMin, max: xMax))
y = Double(randomRange(min: yMin, max: yMax))
r = Double(randomRange(min: 1, max: 32))
public func setup(xMin: Int, yMin: Int, xMax: Int, yMax: Int, using generator: inout SplitMix64) {
x = Double(Int._random(in: xMin...xMax, using: &generator))
y = Double(Int._random(in: yMin...yMax, using: &generator))
r = Double(Int._random(in: 1...32, using: &generator))
}

public func mutate(xMin: Int, yMin: Int, xMax: Int, yMax: Int) {
switch randomRange(min: 0, max: 1) {
public func mutate(xMin: Int, yMin: Int, xMax: Int, yMax: Int, using generator: inout SplitMix64) {
let range16 = -16...16
switch Int._random(in: 0...1, using: &generator) {
case 0:
x = Double((Int(x) + randomRange(min: -16, max: 16)).clamped(to: xMin...xMax))
y = Double((Int(y) + randomRange(min: -16, max: 16)).clamped(to: yMin...yMax))
x = Double((Int(x) + Int._random(in: range16, using: &generator)).clamped(to: xMin...xMax))
y = Double((Int(y) + Int._random(in: range16, using: &generator)).clamped(to: yMin...yMax))
case 1:
r = Double((Int(r) + randomRange(min: -16, max: 16)).clamped(to: 1...xMax))
r = Double((Int(r) + Int._random(in: range16, using: &generator)).clamped(to: 1...xMax))
default:
fatalError()
}
Expand Down
Loading