diff --git a/Sources/geometrize-cli/main.swift b/Sources/geometrize-cli/main.swift index f0d4b3c..1b6db5a 100644 --- a/Sources/geometrize-cli/main.swift +++ b/Sources/geometrize-cli/main.swift @@ -82,62 +82,18 @@ let shapeCount: Int = Int(options.shapeCount ?? 100) let strokeWidth: Int = Int(options.lineWidth ?? 1) -let runnerOptions = ImageRunnerOptions( +var shapeData: [ShapeResult] = [] + +let geometrizingSequence = GeometrizingSequence( + bitmap: targetBitmap, shapeTypes: shapeTypes, strokeWidth: strokeWidth, - alpha: 128, - shapeCount: 100, - maxShapeMutations: 100, - seed: 9001, // TODO: !!! - maxThreads: 5, - shapeBounds: ImageRunnerShapeBoundsOptions( - enabled: false, - xMinPercent: 0, yMinPercent: 0, xMaxPercent: 100, yMaxPercent: 100 - ) + iterations: 10, + shapesPerIteration: shapeCount / 10 ) -var runner = ImageRunner(targetBitmap: targetBitmap) - -var shapeData: [ShapeResult] = [] - -// Hack to add a single background rectangle as the initial shape -let rect = Rectangle(strokeWidth: 1, 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 -// Here in shapeCount set count of shapes final image should have. -// Remember background is the first shape. -loop: while shapeData.count <= shapeCount { - if options.verbose { - print("Step \(counter)", terminator: "") - } - let stepResult = runner.step( - options: runnerOptions, - shapeCreator: nil, - energyFunction: defaultEnergyFunction, - addShapePrecondition: defaultAddShapePrecondition - ) - switch stepResult { - case .success(let shapeResult): - shapeData.append(shapeResult) - if options.verbose { - print(", \(shapeResult.shape.description) added.", terminator: "") - } - case .match: - if options.verbose { - print(", geometrizing matched source image.", terminator: "") - } - break loop - case .failure: - if options.verbose { - print(", no shapes added.", terminator: "") - } - // TODO: should it break as well? - } - if options.verbose { - print(" Total count of shapes \(shapeData.count ).") - } - counter += 1 +for iteration in geometrizingSequence { + shapeData.append(contentsOf: iteration) } do { diff --git a/Sources/geometrize/GeometrizingIterator.swift b/Sources/geometrize/GeometrizingIterator.swift new file mode 100644 index 0000000..042c77f --- /dev/null +++ b/Sources/geometrize/GeometrizingIterator.swift @@ -0,0 +1,153 @@ +import Foundation + +public struct GeometrizingIterator: IteratorProtocol { + + private let originWidth: Int + private let originHeight: Int + private let shapeTypes: [Shape.Type] + private let iterations: Int + private let shapesPerIteration: Int + + private var targetBitmap: Bitmap + private let width, height: Int // downscaled targetBitmap + + private var iterationCounter: Int + + private var shapeData: [ShapeResult] // TODO: remove + + private let runnerOptions: ImageRunnerOptions + private var runner: ImageRunner + + // Counts attempts to add shapes. Not all attempts to add shape result in adding a shape. + private var stepCounter: Int + + private let verbose: Bool + + public enum State { + case running + case succeeded + case succeededMatched + case failed + } + + public var state: State + + public init( + bitmap: Bitmap, + downscaleToMaxSize downscaleSize: Int = 500, + shapeTypes: [Shape.Type], + strokeWidth: Int, + iterations: Int, + shapesPerIteration: Int, + verbose: Bool = true + ) { + self.shapeTypes = shapeTypes + self.iterations = iterations + self.shapesPerIteration = shapesPerIteration + self.verbose = verbose + + targetBitmap = bitmap + originWidth = bitmap.width + originHeight = bitmap.height + let maxSize = max(originWidth, originHeight) + if maxSize > downscaleSize { + targetBitmap = bitmap.downsample(factor: maxSize / downscaleSize) + } + + width = targetBitmap.width + height = targetBitmap.height + + iterationCounter = 0 + + stepCounter = 0 + + // TODO: parameterize this + runnerOptions = ImageRunnerOptions( + shapeTypes: shapeTypes, + strokeWidth: strokeWidth, + alpha: 128, + shapeCount: 500, // TODO: ??? + maxShapeMutations: 100, + seed: 9001, // TODO: !!! + maxThreads: 1, + shapeBounds: ImageRunnerShapeBoundsOptions( + enabled: false, + xMinPercent: 0, yMinPercent: 0, xMaxPercent: 100, yMaxPercent: 100 + ) + ) + + runner = ImageRunner(targetBitmap: targetBitmap) + + shapeData = [] + + state = .running + } + + public mutating func next() -> [ShapeResult]? { // swiftlint:disable:this cyclomatic_complexity function_body_length + guard state == .running else { return nil } + + guard iterationCounter < iterations else { + state = .succeeded + return nil + } + + var iterationShapeData: [ShapeResult] = [] + + if iterationCounter == 0 { + // Hack to add a single background rectangle as the initial shape + iterationShapeData.append( + ShapeResult( + score: 0, + color: targetBitmap.averageColor(), + shape: Rectangle(canvasWidth: targetBitmap.width, height: targetBitmap.height) + ) + ) + } + + loop: while iterationShapeData.count <= shapesPerIteration { + if verbose { + print("Step \(stepCounter)", terminator: "") + } + let stepResult = runner.step( + options: runnerOptions, + energyFunction: defaultEnergyFunction, + addShapePrecondition: defaultAddShapePrecondition + ) + switch stepResult { + case .success(let shapeResult): + iterationShapeData.append(shapeResult) + if verbose { + print(", \(shapeResult.shape.description) added.", terminator: "") + } + case .match: + if verbose { + print(", geometrizing matched source image.", terminator: "") + } + state = .succeededMatched + break loop + case .failure: + if verbose { + print(", no shapes added.", terminator: "") + } + state = .failed + break loop + } + if verbose { + print(" Total count of shapes \(shapeData.count ).") + } + stepCounter += 1 + } + + shapeData.append(contentsOf: iterationShapeData) + + iterationCounter += 1 + + if verbose { + print("Iteration \(iterationCounter) complete, \(iterationShapeData.count) shapes in iteration, " + + "\(shapeData.count) shapes in total.") + } + + return iterationShapeData + } + +} diff --git a/Sources/geometrize/GeometrizingSequence.swift b/Sources/geometrize/GeometrizingSequence.swift new file mode 100644 index 0000000..44332d2 --- /dev/null +++ b/Sources/geometrize/GeometrizingSequence.swift @@ -0,0 +1,41 @@ +import Foundation + +public struct GeometrizingSequence: Sequence { + + public typealias Element = [ShapeResult] + + let bitmap: Bitmap + let shapeTypes: [Shape.Type] + let strokeWidth: Int + let iterations: Int + let shapesPerIteration: Int + let verbose: Bool + + public init( + bitmap: Bitmap, + shapeTypes: [Shape.Type], + strokeWidth: Int, + iterations: Int, + shapesPerIteration: Int, + verbose: Bool = false + ) { + self.bitmap = bitmap + self.shapeTypes = shapeTypes + self.strokeWidth = strokeWidth + self.iterations = iterations + self.shapesPerIteration = shapesPerIteration + self.verbose = verbose + } + + public func makeIterator() -> GeometrizingIterator { + GeometrizingIterator( + bitmap: bitmap, + shapeTypes: shapeTypes, + strokeWidth: strokeWidth, + iterations: iterations, + shapesPerIteration: shapesPerIteration, + verbose: verbose + ) + } + +} diff --git a/Sources/geometrize/State.swift b/Sources/geometrize/State.swift index d434bd7..5782781 100644 --- a/Sources/geometrize/State.swift +++ b/Sources/geometrize/State.swift @@ -23,8 +23,7 @@ struct State { } /// The score of the state, a measure of the improvement applying the state to the current bitmap will have. - // TODO: what is valid range of the score? - var score: Double + var score: Double // TODO: what is valid range of the score? /// The alpha of the shape. var alpha: UInt8