콘텐츠로 건너뛰기
Codex

프레임 정규화와 체크섬

프레임 정규화와 체크섬

TECH NOTE · DESKTOP AI PET

이미지 생성으로 만든 프레임은 배경이 투명하게 나오지 않습니다. 크로마키 관용값이 양쪽으로 위험한 이유와, 프레임 수백 장을 픽셀 수로 검증하는 방법을 실측과 함께 정리했습니다.

프레임 1001장을 눈으로 확인할 수는 없다

지난 편에서 스프라이트 시트를 읽었다. 이번 편은 그 시트를 만드는 쪽이다.

캐릭터 프레임은 이미지 생성으로 만들었다. 그 과정은 앞선 글에 적었다. 여기서 다루는 것은 그다음이다 — 생성된 프레임을 시트에 넣기 전에 무엇을 확인하는가.

문제는 단순하다. 이미지 생성은 배경을 투명하게 안 준다. 단색을 깔고 나중에 빼야 하는데, 그 결과가 프레임마다 다르다. 어떤 건 깨끗하고, 어떤 건 배경이 남고, 어떤 건 캐릭터까지 지워진다.

수백 장을 눈으로 볼 수는 없다. 숫자로 봐야 한다.

이번 편의 코드다.

https://github.com/bluecafe/pet-reference/tree/main/asset-pipeline

배경색은 캐릭터에 없는 색으로

