fix: integrate decoded scene animation verification
This commit is contained in:
@@ -37,6 +37,8 @@ def main():
|
||||
frame_start=scene.frame_start, frame_end=scene.frame_end,
|
||||
logical_to_blender="logical_frame + 1", external_links=False)
|
||||
if job["operation"] == "render":
|
||||
# frame_set과 렌더의 depsgraph가 같은 Scene을 평가하도록 활성 컨텍스트를 맞춘다.
|
||||
bpy.context.window.scene = scene
|
||||
profile = job["profile"]
|
||||
scene.render.engine = profile["engine"]
|
||||
scene.render.resolution_x = profile["width"]
|
||||
|
||||
+49
-6
@@ -3,23 +3,54 @@ import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
from PIL import Image
|
||||
|
||||
import run
|
||||
from test_runner import fixture, production_fixture, endpoint_fixture, ROOT
|
||||
from test_runner import write
|
||||
|
||||
|
||||
def pixels(path):
|
||||
with Image.open(path) as image:
|
||||
return image.convert("RGB").tobytes()
|
||||
|
||||
|
||||
def check_motion(evidence, ffmpeg):
|
||||
frames = evidence["logical_frames"]
|
||||
checked = [frames[0], frames[len(frames) // 2], frames[-1]]
|
||||
media = evidence["media"]
|
||||
attempt = (ROOT / media["path"]).parent
|
||||
rendered_samples = [pixels(attempt / ("frame-" + str(f).zfill(8) + ".png")) for f in checked]
|
||||
# H.264 프레임마다 달라질 수 있는 양자화 흔적을 원본 3D 움직임으로 오인하지 않는다.
|
||||
run.require(len(set(rendered_samples)) == 3, "디코드 PNG RGB 시작/중간/끝에서 3D 움직임이 없습니다: " + evidence["scene_name"])
|
||||
decoded = subprocess.run([ffmpeg, "-v", "error", "-xerror", "-i", str(ROOT / media["path"]),
|
||||
"-map", "0:v:0", "-pix_fmt", "rgb24", "-c:v", "rawvideo", "-f", "image2pipe", "-"], capture_output=True)
|
||||
run.require(decoded.returncode == 0, "MP4 RGB 디코드가 실패했습니다: " + decoded.stderr.decode("utf-8", errors="replace")[-1000:])
|
||||
size = media["width"] * media["height"] * 3
|
||||
run.require(len(decoded.stdout) == size * len(frames), "실제 RGB 디코드 프레임 수가 다릅니다")
|
||||
samples = [decoded.stdout[index * size:(index + 1) * size] for index in (0, len(frames) // 2, len(frames) - 1)]
|
||||
run.require(len(set(samples)) == 3, "디코드 MP4 RGB 시작/중간/끝에서 3D 움직임이 없습니다: " + evidence["scene_name"])
|
||||
return checked
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="설치 정보가 아닌 실제 렌더·디코드 검사")
|
||||
parser.add_argument("--blender", required=True)
|
||||
parser.add_argument("--ffmpeg", required=True)
|
||||
parser.add_argument("--ffprobe", required=True)
|
||||
parser.add_argument("--engine", choices=("CYCLES", "BLENDER_EEVEE"), default="CYCLES")
|
||||
parser.add_argument("--animation-only", action="store_true", help="50fps·5프레임으로 비활성 Scene의 실제 RGB 움직임만 검사")
|
||||
args = parser.parse_args()
|
||||
folder, project, artifacts, job = fixture()
|
||||
if args.engine == "BLENDER_EEVEE":
|
||||
artifacts["08-scenes"]["payload"]["preview"]["render_profile"].update(engine="BLENDER_EEVEE", device="GPU")
|
||||
write(ROOT / job["artifact"], artifacts["08-scenes"])
|
||||
if args.animation_only:
|
||||
artifacts["08-scenes"]["payload"]["preview"]["render_profile"]["fps"] = 50
|
||||
job["end"] = 5
|
||||
write(ROOT / job["artifact"], artifacts["08-scenes"])
|
||||
source = ROOT / job["source"]
|
||||
# fixture의 검사 전 바이트는 생성 원본이 아니다. 실패 흔적은 별도 파일로 보존한다.
|
||||
source.rename(source.with_suffix(".uninspected"))
|
||||
@@ -27,14 +58,26 @@ def main():
|
||||
Path(__file__).with_name("smoke_fixture.py"), "--", source])
|
||||
video = run.render(ROOT, job, args.blender, args.ffmpeg, args.ffprobe)
|
||||
require = run.require
|
||||
require(video["media"]["duration_ms"] == 1000, "검사 영상 길이 불일치")
|
||||
attempt = (ROOT / video["media"]["path"]).parent
|
||||
require(run.digest(attempt / "frame-00000000.png") != run.digest(attempt / "frame-00000009.png"), "3D 움직임이 없습니다")
|
||||
still_job = dict(job, frame=10, start=None, end=None)
|
||||
require(video["media"]["duration_ms"] == (100 if args.animation_only else 1000), "검사 영상 길이 불일치")
|
||||
second_video = run.render(ROOT, dict(job, scene="SceneB", require_object=["BallB"]), args.blender, args.ffmpeg, args.ffprobe)
|
||||
motion_checks, errors = [], []
|
||||
for rendered in (video, second_video):
|
||||
try:
|
||||
motion_checks.append(check_motion(rendered, args.ffmpeg))
|
||||
except ValueError as error:
|
||||
errors.append(str(error))
|
||||
require(not errors, "\n".join(errors))
|
||||
first_motion, second_motion = motion_checks
|
||||
still_job = dict(job, frame=4 if args.animation_only else 10, start=None, end=None)
|
||||
first = run.render(ROOT, still_job, args.blender, args.ffmpeg, args.ffprobe)
|
||||
second = run.render(ROOT, dict(still_job, scene="SceneB"), args.blender, args.ffmpeg, args.ffprobe)
|
||||
require(first["identity"] != second["identity"] and run.digest(ROOT / first["media"]["path"]) != run.digest(ROOT / second["media"]["path"]),
|
||||
second = run.render(ROOT, dict(still_job, scene="SceneB", require_object=["BallB"]), args.blender, args.ffmpeg, args.ffprobe)
|
||||
require(first["identity"] != second["identity"] and pixels(ROOT / first["media"]["path"]) != pixels(ROOT / second["media"]["path"]),
|
||||
"같은 논리 프레임의 다른 Scene 카메라가 혼입되었습니다")
|
||||
if args.animation_only:
|
||||
print(json.dumps(dict(first_video=video["media"], second_video=second_video["media"],
|
||||
first_rgb_frames=first_motion, second_rgb_frames=second_motion, first_scene=first["media"], second_scene=second["media"],
|
||||
renderer=video["render_profile"], source=video["scene_file"], result="초기 Idle Scene과 무관한 두 Scene의 디코드 RGB 움직임·5프레임·100ms·같은 시각의 Scene 구별 검사 통과"), ensure_ascii=False, indent=2))
|
||||
return
|
||||
for invalid in (dict(still_job, scene="Missing"), dict(still_job, require_object=["Missing"])):
|
||||
try:
|
||||
run.render(ROOT, invalid, args.blender, args.ffmpeg, args.ffprobe)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""실행 검사 전용 3D 구와 두 카메라를 만든다. 제작 내용과 무관하다."""
|
||||
"""실행 검사 전용 두 애니메이션 Scene과 초기 Idle Scene을 만든다."""
|
||||
import sys
|
||||
|
||||
import bpy
|
||||
@@ -34,7 +34,10 @@ scene.camera = camera
|
||||
second = bpy.data.scenes.new("SceneB")
|
||||
second.frame_start, second.frame_end = 1, 21
|
||||
second.world = scene.world
|
||||
second.collection.objects.link(ball)
|
||||
second_ball = ball.copy()
|
||||
second_ball.data = ball.data.copy()
|
||||
second_ball.name = "BallB"
|
||||
second.collection.objects.link(second_ball)
|
||||
second.collection.objects.link(light)
|
||||
other = camera.copy()
|
||||
other.data = camera.data.copy()
|
||||
@@ -42,6 +45,13 @@ other.location = (5, -7, 1)
|
||||
other.rotation_euler = (-other.location).to_track_quat("-Z", "Y").to_euler()
|
||||
second.collection.objects.link(other)
|
||||
second.camera = other
|
||||
idle = bpy.data.scenes.new("Idle")
|
||||
idle.world = scene.world
|
||||
idle_camera = camera.copy()
|
||||
idle_camera.data = camera.data.copy()
|
||||
idle.collection.objects.link(idle_camera)
|
||||
idle.camera = idle_camera
|
||||
bpy.context.window.scene = idle
|
||||
if "--external-image" in sys.argv:
|
||||
bpy.data.images.load(sys.argv[sys.argv.index("--external-image") + 1]).use_fake_user = True
|
||||
if "--short-scene" in sys.argv:
|
||||
|
||||
Reference in New Issue
Block a user