ARKit 看这一篇就够了

**欢迎转载----转载请注明出处**

源码 Github 链接 欢迎Star
如果 Github 不能访问,示例程序也可在CSDN下载 点我下载项目

前置条件

在学习ARKit 前,应当有一定的矩阵知识基础方便快速上手和理解背后的逻辑。
如果你对 Metal 有过了解和学习,那么你可以轻松的使用ARKit
如果你想要了解并学习 Metal 请点击本人的另一篇文章 Metal 编程指南
如果没有渲染方面的知识也没有关系,希望你能回顾一下 iOS 中关于 Layer 的 transform 动画

坐标转换

当我们展示 ARKit 时,只能使用 ARSCNViewCAMetalLayerCAEAGLLayer

ARSCNView 继承自 SCNView 关于 SCNView 网上文章有很多 主要是用来加载 3D 模型的。也是在初学者入门时最适合使用的一种 View,自带了 ARSession 和 SCNScene 可以不用去额外处理相机流。关于 SCNView 下面有更详细的使用。
CAMetalLayer 定义在 QuartzCore.framework 中,继承于 CALayer。它管理 Metal Texture Pool,并且负责渲染 MTLTexture 到窗口。我们可以将 ARSession 的相机流以 Texture 形式贴在屏幕上。
CAEAGLLayer 同样继承自 CALayer,主要是在使用 openGLES 时使用,渲染的原理和使用 CAMetalLayer 一样。

在这三种View 中,他们的坐标系都和我们平时使用的不一样。
总的来说就是我们屏幕中的一个点比如(X=100,Y=100)在这些View中是不存在的,他们的坐标系为 (-1,1)
因此如果我们需要添加一个模型或添加一个点则应该把 屏幕坐标系转换为 它们对应的坐标系。

下图表示了如果从 UIKit 坐标转为 Metal 空间坐标
在这里插入图片描述
规范化的设备坐标使用右手坐标系X向右,Y向下,Z朝向自己并映射到视口中的位置。这些坐标与视口大小无关。 Z 值指向远离摄像机的位置(进入屏幕)。


// 我们可以使用一段代码来验证坐标转换
float uikitX = 375;
float uikitY = 667;
float ratioX = 1.0 / 375;
float ratioY = 1.0 / 667;
float resultX = (2.0 *  uikitX * ratioX) - 1.0;
float resultY = (2.0 * -uikitY * ratioY) + 1.0;

NSLog(@"x=%.2f y=%.2f",resultX, resultY);
// x=1.00 y=-1.00

3D模型

当我们了解了坐标系的变化后就可以尝试加载一些模型了。
平时看到的 AR 场景都很炫酷,那是因为模型做的好看,和我们写的代码关系不大~
首先在 SceneKit 中,系统为我们提供了很多的 SCNGeometry,如果你使用过 Unity、UE5、GritGene 等渲染引擎应该对 Geometry 不陌生,做为基础的几何图形,他们构型了一个个复杂的3维模型。
我们先尝试使用 SCNView 来加载一个 SCNBox注:SCNBox 继承自SCNGeometry,简单而言就是一个正方体
关于 SCNView 的介绍可以自行查看官方文档,SCNView 中由SCNScene做为展示,每个模型都是一个节点,将节点添加到 SCNScene 的 rootNode 中即可。

@interface DeWuViewController ()
@property (nonatomic,strong) SCNView *scene;
@end

@implementation DeWuViewController
// 初始化一个 SCNView 
- (void)viewDidLoad {
    [super viewDidLoad];
    self.title = @"得物试衣间";
    self.view.backgroundColor = UIColor.whiteColor;
    
    self.scene = [[SCNView alloc] initWithFrame:
                  CGRectMake(0, 88, self.view.frame.size.width, 200)];
    self.scene.backgroundColor = UIColor.lightGrayColor;
    self.scene.allowsCameraControl = true;
    SCNScene *rootScene = [SCNScene scene];
    self.scene.scene = rootScene;
    [self.view addSubview:self.scene];
}
@end

