開源一個(gè)上架 App Store 的相機(jī) App

Osho 相機(jī)是我獨(dú)立開發(fā)上架的一個(gè)相機(jī) App泽铛,App Store地址:點(diǎn)我。它支持1:1瞄崇,4:3,16:9多種分辨率拍攝壕曼,濾鏡可在取景框的實(shí)時(shí)預(yù)覽苏研,拍攝過程可與濾鏡實(shí)時(shí)合成,支持分段拍攝腮郊,支持回刪等特性摹蘑。下面先分享分享開發(fā)這個(gè) App 的一些心得體會(huì),文末會(huì)給出項(xiàng)目的下載地址轧飞,閱讀本文可能需要一點(diǎn)點(diǎn) AVFoundation 開發(fā)的基礎(chǔ)衅鹿。

1、GLKView和GPUImageVideoCamera

一開始取景框的預(yù)覽我是基于 GLKView 做的过咬,GLKView 是蘋果對OpenGL的封裝大渤,我們可以使用它的回調(diào)函數(shù)-glkView:drawInRect:進(jìn)行對處理后的samplebuffer渲染的工作(samplebuffer是在相機(jī)回調(diào)didOutputSampleBuffer產(chǎn)生的),附上當(dāng)初簡版代碼:

Objective-C

- (CIImage *)renderImageInRect:(CGRect)rect {

CMSampleBufferRef sampleBuffer = _sampleBufferHolder.sampleBuffer;

if (sampleBuffer != nil) {

UIImage *originImage = [self imageFromSamplePlanerPixelBuffer:sampleBuffer];

if (originImage) {

if (self.filterName && self.filterName.length > 0) {

GPUImageOutput<GPUImageInput> *filter;

if ([self.filterType isEqual: @"1"]) {

Class class = NSClassFromString(self.filterName);

filter = [[class alloc] init];

} else {

NSBundle *bundle = [NSBundle bundleForClass:self.class];

NSURL *filterAmaro = [NSURL fileURLWithPath:[bundle pathForResource:self.filterName ofType:@"acv"]];

filter = [[GPUImageToneCurveFilter alloc] initWithACVURL:filterAmaro];

}

[filter forceProcessingAtSize:originImage.size];

GPUImagePicture *pic = [[GPUImagePicture alloc] initWithImage:originImage];

[pic addTarget:filter];

[filter useNextFrameForImageCapture];

[filter addTarget:self.gpuImageView];

[pic processImage];

UIImage *filterImage = [filter imageFromCurrentFramebuffer];

//UIImage *filterImage = [filter imageByFilteringImage:originImage];

_CIImage = [[CIImage alloc] initWithCGImage:filterImage.CGImage options:nil];

} else {

_CIImage = [CIImage imageWithCVPixelBuffer:CMSampleBufferGetImageBuffer(sampleBuffer)];

}

}

CIImage *image = _CIImage;

if (image != nil) {

image = [image imageByApplyingTransform:self.preferredCIImageTransform];

if (self.scaleAndResizeCIImageAutomatically) {

image = [self scaleAndResizeCIImage:image forRect:rect];

}

}

return image;

}

- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {

@autoreleasepool {

rect = CGRectMultiply(rect, self.contentScaleFactor);

glClearColor(0, 0, 0, 0);

glClear(GL_COLOR_BUFFER_BIT);

CIImage *image = [self renderImageInRect:rect];

if (image != nil) {

[_context.CIContext drawImage:image inRect:rect fromRect:image.extent];

}

}

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58-(CIImage*)renderImageInRect:(CGRect)rect{

CMSampleBufferRefsampleBuffer=_sampleBufferHolder.sampleBuffer;

if(sampleBuffer!=nil){

UIImage*originImage=[selfimageFromSamplePlanerPixelBuffer:sampleBuffer];

if(originImage){

if(self.filterName&&self.filterName.length>0){

GPUImageOutput<GPUImageInput>*filter;

if([self.filterTypeisEqual:@"1"]){

Classclass=NSClassFromString(self.filterName);

filter=[[classalloc]init];

}else{

NSBundle*bundle=[NSBundlebundleForClass:self.class];

NSURL*filterAmaro=[NSURLfileURLWithPath:[bundlepathForResource:self.filterNameofType:@"acv"]];

filter=[[GPUImageToneCurveFilteralloc]initWithACVURL:filterAmaro];

}

[filterforceProcessingAtSize:originImage.size];

GPUImagePicture*pic=[[GPUImagePicturealloc]initWithImage:originImage];

[picaddTarget:filter];

[filteruseNextFrameForImageCapture];

[filteraddTarget:self.gpuImageView];

[picprocessImage];

UIImage*filterImage=[filterimageFromCurrentFramebuffer];

//UIImage *filterImage = [filter imageByFilteringImage:originImage];

_CIImage=[[CIImagealloc]initWithCGImage:filterImage.CGImageoptions:nil];

}else{

_CIImage=[CIImageimageWithCVPixelBuffer:CMSampleBufferGetImageBuffer(sampleBuffer)];

}

}

CIImage*image=_CIImage;

if(image!=nil){

image=[imageimageByApplyingTransform:self.preferredCIImageTransform];

if(self.scaleAndResizeCIImageAutomatically){

image=[selfscaleAndResizeCIImage:imageforRect:rect];

}

}

returnimage;

}

-(void)glkView:(GLKView*)viewdrawInRect:(CGRect)rect{

@autoreleasepool{

rect=CGRectMultiply(rect,self.contentScaleFactor);

glClearColor(0,0,0,0);

glClear(GL_COLOR_BUFFER_BIT);

CIImage*image=[selfrenderImageInRect:rect];

if(image!=nil){

[_context.CIContextdrawImage:imageinRect:rectfromRect:image.extent];

}

}

}

這樣的實(shí)現(xiàn)在低端機(jī)器上取景框會(huì)有明顯的卡頓掸绞,而且 ViewController 上的列表幾乎無法滑動(dòng)泵三,雖然手勢倒是還可以支持。 因?yàn)橐獙?shí)現(xiàn)分段拍攝與回刪等功能集漾,采用這種方式的初衷是期望更高度的自定義切黔,而不去使用GPUImageVideoCamera, 畢竟我得在AVCaptureVideoDataOutputSampleBufferDelegate具篇,AVCaptureAudioDataOutputSampleBufferDelegate這兩個(gè)回調(diào)做文章纬霞,為了滿足需求,所以得在不侵入GPUImage源代碼的前提下點(diǎn)功夫驱显。

怎么樣才能在不破壞GPUImageVideoCamera的代碼呢诗芜?我想到兩個(gè)方法瞳抓,第一個(gè)是創(chuàng)建一個(gè)類,然后把GPUImageVideoCamera里的代碼拷貝過來伏恐,這么做簡單粗暴孩哑,缺點(diǎn)是若以后GPUImage升級了,代碼維護(hù)起來是個(gè)小災(zāi)難翠桦;再來說說第二個(gè)方法——繼承横蜒,繼承是個(gè)挺優(yōu)雅的行為,可它的麻煩在于獲取不到私有變量销凑,好在有強(qiáng)大的 runtime丛晌,解決了這個(gè)棘手的問題。下面是用 runtime 獲取私有變量:

Objective-C

- (AVCaptureAudioDataOutput *)gpuAudioOutput {

Ivar var = class_getInstanceVariable([super class], "audioOutput");

id nameVar = object_getIvar(self, var);

return nameVar;

}

1

2

3

4

5

6-(AVCaptureAudioDataOutput*)gpuAudioOutput{

Ivarvar=class_getInstanceVariable([superclass],"audioOutput");

idnameVar=object_getIvar(self,var);

returnnameVar;

}

至此取景框?qū)崿F(xiàn)了濾鏡的渲染并保證了列表的滑動(dòng)幀率斗幼。

