如何用iOS自带摄像头进行拍摄获取视频流以及OpenCV图像处理实时显示

目录

  • 概述
  • 一、如何用Swift调用OpenCV库
    • 1.项目引入OpenCV库
    • 2.桥接OpenCV及Swift
  • 二、运用AVFoundation获取实时图像数据
    • 1.建立视频流数据捕获框架
    • 2.建立 Capture Session
    • 3.取得并配置 Capture Devices
    • 4.设定 Device Inputs
    • 5.配置Video Data Output输出
    • 6.工程隐私权限配置
    • 7.处理相机视频回调
  • 三、视频流原始数据CMSampleBuffer处理
    • 1.CMSampleBuffer数据转换为Mat数据
    • 2.回调中的数据处理
    • 3.Mat数据转换为UIImage数据用于显示
  • 四、Swift界面搭建
    • 1.在UI层捕获相机数据
    • 2.直接显示CMSampleBuffer方法
  • 五、基于Object-C++的OpenCV图像处理部分
    • 1.引入头文件
    • 2.OpenCV人脸识别输出识别框
  • 总结


概述

在2020年6月9日之后,OpenCV可以直接在Objective-C和Swift中使用它,而无需自己编写Objective-C++,可以直接在OpenCV官网下载iOS Package包,使用起来也是比较简单。但由于之前对OpenCV库的使用是使用C++编写,所以Objective-C++在图像处理部分使用起来更顺手,因此本文主要的技术框架是使用Objective-C++编写图像处理流程,Swift编写iOS界面及AVFoundation相机等的调用以获取实时的图像数据。本文主要以实时框出人脸为示例,iOS移动端界面的显示结果大致如下图。

示例图像
OpenCV官网:https://opencv.org/releases/


一、如何用Swift调用OpenCV库

1.项目引入OpenCV库

  1. 使用cocoapods就非常简单:
pod 'OpenCV'
  1. 自行手动添加:在官网下载相应版本的iOS Pack,解压后得到一个 opencv2.framework 库,创建项目并右键添加文件到项目。

2.桥接OpenCV及Swift

  1. 前面说到OpenCV框架是用C++进行编程的,因此要用Objective-C++代码于Swift代码进行桥接。首先添加一个 Objective-C 文件到项目中,会弹出一个是否添加 Bridging-Header 文件,选择添加(若此处没弹出,则可以手动添加Bridging-Header 文件,即添加一个头文件(Header file),重命名为“项目名-Bridging-Header.h”),这就实现了Swift和Object-C的混编。
  2. 将这个Object-C的文件扩展名“.m”改为“.mm”这就将该文件变成了Objective-C++文件,文件大致如下

工程文件图


二、运用AVFoundation获取实时图像数据

Apple预设的APIs 如UIImagePickerController能够直接获取摄像头获取的图像并显示在界面上,操作简单,但无法对原数据进行操作,因此本文中应用AVFoundation的 Capture Sessions来采集图像和视频流。根据官方文档,Capture Session 是用以【管理采集活动、并协调来自 Input Devices 到采集 Outputs 的数据流】。在 AVFoundation 内,Capture Sessions 是由AVCaptureSession来管理的。

1.建立视频流数据捕获框架

首先创建一个NSObject类型的Controller名为CameraController,处理摄像头的事务,设置prepare函数以供主程序调用,其主要负责设立一个新的 Capture Session。设定 Capture Session 分为五个步骤:

  1. 建立一个 Capture Session
  2. 取得并配置 Capture Devices
  3. 在 Capture Device 上建立 Inputs
  4. 设置一个 Video Data Output 物件
  5. 配置Video Data Output Queue参数