创建完成后看到的界面应该如下图


我们继续创建一个正方体,加载到屏幕中。

- (void)addBoxGeometry
{
    SCNBox *box = [SCNBox new];
    SCNNode *boxNode = [SCNNode nodeWithGeometry:box];
    [self.scene.scene.rootNode addChildNode:boxNode];
}

此时我们看到应该如下图所示,正方体的高度默认为 scene 的高度。


现在已经创建了一个正方体,它可能看起来不像,但不管你信不信,它确实是一个正方体!
如果我们打开了 SCNView 的 allowsCameraControl 属性,滑动屏幕应该能看到正方体。
我们可以通过设置 scene.rootNode.position 来使其变的小一些,但是肯定不是最佳的方法。因此我们需要了解矩阵的作用。
如果你做过 3D游戏、Metal、GLES。经常会看到一个 4×4 大小的矩阵,有四列和四行。
SceneKit 中,4x4矩阵使用的是 SCNMatrix4

typedef struct SCNMatrix4 {
    float m11, m12, m13, m14;
    float m21, m22, m23, m24;
    float m31, m32, m33, m34;
    float m41, m42, m43, m44;
} SCNMatrix4;

使用矩阵,可以通过三种方式转换对象:
翻译:沿 x、y 和 z 轴移动对象。
旋转:围绕任意轴旋转对象。
缩放:沿任意轴更改对象大小。

除此之外,还应该了解 投影变换
投影变换将节点的坐标从相机坐标转换为归一化坐标。根据您使用的投影类型,您将获得不同的效果。 如果你想了解更多请阅读 Metal 编程指南

举个简单例子:
如果我们希望图形绕着 X 轴旋转 则公式为:

[1 0 0 0]
[0 cos(-X Angle) -sin(-X Angle) 0]
[0 sin(-X Angle) cos(-X Angle) 0]
[0 0 0 1]

同理 沿着 Y 或 Z 使用对应的公式即可。我们并不是在讲数学的知识,所以在 SceneKit 中,系统已经帮我们做好了方法的封装,直接调用 SCNMatrix4MakeRotation 即可获得旋转后的 4x4 矩阵。 (如果你想了解矩阵的应用也可以查看我的博客 Metal 编程指南
当我们知道原理后,为正方体设置一个角度即可。

- (void)addBoxGeometry
{
    SCNBox *box = [SCNBox new];
    SCNNode *boxNode = [SCNNode nodeWithGeometry:box];
    boxNode.transform = SCNMatrix4MakeRotation(M_PI/2, 1, 1, 1);
    [self.scene.scene.rootNode addChildNode:boxNode];
}

这时我们应该能看到一个正方体了,由于 SCNBox 默认为白色,如果你想设置它的颜色或为它做纹理贴图应该使用 SCNMaterial 关于 SCNMaterial 会在下面继续说明,如果你学习了 Metal 那么可以理解它是一个片元着色器
有兴趣的可以自行搜索 SCNMaterial 学习
我们为 正方体设置一张图片,在来看看效果。

- (void)addBoxGeometry
{
   SCNBox *box = [SCNBox new];
   SCNMaterial *material = box.materials.firstObject;
   UIImage *img = [UIImage imageNamed:@"bricks"];
   material.diffuse.contents = img;
   SCNNode *boxNode = [SCNNode nodeWithGeometry:box];
   boxNode.transform = SCNMatrix4MakeRotation(M_PI/2, 1, 1, 1);
   
   [self.scene.scene.rootNode addChildNode:boxNode];
}

现在,我们可以尝试去加载由专业建模师做好的 3D模型了。
注意:关于 SCNScene 可直接加载的模型请查看 官方文档 SCNSceneSource

由于模型一般比较大,使用异步线程来进行加载

- (void)addAppleWatch
{
    dispatch_queue_t _loadQueue = dispatch_queue_create("load_assets", DISPATCH_QUEUE_SERIAL);
    dispatch_async(_loadQueue, ^{
        SCNScene *scene = [SCNScene sceneNamed:@"AppleWatch.usdz"];
        SCNNode *watchNode = scene.rootNode.childNodes[0];
        SCNNode *watchRoot = [SCNNode node];
        // position 代表模型加载的位置
        // position.x 控制左右
        // position.y 控制上下
        // position.z 控制前后
        watchRoot.position = SCNVector3Make(-0.5, -0.8, -0.5);
        watchRoot.scale = SCNVector3Make(0.2, 0.2, 0.2);
        [watchRoot addChildNode:watchNode];
        [self.scene.scene.rootNode addChildNode:watchRoot];
    });
}

完成加载后如下图



版权声明:代码中的 Apple Watch 模型是在开源网站中下载的,请勿商用

平面监测和绘制

本次演示的平面监测使用 ARSCNView 前文提到过,ARSCNView 本身自带了 ARSession 不用额外去创建,不过在示例代码中有 使用 Metal 渲染的代码。包括横竖屏切换时纹理的裁剪。源码 Github 链接 欢迎Star

本节主要是讲通过 ARSession 找到平面并在平面上绘制一个正方形。
对于 ARSCNView 的一些属性请查看文档
本节和后续章节都是通过 ARSession 的回调来获取 ARAnchor 信息,因此对于展示的 View 选择方面并不是很重要。

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.view.backgroundColor = UIColor.blackColor;
    self.sceneView = [[ARSCNView alloc] initWithFrame:[UIScreen mainScreen].bounds];
    self.sceneView.session.delegate = self;
    self.sceneView.scene = [SCNScene new];
    // 设置debug 当找到特征点时 在屏幕中显示出来
    self.sceneView.debugOptions = ARSCNDebugOptionShowFeaturePoints;
    [self.view addSubview:self.sceneView];
}

