5.9 定义和布局视图控制器
BeanCounter使用两个视图控制器。第一个控制器允许用户捕获和预览斑点的图像。第二个控制器让用户查看斑点的分类结果,然后保存和分享斑点图像。跳转(Segue)对象让第一个视图控制器实例化第二个视图控制器,并传递一个Blob对象和标签。这类似于我们在第四章的项目哺乳动物脸部的检测与融合中ManyMasks应用的逻辑划分,因此我们能够相关的代码.
5.9.1 捕获和预览斑点
导入我们在第二章’捕获,存储和分享照片’中创建的VideoCamera.h和.m文件的备份.这两个文件定义了VideoCamera类,该类继承于OpenCV的cvVideoCamera,修正了bug并增加了一些功能.
同样,导入我们在第四章哺乳动物脸部的检测与融合创建的CpatureViewController.h和.m文件.这两个文件包含了CpatureViewController类,该类负责捕获和检测.当然,我们新版本的CaptureViewController并不依赖我们脸部的旧模型和脸部检测器.新版本依赖于斑点模型和斑点检测器和一个斑点分类器.此外,还持有者一个用于描述可能的分类结果的标签的字符串列表.用户界面也改变了一点点.编辑CaptureViewController.m,他的导入声明和私有接口如下:
#import <opencv2/core.hpp>
#import <opencv2/imgcodecs/ios.h>
#import <opencv2/imgproc.hpp>
#import "CaptureViewController.h"
#import "BlobClassifier.hpp"
#import "BlobDetector.hpp"
#import "ReviewViewController.h"
#import "VideoCamera.h"
const double DETECT_RESIZE_FACTOR = 0.5;
@interface CaptureViewController.h () <CvVideoCameraDelegate> {
BlobClassifier *blobClassifier;
BlobDetector *blobDetector;
std::vector<Blob> detectedBlobs;
}
@property IBOutlet UIView *backgroundView;
@property IBOutlet UIBarButtonItem *classifyButton;
@property VideoCamera *videoCamera; @property BOOL showMask;
@property NSArray<NSString *> *labelDescriptions;
- (IBAction)onTapToSetPointOfInterest:(UITapGestureRecognizer *)tapGesture;
- (IBAction)onPreviewModeSelected:(UISegmentedControl *)segmentedControl;
- (IBAction)onSwitchCameraButtonPressed;
- (void)refresh;
- (void)processImage:(cv::Mat &)mat;
- (UIImage *)imageFromCapturedMat:(const cv::Mat &)mat;
@end
viewDidLoad方法负责初始化检测器,分类器,描述字符串列表和相机.作为这个过程的一部分,我们从BlobClassiferTraining.plist中加载关于分类器训练集的元数据。(有关此PLIST文件的描述,请参阅本章前面的’配置项目’一节.)iOS SDK使我们能够轻松地将PLIST作为键-值对的字典加载。键是字符串,值可以是其他字典、数组、字符串、数字、布尔值、日期或原始二进制数据。出于BeanCounter的目的,PLIST文件提供了标签描述以及成对的训练图像及其对应的标签。我们从文件中加载每个图像,用图像及其标签构造Blob,并将Blob传递给BlobClassifier的update方法来训练分类器。查看下面viewDidLoad实现中的代码:
@implementation CaptureViewController
- (void)viewDidLoad {
[super viewDidLoad];
blobDetector = new BlobDetector();
blobClassifier = new BlobClassifier();
// Load the blob classifier's configuration from file.
NSBundle *bundle = [NSBundle mainBundle];
NSString *configPath = [bundle pathForResource:@"BlobClassifierTraining" ofType:@"plist"];
NSDictionary *config = [NSDictionary dictionaryWithContentsOfFile:configPath];
// Remember the descriptions of the blob labels.
self.labelDescriptions = config[@"labelDescriptions"];
// Create reference blobs and train the blob classifier.
NSArray *configBlobs = config[@"blobs"];
for (NSDictionary *configBlob in configBlobs) {
uint32_t label = [configBlob[@"label"] unsignedIntValue];
NSString *imageFilename = configBlob[@"imageFilename"];
UIImage *image = [UIImage imageNamed:imageFilename];
if (image == nil) { NSLog(@"Image not found in resources: %@", imageFilename);
continue;
}
cv::Mat mat;
UIImageToMat(image, mat);
cv::cvtColor(mat, mat, cv::COLOR_RGB2BGR);
Blob blob(mat, label);
blobClassifier->update(blob);
}
self.videoCamera = [[VideoCamera alloc] initWithParentView:self.backgroundView];
self.videoCamera.delegate = self;
self.videoCamera.defaultAVCaptureSessionPreset =AVCaptureSessionPresetHigh;
self.videoCamera.defaultFPS = 30;
self.videoCamera.letterboxPreview = YES;
self.videoCamera.defaultAVCaptureDevicePosition =AVCaptureDevicePositionBack;
}
BlobClassifier和BlobDetector都是C++动态生成的对象.分类器将占用很大的内存,因为他持有了直方图对象和所有引用图片的关键点描述符.让我们编辑’didReceiveMemoryWarning’和’dealloc’方法来确保这些C++对象被清理掉:
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
if (blobClassifier != NULL) {
delete blobClassifier;
blobClassifier = NULL;
} if (blobDetector != NULL) {
delete blobDetector;
blobDetector = NULL;
}
}
- (void)dealloc {
if (blobClassifier != NULL) {
delete blobClassifier;
blobClassifier = NULL;
} if (blobDetector != NULL) {
delete blobDetector;
blobDetector = NULL;
}
}
当用户在Image和Mask分段控制器中选择了一个选项,我们就设置一个布尔值’showMask’,如下面代码所示:
- (IBAction)onPreviewModeSelected:(UISegmentedControl *)segmentedControl {
switch (segmentedControl.selectedSegmentIndex) {
case 0:
self.showMask = NO;
break;
default:
self.showMask = YES;
break;
}
[self refresh];
}
现在,让我们考虑下BeanCounter是如何处理相机的每一帧的输入的.回调方法’processImage:'以我们常规的修正方向的代码开始.然后,我们传递当前帧数据和vector给BlobDetector的detect方法,该方法将填充vector对象,并在检测到的斑点周围绘制绿色矩形.'Classify’按钮能否启用,取决于当前帧是否检测到了斑点.如果’showMask’属性值为true,我们就显示当前帧的掩码图,否则,用户就将看到原图中,斑点被绿色矩形包围的图像.下面是回调的实现:
- (void)processImage:(cv::Mat &)mat {
switch (self.videoCamera.defaultAVCaptureVideoOrientation) {
case AVCaptureVideoOrientationLandscapeLeft:
case AVCaptureVideoOrientationLandscapeRight:
// The landscape video is captured upside-down.
// Rotate it by 180 degrees.
cv::flip(mat, mat, -1);
break;
default:
break;
}
// Detect and draw any blobs.
blobDetector->detect(mat, detectedBlobs, DETECT_RESIZE_FACTOR, true);
BOOL didDetectBlobs = (detectedBlobs.size() > 0);
dispatch_async(dispatch_get_main_queue(), ^{
self.classifyButton.enabled = didDetectBlobs;
});
if (self.showMask) {
blobDetector->getMask().copyTo(mat);
}
}
CaptureViewController的其它方法和第4章’哺乳动物脸部的检测与融合’的ManyMasks程序中是一样的.
打开Main.storyboard,在场景层次中选择ViewController.在Identity选项卡中将Class的值设置为CpatureViewController.然后,按照下图所示,增加GUI插件作为视图控制器的主视图的子视图.(或者从本书的GitHub仓库中直接下载完整的故事版文件)
在场景层次图中右键点击CaptureViewController,会显示我们在CaptureViewController.m中定义的可用的输出口和动作.按照下图所示进行连线:
5.9.2 观察,保存和分享已分类斑点
在项目中导入第4章’哺乳动物脸部的检测与融合’的ManyMasks项目中的ReviewViewController.h和.m文件.针对BeanCounter的需求,我们将编辑这些文件来支持显示标题,该标题描述了分类的结果.首先,编辑ReviewViewController.h的公共接口增加一个NSString属性,如下面代码所示:
@interface ReviewViewController : UIViewController
@property UIImage *image;
@property NSString *caption;
@end
在ReviewViewController.m的私有接口中添加一个UILabel属性,如下面代码所示:
@interface ReviewViewController ()
@property IBOutlet UIImageView *imageView;
@property IBOutlet UILabel *label;
@property IBOutlet UIActivityIndicatorView *activityIndicatorView;
@property IBOutlet UIToolbar *toolbar;
// ... same methods as in Chapter 4 ...
@end
最后,编辑viewDidLoad方法的实现,将capion属性赋值给label的text的属性:
- (void)viewDidLoad {
[super viewDidLoad];
self.imageView.image = self.image;
self.label.text = self.caption;
}
其余的内容保持不变.特别地,该类仍然支持保存和分享图片.
让我们打开Main.storyborad.从库中拖动一个新的视图控制器到编辑区域.打开新视图控制的Identity检测器,设置Class为ReviewViewController.然后增加合适的插件,参考以下截图(或者直接从本书仓库中下载完整的故事板文件):
在场景层次图中右击ReviewViewController,打开可用输出口和行为列表,这些在ReviewViewController中都有定义.参考下图进行连接:
5.9.3 视图控制器之间的跳转
在prepareForSegue函数中,需要提供一个blob对象和分类结果给ReviewViewController.首先,应该停止相机因为我们不希望proccImage:方法在我们访问当前blob的时候在其他线程中更改blob向量.然后,prepareForSegue中,选择最大的blob,并传递给BlobClassifier的classify函数,该函数返回代表分类信息的整型结果.我们根据这个结果找到标签的描述,最后将blob的image和标签的描述传递给ReviewViewController.下面是相关代码:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:@"showReviewModally"]) {
ReviewViewController *reviewViewController = segue.destinationViewController;
// Stop the camera to prevent conflicting access to the blobs.
[self.videoCamera stop];
// Find the biggest blob.
int biggestBlobIndex = 0;
for (int i = 0, biggestBlobArea = 0;i < detectedBlobs.size(); i++) {
Blob &detectedBlob = detectedBlobs[i];
int blobArea = detectedBlob.getWidth() *detectedBlob.getHeight();
if (blobArea > biggestBlobArea) {
biggestBlobIndex = i;
biggestBlobArea = blobArea;
}
}
Blob &biggestBlob = detectedBlobs[biggestBlobIndex];
// Classify the blob and show the result in the destination // view controller.
blobClassifier->classify(biggestBlob);
reviewViewController.image = [self imageFromCapturedMat:biggestBlob.getMat()];
reviewViewController.caption = self.labelDescriptions[biggestBlob.getLabel()];
}
}
再次打开Main.storyboard,点击并拖动Classify按钮到Review View Controller,创建一个segue跳转对象.segue的Kind是Present Modally,Identifier是"showReviewModally",Transition是FlipHorizontal(或者其它你喜欢的效果).参考以下截图: