Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .omm/data-flow/description.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ JPEG에서 YUV를 고르면 앱이 현재 YUV 크기의 프레임을 JPEG로 변

RecentMediaThumbnail은 MediaStore 변경·화면 복귀 시 저장 완료 항목을 조회합니다. 별도 스레드에서 디코딩하고 활성 화면의 최신 결과만 RecentMediaButton에 반영합니다.

LIVE Depth는 ImageReader의 DEPTH16을 DepthFrame에서 표시 표본으로 바꾸고 DepthMapView로 전달합니다. Telemetry에는 크기·시각·형식과 capture 콜백만 남깁니다. 표시 픽셀은 저장하지 않으며 벤치마크 조립기로 전달하지 않습니다.

CLI는 직접 인자 또는 base64url JSON을 받고 shell UID·허용 설정·프로토콜을 검사합니다. CommandStore는 실행 전 요청을 저장합니다. 저장 완료 뒤 CliArtifactRegistry가 계산한 artifact 크기·SHA-256과 원본 URI를 CommandCoordinator가 기록하며, PC에는 URI 없이 결과와 등록된 artifact ID의 읽기 전용 파일을 제공합니다. ADB 스크립트는 manifest의 이름·크기·SHA-256을 검증하고 Download/HALCamera-cli/요청ID에 파일을 준비해 adb pull을 안내합니다. Python CLI도 검증한 임시 파일만 공개합니다.

벤치마크 JSON은 기기 안에서만 생성하며, files/benchmarks와 baseline 목록으로 비교합니다. 외부 JSON 가져오기는 0.13.0에서 제거했습니다.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
DepthCameraEngine은 LIVE의 DEPTH16 전용 세션을 소유합니다. 공개 ID의 DEPTH_OUTPUT과 선택한 해상도를 재검사하고 ImageReader 하나만 구성합니다. 이전 엔진이 놓은 카메라는 AvailabilityCallback 또는 최대 2초의 대기 뒤 엽니다. 컬러 출력이 없으므로 DEPTH_DEPTH_IS_EXCLUSIVE인 카메라도 대상으로 삼을 수 있습니다. MediaCapture는 구현하지 않으며 촬영과 녹화를 제공하지 않습니다.

ImageReader는 acquireLatestImage로 버퍼를 읽고 use로 즉시 돌려줍니다. DepthFrame은 crop·rowStride·pixelStride와 little-endian 16비트 표본을 해석합니다. 하위 13비트는 mm 거리이며 상위 3비트는 신뢰도입니다. 신뢰도 코드 0은 100%, 1은 0%입니다. 거리가 0이거나 신뢰도가 50% 미만이면 검게 표시하고, 나머지는 고정된 0–8191 mm 범위에서 가까울수록 밝게 표시합니다. 긴 변은 최대 640픽셀로 표본을 추출하고 UI 전달 대기는 한 프레임으로 제한하며 최대 초당 10회 갱신합니다.

DepthMapView는 센서·디스플레이 회전과 전면 반전을 적용해 전체 맵을 fit으로 표시합니다. 중앙 거리와 표시 표본의 유효 비율을 함께 보여줍니다. 표시용 픽셀은 저장하지 않으며 Telemetry에는 크기·시각·형식과 Depth 콜백만 기록합니다.