初始化 ARSCNView 后,我们在页面出现是运行 ARSession
ARSession 作为 ARKit 的核心组件,管理并配置和运行不同的增强现实技术。
就运行而言,可以配置 平面检测、图片识别、人脸检测、骨骼检测、射线检测等… 因此如果可以的话,应该对它要有一定的学习。
回到平面识别,ARConfiguration 作为运行的基类,我们所有的检测都是由它的子类的配置
平面识别:ARPositionalTrackingConfiguration
图片识别:ARImageTrackingConfiguration
骨骼识别:ARBodyTrackingConfiguration
等…

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    // ARPositionalTrackingConfiguration 是标准的平面识别类  但是是在 iOS13 时才推出
    // 在 iOS11 有AR时 就有了 ARWorldTrackingConfiguration 使用 WorldTracking 也能检测到平面
    // 只是精度没有 ARPositionalTrackingConfiguration 高。 如果你只在 iOS13 以上运行可以考虑使用 PositionalTracking
    ARWorldTrackingConfiguration *config = [ARWorldTrackingConfiguration new];
    config.planeDetection = ARPlaneDetectionHorizontal;
    [self.sceneView.session runWithConfiguration:config];
}

我们在初始化 ARSCNView 时为 ARSession 设置了代理。ARSession 当检测到 Anchor 时会进行回调,下列为ARSession 的回调方法

- (void)session:(ARSession *)session didAddAnchors:(NSArray<__kindof ARAnchor*>*)anchors;
- (void)session:(ARSession *)session didUpdateAnchors:(NSArray<__kindof ARAnchor*>*)anchors;
- (void)session:(ARSession *)session didRemoveAnchors:(NSArray<__kindof ARAnchor*>*)anchors;

通过方法名也能知道 当有 Anchor 时,会先进行 didAddAnchors: 方法告诉我们检测到了平面,当我们在转动摄像头时随着检测面的不断扩大,平面也会变大,因此会在 didUpdateAnchors: 告知我们那个面变大或变小了。最后如果一个地方我们长时间不用摄像头对准或两个平面合并成一个了,那么这个平面会消失,ARSession 通过 didRemoveAnchors: 方法告知我们那个平面没有了。

/// 检测到了平面

- (void)session:(ARSession *)session didAddAnchors:(NSArray<__kindof ARAnchor *> *)anchors
{
    for (ARAnchor *anchor in anchors)
    {
    	// 返回的是 ARAnchor 基类 因此需要额外判断一下是不是 平面 ARPlaneAnchor
        if ([anchor isKindOfClass:[ARPlaneAnchor class]])
        {
            ARPlaneAnchor *pAnchor = (ARPlaneAnchor*)anchor;
            // 此时已经获取到了平面 我们创建一个 SCNPlane 
            // SCNPlane 和 上节用到的 SCNBox 一样 都是继承自 SCNGeometry 
            // 对于平面而言 肯定是有大小的,因此在初始化时要设置平面的大小 这个大小在 ARPlaneAnchor 中已经有了
            SCNPlane *geometry;
            if (@available(iOS 16.0, *)) {
            	// 在 iOS16 中 新增了 planeExtent 属性 表明了平面的宽高
                geometry = [SCNPlane planeWithWidth:pAnchor.planeExtent.width height:pAnchor.planeExtent.height];
            } else {
            	// 在 iOS16 以前 只有 extent 属性 用来获取屏幕的大小 X 表示宽 Z表示高
                geometry = [SCNPlane planeWithWidth:pAnchor.extent.x height:pAnchor.extent.z];
            }
            // 为平面设置一个纹理 和上节对 正方体一样都是由 SCNMaterial 来设置的
            SCNMaterial *material = geometry.materials.firstObject;
            UIImage *img = [UIImage imageNamed:@"bricks"];
            // contents 可以是图片、颜色或通过URL 加载的任何能被转为 Texture 的材质
            material.diffuse.contents = img;
            SCNNode *planeNode = [SCNNode nodeWithGeometry:geometry];
            // 为平面设置一个名称 方便后续查找 和 UIView.tag 一样
            // 每个 ARAnchor 都有一个 identifier 用来标识唯一性
            planeNode.name = pAnchor.identifier.UUIDString;
            // ARAnchor 中的 transform 属性用来表示 这个 Anchor 所在的位置和姿态
            // 我们设置一个方法 取出它对应的 X Y Z 
            // 注意 这个位置 是相对于 世界坐标系而言的位置,
            // 因此我们设置的属性应该是 worldPosition 而不是 position
            SCNVector3 pos = ExtractTranslation(pAnchor.transform);
            planeNode.worldPosition = pos;

			// 你也可以先试着不用设置下面这一行 来看看效果
			// SCNPlane 默认是竖着的 因此 我们需要让它沿着 X 轴旋转一次 达到我们想要的效果
            planeNode.transform = SCNMatrix4MakeRotation(-M_PI / 2.0, 1, 0, 0);
            
            [self.sceneView.scene.rootNode addChildNode:planeNode];
            
            // 在检测到的平面上放一个 正方体 
            SCNBox *box = [SCNBox boxWithWidth:0.2 height:0.2 length:0.2 chamferRadius:0];
            SCNNode *boxNode = [SCNNode nodeWithGeometry:box];
            // 这里我们将 Y 加0.15 使正方体看着能更加贴近平面
            boxNode.worldPosition = SCNVector3Make(pos.x, pos.y+0.15, pos.z);
            boxNode.scale = SCNVector3Make(0.5, 0.5, 0.5);
            
            [self.sceneView.scene.rootNode addChildNode:boxNode];
        }
    }
}
// 获取 4x4 矩阵中的 pos
SCNVector3 ExtractTranslation(const simd_float4x4& t)
{
    return SCNVector3Make(t.columns[3][0], t.columns[3][1], t.columns[3][2]);
}

/// 平面更新