2澎蛛、實(shí)時(shí)合成以及 GPUImage 的 outputImageOrientation

顧名思義,outputImageOrientation屬性和圖像方向有關(guān)的蜕窿。GPUImage的這個(gè)屬性是對不同設(shè)備的在取景框的圖像方向做過優(yōu)化的谋逻,但這個(gè)優(yōu)化會(huì)與 videoOrientation 產(chǎn)生沖突,它會(huì)導(dǎo)致切換攝像頭導(dǎo)致圖像方向不對桐经,也會(huì)造成拍攝完之后的視頻方向不對毁兆。 最后的解決辦法是確保攝像頭輸出的圖像方向正確,所以將其設(shè)置為UIInterfaceOrientationPortrait次询,而不對videoOrientation進(jìn)行設(shè)置荧恍,剩下的問題就是怎樣處理拍攝完成之后視頻的方向瓷叫。

先來看看視頻的實(shí)時(shí)合成屯吊,因?yàn)檫@里包含了對用戶合成的CVPixelBufferRef資源處理。還是使用繼承的方式繼承GPUImageView摹菠,其中使用了 runtime 調(diào)用私有方法:

Objective-C

SEL s = NSSelectorFromString(@"textureCoordinatesForRotation:");

IMP imp = [[GPUImageView class] methodForSelector:s];