func prepare(completionHandler: @escaping (Error?) -> Void) {
    //建立一个 Capture Session
    func createCaptureSession() { }
    //取得并配置 Capture Devices
    func configureCaptureDevices() throws { }
    //在 Capture Device 上建立 Inputs
    func configureDeviceInputs() throws { }
    //设置一个 Video Data Output 物件
    func configureVideoDataOutput() throws { }
    //配置Video Data Output Queue参数
    func configureVideoDataOutputQueue() throws{ }
    
    DispatchQueue(label: "prepare").async {
        do {
            createCaptureSession()
            try configureCaptureDevices()
            try configureDeviceInputs()
            try configureVideoDataOutput()
            try configureVideoDataOutputQueue()
        }
            
        catch {
            DispatchQueue.main.async {
                completionHandler(error)
            }         
            return
        }
        
        DispatchQueue.main.async {
            completionHandler(nil)
        }
    }
}

2.建立 Capture Session

建立新的AVCaptureSession,并将它存储在captureSession的属性里,并设定一些用于抛出的错误类型

var captureSession: AVCaptureSession?

func createCaptureSession() { 
     self.captureSession = AVCaptureSession()
}

 //设定prepare过程中遇到的错误类型
 enum CameraControllerError: Swift.Error {
     case captureSessionAlreadyRunning
     case captureSessionIsMissing
     case inputsAreInvalid
     case invalidOperation
     case noCamerasAvailable
     case unknown
 }
 
 //设定相机位置为前后相机
 public enum CameraPosition {
     case front
     case rear
 }

3.取得并配置 Capture Devices

建立了一个AVCaptureSession后,需要建立AVCaptureDevice物件来代表实际的相机

        //前置镜头
        var frontCamera: AVCaptureDevice?
        //后置镜头
        var rearCamera: AVCaptureDevice?
 
        func configureCaptureDevices() throws {
            
            //使用了AVCaptureDeviceDiscoverySession找出设备上所有可用的内置相机 (`.builtInDualCamera`)。
            //若没找到相机则抛出异常。
            let session = AVCaptureDevice.DiscoverySession.init(deviceTypes: [AVCaptureDevice.DeviceType.builtInWideAngleCamera], mediaType: AVMediaType.video, position: .unspecified)           
            let cameras = session.devices.compactMap { $0 }
            guard !cameras.isEmpty else { throw CameraControllerError.noCamerasAvailable }
            
            //遍历前面找到的可用相机,分辨出前后相机。
            //然后,将该相机设定为自动对焦,遇到任何问题也会抛出异常。
            for camera in cameras {
                if camera.position == .front {
                    self.frontCamera = camera
                }
                
                if camera.position == .back {
                    self.rearCamera = camera
                    
                    try camera.lockForConfiguration()
                    camera.focusMode = .continuousAutoFocus
                    camera.unlockForConfiguration()
                }
            }
        }

4.设定 Device Inputs

var currentCameraPosition: CameraPosition?
var frontCameraInput: AVCaptureDeviceInput?
var rearCameraInput: AVCaptureDeviceInput?

func configureDeviceInputs() throws {
            //确认`captureSession`是否存在,若不存在抛出异常
            guard let captureSession = self.captureSession else { throw CameraControllerError.captureSessionIsMissing }
            
            //建立所需的 Capture Device Input 来进行数据采集。
            //`AVFoundation`每一次 Capture Session 仅能允许一台相机输入。
            //由于装置的初始设定为后相相机。先尝试用后相机 Input,再加到 Capture Session;
            if let rearCamera = self.rearCamera {
                self.rearCameraInput = try AVCaptureDeviceInput(device: rearCamera)
                
                if captureSession.canAddInput(self.rearCameraInput!) { captureSession.addInput(self.rearCameraInput!) }
                
                self.currentCameraPosition = .rear
            }
             
             //尝试建立前相机Input   
            else if let frontCamera = self.frontCamera {
                self.frontCameraInput = try AVCaptureDeviceInput(device: frontCamera)
                
                if captureSession.canAddInput(self.frontCameraInput!) { captureSession.addInput(self.frontCameraInput!) }
                else { throw CameraControllerError.inputsAreInvalid }
                
                self.currentCameraPosition = .front
            }
                
            else { throw CameraControllerError.noCamerasAvailable }
}