- (void)session:(ARSession *)session didUpdateAnchors:(NSArray<__kindof ARAnchor *> *)anchors
{
    for (ARAnchor *anchor in anchors)
    {
        if ([anchor isKindOfClass:[ARPlaneAnchor class]])
        {
            ARPlaneAnchor *pAnchor = (ARPlaneAnchor*)anchor;
            // 通过 平面的 identifier 找到 对应平面
            SCNPlane *plane = [self findPlaneWith:pAnchor.identifier.UUIDString];
            if (!plane)
            {
                return;
            }
           // 更新平面的 宽高
            if (@available(iOS 16.0, *)) {
                plane.width = pAnchor.planeExtent.width;
                plane.height = pAnchor.planeExtent.height;
            } else {
                plane.width = pAnchor.extent.x;
                plane.height = pAnchor.extent.z;
            }
        }
    }
}

- (SCNPlane*)findPlaneWith:(NSString*)uuid
{
    for (SCNNode *childNode in self.sceneView.scene.rootNode.childNodes) {
        if ([childNode.geometry isKindOfClass:[SCNPlane class]])
        {
            if ([childNode.name isEqualToString:uuid])
            {
                return (SCNPlane*)childNode.geometry;
            }
        }
    }
    return nil;
}

/// 平面移除

- (void)session:(ARSession *)session didRemoveAnchors:(NSArray<__kindof ARAnchor *> *)anchors
{
    for (ARAnchor *anchor in anchors)
    {
        if ([anchor isKindOfClass:[ARPlaneAnchor class]])
        {
        	// 当我们收到平面被移除的通知时 应该将包含平面的节点找到并移除它
        	// 因此创建一个方法 通过之前设置的name 来找到节点 并删除
            ARPlaneAnchor *pAnchor = (ARPlaneAnchor*)anchor;
            SCNNode *node = [self findNodeWith:pAnchor.identifier.UUIDString];
            if (!node)
                return;
            [node removeFromParentNode];
        }
    }
}

- (SCNNode*)findNodeWith:(NSString*)uuid
{
    for (SCNNode *childNode in self.sceneView.scene.rootNode.childNodes) {
        if ([childNode.geometry isKindOfClass:[SCNPlane class]])
        {
            if ([childNode.name isEqualToString:uuid])
            {
                return childNode;
            }
        }
    }
    return nil;
}

骨骼监测

骨骼监测的 ARConfiguration 为 ARBodyTrackingConfiguration
如果你想了解它的属性可以查看 ARKit 中的定义。
当我们使用 ARBodyTrackingConfiguration 来运行 ARSession 时当找到人体也会进行和平面监测一样的回调,此时 ARAnchor 为 ARBodyAnchor,特征点中包含的骨骼的所有数据,需要注意的是 ARBodyAnchor 中的数据都是3D数据,如果我们希望获取骨骼的2D数据 应该使用 ARSession 的 didUpdateFrame: 回调方法

当监测到骨骼时由 didAddAnchors 方法进行回调通知,注意回调中的 Anchor 类型为 ARBodyAnchor 代表一个整体,里面包含了头、手、脚、肩等91个特征点。

在检测到人体后,ARBodyAnchor 不会在变,当我们移动摄像头切换到下一个人物时,
对于 ARBodyAnchor 而言,只是其中的骨骼位置发生了变化,Anchor 还是同一个。
因此需要我们去追踪  ARBodyAnchor 的 isTracked 来判断人体是否离开了摄像机。

AR穿戴

简单使用骨骼监测,获取到用户关键骨骼的信息,在通过矩阵获取姿态将模型贴在上面。

图片追踪

AR导航

使用 ARKit 的一些常见问题及解决方案

iOS相机输出流的格式转换

在iOS中普通相机和AR相机默认输出的类型是 kCVPixelFormatType_420YpCbCr8BiPlanarFullRangekCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
但是一般我们需要展示的类型是 kCVPixelFormatType_32BGRA 或者 kCVPixelFormatType_32RGBA
关于类型的区别,有肯多文章都有,不在一一讲解,有兴趣的可以自行查看
苹果官网介绍OSType

