asset: 归档第一集已生成镜头与测试素材,排除可再生缓存
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
"""Tracked screen graphics on H3 video; originals preserved."""
|
||||
import sys,os,json,subprocess,math
|
||||
from pathlib import Path
|
||||
sys.path.insert(0,r'C:/Users/zhang/.codex/tools/screen-post')
|
||||
import cv2,numpy as np
|
||||
from PIL import Image,ImageDraw,ImageFont
|
||||
ROOT=Path(__file__).resolve().parent.parent
|
||||
SKILL=Path(r'C:/Users/zhang/.codex/skills/ffmpeg-video-editor/scripts')
|
||||
os.environ['PATH']=r'C:/Users/zhang/.codex/tools/media-bin;'+os.environ.get('PATH','')
|
||||
FONT=r'C:/Windows/Fonts/msyh.ttc'
|
||||
def dump(p,v): p.write_text(json.dumps(v,ensure_ascii=False,indent=2),encoding='utf-8')
|
||||
def font(n): return ImageFont.truetype(FONT,n)
|
||||
def wrap(s,n):return '\n'.join(s[i:i+n] for i in range(0,len(s),n))
|
||||
def graphic(meta,t):
|
||||
cat=meta['shot']=='E01-S01-C02'
|
||||
W,H=(480,480) if cat else (420,760)
|
||||
im=Image.new('RGBA',(W,H),(0,0,0,0));d=ImageDraw.Draw(im)
|
||||
if cat:
|
||||
d.text((55,90),'分类结果',font=font(35),fill=(65,72,78,255))
|
||||
d.text((190,190),'锅',font=font(82),fill=(35,44,53,255))
|
||||
d.line([(170,350),(192,370),(235,320)],fill=(62,118,82,255),width=7)
|
||||
if t>=2.6:d.line([(165,325),(239,373)],fill=(164,65,61,255),width=6)
|
||||
return np.array(im)
|
||||
event=next((e for e in meta['events'] if e['start']<=t<e['end']),meta['events'][-1])
|
||||
d.rounded_rectangle((0,0,W-1,H-1),radius=24,fill=(237,238,234,255))
|
||||
d.rectangle((0,0,W,82),fill=(224,228,225,255))
|
||||
if event['mode']=='rota':
|
||||
d.text((150,22),'排班表',font=font(31),fill=(36,44,48,255))
|
||||
for k,txt in enumerate(event['rows']):
|
||||
y=155+k*98;d.rectangle((24,y-18,396,y+62),outline=(145,155,157),width=2)
|
||||
d.text((45,y),txt,font=font(29),fill=(44,50,53,255))
|
||||
return np.array(im)
|
||||
d.text((177,22),event.get('contact','妈妈'),font=font(32),fill=(36,44,48,255))
|
||||
d.rounded_rectangle((22,124,352,220),radius=15,fill=(255,255,252,255))
|
||||
d.text((42,151),event.get('incoming',''),font=font(29),fill=(39,45,47,255))
|
||||
s=event.get('draft','')
|
||||
if 'typing' in event:
|
||||
a,b=event['typing']['start'],event['typing']['end'];s=s[:int(len(s)*max(0,min(1,(t-a)/(b-a))))]
|
||||
if 'deleting' in event:
|
||||
a,b=event['deleting']['start'],event['deleting']['end'];s=s[:math.ceil(len(s)*(1-max(0,min(1,(t-a)/(b-a)))))]
|
||||
sent=event.get('sent',False)
|
||||
d.rounded_rectangle((18,302,401,470),radius=13,fill=(180,212,170,255) if sent else (252,253,249,255),outline=(164,174,173,255),width=2)
|
||||
if s:d.multiline_text((34,321),wrap(s,12),font=font(28),spacing=12,fill=(32,41,42,255))
|
||||
d.text((34,492),'已发送' if sent else '未发送',font=font(20),fill=(109,121,120,255))
|
||||
# Muted simple keyboard lies behind thumb occlusion.
|
||||
for y in range(560,726,51):
|
||||
for x in range(20,386,39):d.rounded_rectangle((x,y,x+30,y+40),radius=4,fill=(251,251,247,255),outline=(189,197,196,255))
|
||||
return np.array(im)
|
||||
def runtool(name,args,log):
|
||||
p=subprocess.run([sys.executable,str(SKILL/name),*args],capture_output=True,text=True,encoding='utf-8',errors='replace')
|
||||
log.write_text(p.stdout+'\n'+p.stderr,encoding='utf-8')
|
||||
if p.returncode:raise RuntimeError(str(log))
|
||||
return p.stdout
|
||||
def main(sid):
|
||||
folder=ROOT/'shots'/sid;meta=json.loads((folder/'screen_overlay.json').read_text(encoding='utf-8'))
|
||||
src=Path(json.loads((folder/'outputs.json').read_text(encoding='utf-8'))[0])
|
||||
cap=cv2.VideoCapture(str(src));fps=cap.get(cv2.CAP_PROP_FPS);w=int(cap.get(cv2.CAP_PROP_FRAME_WIDTH));h=int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
||||
ok,first=cap.read();assert ok
|
||||
cs=meta['coordinate_space'];scale=np.array([w/cs['width'],h/cs['height']],dtype=np.float32)
|
||||
quad=np.float32(meta.get('panel_quad_clockwise',meta['screen_quad_clockwise']))*scale
|
||||
screen=np.float32(meta['screen_quad_clockwise'])*scale
|
||||
g0=cv2.cvtColor(first,cv2.COLOR_BGR2GRAY)
|
||||
mask=np.zeros((h,w),np.uint8);cv2.fillConvexPoly(mask,np.int32(screen),255)
|
||||
mask=cv2.dilate(mask,np.ones((27,27),np.uint8))
|
||||
inner=np.zeros_like(mask);cv2.fillConvexPoly(inner,np.int32(screen),255);inner=cv2.erode(inner,np.ones((11,11),np.uint8))
|
||||
mask=cv2.subtract(mask,inner)
|
||||
pts=cv2.goodFeaturesToTrack(g0,maxCorners=160,qualityLevel=.005,minDistance=5,mask=mask)
|
||||
if pts is None or len(pts)<8:raise RuntimeError('Insufficient screen-border features')
|
||||
frames=folder/'post_frames';frames.mkdir(exist_ok=True)
|
||||
cap.set(cv2.CAP_PROP_POS_FRAMES,0);idx=0;lastH=np.eye(3);metrics=[]
|
||||
while True:
|
||||
ok,fr=cap.read()
|
||||
if not ok:break
|
||||
gray=cv2.cvtColor(fr,cv2.COLOR_BGR2GRAY)
|
||||
dst,status,err=cv2.calcOpticalFlowPyrLK(g0,gray,pts,None,winSize=(25,25),maxLevel=4)
|
||||
good=(status.ravel()==1)&(err.ravel()<30)
|
||||
H,ins=(None,None)
|
||||
if good.sum()>=8:H,ins=cv2.findHomography(pts[good],dst[good],cv2.RANSAC,3)
|
||||
valid=H is not None and int(ins.sum())>=7
|
||||
if valid:
|
||||
test=cv2.perspectiveTransform(screen[None],H)[0]
|
||||
area=abs(cv2.contourArea(test));base=abs(cv2.contourArea(screen))
|
||||
valid=.7<area/base<1.35 and np.max(np.linalg.norm(test-screen,axis=1))<100
|
||||
if valid:lastH=H
|
||||
target=cv2.perspectiveTransform(quad[None],lastH)[0].astype(np.float32)
|
||||
art=graphic(meta,idx/fps);ah,aw=art.shape[:2]
|
||||
M=cv2.getPerspectiveTransform(np.float32([[0,0],[aw-1,0],[aw-1,ah-1],[0,ah-1]]),target)
|
||||
layer=cv2.warpPerspective(art,M,(w,h),flags=cv2.INTER_CUBIC)
|
||||
alpha=layer[:,:,3].astype(np.float32)/255
|
||||
if sid!='E01-S01-C02':
|
||||
hsv=cv2.cvtColor(fr,cv2.COLOR_BGR2HSV)
|
||||
skin=((hsv[:,:,0]<30)&(hsv[:,:,1]>52)&(hsv[:,:,2]>90)).astype(np.uint8)
|
||||
skin=cv2.dilate(skin,np.ones((3,3),np.uint8))
|
||||
alpha*=1-skin
|
||||
rgb=cv2.cvtColor(fr,cv2.COLOR_BGR2RGB)
|
||||
out=(rgb*(1-alpha[:,:,None])+layer[:,:,:3]*alpha[:,:,None]).clip(0,255).astype(np.uint8)
|
||||
Image.fromarray(out).save(frames/('frame_%04d.png'%idx))
|
||||
metrics.append({'frame':idx,'valid_homography':bool(valid),'inliers':int(ins.sum()) if ins is not None else 0,'quad':target.tolist()})
|
||||
idx+=1
|
||||
cap.release()
|
||||
dump(folder/'screen_tracking.json',{'method':'First-frame screen-border Shi-Tomasi features, pyramidal LK flow, RANSAC homography, bounded area/displacement, skin occlusion mask for phones','frames':metrics,'valid_fraction':sum(x['valid_homography'] for x in metrics)/len(metrics)})
|
||||
video=folder/'post_screen_silent.mp4';out=folder/'post_screen.mp4'
|
||||
args=['--dir',str(frames),'--pattern','frame_%04d.png','--fps',str(fps),'-o',str(video),'--quality','18','--overwrite']
|
||||
runtool('sequence.py',args+['--dry-run','--json'],folder/'post_plan.json')
|
||||
runtool('sequence.py',args+['--json'],folder/'post_sequence.json')
|
||||
args=[str(video),'--replace',str(src),'-o',str(out),'--overwrite']
|
||||
runtool('audio.py',args+['--dry-run','--json'],folder/'post_audio_plan.json')
|
||||
runtool('audio.py',args+['--json'],folder/'post_audio.json')
|
||||
runtool('probe.py',[str(out),'--json'],folder/'post_probe.json')
|
||||
runtool('check.py',[str(out),'--platform','custom','--json'],folder/'post_check.json')
|
||||
runtool('look.py',[str(out),'--tiles','3x2','--overwrite'],folder/'post_look.log')
|
||||
dump(folder/'post_outputs.json',[str(out)])
|
||||
print(json.dumps({'shot':sid,'output':str(out),'frames':idx,'valid_fraction':sum(x['valid_homography'] for x in metrics)/len(metrics)},ensure_ascii=False))
|
||||
if __name__=='__main__':main(sys.argv[1])
|
||||
Reference in New Issue
Block a user