/2024-NC2-M15-RoomPlan

RoomPlan과 Maps를 활용하여 생생하게 공간을 기록하고 공유할 수 있는 앱

Primary LanguageSwift

2024-NC2-M15-RoomPlan

🎥 Youtube Link (Click the image)

Video Label

💡 About RoomPlan

Untitled

RoomPlan은 카메라와 라이다센서를 활용해서 3D 평면도를 작성해주는 기술입니다.

RoomPlan을 활용해서 다른 기술과 결합하여 벽의 색도 바꿔볼 수 있고 쓰이는 페인트의 양 까지 알 수 있습니다. 또한, 공간 설계, 가구 배치 등에도 활용할 수 있는 기술입니다.

제약사항: RoomPlan으로 공간을 스캔할 때는 5분을 초과하면 안되고 최대 스캔 범위는 9mX9m입니다. 그리고, 직육면체만 인식하고 곡선은 인식이 불가능합니다.

🎯 What we focus on?

  • 애플에서 제공한 RoomPlan 예시 코드를 활용하여 공간을 캡쳐하는 기술 자체에 집중했습니다.
  • 캡쳐된 모델을 USD, USDZ 등의 포맷으로 저장하는 것 외에는 모델을 활용할 방법이 마땅치 않아 캡쳐된 모델을 어떻게 사용해야할지 고민했습니다.
  • 캡쳐된 모델을 이미지로 만들어서 다른 이미지, 텍스트 등의 요소들과 함께 배치하는 방식을 사용하기로 결정했습니다.

💼 Use Case

RoomPlan과 Maps를 활용하여 생생하게 공간을 기록하고 공유할 수 있는 서비스를 만들어보자!

🖼️ Prototype

  • Prototype & User Flow
Untitled 2
  • 시연영상
video-output-A109CD30-1E9A-41EB-851C-D2C713525CCB.mov

🛠️ About Code

  • RoomPlan
import RoomPlan
//방 캡쳐를 담당하는 클래스
class RoomController: RoomCaptureViewDelegate {
    func encode(with coder: NSCoder) {
        fatalError("Not Needed")
    }
    
    required init?(coder: NSCoder) {
        fatalError("Not Needed")
    }
    
    //코드의 다른 부분에서 접근 가능하도록 싱글턴으로 인스턴스 생성
    static var instance = RoomController()
    
    var captureView: RoomCaptureView
    var sessionConfig: RoomCaptureSession.Configuration = RoomCaptureSession.Configuration()
    var finalResult: CapturedRoom?
    
    init() {
        captureView = RoomCaptureView(frame: .zero)
        captureView.delegate = self
    }
    
    //캡쳐가 완료되었는지 아닌지에 대해서 Bool 값을 리턴하는 함수
    func captureView(shouldPresent roomDataForProcessing: CapturedRoomData, error: (Error)?) -> Bool {
        return true
    }
    
    //캡쳐가 완료된 결과를 finalResult에 저장하는 함수
    func captureView(didPresent processedResult: CapturedRoom, error: (Error)?) {
        finalResult = processedResult
    }
    
    //캡쳐를 시작하는 함수
    func startSession() {
        captureView.captureSession.run(configuration: sessionConfig)
    }
    
    //캡쳐를 종료하는 함수
    func stopSession() {
        captureView.captureSession.stop()
    }
}
//UIKit으로 작성된 RoomPlan 뷰를 SwiftUI에서 보여지도록 UIViewRepresentable 프로토콜을 사용한 뷰
struct RoomCaptureViewRepresentable : UIViewRepresentable {
    //뷰를 생성하고 초기화하는 함수
    func makeUIView(context: Context) -> RoomCaptureView {
        RoomController.instance.captureView
    }
    
    func updateUIView(_ uiView: RoomCaptureView, context: Context) {
        
    }
}
  • 모델 이미지화 및 배경 제거
