Atelier Clockwork

Functional Edge Approaches

Because Unsolved Problems Bother Me

I’m working on some code to intelligently remove arbitrary border colors from an image for reasons vaguely related to a project at work. I’m close to a solution, but there’s some strangenesses I’m hitting in the core image filter stack that eludes me, so I don’t have a full code dump to share, but I’m particularly proud of how I structured the edge insets code to make it more functional.

private extension UIImage {

    static func cropEdgeInsets(filterInfo: FilterInfo, startingSize: CGSize) -> UIEdgeInsets {
        let inputHeight = Int(startingSize.height)
        let inputWidth = Int(startingSize.width)

        let topInset = filterInfo.calculateInset(1...inputHeight) { row in
            return CGRect(x: 0, y: 0, width: inputWidth, height: row)
            } ?? 0

        let bottomRange = ((topInset + 1)...(inputHeight - 1)).reverse()
        let bottomInset = filterInfo.calculateInset(bottomRange) { row in
            return CGRect(x: 0, y: row, width: inputWidth, height: inputHeight - row)
            } ?? 0

        let leftInset = filterInfo.calculateInset(1...inputWidth) { col in
            return CGRect(x: 0, y: 0, width: col, height: inputHeight)
            } ?? 0

        let rightRange = ((leftInset + 1)...(inputWidth - 1)).reverse()
        let rightInset =  filterInfo.calculateInset(rightRange) { col in
            return CGRect(x: col, y: 0, width: inputWidth - col, height: inputHeight)
            } ?? 0

        return UIEdgeInsets(top: CGFloat(topInset), left: CGFloat(leftInset), bottom: CGFloat(bottomInset), right: CGFloat(rightInset))
    }

    struct FilterInfo {
        let maxFilter :CIFilter
        let minFilter: CIFilter
        let context: CIContext
        let maximumDelta: UInt

        func calculateInset<U: CollectionType>(range: U, rectGenerator: (step: U.Generator.Element) -> CGRect) -> U.Generator.Element? {
            var inset: U.Generator.Element?
            for step in range {
                let rect = rectGenerator(step: step)
                self.maxFilter.updateFilterExtentsWithRect(rect)
                self.minFilter.updateFilterExtentsWithRect(rect)
                guard let rowDiff = self.minFilter.differenceFromFilter(self.maxFilter, context: self.context)
                    where rowDiff <= self.maximumDelta else {
                        break
                }
                inset = step
            }
            return inset
        }
    }

}

The first few passes at the code, I didn’t have a great idea to reduce the amount of duplication in iterating in from each of the sides, which I thought may hurt later as I’m assuming there’s room for optimizing the search for edges, probably involving subdividing and testing regions rather than a linear walk of the space.

I then realized that the only major change in each region was generating the rectangle and the range being worked on. Using a closure for the rectangle generation helped, but making a struct to shove all of the important values into and making the calculateInset code a function on that struct also helped me keep the function signatures sane.

Finally, when I made the calculateInset function properly generic, I also had to make the return type an optional value because I couldn’t set the initial value to 0 in the function body. This means I now added a ?? 0 after each call.

I’m much happier with this than the first few passes, the code doesn’t work yet, but all of the bits that I’ve tested outside of the core image black box for the filters are behaving properly