MaskWork::maskPanos | script img_original = cv2.cvtColor(img_original, cv2.COLOR_BGR2RGB)
RCA: MaskWork::maskPanos cv2.cvtColor assertion failure
Overview#
What Happened#
2026-06-26 16:36 KST에 cupixworks-pano-postprocessor-instance (capture_722430 job, us-west-2)에서 MaskWork::maskPanos가 호출한 Python blur 스크립트(/tmp/lib/face-body-detector/cupix_inference.py:334)가 OpenCV cvtColor assertion (!_src.empty())으로 실패했고, 자식 프로세스가 exit code 1을 반환하여 전체 pano-postprocessor run이 end - false로 종료되었다. 직전 download_panos 단계에서 동일 capture에 속한 다수의 pano 이미지가 S3에서 The specified key does not exist로 다운로드되지 않았다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | cv2.error |
| exception.message | OpenCV(4.10.0) /io/opencv/modules/imgproc/src/color.cpp:196: error: (-215:Assertion failed) !_src.empty() in function 'cvtColor' |
| top_frame | /tmp/lib/face-body-detector/cupix_inference.py:334 (in detect) |
| failing call | img_original = cv2.cvtColor(img_original, cv2.COLOR_BGR2RGB) |
| capture_id | 722430 |
| env | production, us-west-2 |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| gad / pano-postprocessor | 1 cluster (capture_722430 run) | capture의 pano-postprocessor 파이프라인 전체가 실패 처리되어 PanoPostprocessorStateEnum.Error 로 마킹됨 |
Timeline#
- 2026-06-26 16:26:51 KST —
AwsS3Manager::downloadParallel가 capture_722430의 pano 이미지 다수를The specified key does not exist에러로 다운로드 실패 (예:pano_90227502.jpg, ...). - 2026-06-26 16:35:52 KST —
download_panosstep elapsed_time 541833ms 기록 후 종료. 일부 pano 파일은 로컬 디스크에 존재하지 않음. - 2026-06-26 16:34:28 KST —
change_masksstep 완료 (23125ms) — 이전 cycle. - 2026-06-26 16:36:04 KST —
MaskWork::maskPanos가 blur 스크립트 실행 중 누락된 이미지에 도달하여cv2.cvtColor에서 assertion failure(빈 Mat) 발생. - 2026-06-26 16:36:05 KST — 자식 프로세스
error code:1→PanoPostprocessorService::run | end - false, capture 상태가Error로 갱신. - 2026-06-26 16:36:07 KST —
terminateService | force shutdown after 10 seconds로 컨테이너 종료.
Error Log#
MaskWork::maskPanos | script img_original = cv2.cvtColor(img_original, cv2.COLOR_BGR2RGB)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
cv2.error: OpenCV(4.10.0) /io/opencv/modules/imgproc/src/color.cpp:196: error: (-215:Assertion failed) !_src.empty() in function 'cvtColor'
Impact#
- Service:
cupixworks-pano-postprocessor-instance - Team: gad
- 발생 횟수: 1 (이 클러스터)
- 최초 발생: 2026-06-26 16:36 KST
- 최근 발생: 2026-06-26 16:36 KST
- 영향 범위: capture_722430 한 건의 pano-postprocessor run이 실패. 같은 capture의 다수 pano가 S3에서 누락되어 있으므로 retry 만으로는 복구되지 않을 가능성이 높음.
Root Cause Summary#
PanoPostprocessorService::run은 awsS3Manager.downloadParallel(cpPanos)로 capture의 pano 이미지를 전부 로컬 디스크에 다운로드한 뒤 maskWork.maskPanos(downloadDir, ...)를 호출한다. downloadParallel 내부 task는 개별 다운로드 실패를 try/catch로 삼키고(error 로그만 남기고 계속 진행하여) 전체 Promise.all은 정상 resolve된다. 결과적으로 다운로드 디렉터리에는 누락된 pano가 존재하지만, 호출자는 이를 알 수 없고 곧바로 blur 스크립트(/tmp/lib/face-body-detector/run.sh → cupix_inference.py)를 실행한다. Python 측에서는 cv2.imread(<missing_file>)이 None(빈 Mat)을 반환하고, 다음 줄 cv2.cvtColor(img_original, cv2.COLOR_BGR2RGB)가 !_src.empty() assertion으로 죽는다. 자식 프로세스가 비정상 종료(exit 1)되면 maskPanos는 reject(false)가 되고 run()의 catch 블록에서 capture 전체가 Error 상태로 처리된다. 즉, upstream의 S3 key missing을 downstream(maskPanos Python 스크립트)이 fatal assertion으로 폭발시키는 입력 검증 누락이 근본 원인이다.
Technical Analysis#
Code Path#
- Entry point:
applications/agents/packages/cupix-pano-postprocessor/src/pano-postprocessor-service.ts:79(run) - Download:
pano-postprocessor-service.ts:117(awsS3Manager.downloadParallel(cpPanos)) — 실패한 다운로드를 무시하고 resolve. - Mask invoke:
pano-postprocessor-service.ts:126(maskWork.maskPanos(downloadDir, maskDir!, optionPath)) - Spawn:
applications/agents/packages/cupix-pano-postprocessor/src/work/mask-work.ts:29—bash run.sh를 통해cupix_inference.py실행.INPUT_DIRPATH=downloadDir환경변수로 디렉터리만 전달. - Failure point (외부 Python 스크립트, 컨테이너 mount):
/tmp/lib/face-body-detector/cupix_inference.py:334—cv2.cvtColor(img_original, cv2.COLOR_BGR2RGB).cv2.imread가 빈 Mat을 돌려준 상태에서 호출됨.
const tasks = cpPano.map(cpPano => {
return limit(async () => {
try {
// ...
await this.download(/* ... */ cpPano.downloadImagePath!);
} catch (err) {
logger.error('AwsS3Manager::downloadParallel | download failed - pano: %s, error: %s',
cpPano.downloadImagePath, (err as Error).message);
}
});
});
await Promise.all(tasks);
기대 동작: 모든 pano가 정상 다운로드되어 디스크에 존재하거나, 그렇지 못한 경우 누락된 pano를 호출자가 인지하여 blur 스크립트에는 존재 파일만 전달.
실제 동작: 개별 실패가 로그 1줄로 흡수되고 downloadParallel은 항상 정상 resolve된다. run()은 누락된 파일에 대한 정보 없이 maskPanos로 진입한다.
// download pano
let start = Date.now();
await this.awsS3Manager.downloadParallel(cpPanos);
this.panoPostprocessorManager.logElapsedTime(start, 'download_panos');
const PARALLEL_TASK_LIMIT = pLimit(10);
// mask
if (maskTypeOption) {
this.maskWork.createOptionFile(optionPath, maskTypeOption);
start = Date.now();
await this.maskWork.maskPanos(downloadDir, maskDir!, optionPath);
maskPanos(inputDir: string, outputDir: string, optionPath: string): Promise<boolean> {
return new Promise((resolve, reject) => {
// ...
const process: ChildProcessWithoutNullStreams = spawn('stdbuf', ['-oL', 'bash', BlurScriptPath], {
cwd: path.dirname(BlurScriptPath),
stdio: 'pipe',
env: { INPUT_DIRPATH: inputDir, OUTPUT_DIRPATH: outputDir, OPTION_FILE_PATH: optionPath }
});
// ...
process.on('close', (code) => {
if (code === 0) {
resolve(true);
} else {
logger.error('MaskWork::maskPanos | error code:%d', code);
reject(false);
}
});
});
}
maskPanos는 디렉터리 단위로 동작하고 파일 단위 입력 검증을 수행하지 않는다. Python 측에서 한 파일이라도 fatal하게 죽으면 process exit code 1로 전체가 실패한다.
Log Evidence#
사용한 Datadog 쿼리:
service:cupixworks-pano-postprocessor-instance status:error @environment:production "MaskWork::maskPanos"
service:cupixworks-pano-postprocessor-instance "capture_722430"
핵심 로그 발췌 (시간 KST):
2026-06-26 16:26:51 AwsS3Manager::downloadParallel | download failed - pano: /tmp/workspace/capture_722430/download/pano_90227502.jpg, error: The specified key does not exist.
2026-06-26 16:26:51 AwsS3Manager::downloadParallel | download failed - pano: /tmp/workspace/capture_722430/download/pano_90227495.jpg, error: The specified key does not exist.
... (수십 건 유사 메시지) ...
2026-06-26 16:34:14 AwsS3Manager::downloadParallel | download failed - pano: /tmp/workspace/capture_722430/download/pano_90225743.jpg, error: The specified key does not exist.
2026-06-26 16:35:52 AwsS3Manager::downloadParallel | download failed - pano: /tmp/workspace/capture_722430/download/pano_90225643.jpg, error: The specified key does not exist.
2026-06-26 16:35:52 PanoPostprocessorService Elapsed Time - dd_step:download_panos&dd_elapsed_time:541833ms
2026-06-26 16:36:04 MaskWork::maskPanos | script Traceback (most recent call last):
File "/tmp/lib/face-body-detector/cupix_inference.py", line 453, in <module>
detect(args)
File "/tmp/lib/face-body-detector/cupix_inference.py", line 334, in detect
2026-06-26 16:36:04 MaskWork::maskPanos | script img_original = cv2.cvtColor(img_original, cv2.COLOR_BGR2RGB)
cv2.error: OpenCV(4.10.0) ... (-215:Assertion failed) !_src.empty() in function 'cvtColor'
2026-06-26 16:36:05 MaskWork::maskPanos | error code:1
2026-06-26 16:36:05 PanoPostprocessorService::run | end - false
2026-06-26 16:36:07 PanoPostprocessorService::terminateService | force shutdown after 10 seconds
download_panos step이 정상 종료 처럼 elapsed_time 로그를 남긴 직후 maskPanos 가 즉시 OpenCV assertion으로 죽었다는 점이, "downloader가 실패를 호출자에게 전파하지 않음 → 빈 디렉터리/누락 파일이 그대로 Python 입력으로 흘러감" 시나리오와 정확히 일치한다.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | S3에서 pano 객체가 존재하지 않아 다운로드가 누락된 채 blur 스크립트에 빈 디렉터리/파일이 전달되어 cv2.imread가 None을 돌려주고 cvtColor에서 assertion 실패 |
capture_722430 에 대해 download failed - ... The specified key does not exist 가 16:26:51~16:35:52 KST 사이 다수 발생. 직후 16:36:04 KST 에 cvtColor assertion. aws-s3.manager.ts:75-77이 실패를 swallow함 |
— | Confirmed |
| H2 | OpenCV/PyTorch 라이브러리 버전 incompatibility (예: cv2 4.10.0 자체 버그) | 동일 메시지 stack frame이 OpenCV의 정상적인 assert | 같은 컨테이너가 16:38 KST에 다른 capture를 정상 처리하기 시작 (PanoPostprocessorService::init 이후 추가 에러 없음). 코드 자체는 다른 입력에서는 동작 |
Rejected |
| H3 | 손상된(0-byte/유효하지 않은 JPEG) 파일이 S3에서 다운로드됨 | cv2.imread는 빈 Mat을 동일하게 반환 가능 |
로그에 The specified key does not exist 가 명확히 존재하므로 파일 자체가 다운로드되지 않은 케이스. 손상 파일 가설은 불필요 |
Rejected (uncertain — 일부 누락된 파일 외에 손상 파일도 섞였을 가능성은 검증되지 않았음) |
| H4 | 외부 의존성 outage (S3 us-west-2) | 동시간대 다른 capture의 정상 처리 로그 (16:38 KST 신규 init/run 성공) | dep:* status-board에 active 인시던트 없음 (bun cli/incident-board.ts for-cluster ... 결과 active: null). 같은 시간대 다른 객체는 정상 다운로드되었으므로 S3 region 자체 outage 아님 |
Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
applications/agents/packages/cupix-pano-postprocessor/src/manager/aws-s3.manager.ts:60-82downloadParallel가 실패한 pano 목록을 호출자에게 반환하도록 시그니처 변경 (예:Promise<{ succeeded: CPPano[]; failed: Array<{ pano: CPPano; reason: string }>}>). 또는 최소한 실패 개수를 카운트.applications/agents/packages/cupix-pano-postprocessor/src/pano-postprocessor-service.ts:117-127run()에서 다운로드 실패 pano를erroredPanoIds에 추가하고, 후속 mask/infer/resize 루프에서 해당 pano를 skip 하도록 변경. 동시에 해당 pano를UpdatePanoRequest.StateEnum.Error로 업데이트하여 user-facing 상태에 반영.- 모든 pano가 실패한 경우(=blur 입력 디렉터리가 비어있음)에는
maskPanos호출을 skip 하여 fatal 종료를 피한다.
근거: 현재 구조는 다운로드 실패를 logger.error만 남기고 무시하며, 단 한 장의 누락만으로도 OpenCV process가 exit 1 → capture 전체 Error 상태가 된다. 호출자로의 실패 전파만 추가해도 같은 capture의 정상 pano들이 부분 성공으로 처리 가능해진다.
단기 개선 (1주 이내)#
- Python
cupix_inference.py측에 입력 파일별cv2.imreadNone 체크 + 해당 pano만 skip 로직 추가 (스크립트는 별도 repo/이미지에 있는 것으로 보이며 현재 이 repo 트리에는 없음 —repo-mapping추가 조사 필요). 디렉터리 일괄 처리 모델에서 file-level resilience 를 갖춰야 한 장의 누락이 batch 전체를 죽이지 않는다. - pano S3 key 누락의 root cause(왜
panoBucketKey가 존재하지 않는 객체를 가리켰는지) 조사 — capture_722430 의 업로드 파이프라인 로그 /Capture#panoBucketKey생성 로직 검토. tesla 측app/models/concerns/pano_postprocessor.rb와 capture invoker 흐름 확인 권장.
장기 개선 (재발 방지)#
- pano-postprocessor 시작 시
cpPanos목록과 S3 객체 존재 여부를 사전 검증(HEAD 요청)하고, 누락된 pano 가 일정 비율을 넘으면 capture를 fast-fail 처리 + 별도 알림. - 외부 Python 스크립트는
stderr만 의존하는 통신 모델 대신, 처리 결과(성공/실패 pano 목록)를 구조화된 JSON 파일로 출력하도록 contract를 정의해 Node 측이 부분 실패를 인지할 수 있도록 한다.
Monitoring#
- Datadog metric/log 모니터: 같은 capture 내 다운로드 실패가 임계치 이상일 때 알림.
service:cupixworks-pano-postprocessor-instance "AwsS3Manager::downloadParallel | download failed"
- MaskWork fatal failure 추적:
service:cupixworks-pano-postprocessor-instance "MaskWork::maskPanos | error code:1"
- End-to-end 실패율:
PanoPostprocessorService::run | end - falsecount를 dashboard timeseries widget으로 추적.
service:cupixworks-pano-postprocessor-instance "PanoPostprocessorService::run | end - false"
Risk Assessment#
- Risk level: medium — 단일 capture 한정 실패였으나 동일 패턴(미존재 S3 key + 한 장 누락 → 전체 capture 실패)이 다른 capture 에서도 재발 가능하다. status board의 recent incident(
2026-06-24-svc-cupixworks-pano-postprocessor-instance--unknown-1) 역시 같은 서비스 degradation으로, 회귀 가능성을 시사. - 예상 복잡도: standard — Node 측 downloader 시그니처 변경 + 호출자에서 skip 처리, Python 측 input validation 추가. 외부 의존성 없음.