GLfloat *(*func)(id, SEL, GPUImageRotationMode) = (void *)imp;

GLfloat *result = [GPUImageView class] ? func([GPUImageView class], s, inputRotation) : nil;

......

glVertexAttribPointer(self.gpuDisplayTextureCoordinateAttribute, 2, GL_FLOAT, 0, 0, result);

1

2

3

4

5

6

7

8

9SELs=NSSelectorFromString(@"textureCoordinatesForRotation:");

IMPimp=[[GPUImageViewclass]methodForSelector:s];

GLfloat*(*func)(id,SEL,GPUImageRotationMode)=(void*)imp;

GLfloat*result=[GPUImageViewclass]?func([GPUImageViewclass],s,inputRotation): nil;

......

glVertexAttribPointer(self.gpuDisplayTextureCoordinateAttribute,2,GL_FLOAT,0,0,result);

直奔重點(diǎn)——CVPixelBufferRef的處理盒卸,將 renderTarget 轉(zhuǎn)換為 CGImageRef 對象,再使用 UIGraphics 獲得經(jīng)CGAffineTransform處理過方向的 UIImage次氨,此時(shí) UIImage 的方向并不是正常的方向蔽介,而是旋轉(zhuǎn)過90度的圖片,這么做的目的是為 videoInput 的 transform 屬性埋下伏筆煮寡。下面是 CVPixelBufferRef 的處理代碼:

Objective-C

int width = self.gpuInputFramebufferForDisplay.size.width;

int height = self.gpuInputFramebufferForDisplay.size.height;

renderTarget = self.gpuInputFramebufferForDisplay.gpuBufferRef;

NSUInteger paddedWidthOfImage = CVPixelBufferGetBytesPerRow(renderTarget) / 4.0;

NSUInteger paddedBytesForImage = paddedWidthOfImage * (int)height * 4;

glFinish();

CVPixelBufferLockBaseAddress(renderTarget, 0);

GLubyte *data = (GLubyte *)CVPixelBufferGetBaseAddress(renderTarget);

CGDataProviderRef ref = CGDataProviderCreateWithData(NULL, data, paddedBytesForImage, NULL);

CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB();

CGImageRef iref = CGImageCreate((int)width, (int)height, 8, 32, CVPixelBufferGetBytesPerRow(renderTarget), colorspace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst, ref, NULL, NO, kCGRenderingIntentDefault);

UIGraphicsBeginImageContext(CGSizeMake(height, width));

CGContextRef cgcontext = UIGraphicsGetCurrentContext();

CGAffineTransform transform = CGAffineTransformIdentity;

transform = CGAffineTransformMakeTranslation(height / 2.0, width / 2.0);

transform = CGAffineTransformRotate(transform, M_PI_2);

transform = CGAffineTransformScale(transform, 1.0, -1.0);

