- 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:
Christoph Hagen 2019-07-17 11:10:07 +02:00
parent 3dea68e1c6
commit dceb3ca07d
11 changed files with 383 additions and 296 deletions

View File

@ -10,13 +10,12 @@
043EC7C35065DD26F6BB496F /* Pods_CapCollector.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 86546C4DAB5E47A540F6E8DD /* Pods_CapCollector.framework */; }; 043EC7C35065DD26F6BB496F /* Pods_CapCollector.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 86546C4DAB5E47A540F6E8DD /* Pods_CapCollector.framework */; };
5904C33A2199C9FA0046A573 /* SortController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5904C3392199C9FA0046A573 /* SortController.swift */; }; 5904C33A2199C9FA0046A573 /* SortController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5904C3392199C9FA0046A573 /* SortController.swift */; };
5904C33C2199D0260046A573 /* AlwaysShowPopup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5904C33B2199D0260046A573 /* AlwaysShowPopup.swift */; }; 5904C33C2199D0260046A573 /* AlwaysShowPopup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5904C33B2199D0260046A573 /* AlwaysShowPopup.swift */; };
591252EE21A837FB005B1179 /* Squeezenet.mlmodel in Sources */ = {isa = PBXBuildFile; fileRef = 591252EB21A837FB005B1179 /* Squeezenet.mlmodel */; };
591252F021A837FB005B1179 /* Resnet.mlmodel in Sources */ = {isa = PBXBuildFile; fileRef = 591252ED21A837FB005B1179 /* Resnet.mlmodel */; };
59158B1621E37B0200D90CB0 /* GridViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59158B1521E37B0200D90CB0 /* GridViewController.swift */; }; 59158B1621E37B0200D90CB0 /* GridViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59158B1521E37B0200D90CB0 /* GridViewController.swift */; };
59158B1821E4C9AC00D90CB0 /* NavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59158B1721E4C9AC00D90CB0 /* NavigationController.swift */; }; 59158B1821E4C9AC00D90CB0 /* NavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59158B1721E4C9AC00D90CB0 /* NavigationController.swift */; };
591832CE21A2A97E00E5987D /* Cap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 591832CD21A2A97E00E5987D /* Cap.swift */; }; 591832CE21A2A97E00E5987D /* Cap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 591832CD21A2A97E00E5987D /* Cap.swift */; };
5970380D225737F800D21B55 /* LogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5970380C225737F800D21B55 /* LogViewController.swift */; }; 5970380D225737F800D21B55 /* LogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5970380C225737F800D21B55 /* LogViewController.swift */; };
598D60E221B6B4D200C7473E /* ImageClassifier.mlmodel in Sources */ = {isa = PBXBuildFile; fileRef = 598D60E121B6B4D200C7473E /* ImageClassifier.mlmodel */; }; 599BC9DA22CBBDA90061BCDB /* Squeezenet.mlmodel in Sources */ = {isa = PBXBuildFile; fileRef = 599BC9D922CBBDA90061BCDB /* Squeezenet.mlmodel */; };
599BC9DC22CDE2640061BCDB /* MobileNet.mlmodel in Sources */ = {isa = PBXBuildFile; fileRef = 599BC9DB22CDE2640061BCDB /* MobileNet.mlmodel */; };
59C1BBA92174CBB800EC84BB /* SettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59C1BBA82174CBB800EC84BB /* SettingsController.swift */; }; 59C1BBA92174CBB800EC84BB /* SettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59C1BBA82174CBB800EC84BB /* SettingsController.swift */; };
59C1BBAB21762D9600EC84BB /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59C1BBAA21762D9600EC84BB /* UserDefaults.swift */; }; 59C1BBAB21762D9600EC84BB /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59C1BBAA21762D9600EC84BB /* UserDefaults.swift */; };
CE56CECE209D81DE00932C01 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CECD209D81DE00932C01 /* AppDelegate.swift */; }; CE56CECE209D81DE00932C01 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CECD209D81DE00932C01 /* AppDelegate.swift */; };
@ -48,13 +47,12 @@
342A23CD7996DA1E7039C097 /* Pods-CapCollector.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CapCollector.release.xcconfig"; path = "Pods/Target Support Files/Pods-CapCollector/Pods-CapCollector.release.xcconfig"; sourceTree = "<group>"; }; 342A23CD7996DA1E7039C097 /* Pods-CapCollector.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CapCollector.release.xcconfig"; path = "Pods/Target Support Files/Pods-CapCollector/Pods-CapCollector.release.xcconfig"; sourceTree = "<group>"; };
5904C3392199C9FA0046A573 /* SortController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortController.swift; sourceTree = "<group>"; }; 5904C3392199C9FA0046A573 /* SortController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortController.swift; sourceTree = "<group>"; };
5904C33B2199D0260046A573 /* AlwaysShowPopup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlwaysShowPopup.swift; sourceTree = "<group>"; }; 5904C33B2199D0260046A573 /* AlwaysShowPopup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlwaysShowPopup.swift; sourceTree = "<group>"; };
591252EB21A837FB005B1179 /* Squeezenet.mlmodel */ = {isa = PBXFileReference; lastKnownFileType = file.mlmodel; name = Squeezenet.mlmodel; path = ../../../../Dropbox/Models/Squeezenet.mlmodel; sourceTree = "<group>"; };
591252ED21A837FB005B1179 /* Resnet.mlmodel */ = {isa = PBXFileReference; lastKnownFileType = file.mlmodel; name = Resnet.mlmodel; path = ../../../../Dropbox/Models/Resnet.mlmodel; sourceTree = "<group>"; };
59158B1521E37B0200D90CB0 /* GridViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridViewController.swift; sourceTree = "<group>"; }; 59158B1521E37B0200D90CB0 /* GridViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridViewController.swift; sourceTree = "<group>"; };
59158B1721E4C9AC00D90CB0 /* NavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationController.swift; sourceTree = "<group>"; }; 59158B1721E4C9AC00D90CB0 /* NavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationController.swift; sourceTree = "<group>"; };
591832CD21A2A97E00E5987D /* Cap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cap.swift; sourceTree = "<group>"; }; 591832CD21A2A97E00E5987D /* Cap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cap.swift; sourceTree = "<group>"; };
5970380C225737F800D21B55 /* LogViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewController.swift; sourceTree = "<group>"; }; 5970380C225737F800D21B55 /* LogViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewController.swift; sourceTree = "<group>"; };
598D60E121B6B4D200C7473E /* ImageClassifier.mlmodel */ = {isa = PBXFileReference; lastKnownFileType = file.mlmodel; name = ImageClassifier.mlmodel; path = ../../../../Dropbox/Models/ImageClassifier.mlmodel; sourceTree = "<group>"; }; 599BC9D922CBBDA90061BCDB /* Squeezenet.mlmodel */ = {isa = PBXFileReference; lastKnownFileType = file.mlmodel; name = Squeezenet.mlmodel; path = ../../../../Dropbox/Models/Squeezenet.mlmodel; sourceTree = "<group>"; };
599BC9DB22CDE2640061BCDB /* MobileNet.mlmodel */ = {isa = PBXFileReference; lastKnownFileType = file.mlmodel; name = MobileNet.mlmodel; path = ../../../../Dropbox/Models/MobileNet.mlmodel; sourceTree = "<group>"; };
59C1BBA82174CBB800EC84BB /* SettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsController.swift; sourceTree = "<group>"; }; 59C1BBA82174CBB800EC84BB /* SettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsController.swift; sourceTree = "<group>"; };
59C1BBAA21762D9600EC84BB /* UserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaults.swift; sourceTree = "<group>"; }; 59C1BBAA21762D9600EC84BB /* UserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaults.swift; sourceTree = "<group>"; };
86546C4DAB5E47A540F6E8DD /* Pods_CapCollector.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CapCollector.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 86546C4DAB5E47A540F6E8DD /* Pods_CapCollector.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CapCollector.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@ -107,16 +105,6 @@
name = Pods; name = Pods;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
591252E921A837B4005B1179 /* Models */ = {
isa = PBXGroup;
children = (
591252ED21A837FB005B1179 /* Resnet.mlmodel */,
591252EB21A837FB005B1179 /* Squeezenet.mlmodel */,
598D60E121B6B4D200C7473E /* ImageClassifier.mlmodel */,
);
name = Models;
sourceTree = "<group>";
};
9EAE4B3CEE704AF443897B44 /* Frameworks */ = { 9EAE4B3CEE704AF443897B44 /* Frameworks */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -149,7 +137,8 @@
CE56CECD209D81DE00932C01 /* AppDelegate.swift */, CE56CECD209D81DE00932C01 /* AppDelegate.swift */,
CE56CED1209D81DE00932C01 /* Main.storyboard */, CE56CED1209D81DE00932C01 /* Main.storyboard */,
CE56CEF1209D83B500932C01 /* Classifier.swift */, CE56CEF1209D83B500932C01 /* Classifier.swift */,
591252E921A837B4005B1179 /* Models */, 599BC9D922CBBDA90061BCDB /* Squeezenet.mlmodel */,
599BC9DB22CDE2640061BCDB /* MobileNet.mlmodel */,
CEF3874D209D9378001C8D3C /* Capture */, CEF3874D209D9378001C8D3C /* Capture */,
CEF3874E209D9390001C8D3C /* Sync */, CEF3874E209D9390001C8D3C /* Sync */,
CEF38750209D93D1001C8D3C /* Data */, CEF38750209D93D1001C8D3C /* Data */,
@ -360,7 +349,6 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
59C1BBA92174CBB800EC84BB /* SettingsController.swift in Sources */, 59C1BBA92174CBB800EC84BB /* SettingsController.swift in Sources */,
591252EE21A837FB005B1179 /* Squeezenet.mlmodel in Sources */,
CE56CF09209D83B800932C01 /* Classifier.swift in Sources */, CE56CF09209D83B800932C01 /* Classifier.swift in Sources */,
5904C33A2199C9FA0046A573 /* SortController.swift in Sources */, 5904C33A2199C9FA0046A573 /* SortController.swift in Sources */,
CE56CF0B209D83B800932C01 /* Logger.swift in Sources */, CE56CF0B209D83B800932C01 /* Logger.swift in Sources */,
@ -376,7 +364,6 @@
59C1BBAB21762D9600EC84BB /* UserDefaults.swift in Sources */, 59C1BBAB21762D9600EC84BB /* UserDefaults.swift in Sources */,
CE56CECE209D81DE00932C01 /* AppDelegate.swift in Sources */, CE56CECE209D81DE00932C01 /* AppDelegate.swift in Sources */,
CE56CF0D209D83B800932C01 /* ImageSelector.swift in Sources */, CE56CF0D209D83B800932C01 /* ImageSelector.swift in Sources */,
598D60E221B6B4D200C7473E /* ImageClassifier.mlmodel in Sources */,
CE56CEFF209D83B800932C01 /* CameraController.swift in Sources */, CE56CEFF209D83B800932C01 /* CameraController.swift in Sources */,
CE56CF05209D83B800932C01 /* ViewControllerExtensions.swift in Sources */, CE56CF05209D83B800932C01 /* ViewControllerExtensions.swift in Sources */,
5970380D225737F800D21B55 /* LogViewController.swift in Sources */, 5970380D225737F800D21B55 /* LogViewController.swift in Sources */,
@ -387,9 +374,10 @@
CE56CF06209D83B800932C01 /* CameraView.swift in Sources */, CE56CF06209D83B800932C01 /* CameraView.swift in Sources */,
CE56CF0A209D83B800932C01 /* CropView.swift in Sources */, CE56CF0A209D83B800932C01 /* CropView.swift in Sources */,
5904C33C2199D0260046A573 /* AlwaysShowPopup.swift in Sources */, 5904C33C2199D0260046A573 /* AlwaysShowPopup.swift in Sources */,
599BC9DC22CDE2640061BCDB /* MobileNet.mlmodel in Sources */,
599BC9DA22CBBDA90061BCDB /* Squeezenet.mlmodel in Sources */,
CE56CF02209D83B800932C01 /* RoundedImageView.swift in Sources */, CE56CF02209D83B800932C01 /* RoundedImageView.swift in Sources */,
CE56CEF8209D83B800932C01 /* CapCell.swift in Sources */, CE56CEF8209D83B800932C01 /* CapCell.swift in Sources */,
591252F021A837FB005B1179 /* Resnet.mlmodel in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };

View File

@ -10,6 +10,15 @@ import UIKit
import CoreData import CoreData
import SwiftyDropbox import SwiftyDropbox
/**
TODO:
- Mosaic: Prevent swap of tiles when tapping the free space at the right edge
- Show banner with number of unmatched caps when using camera comparison
- Feature: Create mosaic from image
- Feature: Delete cap
- Feature: Delete image of cap
*/
var shouldLaunchCamera = false var shouldLaunchCamera = false
@UIApplicationMain @UIApplicationMain

View File

@ -20,7 +20,7 @@
<color key="backgroundColor" red="0.14168914909999999" green="0.14168914909999999" blue="0.14168914909999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color key="backgroundColor" red="0.14168914909999999" green="0.14168914909999999" blue="0.14168914909999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="separatorColor" red="0.4408732927524982" green="0.4408732927524982" blue="0.4408732927524982" alpha="1" colorSpace="custom" customColorSpace="displayP3"/> <color key="separatorColor" red="0.4408732927524982" green="0.4408732927524982" blue="0.4408732927524982" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
<sections> <sections>
<tableViewSection headerTitle="Mosaic" footerTitle="Show a grid view of all caps." id="gNs-aR-ZXg"> <tableViewSection headerTitle="Cap Recognition" footerTitle="The MobileNet classifier is usually less precise as the default (Squeezenet)." id="gNs-aR-ZXg">
<cells> <cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" textLabel="41x-gC-Qt0" style="IBUITableViewCellStyleDefault" id="jeG-JT-PbE"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" textLabel="41x-gC-Qt0" style="IBUITableViewCellStyleDefault" id="jeG-JT-PbE">
<rect key="frame" x="0.0" y="55.5" width="375" height="44"/> <rect key="frame" x="0.0" y="55.5" width="375" height="44"/>
@ -29,7 +29,29 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/> <rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Show mosaic" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="41x-gC-Qt0"> <label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Use MobileNet classifier" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="41x-gC-Qt0">
<rect key="frame" x="16" y="0.0" width="343" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
<color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection headerTitle="Mosaic" footerTitle="Show a grid view of all caps." id="Yxf-P0-6z8">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" textLabel="c8H-LS-gL0" style="IBUITableViewCellStyleDefault" id="GGc-52-kcu">
<rect key="frame" x="0.0" y="191" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="GGc-52-kcu" id="Z6K-13-Ecl">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Show mosaic" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="c8H-LS-gL0">
<rect key="frame" x="16" y="0.0" width="343" height="43.5"/> <rect key="frame" x="16" y="0.0" width="343" height="43.5"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/> <fontDescription key="fontDescription" type="system" pointSize="17"/>
@ -40,7 +62,7 @@
</tableViewCellContentView> </tableViewCellContentView>
<color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/> <color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
<connections> <connections>
<segue destination="wzG-WL-mtW" kind="show" identifier="showMosaic" id="bU4-Uu-zLI"/> <segue destination="wzG-WL-mtW" kind="show" identifier="showMosaic" id="XLj-qL-BYF"/>
</connections> </connections>
</tableViewCell> </tableViewCell>
</cells> </cells>
@ -48,7 +70,7 @@
<tableViewSection headerTitle="Database" footerTitle="New caps can't be found through matching. Update the app to the newest version to receive new matching models." id="t1t-6Z-uZp"> <tableViewSection headerTitle="Database" footerTitle="New caps can't be found through matching. Update the app to the newest version to receive new matching models." id="t1t-6Z-uZp">
<cells> <cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" textLabel="xVf-9U-8dt" detailTextLabel="cwp-eR-aXo" style="IBUITableViewCellStyleSubtitle" id="pQw-5h-loP"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" textLabel="xVf-9U-8dt" detailTextLabel="cwp-eR-aXo" style="IBUITableViewCellStyleSubtitle" id="pQw-5h-loP">
<rect key="frame" x="0.0" y="175" width="375" height="44"/> <rect key="frame" x="0.0" y="310.5" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="pQw-5h-loP" id="Hkl-nU-jPG"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="pQw-5h-loP" id="Hkl-nU-jPG">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/> <rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
@ -73,7 +95,7 @@
<color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/> <color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" textLabel="og4-Az-LlV" detailTextLabel="wCH-gF-GY2" style="IBUITableViewCellStyleSubtitle" id="5k2-RN-OpJ"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" textLabel="og4-Az-LlV" detailTextLabel="wCH-gF-GY2" style="IBUITableViewCellStyleSubtitle" id="5k2-RN-OpJ">
<rect key="frame" x="0.0" y="219" width="375" height="44"/> <rect key="frame" x="0.0" y="354.5" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="5k2-RN-OpJ" id="l2t-Ds-k5L"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="5k2-RN-OpJ" id="l2t-Ds-k5L">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/> <rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
@ -98,7 +120,7 @@
<color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/> <color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="NLm-th-Ww3" detailTextLabel="Zdl-lL-KYt" style="IBUITableViewCellStyleSubtitle" id="M1J-fv-ztL"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="NLm-th-Ww3" detailTextLabel="Zdl-lL-KYt" style="IBUITableViewCellStyleSubtitle" id="M1J-fv-ztL">
<rect key="frame" x="0.0" y="263" width="375" height="44"/> <rect key="frame" x="0.0" y="398.5" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="M1J-fv-ztL" id="r9E-Qt-wSx"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="M1J-fv-ztL" id="r9E-Qt-wSx">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/> <rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
@ -124,68 +146,10 @@
</tableViewCell> </tableViewCell>
</cells> </cells>
</tableViewSection> </tableViewSection>
<tableViewSection headerTitle="Classifier" footerTitle="Choose which of the available classifiers should be used for image comparison." id="wUs-7C-Kzz">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="U7G-Xu-7Xd" style="IBUITableViewCellStyleDefault" id="h7a-jf-vVw">
<rect key="frame" x="0.0" y="414.5" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="h7a-jf-vVw" id="ZoJ-cw-jsL">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Squeezenet" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="U7G-Xu-7Xd">
<rect key="frame" x="16" y="0.0" width="343" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
<color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="IQ2-te-8MM" style="IBUITableViewCellStyleDefault" id="EBb-eO-HMg">
<rect key="frame" x="0.0" y="458.5" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="EBb-eO-HMg" id="zG0-L8-y2b">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Resnet" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="IQ2-te-8MM">
<rect key="frame" x="16" y="0.0" width="343" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
<color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="voV-Qq-csv" style="IBUITableViewCellStyleDefault" id="NLd-me-0rl">
<rect key="frame" x="0.0" y="502.5" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="NLd-me-0rl" id="ZNG-Cc-N8D">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Xcode" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="voV-Qq-csv">
<rect key="frame" x="16" y="0.0" width="343" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
<color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection headerTitle="Refresh" footerTitle="Update ressources that might be outdated." id="3rn-AC-q60"> <tableViewSection headerTitle="Refresh" footerTitle="Update ressources that might be outdated." id="3rn-AC-q60">
<cells> <cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="tgU-ma-Xhz" style="IBUITableViewCellStyleDefault" id="GoI-GJ-dx1"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="tgU-ma-Xhz" style="IBUITableViewCellStyleDefault" id="GoI-GJ-dx1">
<rect key="frame" x="0.0" y="638" width="375" height="44"/> <rect key="frame" x="0.0" y="550" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="GoI-GJ-dx1" id="AOV-5g-KkH"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="GoI-GJ-dx1" id="AOV-5g-KkH">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/> <rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
@ -203,7 +167,7 @@
<color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/> <color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="NgT-am-9HS" style="IBUITableViewCellStyleDefault" id="d8A-F9-8yL"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="NgT-am-9HS" style="IBUITableViewCellStyleDefault" id="d8A-F9-8yL">
<rect key="frame" x="0.0" y="682" width="375" height="44"/> <rect key="frame" x="0.0" y="594" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="d8A-F9-8yL" id="aA7-ai-YNA"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="d8A-F9-8yL" id="aA7-ai-YNA">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/> <rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
@ -220,12 +184,30 @@
</tableViewCellContentView> </tableViewCellContentView>
<color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/> <color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="bwA-gH-3m5" style="IBUITableViewCellStyleDefault" id="7gV-nT-0lH">
<rect key="frame" x="0.0" y="638" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="7gV-nT-0lH" id="z4m-ZT-3Nj">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Update colors" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="bwA-gH-3m5">
<rect key="frame" x="16" y="0.0" width="343" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
<color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
</tableViewCell>
</cells> </cells>
</tableViewSection> </tableViewSection>
<tableViewSection headerTitle="Dropbox" footerTitle="Sign in to dropbox to access the cap database." id="Nw5-uf-OcQ"> <tableViewSection headerTitle="Dropbox" footerTitle="Sign in to dropbox to access the cap database." id="Nw5-uf-OcQ">
<cells> <cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="dW2-Yd-L4J" style="IBUITableViewCellStyleDefault" id="Hie-e3-jja"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="dW2-Yd-L4J" style="IBUITableViewCellStyleDefault" id="Hie-e3-jja">
<rect key="frame" x="0.0" y="801.5" width="375" height="44"/> <rect key="frame" x="0.0" y="757.5" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Hie-e3-jja" id="z9u-9f-fqP"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Hie-e3-jja" id="z9u-9f-fqP">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/> <rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
@ -247,7 +229,7 @@
<tableViewSection headerTitle="LOG" footerTitle="Show the log entries since the last launch." id="nBk-Vh-WxW"> <tableViewSection headerTitle="LOG" footerTitle="Show the log entries since the last launch." id="nBk-Vh-WxW">
<cells> <cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="ce2-NF-FOB" style="IBUITableViewCellStyleDefault" id="O29-Jn-jJm"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="ce2-NF-FOB" style="IBUITableViewCellStyleDefault" id="O29-Jn-jJm">
<rect key="frame" x="0.0" y="921" width="375" height="44"/> <rect key="frame" x="0.0" y="877" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="O29-Jn-jJm" id="hWH-cq-syr"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="O29-Jn-jJm" id="hWH-cq-syr">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/> <rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
@ -523,6 +505,13 @@
</constraints> </constraints>
<viewLayoutGuide key="safeArea" id="WAE-if-wuA"/> <viewLayoutGuide key="safeArea" id="WAE-if-wuA"/>
</view> </view>
<navigationItem key="navigationItem" id="bdd-GD-zBH">
<barButtonItem key="rightBarButtonItem" title="Average" id="0qm-Vt-S3s">
<connections>
<action selector="toggleAverageColor:" destination="wzG-WL-mtW" id="EhG-8E-FcQ"/>
</connections>
</barButtonItem>
</navigationItem>
<connections> <connections>
<outlet property="scrollView" destination="TCx-cV-mMG" id="Isn-jV-DBf"/> <outlet property="scrollView" destination="TCx-cV-mMG" id="Isn-jV-DBf"/>
</connections> </connections>

View File

@ -33,54 +33,26 @@ class Classifier: Logger {
// MARK: Stored predictions // MARK: Stored predictions
private var predictions = [[Int : Float]]()
private var notify = false private var notify = false
private var image: UIImage? private var image: UIImage?
private func request(for model: MLModel, name: String) -> VNCoreMLRequest {
let model = try! VNCoreMLModel(for: model)
let request = VNCoreMLRequest(model: model, completionHandler: { [weak self] request, error in
self?.process(request: request, error: error)
self?.event("Finished \(name) prediction (\(self!.predictions.count)/\(self!.requestCount))")
})
request.imageCropAndScaleOption = .centerCrop
return request
}
private var requestCount = 0
private var requests: [VNCoreMLRequest] {
var reqs = [VNCoreMLRequest]()
if Persistence.squeezenet {
reqs.append(request(for: Squeezenet().model, name: "Squeezenet"))
}
if Persistence.resnet {
reqs.append(request(for: Resnet().model, name: "Resnet"))
}
if Persistence.xcode {
reqs.append(request(for: ImageClassifier().model, name: "Xcode"))
}
requestCount = reqs.count
return reqs
}
/** /**
Classify an image Classify an image
- parameter image: The image to classify - parameter image: The image to classify
- parameter reportingImage: Set to true, if the delegate should receive the image - parameter reportingImage: Set to true, if the delegate should receive the image
*/ */
func recognise(image: UIImage, reportingImage: Bool = true) { func recognise(image: UIImage, reportingImage: Bool = true) {
predictions.removeAll()
self.image = image self.image = image
notify = reportingImage notify = reportingImage
performClassifications() if Persistence.useMobileNet {
performClassifications(model: MobileNet().model)
} else {
performClassifications(model: Squeezenet().model)
}
} }
private func performClassifications() { private func performClassifications(model: MLModel) {
let orientation = CGImagePropertyOrientation(image!.imageOrientation) let orientation = CGImagePropertyOrientation(image!.imageOrientation)
guard let ciImage = CIImage(image: image!) else { guard let ciImage = CIImage(image: image!) else {
report(error: "Unable to create CIImage") report(error: "Unable to create CIImage")
@ -89,13 +61,18 @@ class Classifier: Logger {
DispatchQueue.global(qos: .userInitiated).async { DispatchQueue.global(qos: .userInitiated).async {
let handler = VNImageRequestHandler(ciImage: ciImage, orientation: orientation) let handler = VNImageRequestHandler(ciImage: ciImage, orientation: orientation)
let requests = self.requests let model = try! VNCoreMLModel(for: model)
guard requests.count > 0 else {
self.report(error: "No classifiers selected") let request = VNCoreMLRequest(model: model, completionHandler: { [weak self] request, error in
return guard self != nil else {
} Classifier.event("Self not captured, instance deallocated?")
return
}
self!.process(request: request, error: error)
})
request.imageCropAndScaleOption = .centerCrop
do { do {
try handler.perform(requests) try handler.perform([request])
} catch { } catch {
DispatchQueue.main.async { DispatchQueue.main.async {
self.report(error: "Classification failed: \(error.localizedDescription)") self.report(error: "Classification failed: \(error.localizedDescription)")
@ -105,62 +82,64 @@ class Classifier: Logger {
} }
private func process(request: VNRequest, error: Error?) { private func process(request: VNRequest, error: Error?) {
guard let result = request.results as? [VNClassificationObservation], if let e = error {
result.isEmpty == false else { report(error: "Unable to classify image: \(e.localizedDescription)")
report(error: "Unable to classify image: \(error?.localizedDescription ?? "No error thrown")") return
return
} }
let current = dict(from: result) if let result = request.results as? [VNClassificationObservation] {
predictions.append(current) let classification = dict(from: result)
process(classification: classification)
return
}
if let result = (request.results as? [VNCoreMLFeatureValueObservation])?.first?.featureValue.multiArrayValue {
let classification = dict(from: result)
process(classification: classification)
return
}
report(error: "Invalid classifier result: \(String(describing: request.results))")
if predictions.count == requestCount {
updateRecognizedCapsCount() }
combine()
private func process(classification: [Int : Float]) {
Cap.unsortedCaps.forEach { cap in
cap.match = classification[cap.id] ?? 0
} }
Cap.hasMatches = true
// Update the count of recognized counts
Persistence.recognizedCapCount = classification.count
report()
} }
/// Create a dictionary from a vision prediction /// Create a dictionary from a vision prediction
private func dict(from results: [VNClassificationObservation]) -> [Int : Float] { private func dict(from results: [VNClassificationObservation]) -> [Int : Float] {
let array = results.map{ item -> (Int, Float) in let array = results.map { item -> (Int, Float) in
return (Int(item.identifier) ?? 0, item.confidence) return (Int(item.identifier) ?? 0, item.confidence)
} }
return [Int : Float](uniqueKeysWithValues: array) return [Int : Float](uniqueKeysWithValues: array)
} }
/// Combine two predictions private func dict(from results: MLMultiArray) -> [Int : Float] {
private func combine() { let length = results.count
Cap.unsortedCaps.forEach { cap in let doublePtr = results.dataPointer.bindMemory(to: Double.self, capacity: length)
var result: Float = 0 let doubleBuffer = UnsafeBufferPointer(start: doublePtr, count: length)
for index in 0..<predictions.count { let output = Array(doubleBuffer).enumerated().map {
result = max(predictions[index][cap.id] ?? 0, result) ($0.offset + 1, Float($0.element))
}
cap.match = result
} }
Cap.hasMatches = true return [Int : Float](uniqueKeysWithValues: output)
report()
}
private func updateRecognizedCapsCount() {
let recognizedCaps = predictions.map { prediction in
return prediction.count
}
Persistence.recognizedCapCount = recognizedCaps.max()!
} }
// MARK: Callbacks // MARK: Callbacks
private func cleanup() {
predictions.removeAll()
image = nil
}
private func report(error message: String) { private func report(error message: String) {
guard delegate != nil else { guard delegate != nil else {
error("No delegate: " + message) error("No delegate: " + message)
return return
} }
DispatchQueue.main.async { DispatchQueue.main.async {
self.cleanup() self.image = nil
self.delegate?.classifier(error: message) self.delegate?.classifier(error: message)
} }
} }
@ -172,7 +151,7 @@ class Classifier: Logger {
} }
DispatchQueue.main.async { DispatchQueue.main.async {
let img = self.notify ? self.image : nil let img = self.notify ? self.image : nil
self.cleanup() self.image = nil
self.delegate?.classifier(finished: img) self.delegate?.classifier(finished: img)
} }
} }

View File

@ -8,6 +8,8 @@
import Foundation import Foundation
import UIKit import UIKit
import CoreImage
import SwiftyDropbox import SwiftyDropbox
protocol CapsDelegate: class { 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 /// The similarity of the cap to the currently processed image
var match: Float? = nil var match: Float? = nil
@ -130,6 +135,10 @@ final class Cap {
return tiles[tile]?.thumbnail return tiles[tile]?.thumbnail
} }
static func tileColor(tile: Int) -> UIColor? {
return tiles[tile]?.averageColor
}
/** /**
Switch two tiles. Switch two tiles.
*/ */
@ -140,7 +149,6 @@ final class Cap {
r.tile = lhs r.tile = lhs
tiles[rhs] = l tiles[rhs] = l
tiles[lhs] = r tiles[lhs] = r
event("Switched tiles \(lhs) and \(rhs)")
} }
// MARK: - Initialization // MARK: - Initialization
@ -164,7 +172,6 @@ final class Cap {
Cap.shouldCreateFolderForCap(self.id) Cap.shouldCreateFolderForCap(self.id)
Cap.save() Cap.save()
Cap.delegate?.capHasUpdates(self) Cap.delegate?.capHasUpdates(self)
//Cap.updateMosaicWithNewCap(id: self.id, image)
DispatchQueue.global(qos: .userInitiated).async { DispatchQueue.global(qos: .userInitiated).async {
self.add(image: image) { _ in self.add(image: image) { _ in
@ -180,7 +187,7 @@ final class Cap {
return nil return nil
} }
let parts = line.components(separatedBy: ";") 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)") Cap.error("Cap names: Invalid line \(line)")
return nil return nil
} }
@ -197,6 +204,14 @@ final class Cap {
Cap.error("Invalid tile in line \(line)") Cap.error("Invalid tile in line \(line)")
return nil 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.id = nr
self.name = parts[1] self.name = parts[1]
self.count = count self.count = count
@ -211,6 +226,9 @@ final class Cap {
/// The main image of the cap /// The main image of the cap
var image: UIImage? { var image: UIImage? {
guard let data = DiskManager.image(for: id) else { guard let data = DiskManager.image(for: id) else {
self.downloadImage { _ in
Cap.delegate?.capHasUpdates(self)
}
return nil return nil
} }
return UIImage(data: data) return UIImage(data: data)
@ -224,6 +242,13 @@ final class Cap {
return makeThumbnail() return makeThumbnail()
} }
var averageColor: UIColor? {
if let c = color {
return c
}
return makeAverageColor()
}
@discardableResult @discardableResult
func makeThumbnail() -> UIImage? { func makeThumbnail() -> UIImage? {
guard let img = image else { 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 - parameter image: The image, if the download was successful, or nil on error
*/ */
func downloadImage(_ number: Int = 0, completion: @escaping (_ image: UIImage?) -> Void) { func downloadImage(_ number: Int = 0, completion: @escaping (_ image: UIImage?) -> Void) {
if number == 0, let image = self.image { let path = imageFolderPath + "/\(id)-\(number).jpg"
event("Main image for cap \(id) already downloaded")
completion(image)
return
}
let path = "/Images/\(id)/\(id)-\(number).jpg"
DropboxController.client.files.download(path: path).response { data, dbError in DropboxController.client.files.download(path: path).response { data, dbError in
if let error = dbError { if let error = dbError {
self.error("Failed to download image data (\(number)) for cap \(self.id): \(error)") 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 { func save(mainImage: UIImage) -> Bool {
guard let data = mainImage.jpegData(compressionQuality: Cap.jpgQuality) else { 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 return false
} }
guard DiskManager.save(imageData: data, for: id) else { guard DiskManager.save(imageData: data, for: id) else {
@ -293,9 +313,7 @@ final class Cap {
return false return false
} }
event("Saved main image for cap \(id) to disk") event("Saved main image for cap \(id) to disk")
guard let _ = makeThumbnail() else { makeThumbnail()
return true
}
Cap.delegate?.capHasUpdates(self) Cap.delegate?.capHasUpdates(self)
return true 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 // MARK: - Image upload
private func folderExists(completion: @escaping (_ exists: Bool?) -> Void) { private func folderExists(completion: @escaping (_ exists: Bool?) -> Void) {
@ -340,7 +370,7 @@ final class Cap {
return return
} }
// Create folder for cap // Create folder for cap
let path = "/Images/\(cap)" let path = imageFolderPath(for: cap)
DropboxController.client.files.createFolderV2(path: path).response { _, error in DropboxController.client.files.createFolderV2(path: path).response { _, error in
if let err = error { if let err = error {
self.event("Could not create folder for cap \(cap): \(err)") 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) { 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 let data: Data
do { 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 // MARK: - Counts
func updateCount(completion: @escaping (Bool) -> Void) { func updateCount(completion: @escaping (Bool) -> Void) {
@ -454,7 +515,8 @@ final class Cap {
} }
private func getImageCount(completion: @escaping (Int?) -> Void) { 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 { if let err = error {
self.error("Error getting folder content of cap \(self.id): \(err)") self.error("Error getting folder content of cap \(self.id): \(err)")
completion(nil) completion(nil)
@ -578,6 +640,50 @@ final class Cap {
} }
Persistence.folderNotCreated = oldCaps.filter { $0 != 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 // MARK: - Protocol Hashable
@ -599,7 +705,22 @@ extension Cap: Hashable {
extension Cap: CustomStringConvertible { extension Cap: CustomStringConvertible {
var description: String { 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"
} }
} }

View File

@ -83,6 +83,14 @@ final class DiskManager {
return fm.fileExists(atPath: url.path) 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 { private static func localUrl(for cap: Int) -> URL {
return URL(fileURLWithPath: "\(cap).jpg", isDirectory: true, relativeTo: LocalDirectory.images.url) return URL(fileURLWithPath: "\(cap).jpg", isDirectory: true, relativeTo: LocalDirectory.images.url)
} }
@ -104,21 +112,6 @@ final class DiskManager {
return readData(from: url) 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. Get the thumbnail for a cap.
If the image exists on disk, it is returned. If the image exists on disk, it is returned.

View File

@ -46,33 +46,13 @@ final class Persistence {
} }
} }
static var squeezenet: Bool { static var useMobileNet: Bool {
get { get {
return UserDefaults.standard.bool(forKey: "squeezenet") return UserDefaults.standard.bool(forKey: "mobileNet")
} }
set { set {
UserDefaults.standard.set(newValue, forKey: "squeezenet") UserDefaults.standard.set(newValue, forKey: "mobileNet")
}
}
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")
} }
} }

View File

@ -9,6 +9,7 @@
import Foundation import Foundation
import UIKit import UIKit
extension UIImage { extension UIImage {
static func templateImage(named: String) -> UIImage { static func templateImage(named: String) -> UIImage {

View File

@ -36,6 +36,19 @@ class GridViewController: UIViewController {
return true return true
} }
private var isShowingColors = false
@IBAction func toggleAverageColor(_ sender: Any) {
isShowingColors = !isShowingColors
for (tile, view) in installedTiles {
if isShowingColors {
view.image = nil
} else {
view.image = Cap.tileImage(tile: tile)
}
}
}
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
@ -131,7 +144,12 @@ class GridViewController: UIViewController {
private func makeTile(_ tile: Int) { private func makeTile(_ tile: Int) {
let view = RoundedImageView(frame: frame(for: tile)) let view = RoundedImageView(frame: frame(for: tile))
myView.addSubview(view) myView.addSubview(view)
view.image = Cap.tileImage(tile: tile) view.backgroundColor = Cap.tileColor(tile: tile)
// Only set image if images are shown
if !isShowingColors {
view.image = Cap.tileImage(tile: tile)
}
installedTiles[tile] = view installedTiles[tile] = view
} }
@ -144,12 +162,20 @@ class GridViewController: UIViewController {
} }
private func switchTiles(oldTile: Int, newTile: Int) { private func switchTiles(oldTile: Int, newTile: Int) {
if oldTile != newTile { guard oldTile != newTile else {
Cap.switchTiles(oldTile, newTile) clearTileSelection()
return
}
Cap.switchTiles(oldTile, newTile)
// Switch cap colors
installedTiles[oldTile]?.backgroundColor = Cap.tileColor(tile: oldTile)
installedTiles[newTile]?.backgroundColor = Cap.tileColor(tile: newTile)
if !isShowingColors {
installedTiles[oldTile]?.image = Cap.tileImage(tile: oldTile) installedTiles[oldTile]?.image = Cap.tileImage(tile: oldTile)
installedTiles[newTile]?.image = Cap.tileImage(tile: newTile) installedTiles[newTile]?.image = Cap.tileImage(tile: newTile)
} }
clearTileSelection() clearTileSelection()
} }
private func clearTileSelection() { private func clearTileSelection() {

View File

@ -56,7 +56,19 @@ class ImageSelector: UIViewController {
private func downloadImages() { private func downloadImages() {
images = [UIImage?](repeating: nil, count: cap.count) images = [UIImage?](repeating: nil, count: cap.count)
event("\(cap.count) images for cap \(cap.id)") event("\(cap.count) images for cap \(cap.id)")
for number in 0..<cap.count { if let image = cap.image {
self.images[0] = image
self.collection.reloadItems(at: [IndexPath(row: 0, section: 0)])
} else {
cap.downloadImage { mainImage in
self.images[0] = mainImage
self.collection.reloadItems(at: [IndexPath(row: 0, section: 0)])
}
}
guard cap.count > 0 else {
return
}
for number in 1..<cap.count {
cap.downloadImage(number) { image in cap.downloadImage(number) { image in
self.images[number] = image self.images[number] = image
self.collection.reloadItems(at: [IndexPath(row: number, section: 0)]) self.collection.reloadItems(at: [IndexPath(row: number, section: 0)])
@ -67,43 +79,11 @@ class ImageSelector: UIViewController {
// MARK: Select // MARK: Select
private func selectedImage(nr: Int) { private func selectedImage(nr: Int) {
guard images[nr] != nil else { guard let image = images[nr] else {
return return
} }
let tempId = cap.count cap.setMainImage(to: nr, image: image)
let tempFile = "/Images/\(cap.id)/\(cap.id)-\(tempId).jpg"
let oldFile = "/Images/\(cap.id)/\(cap.id)-0.jpg"
let newFile = "/Images/\(cap.id)/\(cap.id)-\(nr).jpg"
guard oldFile != newFile else {
return
}
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)")
}
self.finish(with: nr)
}
}
}
}
private func finish(with nr: Int) {
let image = images[nr]!
guard cap.save(mainImage: image) else {
return
}
event("Successfully switched image")
} }
} }

View File

@ -24,19 +24,20 @@ class SettingsController: UITableViewController {
@IBOutlet weak var countsLabel: UILabel! @IBOutlet weak var countsLabel: UILabel!
private var nameFileChanges = false
private var isUpdatingCounts = false private var isUpdatingCounts = false
private var isUploadingNameFile = false
private var isUpdatingThumbnails = false private var isUpdatingThumbnails = false
private var isUpdatingColors = false
override var supportedInterfaceOrientations: UIInterfaceOrientationMask { override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .portrait return .portrait
} }
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
} }
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
@ -49,7 +50,7 @@ class SettingsController: UITableViewController {
override func viewDidAppear(_ animated: Bool) { override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated) super.viewDidAppear(animated)
setAccessories() setClassifierChoice(Persistence.useMobileNet)
} }
private func updateThumbnails() { private func updateThumbnails() {
@ -60,6 +61,16 @@ class SettingsController: UITableViewController {
isUpdatingThumbnails = false isUpdatingThumbnails = false
} }
private func updateColors() {
isUpdatingColors = true
Cap.shouldSave = false
for cap in Cap.all.values {
cap.makeAverageColor()
}
Cap.shouldSave = true
isUpdatingColors = false
}
private func updateDatabaseStats() { private func updateDatabaseStats() {
let totalCaps = Cap.totalCapCount let totalCaps = Cap.totalCapCount
totalCapsLabel.text = "\(totalCaps) caps" totalCapsLabel.text = "\(totalCaps) caps"
@ -75,7 +86,6 @@ class SettingsController: UITableViewController {
private func updateNameFileStats() { private func updateNameFileStats() {
let capCount = Cap.totalCapCount - Persistence.lastUploadedCapCount let capCount = Cap.totalCapCount - Persistence.lastUploadedCapCount
let imageCount = Cap.imageCount - Persistence.lastUploadedImageCount let imageCount = Cap.imageCount - Persistence.lastUploadedImageCount
nameFileChanges = capCount > 0 || imageCount > 0
databaseUpdatesLabel.text = "\(capCount) new caps and \(imageCount) new images" databaseUpdatesLabel.text = "\(capCount) new caps and \(imageCount) new images"
} }
@ -83,36 +93,34 @@ class SettingsController: UITableViewController {
dropboxAccountLabel.text = DropboxController.shared.isEnabled ? "Sign out" : "Sign in" dropboxAccountLabel.text = DropboxController.shared.isEnabled ? "Sign out" : "Sign in"
} }
private func setAccessories() { private func setClassifierChoice(_ useMobileNet: Bool) {
tableView.cellForRow(at: IndexPath(row: 0, section: 2))?.accessoryType = Persistence.squeezenet ? .checkmark : .none tableView.cellForRow(at: IndexPath(row: 0, section: 0))?.accessoryType = useMobileNet ? .checkmark : .none
tableView.cellForRow(at: IndexPath(row: 1, section: 2))?.accessoryType = Persistence.resnet ? .checkmark : .none
tableView.cellForRow(at: IndexPath(row: 2, section: 2))?.accessoryType = Persistence.xcode ? .checkmark : .none
} }
private func toggleClassifier(index: Int) { private func toggleClassifier() {
switch index { let newValue = !Persistence.useMobileNet
case 0: Persistence.squeezenet = !Persistence.squeezenet Persistence.useMobileNet = newValue
case 1: Persistence.resnet = !Persistence.resnet setClassifierChoice(newValue)
case 2: Persistence.xcode = !Persistence.xcode
default:
return
}
setAccessories()
} }
override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
switch indexPath.section { switch indexPath.section {
case 0: // Mosaic case 0: // Choose models
return true return true
case 1: // Database case 1: // Mosaic
return indexPath.row == 2 && nameFileChanges
case 2: // Choose models
return true return true
case 2: // Database
return indexPath.row == 2 && !isUploadingNameFile
case 3: // Refresh case 3: // Refresh
if indexPath.row == 0 { switch indexPath.row {
case 0:
return !isUpdatingCounts return !isUpdatingCounts
} else { case 1:
return !isUpdatingThumbnails return !isUpdatingThumbnails
case 2:
return !isUpdatingColors
default:
return false
} }
case 4: // Dropbox account case 4: // Dropbox account
return true return true
@ -124,17 +132,22 @@ class SettingsController: UITableViewController {
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
switch indexPath.section { switch indexPath.section {
case 0: // Mosaic case 0: // Choose models
return indexPath return indexPath
case 1: // Database case 1: // Mosaic
return (indexPath.row == 2 && nameFileChanges) ? indexPath : nil
case 2: // Choose models
return indexPath return indexPath
case 2: // Database
return (indexPath.row == 2 && !isUploadingNameFile) ? indexPath : nil
case 3: // Refresh count case 3: // Refresh count
if indexPath.row == 0 { switch indexPath.row {
return isUpdatingCounts ? nil : indexPath case 0:
} else { return isUpdatingCounts ? nil : indexPath
case 1:
return isUpdatingThumbnails ? nil : indexPath return isUpdatingThumbnails ? nil : indexPath
case 2:
return isUpdatingColors ? nil : indexPath
default:
return nil
} }
case 4: // Dropbox account case 4: // Dropbox account
return indexPath return indexPath
@ -147,17 +160,22 @@ class SettingsController: UITableViewController {
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true) tableView.deselectRow(at: indexPath, animated: true)
switch indexPath.section { switch indexPath.section {
case 1: // Upload case 0: // Choose models
if indexPath.row == 2 && nameFileChanges { toggleClassifier()
case 2: // Upload
if indexPath.row == 2 && !isUploadingNameFile {
uploadNameFile() uploadNameFile()
} }
case 2: // Choose models
toggleClassifier(index: indexPath.row)
case 3: // Refresh count case 3: // Refresh count
if indexPath.row == 0 { switch indexPath.row {
case 0:
updateCounts() updateCounts()
} else { case 1:
updateThumbnails() updateThumbnails()
case 2:
updateColors()
default:
break
} }
default: default:
break break
@ -205,7 +223,10 @@ class SettingsController: UITableViewController {
} }
private func uploadNameFile() { private func uploadNameFile() {
event("Uploading name file")
isUploadingNameFile = true
Cap.saveAndUpload() { _ in Cap.saveAndUpload() { _ in
self.isUploadingNameFile = false
self.updateNameFileStats() self.updateNameFileStats()
} }
} }