PanoPostprocessorService::run | end - {"stack":"Error: Blur script exited with code 1:
RCA: PanoPostprocessorService::run | Blur script exited with code 1 (OpenCV CV_IO_MAX_IMAGE_PIXELS)
Overview#
What Happened#
2026-08-01 03:04 KST 에 cupixworks-pano-postprocessor-instance (production, us-west-2) 에서 pano 마스킹 파이프라인이 blur script 종료 코드 1 로 실패했다. 원인은 bundled Python 스크립트 /tmp/lib/face-body-detector/cupix_inference.py 에서 cv2.imread 호출 시 OpenCV 의 pixel 상한 (CV_IO_MAX_IMAGE_PIXELS, 기본 2^30 ≈ 1.073B pixels) 을 초과하는 pano 이미지가 있었기 때문이다. MaskWork::maskPanos 는 배치 스크립트 하나로 전체 pano 를 처리하므로 단 하나의 대용량 이미지가 스크립트를 중단시키고, 상위 PanoPostprocessorService::run 의 catch 블록이 capture 전체를 Error 상태로 전이시킨다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | cv2.error (OpenCV assertion) — Node 측에서는 Error: Blur script exited with code 1 |
| exception.message | OpenCV(4.10.0) /io/opencv/modules/imgcodecs/src/loadsave.cpp:79: error: (-215:Assertion failed) pixels <= CV_IO_MAX_IMAGE_PIXELS in function 'validateInputImageSize' |
| top_frame | /tmp/lib/face-body-detector/cupix_inference.py:333 in detect (img_original = cv2.imread(image_filepath)) |
| runtime | Python 3.11, opencv-python 4.10.0.84 (Dockerfile pin) |
| env | production, us-west-2, tenant cupix |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| cadscans (pano postprocessor) | 2 (동일 capture 내 중복 emit) | 해당 capture 의 pano_postprocessor_state 가 Error 로 전이되어 마스킹/blurriness/resize 단계 전체가 미완료 상태로 남는다 (pano-postprocessor-service.ts:190) |
Timeline#
- 2026-08-01 03:04:50 KST —
PanoPostprocessorService::run | end - {"stack":"Error: Blur script exited with code 1: ...cv2.error ..."}두 번 기록됨 (동일 순간, 두 로그 라인 emit) — cluster first_seen == last_seen - 2026-08-01 03:04:50 KST —
run()catch 블록이updatePanoPostprocessorState(captureId, Error)및updateJobState(Stopped)실행 (pano-postprocessor-service.ts:188-197)
Error Log#
PanoPostprocessorService::run | end - {"stack":"Error: Blur script exited with code 1:
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 333, in detect
img_original = cv2.imread(image_filepath)
^^^^^^^^^^^^^^^^^^^^^^^^^^
cv2.error: OpenCV(4.10.0) /io/opencv/modules/imgcodecs/src/loadsave.cpp:79: error: (-215:Assertion failed) pixels <= CV_IO_MAX_IMAGE_PIXELS in function 'validateInputImageSize'
at ChildProcess.<anonymous> (/tmp/agent/dist/app.cjs:7088:18)
at ChildProcess.emit (node:events:524:28)
at maybeClose (node:internal/child_process:1104:16)
at ChildProcess._handle.onexit (node:internal/child_process:304:5)","message":"Blur script exited with code 1: ..."}
Impact#
- Service:
cupixworks-pano-postprocessor-instance - Team: cadscans
- 발생 횟수: 2 (동일 시각의 duplicate emit — 실제 capture 1건)
- 최초 발생: 2026-08-01 03:04 KST
- 최근 발생: 2026-08-01 03:04 KST
동일 서비스에서 최근 (2026-07-28) resolved incident 2026-07-28-svc-cupixworks-pano-postprocessor-instance--unknown-1 가 존재 (4개 cluster). 상태 보드에는 active incident 없음.
Root Cause Summary#
Bundled face-body-detector Python 스크립트가 cv2.imread 로 pano 원본을 로드할 때, 이미지의 총 픽셀 수가 OpenCV 의 컴파일 시 상수 CV_IO_MAX_IMAGE_PIXELS (기본값 2^30 = 1,073,741,824 pixels) 를 초과했다. OpenCV 는 이 조건에서 assertion 을 던지고 프로세스를 종료(exit 1) 시킨다. Node 측 MaskWork::maskPanos 는 자식 프로세스의 close 이벤트에서 code !== 0 이면 reject(false) 하고 (mask-work.ts:49-56), 상위 PanoPostprocessorService::run 의 mask 단계는 try/catch 로 감싸져 있지 않아 (pano-postprocessor-service.ts:126) 예외가 outer catch (error) 로 전파되며 capture 전체가 Error 상태로 마킹된다. 즉 (1) 입력 pano 하나가 OpenCV pixel 상한을 넘겼고, (2) 이 실패가 pano 단위로 격리되지 않아 capture 전체가 실패한 이중 문제다.
Technical Analysis#
Code Path#
Entry point: applications/agents/packages/cupix-pano-postprocessor/src/pano-postprocessor-service.ts:79 (PanoPostprocessorService.run)
run()이 시작되어 capture 의 pano 목록을 가져오고 mask type 이 존재하면maskWork.maskPanos(downloadDir, maskDir, optionPath)를 호출한다.
if (maskTypeOption) {
this.maskWork.createOptionFile(optionPath, maskTypeOption);
start = Date.now();
await this.maskWork.maskPanos(downloadDir, maskDir!, optionPath);
this.panoPostprocessorManager.logElapsedTime(start, 'blur_panos');
이 await 는 개별 try/catch 로 감싸져 있지 않다 — 아래 changeMask/infer/resize 는 pano 단위 try/catch 로 감싸져 있어 pano 하나가 실패해도 다른 pano 는 계속 처리되지만, maskPanos 는 batch 스크립트라 예외가 outer catch (188) 까지 그대로 propagate 된다.
maskPanos는 bash blur 스크립트를 spawn 하고, non-zero exit 시reject(false)한다.
maskPanos(inputDir: string, outputDir: string, optionPath: string): Promise<boolean> {
return new Promise((resolve, reject) => {
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
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) {
logger.debug('MaskWork::maskPanos | success code:%d', code);
resolve(true);
} else {
logger.error('MaskWork::maskPanos | error code:%d', code);
reject(false);
}
});
});
};
- Blur 스크립트는 bundled Python
cupix_inference.py를 호출하며, 문제 라인은:
img_original = cv2.imread(image_filepath)
OpenCV 는 loadsave.cpp:79 의 validateInputImageSize 에서 pixels <= CV_IO_MAX_IMAGE_PIXELS assertion 을 실행한다. 기본값은 1 << 30 (약 1.07B pixels). 예: 32768×32768 이면 1.07B pixel 이므로 초과, 40000×40000 (1.6B pixels) 은 확실히 실패한다.
- Failure point (전파):
pano-postprocessor-service.ts:188catch 블록. 로그 문자열PanoPostprocessorService::run | end - <stack>이 이 클러스터의 대표 에러다.
} catch (error) {
hasError = true;
await this.panoPostprocessorManager.updatePanoPostprocessorState(captureId!, TESLA.UpdateCaptureRequest.PanoPostprocessorStateEnum.Error);
logger.error('PanoPostprocessorService::run | end - %s', stringifyError(error));
} finally {
if (!hasError) {
await this.panoPostprocessorManager.updatePanoPostprocessorState(captureId!, TESLA.UpdateCaptureRequest.PanoPostprocessorStateEnum.Done);
}
await this.panoPostprocessorManager.updateJobState(jobId!, TESLA.UpdateJobRequest.StateEnum.Stopped);
this.panoPostprocessorManager.removeWorkspaceDir(this.workspaceDir);
logger.info('PanoPostprocessorService::run | end');
}
Docker 이미지의 OpenCV 는 4.10.0.84 로 pinned 되어 있고 CV_IO_MAX_IMAGE_PIXELS 를 우회하는 환경 변수 (OPENCV_IO_MAX_IMAGE_PIXELS) 설정이 없다 — Dockerfile 및 constants.ts, agent 코드베이스 전체에 관련 참조 없음 (grep CV_IO_MAX_IMAGE_PIXELS|OPENCV_IO_MAX → no matches).
###############################################################################
# YOLO & SAM3
###############################################################################
RUN pip install --no-cache-dir \
numpy==2.1.1 \
opencv-python==4.10.0.84 \
opencv-python-headless==4.10.0.84 \
...
기대 동작: 대용량 pano 하나가 있어도 해당 pano 만 skip/error 로 처리되고 나머지 pano 및 capture 파이프라인은 완료되어야 한다.
실제 동작: 배치 스크립트가 중단되어 mask 단계 전체가 실패하고, capture 가 통째로 Error 상태가 된다.
Log Evidence#
Datadog query (cluster URL 그대로):
service:cupixworks-pano-postprocessor-instance status:error @environment:production "PanoPostprocessorService::run"
Time window: 2026-07-31T17:04:00Z ~ 2026-07-31T19:05:00Z
핵심 error log (2건, 동일 순간 emit — 상단 Error Log 섹션 참조).
주변 컨텍스트 로그 (동일 서비스, service:cupixworks-pano-postprocessor-instance @environment:production 로 검색):
- 인접 시간대 (같은 서비스의 다른 run 들) 는 정상 처리되어
PanoPostprocessor Metric - dd_metric:pano.postprocess.summary&dd_total:300&dd_completed:300&dd_errored:0처럼 completed=total 로 종료. - Mask 스크립트의 다른 로그들 (같은 인프라, warn 성격):
MaskWork::maskPanos | script /tmp/lib/face-body-detector/cupix_inference.py:266: RuntimeWarning: overflow encountered in exp
sigmoid_mask = 1.0 / (1.0 + np.exp(-logits_resized))
MaskWork::maskPanos | script /tmp/lib/face-body-detector/sam3/model/encoder.py:358: UserWarning: Specified kernel cache directory could not be created! ...
이들은 오늘 문제와 무관한 별개 warning (stderr → error 레벨로 승격) 이며, cluster 의 fingerprint (62dc74b90cde9fac79fa5d8553c39790) 와 다르다. cluster 자체는 Assertion failed) pixels <= CV_IO_MAX_IMAGE_PIXELS 로 국한된다.
Related recent svc:* incident (from status-board):
2026-07-28-svc-cupixworks-pano-postprocessor-instance--unknown-1 (resolved 2026-07-28T16:40:26Z)
cluster_ids: be9c3f17, 4dfce574, a5ec76bc, ee047ed2 — 같은 서비스의 별개 근본원인 여부는 이 RCA 범위 밖.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | 입력 pano 하나가 OpenCV CV_IO_MAX_IMAGE_PIXELS (2^30) 를 초과해 assertion 발생, blur 스크립트 exit 1, 상위에서 capture 전체 Error 로 전파 |
stack trace 명시적: pixels <= CV_IO_MAX_IMAGE_PIXELS in function 'validateInputImageSize' at cv2.imread. mask-work.ts:53-55 가 non-zero exit → reject. pano-postprocessor-service.ts:126 의 await maskPanos 는 pano 단위 격리 없음 |
— | Confirmed |
| H2 | Corrupt/malformed 이미지로 인해 cv2.imread 가 실패 (일반 IO 에러) |
cv2.imread 는 실패 시 None 반환이 일반적 |
assertion 메시지가 명확히 pixel count 조건. loadsave.cpp:79 는 validateInputImageSize — IO 파싱이 아니라 사이즈 검증 단계 |
Rejected |
| H3 | Face-body-detector 버전 업그레이드 회귀 | face-body-detector.json 은 production stable: 1.2.3 |
최근 버전 변경 로그 없음. 다른 capture 는 정상 처리 (total:300 completed:300 errored:0 metric 확인) — 동일 이미지가 아니면 재현 안됨 |
Rejected |
| H4 | GPU/CUDA OOM 또는 kernel cache 실패 | encoder.py 의 Specified kernel cache directory could not be created warning 존재 |
이 warning 은 stderr 로 나오지만 스크립트를 종료시키지 않음. Cluster 의 실제 종료 원인은 명시적 assertion trace |
Rejected |
| H5 | MaskWork::maskPanos 배치 실패가 capture 전체를 Error 로 만드는 격리 부족 (2차 원인) |
pano-postprocessor-service.ts:123-128 은 changeMask/infer/resize 와 달리 개별 try/catch 로 감싸져 있지 않음. Batch bash script 특성상 첫 실패 이미지에서 전체 중단 |
— | Confirmed (contributory) |
Fix Recommendation#
즉시 조치 (Critical)#
- 환경변수로 OpenCV pixel 상한 상향:
packages/cupix-pano-postprocessor/Dockerfile또는run.sh/ bundled blurrun.sh에서OPENCV_IO_MAX_IMAGE_PIXELS를 명시적으로 지정. OpenCV 는 이 env var 로 컴파일 시 상수를 런타임 오버라이드할 수 있다. Pano 는 equirectangular 로 흔히 16384×8192 (134M) ~ 32768×16384 (537M) 이지만, 초고해상도 capture 는 2^30 을 넘길 수 있으므로 예:2^33 = 8,589,934,592정도로 여유롭게 설정. 주의: 값이 커질수록 malformed/adversarial 이미지에 대한 메모리 안전 마진이 줄어드는 tradeoff — QA 팀과 상한 값 협의 필요. - face-body-detector 팀에 사전 사이즈 체크 도입 요청:
cupix_inference.py:333이전에PIL.Image.open또는imghdr로 dimension 을 확인해 상한 초과 시 skip 하고 non-fatal 리턴 (예: exit code 0 이되 결과 파일에 skip marker). 이 저장소 (face-body-detector) 는 별도 repo 이므로 자동 code-fix 스코프에서 제외 — cadscans 팀과 조율 필요.
단기 개선 (1주 이내)#
MaskWork::maskPanos실패 격리:pano-postprocessor-service.ts:123-128를try/catch로 감싸고 mask 단계 실패 시 mask 를 스킵하되 infer/resize 는 계속 진행하도록 변경. 또는 batch script 대신 pano 단위 spawn 으로 리팩터링해 실패 이미지만 error 처리. 후자는 blur 스크립트 인터페이스 변경이 필요하므로face-body-detector팀과 조율 필요.- 재현 데이터 확보: 이번 실패 capture 의 pano 이미지 dimension 을 조사해 실제 어떤 크기가 문제인지 확인 (S3 원본 metadata 조회). Cluster 에는 capture_id/pano_id 가 로그로 남지 않아 자동 추적이 어려움 —
logger.error문에captureId/panoId를 반드시 포함하도록 관측성 개선.
장기 개선 (재발 방지)#
- 업로드 단계 pre-validation: capture 업로드 파이프라인에서 pano dimension 을 검증하고 상한 초과 이미지는 사전에 리사이즈 또는 거부. Postprocessor 에서 사후 실패보다 사용자에게 즉시 피드백이 낫다.
- 관측성:
PanoPostprocessorService::run로그에captureId, 실패panoId, 이미지 dimension 을 구조화 필드로 포함. 현재 대표 에러 로그에는 어떤 이미지/capture 인지 확인할 방법이 없다.
Monitoring#
- Alert: pano-postprocessor 에서 OpenCV pixel 상한 위반 발생 시 즉시 알림.
- Trend: capture 실패율 및 실패 원인 분류 dashboard.
Datadog queries (writing-datadog-monitoring-queries 규칙 준수 — timeseries 위젯 호환 형태):
service:cupixworks-pano-postprocessor-instance status:error @environment:production "CV_IO_MAX_IMAGE_PIXELS"
service:cupixworks-pano-postprocessor-instance status:error @environment:production "Blur script exited"
service:cupixworks-pano-postprocessor-instance status:error @environment:production "PanoPostprocessorService::run | end"
Risk Assessment#
- Risk level: medium — 개별 capture 처리 실패로 유저가 postprocess 완료를 못 받음. 데이터 손실은 없고 원본은 S3 에 존재. 하지만 실패 격리가 미흡해 blast radius 가 pano 1개 → capture 1개 전체로 확대되는 구조적 문제 포함.
- 예상 복잡도: standard — env var 추가는 trivial 이나, 근본적 격리 개선 및 face-body-detector 팀과의 조율이 필요해 표준 복잡도로 분류.