비활성 Blender Scene의 애니메이션 평가와 RGB 움직임 검사 수정
This commit is contained in:
@@ -37,6 +37,8 @@ def main():
|
|||||||
frame_start=scene.frame_start, frame_end=scene.frame_end,
|
frame_start=scene.frame_start, frame_end=scene.frame_end,
|
||||||
logical_to_blender="logical_frame + 1", external_links=False)
|
logical_to_blender="logical_frame + 1", external_links=False)
|
||||||
if job["operation"] == "render":
|
if job["operation"] == "render":
|
||||||
|
# frame_set과 렌더의 depsgraph가 같은 Scene을 평가하도록 활성 컨텍스트를 맞춘다.
|
||||||
|
bpy.context.window.scene = scene
|
||||||
profile = job["profile"]
|
profile = job["profile"]
|
||||||
scene.render.engine = profile["engine"]
|
scene.render.engine = profile["engine"]
|
||||||
scene.render.resolution_x = profile["width"]
|
scene.render.resolution_x = profile["width"]
|
||||||
|
|||||||
+49
-6
@@ -3,23 +3,54 @@ import argparse
|
|||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import shutil
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
import run
|
import run
|
||||||
from test_runner import fixture, production_fixture, endpoint_fixture, ROOT
|
from test_runner import fixture, production_fixture, endpoint_fixture, ROOT
|
||||||
from test_runner import write
|
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():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description="설치 정보가 아닌 실제 렌더·디코드 검사")
|
parser = argparse.ArgumentParser(description="설치 정보가 아닌 실제 렌더·디코드 검사")
|
||||||
parser.add_argument("--blender", required=True)
|
parser.add_argument("--blender", required=True)
|
||||||
parser.add_argument("--ffmpeg", required=True)
|
parser.add_argument("--ffmpeg", required=True)
|
||||||
parser.add_argument("--ffprobe", required=True)
|
parser.add_argument("--ffprobe", required=True)
|
||||||
parser.add_argument("--engine", choices=("CYCLES", "BLENDER_EEVEE"), default="CYCLES")
|
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()
|
args = parser.parse_args()
|
||||||
folder, project, artifacts, job = fixture()
|
folder, project, artifacts, job = fixture()
|
||||||
if args.engine == "BLENDER_EEVEE":
|
if args.engine == "BLENDER_EEVEE":
|
||||||
artifacts["08-scenes"]["payload"]["preview"]["render_profile"].update(engine="BLENDER_EEVEE", device="GPU")
|
artifacts["08-scenes"]["payload"]["preview"]["render_profile"].update(engine="BLENDER_EEVEE", device="GPU")
|
||||||
write(ROOT / job["artifact"], artifacts["08-scenes"])
|
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"]
|
source = ROOT / job["source"]
|
||||||
# fixture의 검사 전 바이트는 생성 원본이 아니다. 실패 흔적은 별도 파일로 보존한다.
|
# fixture의 검사 전 바이트는 생성 원본이 아니다. 실패 흔적은 별도 파일로 보존한다.
|
||||||
source.rename(source.with_suffix(".uninspected"))
|
source.rename(source.with_suffix(".uninspected"))
|
||||||
@@ -27,14 +58,26 @@ def main():
|
|||||||
Path(__file__).with_name("smoke_fixture.py"), "--", source])
|
Path(__file__).with_name("smoke_fixture.py"), "--", source])
|
||||||
video = run.render(ROOT, job, args.blender, args.ffmpeg, args.ffprobe)
|
video = run.render(ROOT, job, args.blender, args.ffmpeg, args.ffprobe)
|
||||||
require = run.require
|
require = run.require
|
||||||
require(video["media"]["duration_ms"] == 1000, "검사 영상 길이 불일치")
|
require(video["media"]["duration_ms"] == (100 if args.animation_only else 1000), "검사 영상 길이 불일치")
|
||||||
attempt = (ROOT / video["media"]["path"]).parent
|
second_video = run.render(ROOT, dict(job, scene="SceneB", require_object=["BallB"]), args.blender, args.ffmpeg, args.ffprobe)
|
||||||
require(run.digest(attempt / "frame-00000000.png") != run.digest(attempt / "frame-00000009.png"), "3D 움직임이 없습니다")
|
motion_checks, errors = [], []
|
||||||
still_job = dict(job, frame=10, start=None, end=None)
|
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)
|
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)
|
second = run.render(ROOT, dict(still_job, scene="SceneB", require_object=["BallB"]), args.blender, args.ffmpeg, args.ffprobe)
|
||||||
require(first["identity"] != second["identity"] and run.digest(ROOT / first["media"]["path"]) != run.digest(ROOT / second["media"]["path"]),
|
require(first["identity"] != second["identity"] and pixels(ROOT / first["media"]["path"]) != pixels(ROOT / second["media"]["path"]),
|
||||||
"같은 논리 프레임의 다른 Scene 카메라가 혼입되었습니다")
|
"같은 논리 프레임의 다른 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"])):
|
for invalid in (dict(still_job, scene="Missing"), dict(still_job, require_object=["Missing"])):
|
||||||
try:
|
try:
|
||||||
run.render(ROOT, invalid, args.blender, args.ffmpeg, args.ffprobe)
|
run.render(ROOT, invalid, args.blender, args.ffmpeg, args.ffprobe)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""실행 검사 전용 3D 구와 두 카메라를 만든다. 제작 내용과 무관하다."""
|
"""실행 검사 전용 두 애니메이션 Scene과 초기 Idle Scene을 만든다."""
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
@@ -34,7 +34,10 @@ scene.camera = camera
|
|||||||
second = bpy.data.scenes.new("SceneB")
|
second = bpy.data.scenes.new("SceneB")
|
||||||
second.frame_start, second.frame_end = 1, 21
|
second.frame_start, second.frame_end = 1, 21
|
||||||
second.world = scene.world
|
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)
|
second.collection.objects.link(light)
|
||||||
other = camera.copy()
|
other = camera.copy()
|
||||||
other.data = camera.data.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()
|
other.rotation_euler = (-other.location).to_track_quat("-Z", "Y").to_euler()
|
||||||
second.collection.objects.link(other)
|
second.collection.objects.link(other)
|
||||||
second.camera = 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:
|
if "--external-image" in sys.argv:
|
||||||
bpy.data.images.load(sys.argv[sys.argv.index("--external-image") + 1]).use_fake_user = True
|
bpy.data.images.load(sys.argv[sys.argv.index("--external-image") + 1]).use_fake_user = True
|
||||||
if "--short-scene" in sys.argv:
|
if "--short-scene" in sys.argv:
|
||||||
|
|||||||
Reference in New Issue
Block a user