oneshim-vision
Edge 이미지 처리를 담당하는 크레이트. 스크린 캡처, 델타 인코딩, OCR, PII 필터링을 수행합니다.
역할
- 스크린 캡처: 활성 창/전체 화면 캡처
- 캡처 결정: 이벤트 분류 + 중요도 계산
- 델타 인코딩: 변경 영역만 추출
- WebP 인코딩: 품질별 압축
- OCR: 텍스트 추출 (옵션)
- PII 필터: 민감 정보 마스킹
- 썸네일 생성: 저해상도 미리보기
디렉토리 구조
oneshim-vision/src/
├── lib.rs # 크레이트 루트
├── capture.rs # ScreenCapture - 화면 캡처
├── trigger.rs # SmartCaptureTrigger - 캡처 결정
├── delta.rs # DeltaEncoder - 변경 영역 감지
├── encoder.rs # WebpEncoder - WebP 인코딩
├── thumbnail.rs # ThumbnailGenerator - 썸네일
├── processor.rs # EdgeFrameProcessor - 통합 처리
├── ocr.rs # OcrExtractor - OCR (옵션)
├── privacy.rs # PII 새니타이징
└── timeline.rs # FrameTimeline - 인메모리 타임라인
처리 파이프라인
주요 컴포넌트
SmartCaptureTrigger (trigger.rs)
이벤트 분석 + 캡처 결정 (CaptureTrigger 포트 구현):
pub struct SmartCaptureTrigger {
throttle_ms: u64,
last_capture: RwLock<Option<Instant>>,
}
#[async_trait]
impl CaptureTrigger for SmartCaptureTrigger {
async fn should_capture(&self, event: &ContextEvent) -> Result<CaptureDecision, CoreError> {
// 쓰로틀 체크 (기본 5초)
if self.is_throttled().await {
return Ok(CaptureDecision::Skip);
}
// 이벤트 분류 → 중요도
let importance = self.calculate_importance(event);
if importance < 0.3 {
Ok(CaptureDecision::Skip)
} else {
self.update_last_capture().await;
Ok(CaptureDecision::Capture { importance })
}
}
}
중요도 계산:
| 이벤트 | 기본 중요도 | 수정자 |
|---|---|---|
ApplicationStart | 0.9 | - |
WindowFocus | 0.7 | +0.1 (새 앱) |
FileOpen | 0.8 | +0.1 (문서) |
UserIdle | 0.2 | - |
UserActive | 0.5 | +0.2 (장시간 유휴 후) |
DeltaEncoder (delta.rs)
16x16 타일 비교로 변경 영역만 추출:
pub struct DeltaEncoder {
tile_size: u32, // 기본 16
threshold: u8, // 변경 감지 임계값
}
impl DeltaEncoder {
pub fn encode(&self, current: &DynamicImage, previous: &DynamicImage) -> DeltaResult {
let mut changed_regions = Vec::new();
// 16x16 타일 단위로 비교
for y in (0..height).step_by(self.tile_size as usize) {
for x in (0..width).step_by(self.tile_size as usize) {
if self.tile_changed(current, previous, x, y) {
changed_regions.push(ChangedRegion {
x, y,
width: self.tile_size,
height: self.tile_size,
data: self.extract_tile(current, x, y),
});
}
}
}
DeltaResult { changed_regions, change_ratio }
}
}
델타 인코딩 장점:
- 변경 없음: 전송 데이터 0
- 부분 변경: 원본 대비 70-90% 감소
- 빠른 비교: SIMD 최적화
EdgeFrameProcessor (processor.rs)
중요도별 분기 처리 (FrameProcessor 포트 구현):
pub struct EdgeFrameProcessor {
delta_encoder: DeltaEncoder,
webp_encoder: WebpEncoder,
thumbnail_gen: ThumbnailGenerator,
ocr_extractor: Option<OcrExtractor>, // #[cfg(feature = "ocr")]
pii_sanitizer: PiiSanitizer,
previous_frame: RwLock<Option<DynamicImage>>,
}
#[async_trait]
impl FrameProcessor for EdgeFrameProcessor {
async fn process(&self, frame: RawFrame) -> Result<ProcessedFrame, CoreError> {
let importance = frame.importance;
let payload = if importance >= 0.8 {
// Full + OCR
let ocr_text = self.extract_ocr(&frame.image).await?;
let sanitized = self.pii_sanitizer.sanitize_text(&ocr_text);
let encoded = self.webp_encoder.encode(&frame.image, Quality::High)?;
ImagePayload::Full {
data: encoded,
width: frame.image.width(),
height: frame.image.height(),
ocr_text: Some(sanitized),
}
} else if importance >= 0.5 {
// Delta encoding
let delta = self.delta_encode(&frame.image).await?;
ImagePayload::Delta { changed_regions: delta.changed_regions }
} else if importance >= 0.3 {
// Thumbnail only
let thumb = self.thumbnail_gen.generate(&frame.image)?;
let encoded = self.webp_encoder.encode(&thumb, Quality::Low)?;
ImagePayload::Thumbnail {
data: encoded,
width: thumb.width(),
height: thumb.height(),
}
} else {
// Metadata only
ImagePayload::MetadataOnly {
window_title: self.pii_sanitizer.sanitize_title(&frame.window_title),
application: frame.application.clone(),
}
};
// 이전 프레임 저장 (델타용)
*self.previous_frame.write().await = Some(frame.image.clone());
Ok(ProcessedFrame { payload, timestamp: frame.timestamp })
}
}
WebpEncoder (encoder.rs)
품질별 WebP 인코딩:
pub struct WebpEncoder;
pub enum Quality {
Low, // 50% - 썸네일용
Medium, // 75% - 일반
High, // 90% - 고화질
}
impl WebpEncoder {
pub fn encode(&self, image: &DynamicImage, quality: Quality) -> Result<Vec<u8>, CoreError> {
let q = match quality {
Quality::Low => 50.0,
Quality::Medium => 75.0,
Quality::High => 90.0,
};
let encoder = webp::Encoder::from_image(image)?;
Ok(encoder.encode(q).to_vec())
}
}
PiiSanitizer (privacy.rs)
민감 정보 마스킹:
pub struct PiiSanitizer {
patterns: Vec<(Regex, &'static str)>,
}
impl PiiSanitizer {
pub fn new() -> Self {
Self {
patterns: vec![
// 이메일
(Regex::new(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}").unwrap(), "[EMAIL]"),
// 신용카드
(Regex::new(r"\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b").unwrap(), "[CARD]"),
// 주민번호
(Regex::new(r"\b\d{6}[-\s]?\d{7}\b").unwrap(), "[SSN]"),
// 파일 경로 (사용자명 포함)
(Regex::new(r"/Users/[^/]+/|C:\\Users\\[^\\]+\\").unwrap(), "[USER_PATH]"),
// 전화번호
(Regex::new(r"\b\d{2,3}[-.\s]?\d{3,4}[-.\s]?\d{4}\b").unwrap(), "[PHONE]"),
],
}
}
pub fn sanitize_text(&self, text: &str) -> String {
let mut result = text.to_string();
for (pattern, replacement) in &self.patterns {
result = pattern.replace_all(&result, *replacement).to_string();
}
result
}
}
마스킹 대상:
- 이메일 주소 →
[EMAIL] - 신용카드 번호 →
[CARD] - 주민등록번호 →
[SSN] - 사용자 경로 →
[USER_PATH] - 전화번호 →
[PHONE]
OcrExtractor (ocr.rs)
Tesseract 기반 OCR (#[cfg(feature = "ocr")]):
#[cfg(feature = "ocr")]
pub struct OcrExtractor {
tessdata_path: Option<PathBuf>,
}
impl OcrExtractor {
pub fn extract(&self, image: &DynamicImage) -> Result<String, OcrError> {
let rgba = image.to_rgba8();
let mut leptess = LepTess::new(
self.tessdata_path.as_ref().map(|p| p.to_str().unwrap()),
"kor+eng"
)?;
leptess.set_image_from_mem(&rgba.as_raw())?;
let text = leptess.get_utf8_text()?;
Ok(text.trim().to_string())
}
}
OCR 설정:
- 언어: 한국어 + 영어
ONESHIM_TESSDATA환경변수로 데이터 경로 지정- 기능 플래그로 비활성화 가능
의존성
| 크레이트 | 용도 |
|---|---|
xcap | 화면 캡처 |
image | 이미지 처리 |
webp | WebP 인코딩 |
fast_image_resize | 썸네일 리사이즈 |
leptess | Tesseract OCR (옵션) |
regex | PII 패턴 매칭 |
플랫폼 지원
| 기능 | macOS | Windows | Linux |
|---|---|---|---|
| 전체 화면 캡처 | ✅ | ✅ | ✅ |
| 활성 창 캡처 | ✅ | ✅ | ⚠️ (X11) |
| OCR | ✅ | ✅ | ✅ |
관련 문서:
- 클라이언트 개요
- oneshim-core - 이미지 페이로드 모델
- oneshim-monitor - 캡처 트리거 이벤트