5.配置Video Data Output输出

        var videoOutput: AVCaptureVideoDataOutput?
        
        //配置相机的视频输出,并开始
        func configureVideoDataOutput() throws {
            guard let captureSession = self.captureSession else { throw CameraControllerError.captureSessionIsMissing }
            
            self.videoOutput = AVCaptureVideoDataOutput()
            
            if captureSession.canAddOutput(self.videoOutput!) { captureSession.addOutput(self.videoOutput!) }
            captureSession.startRunning()
        }
        
        //配置视频的输出代理及输出格式
        func configureVideoDataOutputQueue() throws{
            let videoDataOutputQueue = DispatchQueue(label: "videoDataOutputQueue")
            self.videoOutput!.setSampleBufferDelegate(self, queue: videoDataOutputQueue)
            self.videoOutput!.alwaysDiscardsLateVideoFrames = false
            let BGRA32PixelFormat = NSNumber(value: Int32(kCVPixelFormatType_32BGRA))
            let rgbOutputSetting = [kCVPixelBufferPixelFormatTypeKey.string : BGRA32PixelFormat]
           self.videoOutput!.videoSettings = rgbOutputSetting
        }

6.工程隐私权限配置

根据Apple 规定的安全性要求,必须提供一个app使用相机权限的原因。在工程的Info.plist,加入下图的设置:

相机权限设置

7.处理相机视频回调

能够从下方的回调中得到相机返回的实时数据,格式为CMSampleBuffer,该视频流格式不止包含图像信息还包含时间戳信息等,若想通过opencv进行处理还需进行数据转换。

extension CameraController: AVCaptureVideoDataOutputSampleBufferDelegate{
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
    }
}

参考地址:https://www.appcoda.com.tw/avfoundation-camera-app/


三、视频流原始数据CMSampleBuffer处理

1.CMSampleBuffer数据转换为Mat数据

OpenCV提供了UIImageToMat的函数,根据这个思路,我们应当将CMSampleBuffer转换为UIImage数据,CMSsampleBuffer不止包含ImageBuffer,通过API自带的CMSampleBufferGetImageBuffer(),可以得到与我们希望得到的图像数据更为接近的cvPixelBuffer。

总的来说,下方是CMSampleBuffer转换为UIImage的两种方式,第一种通过CIImage第二种通过CGImage,通过CIImage转换成的UIImage虽然能显示在UIImageVIew上,但是在转换成Mat格式的时候会报错,因此选用第二种通过CGImage的转换。最后调用opencv库的UIImageToMat函数便能得到Mat数据了。

    func image(orientation: UIImage.Orientation = .up, scale: CGFloat = 1.0) -> UIImage? {
        if let buffer = CMSampleBufferGetImageBuffer(self) {
            let ciImage = CIImage(cvPixelBuffer: buffer)

            return UIImage(ciImage: ciImage, scale: scale, orientation: orientation)
        }
        return nil
    }

    func imageWithCGImage(orientation: UIImage.Orientation = .up, scale: CGFloat = 1.0) -> UIImage? {
        if let buffer = CMSampleBufferGetImageBuffer(self) {
            let ciImage = CIImage(cvPixelBuffer: buffer)

            let context = CIContext(options: nil)

            guard let cg = context.createCGImage(ciImage, from: ciImage.extent) else {
                return nil
            }           
            return UIImage(cgImage: cg, scale: scale, orientation: orientation)
        }
        return nil
    }

2.回调中的数据处理