마젠타(#FF00FF)를 깐다. 형광 분홍이라 눈이 아프지만 그게 요점이다.

살구색이나 회색을 쓰면 피부와 옷이 같이 지워진다. 배경색은 예쁠 필요가 없고 캐릭터에 안 나오면 된다. 초록을 쓰는 관행이 있지만 캐릭터에 초록이 들어가면 같은 문제가 생기므로, 그 캐릭터에 뭐가 있는지를 보고 정하는 게 맞다.

관용값은 양쪽으로 위험하다

배경을 뺄 때 “정확히 이 색”만 지우면 안 된다. 생성 이미지는 배경도 미세하게 번져서, 가장자리에 #FA05FA 같은 값이 남는다.

그래서 색 거리로 판정한다. RGB 세 채널의 차이를 제곱해 더한 값이다.

fn distance(a: Rgba, b: Rgba) -> u32 {
    let dr = a[0].abs_diff(b[0]) as u32;
    let dg = a[1].abs_diff(b[1]) as u32;
    let db = a[2].abs_diff(b[2]) as u32;
    dr * dr + dg * dg + db * db
}

pub fn remove_chroma(frame: &Frame, key: Rgba, tolerance: u32) -> Frame {
    let threshold = tolerance * tolerance;
    let pixels = frame
        .pixels
        .iter()
        .map(|&pixel| {
            if distance(pixel, key) <= threshold {
                [0, 0, 0, 0]
            } else {
                pixel
            }
        })
        .collect();
    Frame::new(frame.width, frame.height, pixels)
}

제곱근을 쓰지 않는다. 비교만 하면 되므로 양쪽을 제곱한 채로 두는 편이 빠르고, 부동소수점도 안 쓴다. 그래서 tolerance도 미리 제곱해 둔다.

얼마나 비슷하면 배경으로 볼 것인가 — 이 값이 관용값이다. 여기서 양쪽으로 함정이 있다.

관용값 결과
0 번진 가장자리가 남는다. 캐릭터 주변에 분홍 테두리가 생긴다
적당히 배경만 지워진다
크게 붉은 계열 캐릭터가 통째로 사라진다

마지막이 특히 위험하다. 마젠타는 빨강과 파랑이 섞인 색이라, 붉은 옷이나 분홍 머리가 색 거리상 가깝다. 관용값을 키우다 보면 어느 순간 캐릭터가 지워지는데, 프레임 몇 장만 그렇게 되면 알아채기 어렵다.

예제 테스트에 두 방향을 다 넣어 뒀다. 관용값 0에서는 번진 픽셀이 남고, 90에서는 붉은 캐릭터가 사라진다.

프레임 하나만 보면 정상인지 알 수 없다

배경을 뺐다. 이제 제대로 됐는지 확인해야 한다.

가장 단순한 지표는 불투명 픽셀 수다. 배경이 다 빠졌으면 캐릭터가 차지하는 픽셀만 남는다.

그런데 그 숫자 하나만 보면 판단할 수 없다. 불투명 픽셀이 3만 개라는 게 많은 건지 적은 건지 알 방법이 없다. 캐릭터가 크면 많고 작으면 적다.

다른 프레임과 비교해야 안다.

실제 파이프라인이 남긴 리포트에 걷기 동작 네 장의 값이 이렇게 적혀 있다.

24643   24587   24626   24682

같은 캐릭터가 조금씩 다른 자세를 취한 것이라 이 정도로 붙어 있다. 여기서 하나만 4만이 나오면 배경이 덜 빠진 것이고, 0이 나오면 캐릭터가 통째로 지워진 것이다.

그래서 프레임을 묶어서 보고 중앙값에서 벗어나는 것을 찾는다.

let median = median_of(&densities);
if median > 0 {
    for (index, &count) in densities.iter().enumerate() {
        if count == 0 {
            continue;
        }
        let diff = count.abs_diff(median) * 100 / median;
        if diff > tolerance_percent {
            issues.push(FrameIssue::OutlierDensity { index, pixels: count, median });
        }
    }
}

평균이 아니라 중앙값을 쓰는 이유는, 망가진 프레임 하나가 평균을 끌어올려서 기준 자체를 망가뜨리기 때문이다. 배경이 안 빠진 프레임이 16만 픽셀로 나오면 평균이 그쪽으로 끌려가고, 그러면 정상 프레임들이 오히려 “너무 작다”고 걸린다.

count == 0을 건너뛰는 것도 같은 이유다. 빈 프레임은 이미 별도로 보고했으므로 여기서 또 세면 같은 문제가 두 번 나온다.

여백에 닿으면 시트에서 붙어 보인다

검사가 하나 더 있다. 캐릭터가 셀 가장자리에 닿는지 본다.

프레임은 나중에 시트에 격자로 배치된다. 캐릭터가 셀을 꽉 채우면 옆 프레임과 이어져 보인다. 걷는 캐릭터 옆에 다음 프레임의 팔이 삐져나온 것처럼 보이는 증상이다.

그래서 셀 가장자리에 여백을 두고, 그 안쪽에 불투명 픽셀이 있으면 걸러낸다. 실제 파이프라인은 24픽셀을 뒀다.

이건 생성 단계에서 프롬프트로 완전히 통제되지 않는다. “캐릭터를 화면 가운데 작게”라고 해도 어떤 프레임은 크게 나온다. 그래서 나중에 재는 쪽이 확실하다.

지문은 무엇이 바뀌었는지 보려고 있다

마지막으로 각 프레임의 내용 해시를 남긴다.

용도가 단순하다. 빌드 사이에 이 프레임이 바뀌었는가. 에셋을 다시 생성했는데 결과가 같으면 시트를 다시 만들 필요가 없고, 다르면 어느 프레임이 달라졌는지 알 수 있다.

크기도 해시에 넣는다. 픽셀 수가 같아도 4×2와 2×4는 다른 프레임이다. 픽셀 값만 넣으면 두 프레임이 같은 지문을 갖는다.

예제는 의존성을 안 넣으려고 FNV-1a를 썼다. 실제 파이프라인은 SHA-256을 쓴다. 여기서 쓰는 해시는 “바뀌었나”를 보는 용도지 보안 용도가 아니라는 걸 예제 주석에도 적어 뒀다.

검사가 없으면 언제 드러나나

이 검사들을 안 하면 문제는 세 단계 뒤에 드러난다. 프레임을 시트에 합치고, 앱에 넣고, 실행해서, 그 상태가 재생될 때다.

supplied_09 같은 상태는 특정 조건에서만 나온다. 그때까지 아무도 모른다. 그리고 발견해도 원인이 생성 단계인지 합치는 단계인지 알 수 없다.

프레임 단계에서 걸러내면 어느 프레임이 왜 이상한지가 바로 나온다.

정리

이미지 생성으로 에셋을 만들면 결과가 매번 다르다. 그 차이를 사람이 매번 확인할 수는 없다.

  • 배경색은 캐릭터에 안 나오는 색으로 고른다
  • 관용값은 작으면 배경이 남고 크면 캐릭터가 지워진다. 양쪽을 다 테스트한다
  • 프레임 하나로는 정상 여부를 알 수 없다. 묶어서 중앙값과 비교한다
  • 평균이 아니라 중앙값을 쓴다. 망가진 프레임이 기준을 흔들면 안 된다
  • 셀 여백에 닿는지 재서 시트에서 붙어 보이는 것을 막는다
  • 내용 지문으로 무엇이 바뀌었는지 추적한다

돌아가는 코드는 pet-reference/asset-pipeline에 있다. 정상 프레임과 망가진 프레임을 섞어 넣고 검사를 돌리면, 어느 것이 왜 걸리는지 그대로 나온다.

에 들어 있는 캐릭터가 이 과정을 거친 것이다.

다음 편은 그렇게 만든 앱을 남에게 주는 이야기다. 애플 개발자 계정 없이 배포하면 무슨 일이 생기는지 다룬다.

이 블로그 더 보기

이 블로그는 실제 프로젝트를 AI와 함께 굴리면서 남긴 기록입니다.

  • 새 글은 RSS로 받아볼 수 있습니다.
  • 시리즈 전체는 여기에 정리해 두었습니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다