普通相机输出流是可控的,在设置中添加 输出的是BGRA格式

		// Add the video frame output
       AVCaptureVideoDataOutput *videoOutput = [[AVCaptureVideoDataOutput alloc] init];
        [videoOutput setAlwaysDiscardsLateVideoFrames:YES];
        // Use RGB frames instead of YUV to ease color processing
        [videoOutput setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_32BGRA] forKey:(id)kCVPixelBufferPixelFormatTypeKey]];
        

AR相机的输出不能改,找了半天没找到系统方法。查了一些资料才知道要手动转。
本来公司引擎代码可以转。但是考虑到CPU占用问题,不能给引擎太多压力,只能前端转,并且保证转一帧 耗时不能超过3毫秒。
当时第一种转换办法就是 YUA转CIImage CIImage转RGB 内存占用特别高, 大概花费了3天左右吧,参考了许多文章和苹果官网文档。最终使用Accelerate来进行转换。

这里只贴出OC代码 swift移步到GitHub上 demo中有完整使用 点击跳转

#import <Accelerate/Accelerate.h>
#import <ARKit/ARKit.h>

@interface AECapturedTools : NSObject

@property(nonatomic, assign) CVPixelBufferRef rgbPixel;

// 如果使用单例 反而会让内存和cpu占用率更高 已经进行测试
// If you use a single example, Memory and CPU will be overloaded
- (instancetype)initWithFrame:(ARFrame*)frame;
- (void)deinit;

@end
@interface AECapturedTools () {
    void *aRgbBuffer;
}
@end

@implementation AECapturedTools

- (instancetype)initWithFrame:(ARFrame*)frame {
    if (self = [super init]) {
        CVPixelBufferRef pixelBuffer = frame.capturedImage;
        
        CVPixelBufferLockBaseAddress(pixelBuffer, 0);
        uint8_t *yData = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0);
        size_t yHeight = CVPixelBufferGetHeightOfPlane(pixelBuffer, 0);
        size_t yWidth = CVPixelBufferGetWidthOfPlane(pixelBuffer, 0);
        size_t yPitch = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0);
        
        vImage_Buffer yImage = {yData, yHeight, yWidth, yPitch};
        CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
        
        CVPixelBufferLockBaseAddress(pixelBuffer, 0);
        uint8_t *cDate = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1);
        size_t cHeight = CVPixelBufferGetHeightOfPlane(pixelBuffer, 1);
        size_t cWidth = CVPixelBufferGetWidthOfPlane(pixelBuffer, 1);
        size_t cBytesPerRow = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1);

        vImage_Buffer cImage = {cDate, cHeight, cWidth, cBytesPerRow};
        CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
        
        
        CVPixelBufferRef outRef =NULL;
        size_t aRgbPitch = yWidth *  4;
        aRgbBuffer = malloc(yHeight *aRgbPitch);
        
        CVPixelBufferCreateWithBytes(NULL, yWidth, yHeight, kCVPixelFormatType_32BGRA, aRgbBuffer, aRgbPitch, nil, nil, nil, &outRef);
        
        CVPixelBufferLockBaseAddress(outRef, 0);
        uint8_t *oDate = CVPixelBufferGetBaseAddress(outRef);
        size_t oHeight = CVPixelBufferGetHeight(outRef);
        size_t oWidth = CVPixelBufferGetWidth(outRef);
        size_t oBytesPerRow = CVPixelBufferGetBytesPerRow(outRef);
        vImage_Buffer oImage = {oDate, oHeight, oWidth, oBytesPerRow};
        CVPixelBufferUnlockBaseAddress(outRef, 0);
        
        vImage_YpCbCrPixelRange pixelRange = {0, 128, 255, 255, 255, 1, 255, 0};

        vImage_YpCbCrToARGB infoYpCbCrToARGB = {};
        vImage_Error error = vImageConvert_YpCbCrToARGB_GenerateConversion(kvImage_YpCbCrToARGBMatrix_ITU_R_709_2, &pixelRange, &infoYpCbCrToARGB, kvImage420Yp8_CbCr8, kvImageARGB8888, kvImageNoFlags);
        if (error != kvImageCVImageFormat_NoError) {
            NSLog(@"%zd", error);
        }
        
        vImage_Error ToError = vImageConvert_420Yp8_CbCr8ToARGB8888(&yImage, &cImage, &oImage, &infoYpCbCrToARGB, nil, 255, kvImageNoFlags);
        if (ToError != kvImageCVImageFormat_NoError) {
            NSLog(@"%zd", ToError);
        }
        uint8_t permuteMap[4] = { 3, 2, 1, 0 };
        vImagePermuteChannels_ARGB8888(&oImage, &oImage, permuteMap, 0);
        
        self.rgbPixel = outRef;
    }
    return self;
}