这边选用的方案是UIImageView来显示原始图像,并且在UIImageView上添加一个蒙层图像来显示识别框。此处选用蒙层的原因是,图像处理每帧需要70ms的处理时间,若直接显示处理后的图片会有延迟丢帧的情况视觉效果较差,因此实时图像采用原始图像数据,而识别框丢帧并不影响视觉效果。

   //回调原始图像
   var videoCpatureCompletionBlock: ((UIImage) -> Void)?
   //回调CMSsmapleBuffer图像
   var videoCaptureCompletionBlockCMS: ((CMSampleBuffer)-> Void)?
   //回调蒙层图像
   var videoCaptureCompletionBlockMask: ((UIImage) -> Void)?
   //用于记录帧数
   var frameFlag : Int = 0
   //用于给异步线程加锁
   var lockFlagBool : Bool = false

    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
 
        if let image = sampleBuffer.imageWithCGImage(orientation: .up, scale: 1.0){
            self.frameFlag = self.frameFlag + 1
            var output = image
            if(self.frameFlag != -1){
                self.videoCaptureCompletionBlockCMS?(sampleBuffer)
                self.videoCpatureCompletionBlock?(output)
                if(self.lockFlagBool == false){
                    //此处必须开线程处理,否则会报错
                    DispatchQueue.global().async {
                    
                        lockFlagBool = true
                        var output = image
                        //addimageProcess为opencv图像处理过程,写在Objecj-C++文件中,本文后面记录
                        output = opencv_test.addimageProcess(output)
                        self.videoCaptureCompletionBlockMask?(output)
                        lockFlagBool = false
                    }
                }
            }else{
                print("丢帧")
                self.frameFlag = 0
            }
        }
    }

3.Mat数据转换为UIImage数据用于显示

为了最后能用于显示,还要转换为UImage,该部分很简单,直接调用OpenCV的库函数,当然如果想转换为CMSampleBuffer的话还需要重新添加丢失的数据,比如时间戳。

MatToUIImage()

参考地址:https://stackoverflow.com/questions/15726761/make-an-uiimage-from-a-cmsamplebuffer


四、Swift界面搭建

1.在UI层捕获相机数据

UI界面的操作比较简单,实例化之前的CameraController类,并设定configureCameraController函数来调用类中的prepare函数,以及接受回调的图像数据,这些回调对UIImageView的图像刷新必须要在主线程中,否则会报错。其中,selfImageView和maskImageView是两个自己创建的UImageView来显示UIImage图像的,这两个UIImageView要保持在同样位置同样大小。

    let cameraController = CameraController()
    
    override func viewDidLoad() {
         configureCameraController() 
    }
    
    func configureCameraController() {
        cameraController.prepare {(error) in
            if let error = error {
                print(error)
            }
            
            self.cameraController.videoCpatureCompletionBlock = { image in
                DispatchQueue.main.async {
                    self.selfImageView.image = image
                }
            }
            self.cameraController.videoCaptureCompletionBlockMask = { image in
                DispatchQueue.main.async {
                    self.maskImageView.image = image
                }
            }
            
            //直接显示CMSampleBuffer的方法
           // self.cameraController.videoCaptureCompletionBlockCMS = { CMSampleBuffer in
                //self.displayLayer.enqueue(CMSampleBuffer)
            //}
        }
    }

2.直接显示CMSampleBuffer方法

其实苹果的API也提供了直接显示CMSampleBuffer的简单方法,通过AVSampleBufferDisplayLayer以及其.enqueue方法,其展示方式如下:

var displayLayer:AVSampleBufferDisplayLayer!

    override func viewDidLoad() {
        displayLayer = AVSampleBufferDisplayLayer()
        displayLayer.videoGravity = .resizeAspect     
        self.imageView.layer.addSublayer(displayLayer)
        self.displayLayer.frame.origin.y = self.imageView.frame.origin.y
        self.displayLayer.frame.origin.x = self.imageView.frame.origin.x
    }
    
    func configureCameraController() {
        cameraController.prepare {(error) in
            if let error = error {
                print(error)
            }
            //直接显示CMSampleBuffer的方法
            self.cameraController.videoCaptureCompletionBlockCMS = { CMSampleBuffer in
                self.displayLayer.enqueue(CMSampleBuffer)
            }
        }
    }

