본문으로 건너뛰기

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 })
}
}
}

중요도 계산:

이벤트기본 중요도수정자
ApplicationStart0.9-
WindowFocus0.7+0.1 (새 앱)
FileOpen0.8+0.1 (문서)
UserIdle0.2-
UserActive0.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이미지 처리
webpWebP 인코딩
fast_image_resize썸네일 리사이즈
leptessTesseract OCR (옵션)
regexPII 패턴 매칭

플랫폼 지원

기능macOSWindowsLinux
전체 화면 캡처
활성 창 캡처⚠️ (X11)
OCR

관련 문서: