{"id":20738050,"url":"https://github.com/developeracademy-postech/2024-nc2-m15-roomplan","last_synced_at":"2025-04-24T01:43:08.263Z","repository":{"id":244875013,"uuid":"816535576","full_name":"DeveloperAcademy-POSTECH/2024-NC2-M15-RoomPlan","owner":"DeveloperAcademy-POSTECH","description":"RoomPlan과 Maps를 활용하여 생생하게 공간을 기록하고 공유할 수 있는 앱","archived":false,"fork":false,"pushed_at":"2025-03-15T03:50:50.000Z","size":3274,"stargazers_count":5,"open_issues_count":0,"forks_count":2,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-03-30T05:41:21.312Z","etag":null,"topics":["corelocation","mapkit","roomplan","swift","swiftdata","swiftui"],"latest_commit_sha":null,"homepage":"https://apps.apple.com/kr/app/my-raum/id6504674031","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/DeveloperAcademy-POSTECH.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-06-18T00:25:47.000Z","updated_at":"2025-03-16T02:36:56.000Z","dependencies_parsed_at":"2024-07-17T19:15:42.086Z","dependency_job_id":null,"html_url":"https://github.com/DeveloperAcademy-POSTECH/2024-NC2-M15-RoomPlan","commit_stats":null,"previous_names":["developeracademy-postech/2024-nc2-m15-roomplan"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DeveloperAcademy-POSTECH%2F2024-NC2-M15-RoomPlan","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DeveloperAcademy-POSTECH%2F2024-NC2-M15-RoomPlan/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DeveloperAcademy-POSTECH%2F2024-NC2-M15-RoomPlan/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DeveloperAcademy-POSTECH%2F2024-NC2-M15-RoomPlan/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/DeveloperAcademy-POSTECH","download_url":"https://codeload.github.com/DeveloperAcademy-POSTECH/2024-NC2-M15-RoomPlan/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250545851,"owners_count":21448249,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["corelocation","mapkit","roomplan","swift","swiftdata","swiftui"],"created_at":"2024-11-17T06:16:20.550Z","updated_at":"2025-04-24T01:43:08.245Z","avatar_url":"https://github.com/DeveloperAcademy-POSTECH.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 2024-NC2-M15-RoomPlan\n\n## 🎥 Youtube Link (Click the image)\n[![Video Label](https://github.com/DeveloperAcademy-POSTECH/2024-NC2-M15-RoomPlan/assets/52344592/6f631d09-955f-420c-bd7c-9b06ce0888b5)](https://youtu.be/Ne4yzAaqqkM?si=25OEI2bubSXvJbl4)\n\n## 💡 About RoomPlan\n\n\u003cimg width=\"600\" alt=\"Untitled\" src=\"https://github.com/DeveloperAcademy-POSTECH/2024-NC2-M15-RoomPlan/assets/52344592/f9e25cee-4259-4663-83ff-33f0d38ea918\"\u003e\n  \n\u003eRoomPlan은 카메라와 라이다센서를 활용해서 3D 평면도를 작성해주는 기술입니다.\n\n\u003eRoomPlan을 활용해서 다른 기술과 결합하여 벽의 색도 바꿔볼 수 있고 쓰이는 페인트의 양 까지 알 수 있습니다. 또한, 공간 설계, 가구 배치 등에도 활용할 수 있는 기술입니다.\n\n\u003e제약사항:  RoomPlan으로 공간을 스캔할 때는 5분을 초과하면 안되고 최대 스캔 범위는 9mX9m입니다. 그리고, 직육면체만 인식하고 곡선은 인식이 불가능합니다.\n\n## 🎯 What we focus on?\n- 애플에서 제공한 RoomPlan 예시 코드를 활용하여 공간을 캡쳐하는 기술 자체에 집중했습니다.\n- 캡쳐된 모델을 USD, USDZ 등의 포맷으로 저장하는 것 외에는 모델을 활용할 방법이 마땅치 않아 캡쳐된 모델을 어떻게 사용해야할지 고민했습니다.\n- 캡쳐된 모델을 이미지로 만들어서 다른 이미지, 텍스트 등의 요소들과 함께 배치하는 방식을 사용하기로 결정했습니다.\n\n## 💼 Use Case\n**RoomPlan과 Maps를 활용하여 생생하게 공간을 기록하고 공유할 수 있는 서비스를 만들어보자!**\n\n## 🖼️ Prototype\n- Prototype \u0026 User Flow\n\u003cimg width=\"1932\" alt=\"Untitled 2\" src=\"https://github.com/DeveloperAcademy-POSTECH/2024-NC2-M15-RoomPlan/assets/52344592/18ca4200-5829-4d0a-a5f8-b17c2ff01d4e\"\u003e\n\n- 시연영상\n\nhttps://github.com/DeveloperAcademy-POSTECH/2024-NC2-M15-RoomPlan/assets/52344592/330e2af1-cd0a-4aaf-a028-d8ba90d4cfa6\n\n## 🛠️ About Code\n- RoomPlan\n\n```swift\nimport RoomPlan\n```\n\n```swift\n//방 캡쳐를 담당하는 클래스\nclass RoomController: RoomCaptureViewDelegate {\n    func encode(with coder: NSCoder) {\n        fatalError(\"Not Needed\")\n    }\n    \n    required init?(coder: NSCoder) {\n        fatalError(\"Not Needed\")\n    }\n    \n    //코드의 다른 부분에서 접근 가능하도록 싱글턴으로 인스턴스 생성\n    static var instance = RoomController()\n    \n    var captureView: RoomCaptureView\n    var sessionConfig: RoomCaptureSession.Configuration = RoomCaptureSession.Configuration()\n    var finalResult: CapturedRoom?\n    \n    init() {\n        captureView = RoomCaptureView(frame: .zero)\n        captureView.delegate = self\n    }\n    \n    //캡쳐가 완료되었는지 아닌지에 대해서 Bool 값을 리턴하는 함수\n    func captureView(shouldPresent roomDataForProcessing: CapturedRoomData, error: (Error)?) -\u003e Bool {\n        return true\n    }\n    \n    //캡쳐가 완료된 결과를 finalResult에 저장하는 함수\n    func captureView(didPresent processedResult: CapturedRoom, error: (Error)?) {\n        finalResult = processedResult\n    }\n    \n    //캡쳐를 시작하는 함수\n    func startSession() {\n        captureView.captureSession.run(configuration: sessionConfig)\n    }\n    \n    //캡쳐를 종료하는 함수\n    func stopSession() {\n        captureView.captureSession.stop()\n    }\n}\n```\n\n```swift\n//UIKit으로 작성된 RoomPlan 뷰를 SwiftUI에서 보여지도록 UIViewRepresentable 프로토콜을 사용한 뷰\nstruct RoomCaptureViewRepresentable : UIViewRepresentable {\n    //뷰를 생성하고 초기화하는 함수\n    func makeUIView(context: Context) -\u003e RoomCaptureView {\n        RoomController.instance.captureView\n    }\n    \n    func updateUIView(_ uiView: RoomCaptureView, context: Context) {\n        \n    }\n}\n```\n\n- 모델 이미지화 및 배경 제거\n\n```swift\n//캡쳐된 모델이 보이는 뷰를 이미지로 캡쳐하기 위한 함수\n//3D모델이 존재하는 부분만 캡쳐되도록 영역을 조정해서 이미지로 만듦\nfunc captureScreen() {\n    if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {\n        if let window = windowScene.windows.first(where: { $0.isKeyWindow }) {\n            let rootView = window.rootViewController?.view\n            let topMargin: CGFloat = 100\n            let bottomMargin: CGFloat = 300\n            let screenHeight = UIScreen.main.bounds.height\n            let screenWidth = UIScreen.main.bounds.width\n            let rect = CGRect(x: 0, y: topMargin, width: screenWidth, height: screenHeight - topMargin - bottomMargin)\n            capturedView = rootView?.snapshot(of: rect)\n        }\n    }\n}\n```\n\n```swift\n//이미지의 배경을 제거하는 함수\nfunc createSticker(image: UIImage) {\n    let processingQueue = DispatchQueue(label: \"ProcessingQueue\")\n        \n    guard let inputImage = CIImage(image: image) else {\n        print(\"Failed to create CIImage\")\n        return\n    }\n    processingQueue.async {\n        guard let maskImage = subjectMaskImage(from: inputImage) else {\n            print(\"Failed to create mask image\")\n            DispatchQueue.main.async {\n            }\n            return\n        }\n        let outputImage = apply(mask: maskImage, to: inputImage)\n        let image = render(ciImage: outputImage)\n        DispatchQueue.main.async {\n            self.model = image\n        }\n    }\n}\n```\n\n```swift\n//이미지의 배경을 제거하기 위한 Mask를 만들어주는 함수\nfunc subjectMaskImage(from inputImage: CIImage) -\u003e CIImage? {\n    let handler = VNImageRequestHandler(ciImage: inputImage, options: [:])\n    let request = VNGenerateForegroundInstanceMaskRequest()\n        \n    do {\n        try handler.perform([request])\n        guard let result = request.results?.first else {\n            print(\"No observations found\")\n            return nil\n        }\n        let maskPixelBuffer = try result.generateScaledMaskForImage(forInstances: result.allInstances, from: handler)\n        return CIImage(cvPixelBuffer: maskPixelBuffer)\n    } catch {\n        print(error)\n        return nil\n    }\n}\n```\n\n```swift\n//이미지에 Mask를 적용하는 함수\nfunc apply(mask: CIImage, to image: CIImage) -\u003e CIImage {\n    let filter = CIFilter.blendWithMask()\n    filter.inputImage = image\n    filter.maskImage = mask\n    filter.backgroundImage = CIImage.empty()\n    return filter.outputImage!\n}\n```\n\n```swift\n//이미지에 적용된 Mask에 따라 배경을 제거한 이미지를 리턴하는 함수\nfunc render(ciImage: CIImage) -\u003e UIImage {\n    guard let cgImage = CIContext(options: nil).createCGImage(ciImage, from: ciImage.extent) else {\n        fatalError(\"Failed to render CGImage\")\n    }\n    return UIImage(cgImage: cgImage)\n}\n```\n\n- 정보 저장 및 관리(SwiftData)\n\n```swift\n@Model\nfinal class SpaceData: Identifiable {\n    var id: UUID\n    var date: String\n    var comment: String\n    @Attribute(.externalStorage) var model: Data\n    @Attribute(.externalStorage) var background: Data\n    var latitude: Double\n    var longitude: Double\n    \n    init(id: UUID, date: String, comment: String, model: Data, background: Data, latitude: Double, longitude: Double) {\n        self.id = id\n        self.date = date\n        self.comment = comment\n        self.model = model\n        self.background = background\n        self.latitude = latitude\n        self.longitude = longitude\n    }\n}\n```\n\n```swift\n//SwiftData에 공간 정보를 저장하는 함수\nfunc addSpace() {\n    let formatter = DateFormatter()\n    formatter.dateFormat = \"yyyy년 M월 d일\"\n    let savedDate = formatter.string(from: date)\n        \n    do {\n        if let model {\n            if let background {\n                let newSpace = SpaceData(\n                    id: UUID(),\n                    date: savedDate,\n                    comment: comment,\n                    model: model.pngData()!,\n                    background: background.pngData()!,\n                    latitude: latitude,\n                    longitude: longitude\n                )\n                    \n                modelContext.insert(newSpace)\n                try modelContext.save()\n            }\n        }\n    } catch {\n        print(\"Failed to save data\")\n    }\n}\n```\n\n```swift\n//SwiftData에서 데이터를 삭제하는 함수\nfunc deleteSpace(space: SpaceData) {\n    do {\n        modelContext.delete(space)\n        try modelContext.save()\n    } catch {\n        print(\"Failed to save data\")\n    }\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdeveloperacademy-postech%2F2024-nc2-m15-roomplan","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdeveloperacademy-postech%2F2024-nc2-m15-roomplan","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdeveloperacademy-postech%2F2024-nc2-m15-roomplan/lists"}