五、基于Object-C++的OpenCV图像处理部分

1.引入头文件

这部分用C++编写过OpenCV的都相当熟悉了,在.mm文件中引入以下头文件,并引入命名空间,若该部分找不到文件应当确认是否已正确安装OpenCV库。

#import <opencv2/opencv.hpp>
#import "opencv-test.h"
#import <opencv2/imgcodecs/ios.h>

//对iOS支持
#import <opencv2/imgcodecs/ios.h>
//导入矩阵帮助类
#import <opencv2/highgui.hpp>
#import <opencv2/core/types.hpp>
#import <iostream>

using namespace std;
using namespace cv;

@implementation opencv_test
   //各类处理函数
@end

2.OpenCV人脸识别输出识别框

本文使用了OpenCV自带的人脸识别框架CascadeClassifier,将得到的人脸坐标放入vector中,最后绘制在蒙层上,最后输出蒙层图片。其它对于图像的处理也可以用相同的方式处理,在参考资料中有马赛克操作。

+(UIImage*)addimageProcess:(UIImage*)image {
    //用于记录时间
    CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
    Mat src;
    //将iOS图片->OpenCV图片(Mat矩阵)
    UIImageToMat(image, src);
    Mat src_gray;
    //图像灰度化
    cvtColor(src, src_gray, COLOR_RGBA2GRAY, 1);
    std::vector<cv::Rect> faces;
    //初始化OpenCV的人脸识别检测器
    CascadeClassifier faceDetector;
    //获取权重文件,文件需要提前导入至工程目录中
    NSString* cascadePath = [[NSBundle mainBundle]
                     pathForResource:@"haarcascade_frontalface_alt"
                              ofType:@"xml"];
    //配置检测器
    faceDetector.load([cascadePath UTF8String]);
    faceDetector.detectMultiScale(src_gray, faces, 1.1,2, 0|CASCADE_SCALE_IMAGE, cv::Size(30, 30));
    
    //确定图像宽高
    int width = src.cols;
    int height = src.rows;
    //Mat Mask = Mat::zeros(width, height, CV_8UC4);
    
    //创建透明蒙层图像 Scalar(0,0,0,0) 分别是RGBA A为透明度
    Mat Mask = Mat(height, width, CV_8UC4, Scalar(0,0,0,0));
    
    // Draw all detected faces
    for(unsigned int i = 0; i < faces.size(); i++)
    {
        const cv::Rect& face = faces[i];
        // Get top-left and bottom-right corner points
        cv::Point tl(face.x, face.y);
        cv::Point br = tl + cv::Point(face.width, face.height);

        // Draw rectangle around the face
        Scalar magenta = Scalar(0, 255, 0, 255);
        cv::rectangle(Mask, tl, br, magenta, 4, 8, 0);
    }
    
    //打印处理时间
    CFAbsoluteTime endTime = (CFAbsoluteTimeGetCurrent() - startTime);
    NSLog(@"normalProcess方法耗时: %f ms", endTime * 1000.0);
    return MatToUIImage(Mask);
}

参考资料:https://www.twblogs.net/a/5b830b452b717766a1eadb20/?lang=zh-cn


总结

遇到的困难:一是在于方案中用UIImageView来进行显示,必须在主线程中进行渲染,对于线程的处理相对繁琐,若是处理不得当便会有延时丢帧不刷新等的问题。
存在的问题:OpenCV自带的人脸识别算法比较老旧,处理速度也比较慢效果也一般,要引入其他神经网络框架在客户端上的可行性有待讨论,处理速度也未知。

另外,若有需要总的工程文件的可以私聊我。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/4149.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于Java Web的图书管理系统

目录 1.系统简要概述 2.系统主要用到的数据库表 3.主要功能 管理员&#xff1a; 用户&#xff1a; 3.1管理员功能 3.11登录 3.12添加学生 3.13查看学生 3.14删除学生 3.15添加书籍 3.16查看书籍 3.2用户端功能 3.2.1登录 3.2.2注册 3.2.3查询图书 3.2.4借阅书籍…

【云原生】初识 Kubernetes — pod 的前世今生

目录标题前言&#x1f433; Kubernetes到底是什么&#xff1f;&#x1f42c; K8s 的由来&#x1f42c;K8s 的工作方式&#x1f42c; K8s 主要组件&#x1f40b;Master 组件&#x1f40b;Node 组件&#x1f433; pod 是什么&#xff1f;&#x1f42c;pod 的概念&#x1f42c;控制…

Kafka在Mac下的安装与使用

mac 安装kafka安装kafka的原因安装kafka启动Zookeeper启动Kafka创建topic查看topic生产数据消费数据关闭zookeeper关闭kafka测试安装kafka的原因 用户微服务登录后需要向广告微服务中发送用户登录的信息以获取用户画像&#xff08;这个过程是异步的&#xff09;&#xff0c;故…

雷电4模拟器安装xposed框架(2022年)

别问我都2202年了为什么还在用雷电4安卓7。我特么哪知道Xposed的相关资料这么难找啊&#xff0c;只能搜到一些老旧的资料&#xff0c;尝试在老旧的平台上实现了。 最初的Xposed框架现在已经停止更新了&#xff0c;只支持到安卓8。如果要在更高版本的安卓系统上使用Xposed得看看…

mac程序员必备的20款软件

今天给大家分享一下我作为一名后端程序员工作中常用的软件&#xff0c;相信下面我要介绍的很多软件对大家来说并不陌生&#xff0c;mac程序员必备的20款软件能够在不同岗位上提升大家的效率和体验。 1、Chrome 我们首先来介绍一些开发常用工具&#xff0c;先是浏览器&#xff…

手撕二叉树--堆的接口实现(附源码+图解)

堆的接口实现&#xff08;附源码图解&#xff09; 文章目录堆的接口实现&#xff08;附源码图解&#xff09;前言一、定义结构体二、接口实现&#xff08;附图解源码&#xff09;1.初始化堆2.销毁堆3.尾插数据&#xff08;1&#xff09;向上调整&#xff08;2&#xff09;交换函…

Elasticsearch 需要了解的都在这

ES选主过程&#xff1f;其实ES的选主过程其实没有很高深的算法加持&#xff0c;启动过程中对接点的ID进行排序&#xff0c;取ID最大节点作为Master节点&#xff0c;那么如果选出来的主节点中存储的元信息不是最新的怎么办&#xff1f;其实他是分了2个步骤做这件事&#xff0c;先…

es-head插件插入查询以及条件查询(五)

es-head插件插入查询以及条件查询 1.es-head插件页面介绍 页面详细介绍 2.es-head查询语句 2.1.查询索引中的全部数据 curl命令交互&#xff0c;采用GET请求 语法格式&#xff1a; curl -XGET es地址:9200/索引名/_search?pretty [rootelaticsearch ~]# curl -XGET 192…

Mac环境变量配置(Java)

1.打开终端&#xff1a; 2.输入命令&#xff1a;【/usr/libexec/java_home -V】,查看默认的jdk下载地址&#xff08;绿色下划线的就是jdk默认路径&#xff09;&#xff08;注意⚠️&#xff1a;命令行终端是区分大小写的【-v 是不对的&#xff0c;必须是大写 -V】&#xff09; …

Windows Server 2016远程桌面配置全过程

镜像下载 系统镜像网址 本次下载的是 Windows Server 2016 (Updated Feb 2018) (x64) - DVD (Chinese-Simplified) 远程桌面配置 Step 1 在开始菜单搜索服务&#xff0c;打开服务器管理器&#xff0c;点击右上角的管理按钮 Step 2 添加角色控制&#xff0c;点击下一步 S…