세션 거절·버퍼 오류·카메라 오류는 상태 메시지로 보고하고 장치를 닫습니다. repeating request 제출 후 8초 이내 첫 표시용 프레임이 없으면 실패로 처리합니다. close(done)는 열기 대기 중인 장치도 onOpened에서 닫고 onClosed 이후 자원을 회수한 뒤 UI 스레드에서 완료를 전달합니다. 이미 해제된 엔진의 close도 완료를 전달합니다.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: nested-class
title: depth-engine
children: []
parentPath:
- camera-engines
2 changes: 1 addition & 1 deletion .omm/overall-architecture/camera-engines/description.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
camera/는 CameraEngine 인터페이스와 Camera2Engine·CameraXEngine을 제공합니다. Camera2Engine은 기본 크기 선택 경로와 벤치마크용 명시적 StreamSpec 경로를 구분합니다. 세션별 콜백과 close(done) 완료 통지가 러너에 연결됩니다.
camera/는 CameraEngine 인터페이스와 Camera2Engine·CameraXEngine·DepthCameraEngine을 제공합니다. Camera2Engine은 기본 크기 선택 경로와 벤치마크용 명시적 StreamSpec 경로를 구분합니다. 세션별 콜백과 close(done) 완료 통지가 러너에 연결됩니다.
StreamConfiguration은 실제 카메라 출력으로 Callback 목록을 만듭니다. 앱의 YUV JPEG 변환·PIP 합성·GPU 인코더 출력은 Callback에 추가하지 않습니다.
1 change: 1 addition & 0 deletions .omm/overall-architecture/camera-engines/meta.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ kind: nested-class
title: camera-engines
children:
- camera2-engine
- depth-engine
- camerax-engine
- endpoint-model
- endpoint-resolver
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
MainActivity는 Live 런처입니다. CameraEngine을 선택하고 카메라 수명주기, 권한, 촬영 모드와 버튼 상태를 관리합니다. LiveReadings가 실시간 측정값을 갱신하고 IncidentActions가 이벤트 ZIP 저장과 공유를 담당합니다. Benchmark는 Lab에서 엽니다.
MainActivity는 Live 런처로 CameraEngine 선택·카메라 수명주기·권한·촬영 모드·버튼 상태를 관리합니다. LiveReadings는 측정값을 갱신하고 IncidentActions는 이벤트 ZIP 저장·공유를 담당합니다. Benchmark는 Lab에서 엽니다.

프리뷰의 제어·측정값·촬영 버튼 배치는 docs/design/APP-UI.md를 따릅니다. 아이콘 터치 영역은 48dp 이상이며 색·서체는 Look을 사용합니다.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
camera/CameraProbe.kt는 Probe의 순수 Kotlin 모델과 렌더러입니다. ProbeRow·ProbeSection·CameraProbeEntry·CameraProbeSnapshot이 카메라별 섹션 표를 담고, 물리 카메라는 "논리ID.물리ID" 키로 구분합니다. ProbeFormat이 값의 표기(크기·범위·enum 이름)를 통일하고, CameraProbeText가 TXT를, toJsonMap이 JSON을 만듭니다. 이 파일은 Android를 모르므로 JVM 테스트로 검증합니다.

camera/CameraProbeReader.kt만 CameraManager를 알고, 공개 카메라 ID와 논리 카메라의 physicalCameraIds를 순회하며 CameraCharacteristics를 섹션별로 읽어 스냅샷을 채웁니다. 섹션 하나가 예외로 실패해도 나머지를 계속 읽고 실패 목록에 남깁니다. SESSION KEYS와 MANDATORY STREAM COMBINATIONS는 API 28·29 이상에서만 읽고, STREAMS는 SCALER_STREAM_CONFIGURATION_MAP의 출력 형식마다 별도 섹션으로 만듭니다.

DepthCapabilities는 DEPTH_OUTPUT, DEPTH16 해상도와 DEPTH_DEPTH_IS_EXCLUSIVE를 독립적으로 읽습니다. DepthSupport는 DEPTH 섹션의 지원 여부·해상도·컬러 동시 요청 가능 여부를 만듭니다. null이나 읽기 실패는 Unknown 또는 Read error로 보존하고, HAL이 동시 요청을 허용해도 출력 조합을 별도로 확인해야 한다고 표시합니다. 단독으로 열 수 없는 물리 ID는 LIVE에서 직접 선택할 수 없다고 안내합니다. 같은 행을 화면·TXT·JSON에 사용합니다.
2 changes: 2 additions & 0 deletions .omm/state-transitions/engine-lifecycle/description.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ Camera2Engine은 HandlerThread에서 카메라를 열고 세션과 반복 요청
Live 녹화는 선택한 설정, Benchmark 녹화는 profile을 사용합니다. 녹화 상태는 overall-architecture/camera-engines/camera2-engine/live-recorder, 측정 순서는 runner-sequence에 있습니다. PIP 종료 전에 메인 사진 대기를 취소하여 순환 대기를 막습니다.

