基于MacOSX平台下的二维码扫码功能
最近项目接受了一个需求,要在macOS的系统上实现一个二维码扫码的功能.
通过多方面的学习,最终实现了要求,具体的实现步骤将会在下面展示。
核心功能模块
实现途径上,最开始我是打算采用iOS自带有的原生Api实现的,也是一般iOS通用的二维码识别方式,代码如下:
_output.metadataObjectTypes=@[AVMetadataObjectTypeQRCode,
AVMetadataObjectTypeEAN13Code, AVMetadataObjectTypeEAN8Code, AVMetadataObjectTypeCode128Code];
但是非常遗憾的是,此api只支持iOS7.0+,对于OS系统是不支持的,那么就不能采用这个方法来识别二维码了。
如果使用ZXing的话,存在相当多的问题,需要修改的内容也是非常多的。
楼主能力与时间有限,所以没有走通,如果有好的方法大家可以互相交流下。
而这里主要使用的解析模块是系统自带的一个模块CIDetector.
利用AVCaptureSession截取某一时刻的图像,然后转换成CIImage,传输给CIDetector解析。
1、初始化session
与iOS的扫码功能一样,需要创建一个AVCaptureSession的上下文处理器。
设备和输入设备都是别无二致的,但是采集信息的时候不能采用AVCaptureMetadataOutput(iOS独有的)。
这里采用的是AVCaptureStillImageOutput,用来获取某一帧的图像。具体代码如下:
/**
设置上下文处理器和输入输出
*/
- (void)setupCamera
{
_session = [[AVCaptureSession alloc] init];
[_session setSessionPreset:AVCaptureSessionPresetHigh];
NSError *error = nil;
_device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
AVCaptureDeviceInput *myDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:_device error:&error];
[self.session addInput:myDeviceInput];
_output = [[AVCaptureStillImageOutput alloc] init];
NSDictionary *myOutputSettings = [[NSDictionary alloc] initWithObjectsAndKeys:AVVideoCodecJPEG,AVVideoCodecKey,nil];
[_output setOutputSettings:myOutputSettings];
[self.session addOutput:_output];
}
2、设置加载图层
这块的部分和iOS的功能和作用一样,无需多言。
/**
设置视频加载图层
@param layer 图层
@param bounds 范围
*/
- (void)setAVCaptureVideoPreviewLayerWithSupLayer:(CALayer *)layer
withBounds:(NSRect)bounds
{
if (preview!=nil)
{
[preview removeFromSuperlayer];
}
preview = [[AVCaptureVideoPreviewLayer alloc] initWithSession:self.session];
[preview setVideoGravity:AVLayerVideoGravityResizeAspectFill];
[preview setBounds:bounds];
[layer addSublayer:preview];
}
3、调用方法识别图片
因为不是使用的AVCaptureMetadataOutput,所以获取图像的方法,可以是用一个计时器定时每秒去获取到当前的图像信息。
用一个计时器调用这个方法即可。
/**
识别图片中的二维码
@param fib 完成识别后返回的block
*/
- (void)identifyImageQR:(FinishIdentifyBlock)fib
{
AVCaptureConnection *myVideoConnection =[self connectionWithMediaType:AVMediaTypeVideo fromConnections:[[self output] connections]];
[_output captureStillImageAsynchronouslyFromConnection:myVideoConnection completionHandler:^(CMSampleBufferRef imageDataSampleBuffer, NSError *error) {
if (imageDataSampleBuffer) {
NSData *imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageDataSampleBuffer];
NSImage *myImage = [[NSImage alloc] initWithData:imageData];
NSString * qrString_ = [self stringFromCiImage:[self getCIImage:myImage]];
if (![qrString_ isEqualToString:@""]) {
fib(qrString_,nil);
}
else
{
fib(nil,@"");
}
}
}];
}
/**
根据媒体类型,总共得到的数据,筛选出自己需要的数据
@param mediaType 媒体类型
@param connections 数组组
@return 需要的数据
*/
- (AVCaptureConnection *)connectionWithMediaType:(NSString *)mediaType fromConnections:(NSArray *)connections
{
for ( AVCaptureConnection *connection in connections ) {
for ( AVCaptureInputPort *port in [connection inputPorts] ) {
if ( [[port mediaType] isEqual:mediaType] ) {
return connection;
}
}
}
return nil;
}
5、NSImage转换成CIImage,并进行解析。
截取到的图片获取到的是NSImage(类似于UIImage),这个图像是不能用于二维码识别的。
利用getCIImage方法可以将输入的NSImage转换成CIImage,然后进一步交给后续的方法解析。
/**
NIImage转换成CIIMage图像
@param myImage 输入的NSimage图线
@return 返回CIImage图像
*/
- (CIImage *)getCIImage:(NSImage *)myImage
{
NSData * tiffData = [myImage TIFFRepresentation];
NSBitmapImageRep * bitmap;
bitmap = [NSBitmapImageRep imageRepWithData:tiffData];
CIImage * ciImage = [[CIImage alloc] initWithBitmapImageRep:bitmap];
NSAffineTransform *affineTransform = [NSAffineTransform transform];
[affineTransform translateXBy:0 yBy:128];
[affineTransform scaleXBy:1 yBy:-1];
CIFilter *transform = [CIFilter filterWithName:@"CIAffineTransform"];
[transform setValue:ciImage forKey:@"inputImage"];
[transform setValue:affineTransform forKey:@"inputTransform"];
CIImage * result = [transform valueForKey:@"outputImage"];
[result drawAtPoint: NSMakePoint ( 0,0 )
fromRect: NSMakeRect ( 0,0,128,128 )
operation: NSCompositingOperationSourceOver
fraction: 1.0];
return ciImage;
}
/**
根据CIImage图像来进行二维码解析工作
@param ciimage 输入的CIImage图像
@return 返回解析的字符串
*/
- (NSString *)stringFromCiImage:(CIImage *)ciimage {
NSString *content = @"" ;
if (!ciimage) {
return content;
}
CIDetector *detector = [CIDetector detectorOfType:CIDetectorTypeQRCode
context:[CIContext contextWithOptions:nil]
options:@{CIDetectorAccuracy:CIDetectorAccuracyHigh}];
NSArray *features = [detector featuresInImage:ciimage];
if (features.count) {
for (CIFeature *feature in features) {
if ([feature isKindOfClass:[CIQRCodeFeature class]]) {
content = ((CIQRCodeFeature *)feature).messageString;
break;
}
}
} else {
NSLog(@"未正常解析二维码图片, 请确保iphone5/5c以上的设备");
}
return content;
}
扫码功能单例化头实现
以上的方法全是自定的对象方法,可以用于写成一个单例类进行使用,头文件内容如下:
#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
typedef void(^FinishIdentifyBlock)(NSString * qrString,NSString * error);
@interface MacQRScanSDK : NSObject
@property (strong,nonatomic)AVCaptureDevice * device;
@property (strong,nonatomic)AVCaptureDeviceInput * input;
@property (strong,nonatomic)AVCaptureStillImageOutput * output;
@property (strong,nonatomic)AVCaptureSession * session;
@property (strong,nonatomic)AVCaptureVideoPreviewLayer * preview;
/**
创建MacQRScanSDK的单利对象
@return 返回MacQRScanSDK对象
*/
+ (MacQRScanSDK *)sharedManager;
/**
设置上下文处理器和输入输出
*/
- (void)setupCamera;
/**
设置视频加载的图层
@param layer 图层
*/
/**
设置视频加载图层
@param layer 图层
@param bounds 范围
*/
- (void)setAVCaptureVideoPreviewLayerWithSupLayer:(CALayer*)layer
withBounds:(NSRect)bounds;
/**
开始扫描
*/
- (void)beginScan;
/**
结束扫描
*/
- (void)stopScan;
/**
识别图片中的二维码
@param fib 完成识别后返回的block
*/
- (void)identifyImageQR:(FinishIdentifyBlock)fib;
@end
在ViewController中的调用
在viewDidLoad中实现初始化,并且设置对应的代码
- (void)viewDidLoad {
[super viewDidLoad];
mac_qr_scan_ = [MacQRScanSDK sharedManager];
[mac_qr_scan_ setupCamera];
[mac_qr_scan_ setAVCaptureVideoPreviewLayerWithSupLayer:self.centView_.layer withBounds:NSMakeRect(0, 0, window_width*2 , window_height*2 )];
}
上面的setAVCaptureVideoPreviewLayerWithSupLayer,在Mac软件中会因为拖拽界面导致window重新设置。
那么这个layer也是需要实时获取到window的宽和高来修改范围。具体的方法可以参考使用windowDidResize。
windowDidResize会在屏幕改变的时候调用,是系统的一个通知,具体的使用方法可以自己查看一下api或者谷歌百度。
- (void)windowDidResize:(NSNotification *)aNotification
{
window_width=self.window.frame.size.width
window_height=self.window.frame.size.height
}
因为使用AVCaptureStillImageOutput,它的作用是可以截取某一时刻摄像头捕捉的某一帧的图像。
那么对于二维码来说,我们可以采用一个计时器,然后定时去调用下面的方法,获取图像并解析。
[mac_qr_scan_ identifyImageQR:^(NSString *qrString, NSString *error) {
if (error==nil) {
// 没有error,可以对qr进行操作
}
else
{
//有error,说明有问题,需要追踪
}
}];
通过这些方法,基本可以实现摄像头采集二维码并解析的功能。
对于二维码采集,前置摄像头识别率情况堪忧,尽量保证在光线充足,且不要反光的情况下扫描,不然容易失败。