静态路由+DHCP实验(四路由器八PC)

一.200.1.1.0/24子网划分 1.划分八个子网 2.选用前5个&#xff0c;第五个子网再划分4个子网作为骨干 二.规划路由 三.配置&#xff08;下一跳&#xff09; 1.先依次实现四个路由器之间全网可通 2.为路由器配置地址池&#xff0c;使用全局模式获取dhcp&#xff0c;指定网关…

Springboot是什么

目录 为什么会要用springboot 1、之前 2、现在 springboot优点 springboot四大核心 自动装配介绍 1、自动装配作用是什么 2、自动装配原理 springboot starter是什么 1、starter作用 2、比如&#xff1a;我们想搭建java web框架 3、starter原理 SpringBootApplica…

Endor Labs:2023年十大开源安全风险

近日&#xff0c;Endor Labs发布了一份新报告&#xff0c;确定了2023年的十大开源安全风险。报告显示&#xff0c;许多软件公司依赖于开源软件代码&#xff0c;但在如何衡量和处理与开源软件相关的风险和漏洞方面缺乏一致性。调查发现&#xff0c;在应用程序中超过80%的代码可能…

Go 结构体

目录 什么是结构体 定义结构体 基本的方式实例化结构体 访问结构体的成员变量 指针类型的方式实例化结构体 取结构体地址的方式实例化 知识扩展&#xff1a;*号 和 &号 构造函数 成员函数&#xff08;成员方法&#xff09; 匿名成员变量 方法传入指针类型的结构…

Mac M1通过VMWare Fusion安装Centos7记录(镜像和网络有大坑)

以前用linux系统基本都在我的服务器上或者是在win上进行&#xff0c;从没有在M1上进行创建&#xff0c;因此走了一些坑吧&#xff0c;这里会列出我的详细安装步骤。 下载镜像 镜像的下载网站&#xff1a;https://www.centos.org/download/ 在该网站中&#xff0c;不管是Every…

多级评论单表结构设计

这里的多级&#xff0c;本质上其实也就二级&#xff0c;例如微博的评论&#xff0c; 一级评论&#xff1a; 对微博的评论 二级评论&#xff1a; 对微博下的评论的回复评论 &#xff0c;这里包括二种 1. 回复的是一级评论&#xff0c; 2, 回复的是二级评论 效果如下 表数据 查…

基于微信小程序的图书馆选座系统源码

开发环境及工具&#xff1a; 大等于jdk1.8&#xff0c;大于mysql5.5&#xff0c;idea&#xff08;eclipse&#xff09;&#xff0c;微信开发者工具 技术说明&#xff1a; springboot mybatis 小程序 代码注释齐全&#xff0c;没有多余代码&#xff0c;适合学习&#xff08;…

Android Studio模拟器运行无反应

当Android Studio模拟器点击运行无反应 报以下错误&#xff1a; Emulator: PANIC: Cannot find AVD system path. Please define ANDROID_SDK_ROOT 问题分析 大多是由于默认路径带有中文&#xff0c;所以找不到 解决方法 1&#xff0c;删除镜像 2&#xff0c;配置环境变量 …

Unity强化学习之ML-Agents的使用

Github下载链接&#xff1a;https://github.com/Unity-Technologies/ml-agents ML-Agents是游戏引擎Unity3D中的一个插件&#xff0c;也就是说&#xff0c;这个软件的主业是用来开发游戏的&#xff0c;实际上&#xff0c;它也是市面上用得最多的游戏引擎之一。而在几年前随着人…

Maven配置—idea版

在java开发中&#xff0c;maven是个不可或缺的工具&#xff0c;可以简单理解成maven是个仓库&#xff0c;可以远程下载各种需要的插件&#xff0c;而且对于项目的打包编译等也非常简单&#xff1b;下面来说说如何配置maven 1. 首先下载maven的工具包&#xff0c;解压之后放在D…