단계 전이의 시간 제한과 close 완료 처리는 runner-sequence에 있습니다.

DepthCameraEngine은 DEPTH16만 구성합니다. 열기 중 취소도 onOpened→onClosed 후 완료하며 active로 늦은 신호를 차단합니다. 첫 맵은 요청 후 8초까지 기다리고 해상도 대체·일반 Camera2 재시도는 하지 않습니다.
2 changes: 2 additions & 0 deletions .omm/ui-tool-handoff/description.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ Lab의 시스템 뒤로 가기와 상단 Live 링크은 기존 Live로 복귀합
Live Streams는 Lab 또는 Live의 크기 표시에서 열 수 있으며 카메라를 열지 않습니다. 진입 전에 Live 엔진의 close(done)를 기다립니다. 상단·시스템 뒤로 가기는 초안을 버리고 진입한 화면으로 돌아갑니다. 저장과 직전 정상 구성 복원도 같은 화면으로 돌아갑니다. Lab은 결과를 보관했다가 Live 복귀 시 전달하며, 직접 진입은 결과를 Live에 바로 전달합니다. 설정과 직전 정상 구성은 카메라·엔진별로 분리하고 Activity 재생성 시 복원합니다.

Multi도 Live의 close(done) 뒤 ConcurrentCameraActivity를 엽니다. Live에서 선택한 엔진을 전달하며 Multi 화면에서도 Camera2와 CameraX를 바꿀 수 있습니다. 이 화면은 모든 카메라의 onClosed를 기다린 뒤 Live로 돌아가며, Live는 기존 엔진 선택으로 프리뷰를 다시 엽니다.

Depth 보기에서도 Lab 진입 전에 close(done)를 기다립니다. 촬영 모드를 바꾸지 않고 Lab에서 돌아오면 선택한 Depth 해상도를 다시 엽니다. Depth 보기 중에는 크기 표시로 Live Streams를 여는 동작을 비활성화합니다.
2 changes: 1 addition & 1 deletion .omm/ui-zoom/description.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Live 프리뷰에서 두 손가락을 벌리거나 모으면 지원 범위 안에서 연속으로 줌을 바꿉니다. 가장 가까운 프리셋의 원에 현재 배율을 소수 첫째 자리까지 표시하며, 펼친 상태에서 그 원을 누르면 해당 프리셋 배율로 돌아갑니다. 화면의 카메라 선택 정보를 갱신해도 중간 배율은 유지합니다.

핀치와 버튼 선택은 같은 MainActivity.applyZoom 경로로 엔진과 표시를 갱신합니다. 프리뷰 준비 전, 일시정지·종료 처리·CLI 작업·연사 중에는 변경하지 않으며 녹화 중에는 허용합니다. 두 손가락 입력이 시작되면 대기 중인 터치 초점과 길게 누르기 인식을 취소하고, 마지막 손가락을 뗄 때까지 초점 터치로 처리하지 않습니다.
핀치와 버튼 선택은 같은 MainActivity.applyZoom 경로로 엔진과 표시를 갱신합니다. Depth 보기, 프리뷰 준비 전, 일시정지·종료 처리·CLI 작업·연사 중에는 변경하지 않으며 녹화 중에는 허용합니다. 두 손가락 입력이 시작되면 대기 중인 터치 초점과 길게 누르기 인식을 취소하고, 마지막 손가락을 뗄 때까지 초점 터치로 처리하지 않습니다.

