diff --git a/Sources/Internal/RCImageDecoder.swift b/Sources/Internal/RCImageDecoder.swift index 517a24b..d836359 100644 --- a/Sources/Internal/RCImageDecoder.swift +++ b/Sources/Internal/RCImageDecoder.swift @@ -21,20 +21,120 @@ // SOFTWARE. import UIKit +import CoreImage -class RCImageDecoder { +final class RCImageDecoder { + + private var context = CIContext() + + func process(buffer: UnsafeMutableBufferPointer, size: Int) throws -> [RCBit] { + let data = RCMatrix(rows: size, items: buffer) + let points = try scanControlPoints(data) + let imageData = fixPerspective(data, points: points) + let bits = decode(imageData) + return bits + } func decode(_ image: UIImage) throws -> [RCBit] { - guard let cgImage = image.cgImage else { throw RCCoder.RCCoderError.decoding } - var pixelValues = [UInt8](repeating: 0, count: cgImage.width * cgImage.height) + let size = image.cgImage!.height + var pixelData = UnsafeMutableRawPointer.allocate(byteCount: size * size, alignment: MemoryLayout.alignment) + let context = CGContext(data: pixelData, width: size, height: size, bitsPerComponent: 8, bytesPerRow: size, space: CGColorSpaceCreateDeviceGray(), bitmapInfo: CGImageAlphaInfo.none.rawValue) + context?.draw(image.cgImage!, in: CGRect(origin: .zero, size: CGSize(width: size, height: size))) + let buffer = UnsafeMutableBufferPointer(start: pixelData.assumingMemoryBound(to: UInt8.self), count: size * size) + let bits = try process(buffer: buffer, size: size) + defer { + pixelData.deallocate() + } + return bits + } +} + +extension RCImageDecoder { + + private func scanControlPoints( _ data: RCMatrix) throws -> [CGPoint] { + var points = [CGPoint]() + + let range = (CGFloat(1.8)...CGFloat(2.2)) + for rowIndex in 0.. 180 ? RCBit.zero : RCBit.one), startPoint: CGPoint.init(x: 0, y: CGFloat(rowIndex)), count: 0) + pixelBuffer.append(first) + rowBuffer.enumerated().forEach { value in + let bit = value.element > 180 ? RCBit.zero : RCBit.one + if pixelBuffer.last!.bit == bit { + pixelBuffer[pixelBuffer.count - 1].count += 1 + } else { + let pixel = RCPixelPattern.init(bit: bit, startPoint: CGPoint.init(x: CGFloat(value.offset), y: CGFloat(rowIndex)), count: 1) + pixelBuffer.append(pixel) + } + } + guard pixelBuffer.count >= 7 else { break } + for pixelIndex in 2..<(pixelBuffer.count - 4) { + let pixel = pixelBuffer[pixelIndex] + guard pixel.bit == .one, pixel.count >= 10 else { continue } + + let leftTwo = pixelBuffer[pixelIndex - 2] + let leftOne = pixelBuffer[pixelIndex - 1] + let rightOne = pixelBuffer[pixelIndex + 1] + let rightTwo = pixelBuffer[pixelIndex + 2] + + guard leftTwo.bit == .one else { continue } + guard leftOne.bit == .zero else { continue } + guard rightOne.bit == .zero else { continue } + guard rightTwo.bit == .one else { continue } + guard range.contains(pixel.count / leftTwo.count) else { continue } + guard range.contains(pixel.count / leftOne.count) else { continue } + guard range.contains(pixel.count / rightOne.count) else { continue } + guard range.contains(pixel.count / rightTwo.count) else { continue } + + + let point = CGPoint.init(x: pixel.startPoint.x, y: pixel.startPoint.y + CGFloat(pixel.count) / 2) + points.append(point) + + + } + + } + return points + } + + private func fixPerspective(_ data: RCMatrix, points: [CGPoint]) -> RCMatrix { + let path = UIBezierPath() + path.move(to: points[0]) + path.addLine(to: points[1]) + path.addLine(to: points[2]) + path.addLine(to: points[3]) + path.close() + let originalBounds = path.bounds + path.apply(CGAffineTransform(scaleX: sqrt(2.0), y: sqrt(2.0))) + let scaledBounds = path.bounds + path.apply(CGAffineTransform(translationX: (originalBounds.width - scaledBounds.width) / 2 , y: (originalBounds.height - scaledBounds.height) / 2)) + let vectors = points.map{CGVector(dx: $0.x, dy: $0.y)} + let image = CIImage() //.image(from: data) + let perspectiveFilter = CIFilter(name: "CIPerspectiveCorrection")! + perspectiveFilter.setValue(image, forKey: "inputImage") + perspectiveFilter.setValue(vectors[0], forKey: "inputTopLeft") + perspectiveFilter.setValue(vectors[1], forKey: "inputTopRight") + perspectiveFilter.setValue(vectors[2], forKey: "inputBottomRight") + perspectiveFilter.setValue(vectors[3], forKey: "inputBottomLeft") + let transform = CGAffineTransform(translationX: image.extent.midX, y: image.extent.midY).rotated(by: CGFloat.pi / 4).translatedBy(x: -image.extent.midX, y: -image.extent.midY) + let correctedImage = perspectiveFilter.outputImage!.transformed(by: transform) + let cgImage = context.createCGImage(correctedImage, from: CGRect(x: 0, y: 0, width: data.columns, height: data.columns))! + var pixelValues = UnsafeMutableRawPointer.allocate(byteCount: cgImage.width * cgImage.height, alignment: MemoryLayout.alignment) let colorSpace = CGColorSpaceCreateDeviceGray() let contextRef = CGContext(data: &pixelValues, width: cgImage.width, height: cgImage.height, bitsPerComponent: 8, bytesPerRow: cgImage.width, space: colorSpace, bitmapInfo: 0) contextRef?.draw(cgImage, in: CGRect(origin: .zero, size: CGSize(width: cgImage.width, height: cgImage.height))) - let imageMatrix = RCMatrix(rows: cgImage.width, items: pixelValues) - let center = CGPoint(x: cgImage.width, y: cgImage.width) - let lineWidth = CGFloat(cgImage.width) * RCConstants.dotSizeScale / 11 * 2 //number of lines including spaces - let mainRadius = (CGFloat(cgImage.width) - lineWidth) / 2 - let startAngle = asin(CGFloat(cgImage.width) * RCConstants.dotSizeScale / mainRadius) + data.data.deallocate() + data.data = UnsafeMutableBufferPointer(start: pixelValues.bindMemory(to: UInt8.self, capacity: data.columns * data.rows), count: data.columns * data.rows) + return data + } + + private func decode(_ data: RCMatrix) -> [RCBit] { + let size = CGFloat(data.columns) + let lineWidth = size * RCConstants.dotSizeScale / 11 * 2 //number of lines including spaces + let mainRadius = (size - lineWidth) / 2 + let startAngle = asin(size * RCConstants.dotSizeScale / mainRadius) let distancePerBit = (CGFloat.pi / 2 - startAngle * 2) / CGFloat(RCConstants.level1BitesCount) var points = [CGPoint]() [RCConstants.level1BitesCount, RCConstants.level2BitesCount, RCConstants.level3BitesCount].enumerated().forEach { row in @@ -45,13 +145,26 @@ class RCImageDecoder { let angle = baseDistance + distancePerBit * CGFloat(bitCount) + distancePerBit / 2 let x = cos(angle) * radius let y = sin(angle) * radius - let point = CGPoint(x: x + center.x.rounded() / 2, y: y + center.y.rounded() / 2) + let point = CGPoint(x: x + size / 2, y: y + size / 2) points.append(point) } } } - return points.map { imageMatrix[Int($0.x), Int($0.y)] > 200 ? RCBit.zero : RCBit.one } + return points.map { data[Int($0.x), Int($0.y)] > 200 ? RCBit.zero : RCBit.one } } } - +private extension RCImageDecoder { + + struct RCPixelPattern: CustomStringConvertible { + + let bit: RCBit + let startPoint: CGPoint + var count: CGFloat + + + var description: String { + "\(bit.debugDescription), start: \(startPoint.x), count: \(count), \n " + } + } +} diff --git a/Sources/Public/RCCoder.swift b/Sources/Public/RCCoder.swift index 00c48e2..0022fb2 100644 --- a/Sources/Public/RCCoder.swift +++ b/Sources/Public/RCCoder.swift @@ -43,11 +43,9 @@ public extension RCCoder { func decode(_ image: UIImage) throws -> String { guard image.size.width == image.size.height else { throw RCError.wrongImageSize } - //make grayscale - //make CVPixelBuffer - //decode image - //decode bits - return "message" + let bits = try imageDecoder.decode(image) + let message = try bitCoder.decode(bits) + return message } func validate(_ text: String) -> Bool {