Real-Time Camera

Heedon Ham·2023년 3월 23일
0

filmNoise 개발 회고

목록 보기
3/4
post-thumbnail

디자인까지 생성했다면 이제부터는 기능 구현을 해야한다.

카메라 기능에는 Apple이 제공하는 카메라를 활용하거나 직접 Custom Camera를 만들어서 구현할 수 있다. 필터 적용 결과물을 실시간으로 화면에 비추어야 하기 때문에 Custom Camera를 만들기로 했다.

일단 커스텀 카메라를 만드는데 필요한 내용은 다음을 참고했다.

Medium: Make Custom Camera in iOS

Apple Documents: Setting Up A Capture Session


사진 촬영은 비디오 스트림에서 한 프레임씩 화면에 그리는 작업 중 촬영 순간의 프레임을 따로 떼내는 과정이라고 할 수있다.

1) 전체적인 큰 흐름을 AVCaptureSession이라고 한다.
2) AVCaptureDevice와 AVCaptureInput 설정 뒤, AVCaptureConnection에 연결한다.
3) Session의 결과물을 AVCaptureOutput라고 한다.
4) AVCapturePhotoOutput과 AVCaptureVideoOutput으로 나뉠 수 있다.
5) PhotoOutput은 한 프레임을 저장하는데 활용되며, VideoOutput은 스크린에 나타내는데 활용된다.
6) AVCaptureVideoPreviewLayer가 제공되는데, UIView 위에 더해져서 CaptureSession을 실시간으로 확인할 수 있도록 해준다.


코드는 다음과 같다.

func setUpAndStartCaptureSession() {
	DispatchQueue.global(qos: .userInitiated).async {
    	self.captureSession = AVCaptureSession()
        self.captureSession.beginConfiguration()
        
        //.photo: highest quality
        if self.captureSession.canSetSessionPreset(.photo) {
           		self.captureSession.sessionPreset = .photo
        }
        self.captureSession.automaticallyConfiguresCaptureDeviceForWideColor = true            

     	self.setUpInputs()    
        self.setUpOutput()
            
        self.captureSession.commitConfiguration()
        self.captureSession.startRunning()
    }
}

DispatchQueue.global()로 concurrent하게 처리하는 이유는 Apple이 다음과 같이 설명한다.

The startRunning() method is a blocking call which can take some time, therefore start the session on a serial dispatch queue so that you don’t block the main queue (which keeps the UI responsive)

func setUpInputs() {
	//back camera
    //메인 wide 카메라가 가장 고화소이며 센서 크기가 큼 ~ 보정 관용도가 높음
    if let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) {
    	backCamera = device
    } else {   
        fatalError("no back camera")
    }
        
    //front camera
    if let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) {
    	frontCamera = device
    } else {
        fatalError("no front camera")
    }
        
    //create an input objects from devices
    guard let bInput = try? AVCaptureDeviceInput(device: backCamera) else {
    	fatalError("could not create input device from back camera")
    }
    backInput = bInput
    if !captureSession.canAddInput(backInput) {
    	fatalError("could not add back camera input to capture session")
    }
        
    guard let fInput = try? AVCaptureDeviceInput(device: frontCamera) else {
    	fatalError("could not create input device from front camera")
    }
    frontInput = fInput
    if !captureSession.canAddInput(frontInput) {
    	fatalError("could not add front camera input to capture session")
    }
        
    //connect camera input to session
    cameraPosition == .back ? captureSession.addInput(backInput) : captureSession.addInput(frontInput)
    
}
func setUpOutput(){
	//set photoOutput
    photoOutput = AVCapturePhotoOutput()
    photoOutput.isHighResolutionCaptureEnabled = true
        
    if captureSession.canAddOutput(photoOutput) {
    	captureSession.addOutput(photoOutput)
    } else {
    	fatalError("could not add photo output")
    }
        
    photoOutput.connections.first?.videoOrientation = .portrait
  
    //set videoOutput
    videoOutput = AVCaptureVideoDataOutput()
    let videoQueue = DispatchQueue(label: "videoQueue", qos: .userInteractive)
        videoOutput.setSampleBufferDelegate(self, queue: videoQueue)
        
    if captureSession.canAddOutput(videoOutput) {
    	captureSession.addOutput(videoOutput)
    } else {
        fatalError("could not add video output")
    }
        
    videoOutput.connections.first?.videoOrientation = .portrait
    
}

기본 CaptureSession이 완성되었다. 여기서 화면에 나타내기 위한 PreviewLayer를 설정하면 다음과 같다.

@IBOutlet weak var previewView: UIView!

DispatchQueue.global(qos: .userInitiated).async {
		//AVCaptureSession 프로세스 과정
		DispatchQueue.main.async {
			  //setup preview layer
	      self.setUpPreviewLayer()
	  }
}
//CaptureVideoPreviewLayer to show videostream, which is what the camera is seeing, on previewView in real-time (not applying filtered or any custom data)

func setUpPreviewLayer(){
	self.videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
    guard let previewLayer = self.videoPreviewLayer else {
	    print("previewLayer error")
	    return
    }
    
    //크기 조정
    previewLayer.frame = self.previewView.layer.bounds
    
    //previewLayer 올리기
    self.previewView.layer.addSublayer(previewLayer)
}

사진 저장을 하면 필터 데이터를 적용하지 않은 원본 그대로의 사진을 저장할 수 있다.

@IBAction func takePhotoButtonTapped(_ sender: UIButton) {
	photoSettings = getSettings()
    photoOutput.capturePhoto(with: photoSettings, delegate: self)       
}        
func getSettings() -> AVCapturePhotoSettings {
	let settings = AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.jpeg])
    settings.isHighResolutionPhotoEnabled = true
    settings.flashMode = isFlashOn ? .on : .off    
    return settings
}

capturePhoto() 메서드를 활용하기 위해선 delegate를 만족해야 한다.

//MARK: - Delegate for saving captured photos from AVCapturePhotoOutput into photo library

extension ViewController: AVCapturePhotoCaptureDelegate {
    
	func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
    	//error 체크
        guard error == nil else {
            print("Error capturing photo: \(error!)")
            return
        }
        
        //Photo Library 접근 허용 확인
        PHPhotoLibrary.requestAuthorization { status in
            guard status == .authorized else {
                self.showPhotoSavePermissionAlert()
                return
            }
            
            PHPhotoLibrary.shared().performChanges({
            	PHAssetCreationRequest.forAsset().addResource(with: .photo, data: photo.fileDataRepresentation()!, options: nil)
            }) { (success, error) in
                if success {
                    //저장 완료 alert창
                    DispatchQueue.main.async {
                    	let alert = UIAlertController(title: nil, message: "저장 완료!", preferredStyle: .alert)
                        DispatchQueue.main.asyncAfter(deadline: .now()+0.5, execute: {
                            self.dismiss(animated: true, completion: nil)
                        })
                        self.present(alert, animated: true, completion: nil)
            		}
    			} 
			}
    	}
	}
}

여기까지 완료하면 real-time으로 카메라에 들어오는 input들을 previewView에서 PreviewLayer로 확인하면서 촬영하면 해당 프레임을 저장할 수 있다.

우리는 여기서 더 나아가야 한다. 이제는 필터 데이터를 적용할 차례이다.

profile
 iOS 개발

0개의 댓글