| 조건 | 동작 |
| --- | --- |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class CliStoreInstrumentation : Instrumentation() {
private var multiSplit = false
private var lintChecks = false
private var splitDrag = false
private var depthChecks = false
override fun onCreate(arguments: Bundle?) {
super.onCreate(arguments)
exportReports = arguments?.getString("export_reports") == "true"
Expand All @@ -29,10 +30,20 @@ class CliStoreInstrumentation : Instrumentation() {
multiSplit = arguments?.getString("multi_split") == "true"
lintChecks = arguments?.getString("lint_checks") == "true"
splitDrag = arguments?.getString("split_drag") == "true"
depthChecks = arguments?.getString("depth_checks") == "true"
start()
}

override fun onStart() {
if (depthChecks) {
try {
val result = dev.halcamera.ui.DepthMapChecks.run(this)
finish(Activity.RESULT_OK, Bundle().apply { putString("stream", "$result\n") })
} catch (error: Throwable) {
finish(Activity.RESULT_CANCELED, Bundle().apply { putString("stream", "DEPTH_MAP_CHECKS_FAILED: ${error.stackTraceToString()}\n") })
}
return
}
if (splitDrag) {
try {
val result = dev.halcamera.ui.SplitDragChecks.run(this)
Expand Down
58 changes: 58 additions & 0 deletions app/src/androidTest/java/dev/halcamera/ui/DepthMapChecks.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package dev.halcamera.ui

import android.app.Instrumentation
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Canvas
import android.widget.FrameLayout
import dev.halcamera.WorkbenchActivity
import dev.halcamera.camera.DepthFrame
import dev.halcamera.camera.LiveSize
import java.io.File
import java.nio.ByteBuffer
import java.nio.ByteOrder

/** Synthetic DEPTH16 display validation; deliberately makes no claim about real depth camera support. */
object DepthMapChecks {
fun run(instrumentation: Instrumentation): String {
val context = instrumentation.targetContext
val activity = instrumentation.startActivitySync(Intent(context, WorkbenchActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
val output = File(context.getExternalFilesDir(null), "depth-map-synthetic.png")
var failure: Throwable? = null
try {
instrumentation.runOnMainSync {
try {
val width = 160; val height = 120
val buffer = ByteBuffer.allocate(width * height * 2).order(ByteOrder.LITTLE_ENDIAN)
for (y in 0 until height) for (x in 0 until width) {
val person = x in 55..105 && y in 20..100
val mm = if (person) 1000 else 4000 + x * 20
val code = if (x < 12) 1 else 0
buffer.putShort(((code shl 13) or mm).toShort())
}
buffer.flip()
val frame = DepthFrame.render(buffer, width, height, width * 2, 2)
check(frame.centerMm == 1000)
val view = DepthMapView(activity, LiveSize(width, height), 90, false)
val stage = FrameLayout(activity).apply { setBackgroundColor(android.graphics.Color.BLACK); addView(view, FrameLayout.LayoutParams(-1, -1)) }
activity.setContentView(stage)
view.setChromeInsets(Look.dp(activity, 160), Look.dp(activity, 180))
view.bind(frame)
val metrics = context.resources.displayMetrics
stage.measure(android.view.View.MeasureSpec.makeMeasureSpec(metrics.widthPixels, android.view.View.MeasureSpec.EXACTLY),
android.view.View.MeasureSpec.makeMeasureSpec(metrics.heightPixels, android.view.View.MeasureSpec.EXACTLY))
stage.layout(0, 0, metrics.widthPixels, metrics.heightPixels)
check(view.contentDescription.toString().contains("Center 1.000 m"))
val image = Bitmap.createBitmap(stage.width, stage.height, Bitmap.Config.ARGB_8888)
try {
stage.draw(Canvas(image))
check((image.getPixel(stage.width / 2, stage.height / 2) and 255) > 0)
output.outputStream().use { check(image.compress(Bitmap.CompressFormat.PNG, 100, it)) }
} finally { image.recycle() }
} catch (error: Throwable) { failure = error }
}
failure?.let { throw it }
} finally { instrumentation.runOnMainSync { activity.finish() } }
return "DEPTH_MAP_CHECKS_OK · synthetic stride decode, center distance, display and accessibility · ${output.absolutePath}"
}
}
Loading
Loading