CGContextConcatCTM(cgcontext, transform);

CGContextSetBlendMode(cgcontext, kCGBlendModeCopy);

CGContextDrawImage(cgcontext, CGRectMake(0.0, 0.0, width, height), iref);

UIImage *image = UIGraphicsGetImageFromCurrentImageContext();

UIGraphicsEndImageContext();

self.img = image;

CFRelease(ref);

CFRelease(colorspace);

CGImageRelease(iref);

CVPixelBufferUnlockBaseAddress(renderTarget, 0);

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34intwidth=self.gpuInputFramebufferForDisplay.size.width;

intheight=self.gpuInputFramebufferForDisplay.size.height;

renderTarget=self.gpuInputFramebufferForDisplay.gpuBufferRef;

NSUIntegerpaddedWidthOfImage=CVPixelBufferGetBytesPerRow(renderTarget)/4.0;

NSUIntegerpaddedBytesForImage=paddedWidthOfImage*(int)height*4;

glFinish();

CVPixelBufferLockBaseAddress(renderTarget,0);

GLubyte*data=(GLubyte*)CVPixelBufferGetBaseAddress(renderTarget);

CGDataProviderRefref=CGDataProviderCreateWithData(NULL,data,paddedBytesForImage,NULL);

CGColorSpaceRefcolorspace=CGColorSpaceCreateDeviceRGB();

CGImageRefiref=CGImageCreate((int)width,(int)height,8,32,CVPixelBufferGetBytesPerRow(renderTarget),colorspace,kCGBitmapByteOrder32Little|kCGImageAlphaPremultipliedFirst,ref,NULL,NO,kCGRenderingIntentDefault);

UIGraphicsBeginImageContext(CGSizeMake(height,width));

CGContextRefcgcontext=UIGraphicsGetCurrentContext();

CGAffineTransformtransform=CGAffineTransformIdentity;

transform=CGAffineTransformMakeTranslation(height/2.0,width/2.0);

transform=CGAffineTransformRotate(transform,M_PI_2);

transform=CGAffineTransformScale(transform,1.0,-1.0);

CGContextConcatCTM(cgcontext,transform);

CGContextSetBlendMode(cgcontext,kCGBlendModeCopy);

CGContextDrawImage(cgcontext,CGRectMake(0.0,0.0,width,height),iref);

UIImage*image=UIGraphicsGetImageFromCurrentImageContext();

UIGraphicsEndImageContext();

self.img=image;

CFRelease(ref);

CFRelease(colorspace);

CGImageRelease(iref);

CVPixelBufferUnlockBaseAddress(renderTarget,0);

而 videoInput 的 transform 屬性設(shè)置如下:

Objective-C

_videoInput.transform = CGAffineTransformRotate(_videoConfiguration.affineTransform, -M_PI_2);

1

2_videoInput.transform=CGAffineTransformRotate(_videoConfiguration.affineTransform,-M_PI_2);

經(jīng)過這兩次方向的處理虹蓄,合成的小視頻終于方向正常了。此處為簡版的合成視頻代碼:

Objective-C

CIImage *image = [[CIImage alloc] initWithCGImage:img.CGImage options:nil];

CVPixelBufferLockBaseAddress(pixelBuffer, 0);

[self.context.CIContext render:image toCVPixelBuffer:pixelBuffer];

...

[_videoPixelBufferAdaptor appendPixelBuffer:pixelBuffer withPresentationTime:bufferTimestamp]

1

2

3

4

5

6CIImage*image=[[CIImagealloc]initWithCGImage:img.CGImageoptions:nil];

CVPixelBufferLockBaseAddress(pixelBuffer,0);

[self.context.CIContextrender:imagetoCVPixelBuffer:pixelBuffer];

...

[_videoPixelBufferAdaptorappendPixelBuffer:pixelBufferwithPresentationTime:bufferTimestamp]

