- Remove Xcode and Regnet classifier
- Add MobileNet classifier - Add average color for each cap - Add option to show average colors in mosaic
This commit is contained in:
@@ -8,6 +8,8 @@
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import CoreImage
|
||||
|
||||
import SwiftyDropbox
|
||||
|
||||
protocol CapsDelegate: class {
|
||||
@@ -110,6 +112,9 @@ final class Cap {
|
||||
}
|
||||
}
|
||||
|
||||
/// The average color of the cap
|
||||
var color: UIColor?
|
||||
|
||||
/// The similarity of the cap to the currently processed image
|
||||
var match: Float? = nil
|
||||
|
||||
@@ -130,6 +135,10 @@ final class Cap {
|
||||
return tiles[tile]?.thumbnail
|
||||
}
|
||||
|
||||
static func tileColor(tile: Int) -> UIColor? {
|
||||
return tiles[tile]?.averageColor
|
||||
}
|
||||
|
||||
/**
|
||||
Switch two tiles.
|
||||
*/
|
||||
@@ -140,7 +149,6 @@ final class Cap {
|
||||
r.tile = lhs
|
||||
tiles[rhs] = l
|
||||
tiles[lhs] = r
|
||||
event("Switched tiles \(lhs) and \(rhs)")
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
@@ -164,7 +172,6 @@ final class Cap {
|
||||
Cap.shouldCreateFolderForCap(self.id)
|
||||
Cap.save()
|
||||
Cap.delegate?.capHasUpdates(self)
|
||||
//Cap.updateMosaicWithNewCap(id: self.id, image)
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
self.add(image: image) { _ in
|
||||
|
||||
@@ -180,7 +187,7 @@ final class Cap {
|
||||
return nil
|
||||
}
|
||||
let parts = line.components(separatedBy: ";")
|
||||
guard parts.count == 4 else {
|
||||
guard parts.count == 4 || parts.count == 8 else {
|
||||
Cap.error("Cap names: Invalid line \(line)")
|
||||
return nil
|
||||
}
|
||||
@@ -197,6 +204,14 @@ final class Cap {
|
||||
Cap.error("Invalid tile in line \(line)")
|
||||
return nil
|
||||
}
|
||||
if parts.count == 8 {
|
||||
guard let r = Int(parts[4]), let g = Int(parts[5]), let b = Int(parts[6]), let a = Int(parts[7]) else {
|
||||
Cap.error("Invalid color in line \(line)")
|
||||
return nil
|
||||
}
|
||||
self.color = UIColor(red: CGFloat(r)/255, green: CGFloat(g)/255, blue: CGFloat(b)/255, alpha: CGFloat(a)/255)
|
||||
}
|
||||
|
||||
self.id = nr
|
||||
self.name = parts[1]
|
||||
self.count = count
|
||||
@@ -211,6 +226,9 @@ final class Cap {
|
||||
/// The main image of the cap
|
||||
var image: UIImage? {
|
||||
guard let data = DiskManager.image(for: id) else {
|
||||
self.downloadImage { _ in
|
||||
Cap.delegate?.capHasUpdates(self)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return UIImage(data: data)
|
||||
@@ -224,6 +242,13 @@ final class Cap {
|
||||
return makeThumbnail()
|
||||
}
|
||||
|
||||
var averageColor: UIColor? {
|
||||
if let c = color {
|
||||
return c
|
||||
}
|
||||
return makeAverageColor()
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func makeThumbnail() -> UIImage? {
|
||||
guard let img = image else {
|
||||
@@ -249,12 +274,7 @@ final class Cap {
|
||||
- parameter image: The image, if the download was successful, or nil on error
|
||||
*/
|
||||
func downloadImage(_ number: Int = 0, completion: @escaping (_ image: UIImage?) -> Void) {
|
||||
if number == 0, let image = self.image {
|
||||
event("Main image for cap \(id) already downloaded")
|
||||
completion(image)
|
||||
return
|
||||
}
|
||||
let path = "/Images/\(id)/\(id)-\(number).jpg"
|
||||
let path = imageFolderPath + "/\(id)-\(number).jpg"
|
||||
DropboxController.client.files.download(path: path).response { data, dbError in
|
||||
if let error = dbError {
|
||||
self.error("Failed to download image data (\(number)) for cap \(self.id): \(error)")
|
||||
@@ -285,7 +305,7 @@ final class Cap {
|
||||
|
||||
func save(mainImage: UIImage) -> Bool {
|
||||
guard let data = mainImage.jpegData(compressionQuality: Cap.jpgQuality) else {
|
||||
error("Failed to convert image to data")
|
||||
error("Failed to convert main image to data for cap \(id)")
|
||||
return false
|
||||
}
|
||||
guard DiskManager.save(imageData: data, for: id) else {
|
||||
@@ -293,9 +313,7 @@ final class Cap {
|
||||
return false
|
||||
}
|
||||
event("Saved main image for cap \(id) to disk")
|
||||
guard let _ = makeThumbnail() else {
|
||||
return true
|
||||
}
|
||||
makeThumbnail()
|
||||
|
||||
Cap.delegate?.capHasUpdates(self)
|
||||
return true
|
||||
@@ -313,6 +331,18 @@ final class Cap {
|
||||
}
|
||||
}
|
||||
|
||||
private var imageFolderPath: String {
|
||||
return String(format: "/Images/%04d", id)
|
||||
}
|
||||
|
||||
private static func imageFolderPath(for cap: Int) -> String {
|
||||
return String(format: "/Images/%04d", cap)
|
||||
}
|
||||
|
||||
private func imageFilePath(imageId: Int) -> String {
|
||||
return imageFolderPath + "/\(id)-\(imageId).jpg"
|
||||
}
|
||||
|
||||
// MARK: - Image upload
|
||||
|
||||
private func folderExists(completion: @escaping (_ exists: Bool?) -> Void) {
|
||||
@@ -340,7 +370,7 @@ final class Cap {
|
||||
return
|
||||
}
|
||||
// Create folder for cap
|
||||
let path = "/Images/\(cap)"
|
||||
let path = imageFolderPath(for: cap)
|
||||
DropboxController.client.files.createFolderV2(path: path).response { _, error in
|
||||
if let err = error {
|
||||
self.event("Could not create folder for cap \(cap): \(err)")
|
||||
@@ -386,7 +416,7 @@ final class Cap {
|
||||
}
|
||||
|
||||
private static func uploadCapImage(at url: URL, forCapWithExistingFolder cap: Int, completion: @escaping (Bool) -> Void) {
|
||||
let path = "/Images/\(cap)/" + url.lastPathComponent
|
||||
let path = imageFolderPath(for: cap) + "/" + url.lastPathComponent
|
||||
|
||||
let data: Data
|
||||
do {
|
||||
@@ -439,6 +469,37 @@ final class Cap {
|
||||
}
|
||||
}
|
||||
|
||||
func setMainImage(to imageId: Int, image: UIImage) {
|
||||
guard imageId != 0 else {
|
||||
self.event("No need to switch main image with itself")
|
||||
return
|
||||
}
|
||||
let tempFile = imageFilePath(imageId: count)
|
||||
let oldFile = imageFilePath(imageId: 0)
|
||||
let newFile = imageFilePath(imageId: imageId)
|
||||
DropboxController.shared.move(file: oldFile, to: tempFile) { success in
|
||||
guard success else {
|
||||
self.error("Could not move \(oldFile) to \(tempFile)")
|
||||
return
|
||||
}
|
||||
DropboxController.shared.move(file: newFile, to: oldFile) { success in
|
||||
guard success else {
|
||||
self.error("Could not move \(newFile) to \(oldFile)")
|
||||
return
|
||||
}
|
||||
DropboxController.shared.move(file: tempFile, to: newFile) { success in
|
||||
if !success {
|
||||
self.error("Could not move \(tempFile) to \(newFile)")
|
||||
}
|
||||
guard self.save(mainImage: image) else {
|
||||
return
|
||||
}
|
||||
self.event("Successfully set image \(imageId) to main image for cap \(self.id)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Counts
|
||||
|
||||
func updateCount(completion: @escaping (Bool) -> Void) {
|
||||
@@ -454,7 +515,8 @@ final class Cap {
|
||||
}
|
||||
|
||||
private func getImageCount(completion: @escaping (Int?) -> Void) {
|
||||
DropboxController.client.files.listFolder(path: "/Images/\(id)").response { response, error in
|
||||
let path = imageFolderPath
|
||||
DropboxController.client.files.listFolder(path: path).response { response, error in
|
||||
if let err = error {
|
||||
self.error("Error getting folder content of cap \(self.id): \(err)")
|
||||
completion(nil)
|
||||
@@ -578,6 +640,50 @@ final class Cap {
|
||||
}
|
||||
Persistence.folderNotCreated = oldCaps.filter { $0 != cap }
|
||||
}
|
||||
|
||||
// MARK: - Average color
|
||||
|
||||
@discardableResult
|
||||
func makeAverageColor() -> UIColor? {
|
||||
guard let url = DiskManager.imageUrlForCap(id) else {
|
||||
event("No main image for cap \(id), no average color calculated")
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let inputImage = CIImage(contentsOf: url) else {
|
||||
error("Failed to read CIImage for main image of cap \(id)")
|
||||
return nil
|
||||
}
|
||||
let extentVector = CIVector(x: inputImage.extent.origin.x, y: inputImage.extent.origin.y, z: inputImage.extent.size.width, w: inputImage.extent.size.height)
|
||||
|
||||
guard let filter = CIFilter(name: "CIAreaAverage", parameters: [kCIInputImageKey: inputImage, kCIInputExtentKey: extentVector]) else {
|
||||
error("Failed to create filter to calculate average for cap \(id)")
|
||||
return nil
|
||||
}
|
||||
guard let outputImage = filter.outputImage else {
|
||||
error("Failed get filter output for image of cap \(id)")
|
||||
return nil
|
||||
}
|
||||
|
||||
var bitmap = [UInt8](repeating: 0, count: 4)
|
||||
let context = CIContext(options: [.workingColorSpace: kCFNull])
|
||||
context.render(outputImage, toBitmap: &bitmap, rowBytes: 4, bounds: CGRect(x: 0, y: 0, width: 1, height: 1), format: .RGBA8, colorSpace: nil)
|
||||
|
||||
color = UIColor(
|
||||
red: saturate(bitmap[0]),
|
||||
green: saturate(bitmap[1]),
|
||||
blue: saturate(bitmap[2]),
|
||||
alpha: CGFloat(bitmap[3]) / 255)
|
||||
|
||||
event("Average color updated for cap \(id)")
|
||||
Cap.save()
|
||||
return color
|
||||
}
|
||||
}
|
||||
|
||||
/// Map expected range 75-200 to 0-255
|
||||
private func saturate(_ component: UInt8) -> CGFloat {
|
||||
return max(min(CGFloat(component) * 2 - 150, 255), 0) / 255
|
||||
}
|
||||
|
||||
// MARK: - Protocol Hashable
|
||||
@@ -599,7 +705,22 @@ extension Cap: Hashable {
|
||||
extension Cap: CustomStringConvertible {
|
||||
|
||||
var description: String {
|
||||
return "\(id);\(name);\(count);\(tile)\n"
|
||||
guard let c = color else {
|
||||
return String(format: "%04d", id) + ";\(name);\(count);\(tile)\n"
|
||||
}
|
||||
|
||||
var fRed: CGFloat = 0
|
||||
var fGreen: CGFloat = 0
|
||||
var fBlue: CGFloat = 0
|
||||
var fAlpha: CGFloat = 0
|
||||
guard c.getRed(&fRed, green: &fGreen, blue: &fBlue, alpha: &fAlpha) else {
|
||||
return String(format: "%04d", id) + ";\(name);\(count);\(tile)\n"
|
||||
}
|
||||
let r = Int(fRed * 255.0)
|
||||
let g = Int(fGreen * 255.0)
|
||||
let b = Int(fBlue * 255.0)
|
||||
let a = Int(fAlpha * 255.0)
|
||||
return String(format: "%04d", id) + ";\(name);\(count);\(tile);\(r);\(g);\(b);\(a)\n"
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -82,6 +82,14 @@ final class DiskManager {
|
||||
let url = localUrl(for: cap)
|
||||
return fm.fileExists(atPath: url.path)
|
||||
}
|
||||
|
||||
static func imageUrlForCap(_ id: Int) -> URL? {
|
||||
let url = localUrl(for: id)
|
||||
guard fm.fileExists(atPath: url.path) else {
|
||||
return nil
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
private static func localUrl(for cap: Int) -> URL {
|
||||
return URL(fileURLWithPath: "\(cap).jpg", isDirectory: true, relativeTo: LocalDirectory.images.url)
|
||||
@@ -104,21 +112,6 @@ final class DiskManager {
|
||||
return readData(from: url)
|
||||
}
|
||||
|
||||
private static let mosaicURL: URL = documentsDirectory.appendingPathComponent("mosaic.png")
|
||||
|
||||
|
||||
static var mosaicExists: Bool {
|
||||
return fm.fileExists(atPath: mosaicURL.path)
|
||||
}
|
||||
|
||||
static func saveMosaicData(_ data: Data) -> Bool {
|
||||
return write(data, to: mosaicURL)
|
||||
}
|
||||
|
||||
static var mosaicData: Data? {
|
||||
return readData(from: mosaicURL)
|
||||
}
|
||||
|
||||
/**
|
||||
Get the thumbnail for a cap.
|
||||
If the image exists on disk, it is returned.
|
||||
|
@@ -46,33 +46,13 @@ final class Persistence {
|
||||
}
|
||||
}
|
||||
|
||||
static var squeezenet: Bool {
|
||||
static var useMobileNet: Bool {
|
||||
get {
|
||||
return UserDefaults.standard.bool(forKey: "squeezenet")
|
||||
return UserDefaults.standard.bool(forKey: "mobileNet")
|
||||
}
|
||||
|
||||
set {
|
||||
UserDefaults.standard.set(newValue, forKey: "squeezenet")
|
||||
}
|
||||
}
|
||||
|
||||
static var resnet: Bool {
|
||||
get {
|
||||
return UserDefaults.standard.bool(forKey: "resnet")
|
||||
}
|
||||
|
||||
set {
|
||||
UserDefaults.standard.set(newValue, forKey: "resnet")
|
||||
}
|
||||
}
|
||||
|
||||
static var xcode: Bool {
|
||||
get {
|
||||
return UserDefaults.standard.bool(forKey: "xcode")
|
||||
}
|
||||
|
||||
set {
|
||||
UserDefaults.standard.set(newValue, forKey: "xcode")
|
||||
UserDefaults.standard.set(newValue, forKey: "mobileNet")
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user