//캡쳐된 모델이 보이는 뷰를 이미지로 캡쳐하기 위한 함수
//3D모델이 존재하는 부분만 캡쳐되도록 영역을 조정해서 이미지로 만듦
func captureScreen() {
    if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
        if let window = windowScene.windows.first(where: { $0.isKeyWindow }) {
            let rootView = window.rootViewController?.view
            let topMargin: CGFloat = 100
            let bottomMargin: CGFloat = 300
            let screenHeight = UIScreen.main.bounds.height
            let screenWidth = UIScreen.main.bounds.width
            let rect = CGRect(x: 0, y: topMargin, width: screenWidth, height: screenHeight - topMargin - bottomMargin)
            capturedView = rootView?.snapshot(of: rect)
        }
    }
}
//이미지의 배경을 제거하는 함수
func createSticker(image: UIImage) {
    let processingQueue = DispatchQueue(label: "ProcessingQueue")
        
    guard let inputImage = CIImage(image: image) else {
        print("Failed to create CIImage")
        return
    }
    processingQueue.async {
        guard let maskImage = subjectMaskImage(from: inputImage) else {
            print("Failed to create mask image")
            DispatchQueue.main.async {
            }
            return
        }
        let outputImage = apply(mask: maskImage, to: inputImage)
        let image = render(ciImage: outputImage)
        DispatchQueue.main.async {
            self.model = image
        }
    }
}
//이미지의 배경을 제거하기 위한 Mask를 만들어주는 함수
func subjectMaskImage(from inputImage: CIImage) -> CIImage? {
    let handler = VNImageRequestHandler(ciImage: inputImage, options: [:])
    let request = VNGenerateForegroundInstanceMaskRequest()
        
    do {
        try handler.perform([request])
        guard let result = request.results?.first else {
            print("No observations found")
            return nil
        }
        let maskPixelBuffer = try result.generateScaledMaskForImage(forInstances: result.allInstances, from: handler)
        return CIImage(cvPixelBuffer: maskPixelBuffer)
    } catch {
        print(error)
        return nil
    }
}
//이미지에 Mask를 적용하는 함수
func apply(mask: CIImage, to image: CIImage) -> CIImage {
    let filter = CIFilter.blendWithMask()
    filter.inputImage = image
    filter.maskImage = mask
    filter.backgroundImage = CIImage.empty()
    return filter.outputImage!
}
//이미지에 적용된 Mask에 따라 배경을 제거한 이미지를 리턴하는 함수
func render(ciImage: CIImage) -> UIImage {
    guard let cgImage = CIContext(options: nil).createCGImage(ciImage, from: ciImage.extent) else {
        fatalError("Failed to render CGImage")
    }
    return UIImage(cgImage: cgImage)
}
  • 정보 저장 및 관리(SwiftData)
@Model
final class SpaceData: Identifiable {
    var id: UUID
    var date: String
    var comment: String
    @Attribute(.externalStorage) var model: Data
    @Attribute(.externalStorage) var background: Data
    var latitude: Double
    var longitude: Double
    
    init(id: UUID, date: String, comment: String, model: Data, background: Data, latitude: Double, longitude: Double) {
        self.id = id
        self.date = date
        self.comment = comment
        self.model = model
        self.background = background
        self.latitude = latitude
        self.longitude = longitude
    }
}
//SwiftData에 공간 정보를 저장하는 함수
func addSpace() {
    let formatter = DateFormatter()
    formatter.dateFormat = "yyyy년 M월 d일"
    let savedDate = formatter.string(from: date)
        
    do {
        if let model {
            if let background {
                let newSpace = SpaceData(
                    id: UUID(),
                    date: savedDate,
                    comment: comment,
                    model: model.pngData()!,
                    background: background.pngData()!,
                    latitude: latitude,
                    longitude: longitude
                )
                    
                modelContext.insert(newSpace)
                try modelContext.save()
            }
        }
    } catch {
        print("Failed to save data")
    }
}
//SwiftData에서 데이터를 삭제하는 함수
func deleteSpace(space: SpaceData) {
    do {
        modelContext.delete(space)
        try modelContext.save()
    } catch {
        print("Failed to save data")
    }
}