- 문제 상황 – 안드로이드 오버레이가 검게 캡쳐될 때 해결 방법

ShortCha의 광고 영상을 d.screenshot으로 캡처할 때, 왼쪽과 같이 화면 전체가 검은색으로 캡처되는 경우가 있습니다. 그 이유는 일반적으로 window에 secure flag가 있거나, 오버레이 화면에서 발생했습니다. 이렇게 검은색으로 캡처되면, YOLO에서 “X” 좌표를 찾지 못해 광고 SKIP이 Fail 하는 경우가 발생했습니다. 특히 최근에 더 자주 발생하고 있습니다. 사실 안드로이드를 커스텀으로 빌드해서 사용하기 때문에, 프레임워크에서 해결할수도 있었지만, 다른 환경에서도 쉽게 적용 할수 있는 방법을 찾기로 했습니다.
2. 해결 방법 – 우회 방법
스크린샷을 사용하는 것은 불가능하지만, 스크린 레코딩은 가능했습니다. Shortcha에서 테스트 할 때도 가능했습니다.
아래와 같이 구현했습니다.
a. 짧은 화면 녹화 -> 녹화된 영상에서 1frame 추출 -> 기존 screenshot 처럼 return
3. 결과 – 문제 해결
- 영상의 품질은 조금 저하되지만, YOLO에서 인식에는 전혀 문제가 없이 버튼들이 인식됩니다.
- 아래와 같이 육안으로 보면 둘 사이에 차이가 크지 않습니다.

d.screenshot

video frame capture
동영상 캡처 구현 – 다음은 Python 코드입니다. safe_screenshot은 기존의 스크린샷 코드이고, capture_android_frame()은 동영상 녹화 후 1프레임을 추출하는 함수입니다. (Linux 기준)
def safe_screenshot(d):
"""uiautomator2 → adb fallback screenshot"""
try:
img = d.screenshot(format="pillow")
if not img.getbbox():
raise ValueError("Black screen detected.")
return img
except Exception as e:
print(f"⚠️ fallback to adb screencap: {e}")
img = capture_android_frame()
if img is not None:
return img
raise ValueError("Failed to capture screenshot.")
Code
import subprocess
import numpy as np
import cv2
import time
import signal
import sys
import threading
import os
from PIL import Image
def capture_android_frame(
output_jpg="frame.jpg",
tmp_video="/tmp/frame.mp4",
width=1080,
height=1920,
fps=30,
duration=2.0,
record_format="mp4",
cleanup=True,
):
# Remove existing temporary file
if os.path.exists(tmp_video):
os.remove(tmp_video)
print(f"[info] Starting scrcpy capture ({record_format})...")
scrcpy = subprocess.Popen(
[
"scrcpy",
"--no-display",
"--record", tmp_video,
"--record-format", record_format,
"-m", str(height),
"--max-fps", str(fps),
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
bufsize=10**8,
text=True,
)
# Thread to print scrcpy logs in real time
def read_logs(pipe):
for line in iter(pipe.readline, ''):
sys.stdout.write(f"[scrcpy] {line}")
pipe.close()
log_thread = threading.Thread(target=read_logs, args=(scrcpy.stderr,))
log_thread.daemon = True
log_thread.start()
# Wait for scrcpy to start and capture a few frames
time.sleep(duration)
# Gracefully stop scrcpy recording
scrcpy.send_signal(signal.SIGINT)
log_thread.join(timeout=1.0)
scrcpy.wait()
# Verify that the video was created
if not os.path.exists(tmp_video) or os.path.getsize(tmp_video) < 5000:
raise RuntimeError("⚠️ Recording failed — no video created")
print(f"✅ Recorded temporary stream: {tmp_video} ({os.path.getsize(tmp_video)/1024:.1f} KB)")
# Extract one frame using ffmpeg
subprocess.run(
[
"ffmpeg",
"-y",
"-loglevel", "error",
"-i", tmp_video,
"-frames:v", "1",
output_jpg,
],
check=True,
)
# Load the frame using OpenCV
frame = cv2.imread(output_jpg)
if frame is None:
raise RuntimeError("⚠️ Failed to decode frame")
# Convert to Pillow Image before returning
img = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
print(f"✅ Saved frame: {output_jpg} ({frame.shape[1]}x{frame.shape[0]})")
# Optionally delete the temporary video
if cleanup:
os.remove(tmp_video)
return img
if __name__ == "__main__":
img = capture_android_frame(
output_jpg="frame.jpg",
tmp_video="/tmp/frame.mp4",
duration=2.5,
record_format="mp4",
)
print(f"[done] Captured image size: {img.size}") # (width, height)