可以看到關(guān)鍵點(diǎn)還是在于上面繼承自GPUImageView這個(gè)類獲取到的 renderTarget 屬性幸撕,它應(yīng)該即是取景框?qū)崟r(shí)預(yù)覽的結(jié)果薇组,我在最初的合成中是使用 sampleBuffer 轉(zhuǎn) UIImage,再通過 GPUImage 添加濾鏡坐儿,最后將 UIImage 再轉(zhuǎn) CIImage律胀,這么做導(dǎo)致拍攝時(shí)會(huì)卡宋光。當(dāng)時(shí)我?guī)缀跸敕艞壛耍踔料氩捎门暮煤笤偌訛V鏡的方式繞過去炭菌,最后這些不純粹的方法都被我 ban 掉了罪佳。

既然濾鏡可以在取景框?qū)崟r(shí)渲染,我想到了GPUImageView可能有料黑低。在閱讀過 GPUImage 的諸多源碼后赘艳,終于在GPUImageFramebuffer.m找到了一個(gè)叫 renderTarget 的屬性。至此克握,合成的功能也告一段落第练。

3、關(guān)于濾鏡

這里主要分享個(gè)有意思的過程玛荞。App 里有三種類型的濾鏡娇掏。基于 glsl 的勋眯、直接使用 acv 的以及直接使用 lookuptable

的婴梧。lookuptable 其實(shí)也是 photoshop

可導(dǎo)出的一種圖片,但一般的軟件都會(huì)對其加密客蹋,下面簡單提下我是如何反編譯“借用”某軟件的部分濾鏡吧塞蹭。使用 Hopper Disassembler

軟件進(jìn)行反編譯,然后通過某些關(guān)鍵字的搜索讶坯,幸運(yùn)地找到了下圖的一個(gè)方法名番电。

reverse 只能說這么多了….在開源代碼里我已將這一類敏感的濾鏡剔除了。

小結(jié)

開發(fā)相機(jī) App 是個(gè)挺有意思的過程辆琅,在其中邂逅不少優(yōu)秀開源代碼漱办,向開源代碼學(xué)習(xí),才能避免自己總是寫出一成不變的代碼婉烟。最后附上項(xiàng)目的開源地址https://github.com/hawk0620/ZPCamera娩井,希望能夠幫到有需要的朋友,也歡迎 star 和 pull request似袁。


原文地址:http://ios.jobbole.com/92926/?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末洞辣,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子昙衅,更是在濱河造成了極大的恐慌扬霜,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件而涉,死亡現(xiàn)場離奇詭異著瓶,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)婴谱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進(jìn)店門蟹但,熙熙樓的掌柜王于貴愁眉苦臉地迎上來躯泰,“玉大人,你說我怎么就攤上這事华糖÷笙颍” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵客叉,是天一觀的道長诵竭。 經(jīng)常有香客問我,道長兼搏,這世上最難降的妖魔是什么卵慰? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮佛呻,結(jié)果婚禮上裳朋,老公的妹妹穿的比我還像新娘。我一直安慰自己吓著,他們只是感情好鲤嫡,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著绑莺,像睡著了一般暖眼。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上纺裁,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天诫肠,我揣著相機(jī)與錄音,去河邊找鬼欺缘。 笑死栋豫,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的浪南。 我是一名探鬼主播笼才,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼络凿!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起昂羡,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤絮记,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后虐先,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體怨愤,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年蛹批,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了撰洗。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片篮愉。...
    茶點(diǎn)故事閱讀 38,059評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖差导,靈堂內(nèi)的尸體忽然破棺而出试躏,到底是詐尸還是另有隱情,我是刑警寧澤设褐,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布颠蕴,位于F島的核電站,受9級特大地震影響助析,放射性物質(zhì)發(fā)生泄漏犀被。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一外冀、第九天 我趴在偏房一處隱蔽的房頂上張望寡键。 院中可真熱鬧,春花似錦雪隧、人聲如沸昌腰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽遭商。三九已至,卻和暖如春捅伤,著一層夾襖步出監(jiān)牢的瞬間劫流,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工丛忆, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留祠汇,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓熄诡,卻偏偏與公主長得像可很,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子凰浮,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容