- (void)dealloc {
//    [super dealloc];  // MRC open this
    NSLog(@"释放");
    if (aRgbBuffer) {
        NSLog(@"aRgbBuffer!=nil");
        free(aRgbBuffer);
        aRgbBuffer = NULL;
    }
}

- (void)deinit {
    free(aRgbBuffer);
    aRgbBuffer = NULL;
}
@end

注意 : 如果是在.mm中调用此代码,需要手动调用deinit 方法 释放内存。否则会导致内存过高

如果需要裁剪buffer在转还有一种方式

// RGB 方式2
- (void)test {
	CGImageRef img;
	VTCreateCGImageFromCVPixelBuffer(frame.capturedImage, nil, &img);
	CGImageRef newImg = CGImageCreateWithImageInRect(img, CGRectMake(0, (1440-960)/2, 1920, 960));

	CVPixelBufferRef newBuffer = [self pixelBufferFromCGImage: img];

	CGImageRelease(img);
	CGImageRelease(newImg);
}


- (CVPixelBufferRef)pixelBufferFromCGImage:(CGImageRef)image {
    
    NSDictionary *options = @{(NSString*)kCVPixelBufferCGImageCompatibilityKey:@YES,
                              (NSString*)kCVPixelBufferCGBitmapContextCompatibilityKey:@YES,
                              (NSString*)kCVPixelBufferIOSurfacePropertiesKey:[NSDictionary dictionary]
                              };
    
    CVPixelBufferRef pxbuffer = NULL;
    CGFloat frameWidth = CGImageGetWidth(image);
    CGFloat frameHeight = CGImageGetHeight(image);
    // kCVPixelFormatType_32ARGB kCVPixelFormatType_32BGRA 不支持kCVPixelFormatType_32RGBA
    CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, frameWidth, frameHeight, kCVPixelFormatType_32BGRA, (__bridge CFDictionaryRef) options, &pxbuffer);

    NSParameterAssert(status == kCVReturnSuccess && pxbuffer != NULL);
    
    CVPixelBufferLockBaseAddress(pxbuffer, 0);
    void *pxdata = CVPixelBufferGetBaseAddress(pxbuffer);
    NSParameterAssert(pxdata != NULL);
    
    CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
    
    uint32_t bitmapInfo = kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst;
    //uint32_t bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Big;
    CGContextRef context = CGBitmapContextCreate(pxdata, frameWidth, frameHeight, 8, CVPixelBufferGetBytesPerRow(pxbuffer), rgbColorSpace, bitmapInfo);
    NSParameterAssert(context);
    CGContextConcatCTM(context, CGAffineTransformIdentity);
    // CGRectMake(0, 0, frameWidth, frameHeight)
    CGContextDrawImage(context, CGRectMake(0, 0, frameWidth, frameHeight), image);

    CGColorSpaceRelease(rgbColorSpace);
    CGContextRelease(context);
    
    CVPixelBufferUnlockBaseAddress(pxbuffer, 0);
    return pxbuffer;
}


最简单的方式还是对纹理做 UV 的裁剪 代码中有

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值