備忘錄_20160105(定位) 修改 回首頁

程式 2022-05-29 16:04:29 1653811469 100
製作類似卡拉OK字幕逐字亮起的流程─04產生frames

製作類似卡拉OK字幕逐字亮起的流程─04產生frames

canvas + js + download (不好用)
前往 20220521_makeFrames_very_slow.htm
  1. 時間:經推估約需21小時29分鐘45秒
  2. 空間:不小於41.4GB (之前透過瀏覽器產生的圖片檔案,壓縮率似乎沒有很好)
python 3.9.7 + open cv (尚可接受)
查看 20220528_data_from_excel_utf8.txt
查看 20220528_makeFrames_slow.py
  1. 時間:實測需要 1小時13分鐘15秒
  2. 空間:41.4GB (86KB~922KB不等,共67200張圖片)
python 3.9.7 + open cv (可接受)
查看 20220528_data_from_excel_utf8.txt
查看 20220608_makeFrames.py
  1. 時間大幅縮短,只有遇到換頁才讀取檔案。
  2. 空間大幅縮小,因為直接產生 mp4 檔案。(缺點是畫面會變暗一些,主因是編碼的關係。若要改善,可能要改成匯出一堆圖檔,而非一個 mp4 檔案)

●20220608_makeFrames.py

from datetime import datetime
import numpy as np
import cv2
#from decimal import Decimal
import math

strExeBeginTime=datetime.now().strftime("%H:%M:%S")

iWidth=1920 # pixels
iHeight=2541 # pixels
fFPS=24 # frames/second

# total 445 seconds
iIdxFrameBegin=1
iIdxFrameEnd=445*fFPS

oEncode=cv2.VideoWriter_fourcc(*'mp4v') # 指定視訊編碼
oOut=cv2.VideoWriter( '../videos/tb14_only_video.mp4',oEncode,fFPS,(iWidth,iHeight),True)

class TheData:
  def __init__(self,straItem):
    self.strDescription=straItem[0]
    self.fSecs1=float(straItem[1])
    self.fSecs2=float(straItem[2])
    self.strBGFileName=straItem[3].strip()
    self.strFGFileName="fg"+(straItem[3].strip())[2:]
    
    # 由上而下 或 由左而右 不一定,全看 beg -> end
    self.fBegX1=float(straItem[4])
    self.fBegY1=float(straItem[5])
    self.fBegX2=float(straItem[6])
    self.fBegY2=float(straItem[7])
    '''
    if self.fBegX1>self.fBegX2:
      self.fBegX1,self.fBegX2=self.fBegX2,self.fBegX1
      self.fBegY1,self.fBegY2=self.fBegY2,self.fBegY1
    '''  
    self.fEndX1=float(straItem[8])
    self.fEndY1=float(straItem[9])
    self.fEndX2=float(straItem[10])
    self.fEndY2=float(straItem[11])
    '''
    if self.fEndX1>self.fEndX2:
      self.fEndX1,self.fEndX2=self.fEndX2,self.fEndX1
      self.fEndY1,self.fEndY2=self.fEndY2,self.fEndY1
    '''
    
# 讀取檔案內容
oFile=open('20220608_data_from_excel_utf8.txt','r',encoding='UTF-8')
oContent=oFile.read()
oFile.close()

# 拿掉註解行
straSourceLine=("\n".join(("\n".join(oContent.split("\r\n"))).split("\r"))).split("\n")
straWantedLine=[]
for i in range(0,len(straSourceLine)):
  straItem=straSourceLine[i].split("\t")
  iItems=0
  for j in range(0,len(straItem)):
    if straItem[j].strip()!="":
      iItems+=1
  if iItems>=12:
    straWantedLine.append(TheData(straItem))

iMaxLenOfNum=len(str(iIdxFrameEnd)) # 數字總共幾位數
iIdxFrameBegin-=1 # base 0
iIdxFrameEnd-=1 # base 0

iIdxLastTime=-1

#oLog=open("log.log","w")

for iIdxFrame in range(iIdxFrameBegin,iIdxFrameEnd+1):
  
  fCurrentSeconds=(iIdxFrame+1)/fFPS
  strCurrentBGFileName=""
  strCurrentFGFileName=""
  iIdxDataBegin=-1
  iIdxDataEnd=-1
  
  # 尋找這個頁面的最後一筆
  
  if iIdxLastTime!=-1: iIdxTmpBegin=iIdxLastTime
  else: iIdxTmpBegin=0
  
  for iIdxData in range(iIdxTmpBegin, len(straWantedLine)):
    if fCurrentSeconds<straWantedLine[iIdxData].fSecs1:
      iIdxDataEnd=iIdxData-1
      if iIdxDataEnd<0: iIdxDataEnd=0
      strCurrentBGFileName=straWantedLine[iIdxDataEnd].strBGFileName
      strCurrentFGFileName=straWantedLine[iIdxDataEnd].strFGFileName
      break
  
  if iIdxDataEnd==-1:
    iIdxDataEnd=len(straWantedLine)-1
    strCurrentBGFileName=straWantedLine[iIdxDataEnd].strBGFileName
    strCurrentFGFileName=straWantedLine[iIdxDataEnd].strFGFileName
    
  # 尋找這個頁面的第一筆
  if iIdxLastTime!=-1:
    iIdxDataBegin=iIdxLastTime
  else:
    for iIdxData in range(iIdxDataEnd,-1,-1):
      if strCurrentBGFileName==straWantedLine[iIdxData].strBGFileName:
        iIdxDataBegin=iIdxData
      else:
        break
  
  # nmomtf
  #print("iIdxFrame="+str(iIdxFrame), "iIdxDataBegin="+str(iIdxDataBegin), "iIdxDataEnd="+str(iIdxDataEnd))
  
  # 繪製這個 frame
  
  for iIdxCB in range(iIdxDataBegin,iIdxDataEnd+1):
    if straWantedLine[iIdxCB].fSecs1==straWantedLine[iIdxCB].fSecs2:
      
      oFG=cv2.imread('../images/'+strCurrentFGFileName,cv2.IMREAD_COLOR)
      oBG=cv2.imread('../images/'+strCurrentBGFileName,cv2.IMREAD_COLOR)
      
    else:
      
      # 方向 beg -> end
      # 目前先是矩形好了,未來才用平行四邊形
      
      '''
      fX=straWantedLine[iIdxCB].fBegX1
      fY=straWantedLine[iIdxCB].fBegY1
      fW=straWantedLine[iIdxCB].fBegX2-straWantedLine[iIdxCB].fBegX1
      fH=straWantedLine[iIdxCB].fEndY1-straWantedLine[iIdxCB].fBegY1
      
      if fCurrentSeconds<straWantedLine[iIdxCB].fSecs2:
        fH=fH*(fCurrentSeconds-straWantedLine[iIdxCB].fSecs1)/(straWantedLine[iIdxCB].fSecs2-straWantedLine[iIdxCB].fSecs1)
      
      x1=math.floor(fX)
      y1=math.floor(fY)
      x2=math.floor(fX+fW)
      y2=math.floor(fY+fH)
      '''
      
      fBegX1=straWantedLine[iIdxCB].fBegX1
      fBegY1=straWantedLine[iIdxCB].fBegY1
      fBegX2=straWantedLine[iIdxCB].fBegX2
      fBegY2=straWantedLine[iIdxCB].fBegY2
      fEndX1=straWantedLine[iIdxCB].fEndX1
      fEndY1=straWantedLine[iIdxCB].fEndY1
      fEndX2=straWantedLine[iIdxCB].fEndX2
      fEndY2=straWantedLine[iIdxCB].fEndY2
      fNewEndX1=fEndX1
      fNewEndY1=fEndY1
      fNewEndX2=fEndX2
      fNewEndY2=fEndY2

      if fCurrentSeconds<straWantedLine[iIdxCB].fSecs2:
        fTheRatio=(fCurrentSeconds-straWantedLine[iIdxCB].fSecs1)/(straWantedLine[iIdxCB].fSecs2-straWantedLine[iIdxCB].fSecs1)
        fNewEndX1=fTheRatio*(fEndX1-fBegX1)+fBegX1
        fNewEndY1=fTheRatio*(fEndY1-fBegY1)+fBegY1
        fNewEndX2=fTheRatio*(fEndX2-fBegX2)+fBegX2
        fNewEndY2=fTheRatio*(fEndY2-fBegY2)+fBegY2
      
      x1 = math.floor(min(fBegX1, fBegX2, fNewEndX1, fNewEndX2))
      y1 = math.floor(min(fBegY1, fBegY2, fNewEndY1, fNewEndY2))
      x2 = math.floor(max(fBegX1, fBegX2, fNewEndX1, fNewEndX2))
      y2 = math.floor(max(fBegY1, fBegY2, fNewEndY1, fNewEndY2))
      
      #oLog.write(str(fBegX1)+','+str(fBegY1)+','+str(fBegX2)+','+str(fBegY2)+"\n") 
      #oLog.write(str(fNewEndX1)+','+str(fNewEndY1)+','+str(fNewEndX2)+','+str(fNewEndY2)+"\n") 
      #oLog.write(str(x1)+','+str(y1)+','+str(x2)+','+str(y2)+','+"\n\n") 
      
      oBG[y1:y2,x1:x2]=oFG[y1:y2,x1:x2]
  
  #strNewFileName='frame'+str(iIdxFrame+1).zfill(iMaxLenOfNum)+'.jpg'
  #cv2.imwrite('frames/'+strNewFileName, oBG, [cv2.IMWRITE_JPEG_QUALITY, 100])
  oOut.write(oBG)
  print("Frame "+str(iIdxFrame)+" / "+str(iIdxFrameEnd+1)+" finished.")
  
  iIdxLastTime=iIdxDataEnd

oOut.release()
print("All finished!")

strExeEndTime=datetime.now().strftime("%H:%M:%S")
print(strExeBeginTime, strExeEndTime)

#oLog.close()

●20220528_makeFrames_slow.py

import numpy as np
import cv2
#from decimal import Decimal
import math

iWidth=1920 # pixels
iHeight=1080 # pixels
fFPS=24 # frames/second

# total 2793.576 seconds
# want 2800 seconds => 2800*24 => 67200 frames
iIdxFrameBegin=1
iIdxFrameEnd=2800*fFPS

class TheData:
  def __init__(self,straItem):
    self.strDescription=straItem[0]
    self.fSecs1=float(straItem[1])
    self.fSecs2=float(straItem[2])
    self.strBGFileName=straItem[3].strip()
    self.strFGFileName="fg"+(straItem[3].strip())[2:]
    
    # 假定由上而下
    self.fBegX1=float(straItem[4])
    self.fBegY1=float(straItem[5])
    self.fBegX2=float(straItem[6])
    self.fBegY2=float(straItem[7])
    if self.fBegX1>self.fBegX2:
      self.fBegX1,self.fBegX2=self.fBegX2,self.fBegX1
      self.fBegY1,self.fBegY2=self.fBegY2,self.fBegY1
      
    self.fEndX1=float(straItem[8])
    self.fEndY1=float(straItem[9])
    self.fEndX2=float(straItem[10])
    self.fEndY2=float(straItem[11])
    if self.fEndX1>self.fEndX2:
      self.fEndX1,self.fEndX2=self.fEndX2,self.fEndX1
      self.fEndY1,self.fEndY2=self.fEndY2,self.fEndY1
    
# 讀取檔案內容
oFile=open('20220528_data_from_excel_utf8.txt','r',encoding='UTF-8')
oContent=oFile.read()
oFile.close()

# 拿掉註解行
straSourceLine=("\n".join(("\n".join(oContent.split("\r\n"))).split("\r"))).split("\n")
straWantedLine=[]
for i in range(0,len(straSourceLine)):
  straItem=straSourceLine[i].split("\t")
  iItems=0
  for j in range(0,len(straItem)):
    if straItem[j].strip()!="":
      iItems+=1
  if iItems>=12:
    straWantedLine.append(TheData(straItem))

iMaxLenOfNum=len(str(iIdxFrameEnd)) # 數字總共幾位數
iIdxFrameBegin-=1 # base 0
iIdxFrameEnd-=1 # base 0

for iIdxFrame in range(iIdxFrameBegin,iIdxFrameEnd+1):
  
  fCurrentSeconds=(iIdxFrame+1)/fFPS
  strCurrentBGFileName=""
  strCurrentFGFileName=""
  iIdxDataBegin=-1
  iIdxDataEnd=-1
  
  # 首先尋找這個頁面的最後一筆
  for iIdxData in range(len(straWantedLine)-1,-1,-1):
    if fCurrentSeconds>=straWantedLine[iIdxData].fSecs1:
      iIdxDataEnd=iIdxData
      strCurrentBGFileName=straWantedLine[iIdxData].strBGFileName
      strCurrentFGFileName=straWantedLine[iIdxData].strFGFileName
      break
  
  # 再來尋找這個頁面的第一筆
  for iIdxData in range(iIdxDataEnd,-1,-1):
    if strCurrentBGFileName==straWantedLine[iIdxData].strBGFileName:
      iIdxDataBegin=iIdxData
    else:
      break
  
  # 繪製這個 frame
  oBG=cv2.imread('../'+strCurrentBGFileName,cv2.IMREAD_COLOR)
  oFG=cv2.imread('../'+strCurrentFGFileName,cv2.IMREAD_COLOR)
  for iIdxCB in range(iIdxDataBegin,iIdxDataEnd+1):
    if straWantedLine[iIdxCB].fSecs1==straWantedLine[iIdxCB].fSecs2:
      # 可以省略
      pass
    else:
      # 假定是由上而下
      fX=straWantedLine[iIdxCB].fBegX1
      fY=straWantedLine[iIdxCB].fBegY1
      fW=straWantedLine[iIdxCB].fBegX2-straWantedLine[iIdxCB].fBegX1
      fH=straWantedLine[iIdxCB].fEndY1-straWantedLine[iIdxCB].fBegY1
      
      if fCurrentSeconds<straWantedLine[iIdxCB].fSecs2:
        fH=fH*(fCurrentSeconds-straWantedLine[iIdxCB].fSecs1)/(straWantedLine[iIdxCB].fSecs2-straWantedLine[iIdxCB].fSecs1)
      
      x1=math.floor(fX)
      y1=math.floor(fY)
      x2=math.floor(fX+fW)
      y2=math.floor(fY+fH)
      
      oBG[y1:y2,x1:x2]=oFG[y1:y2,x1:x2]
  
  strNewFileName='frame'+str(iIdxFrame+1).zfill(iMaxLenOfNum)+'.jpg'
  cv2.imwrite('frames/'+strNewFileName, oBG, [cv2.IMWRITE_JPEG_QUALITY, 100])
  
  print("Frame "+str(iIdxFrame)+" / "+str(iIdxFrameEnd+1)+" finished.")

print("All finished!")

●20220521_makeFrames_very_slow.htm

<!doctype html>
<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
    <title></title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
      
      *
      {
        font-family: "WenQuanYi Zen Hei","文泉驛正黑","Heiti TC","黑體-繁","LiHei Pro","儷黑 Pro","PingFang TC","Droid Sans","Roboto","Microsoft JhengHei","微軟正黑體",sans-serif;
        /* 【linux的字型】【ios字型】【android字型】【微軟正黑體】【無襯線字=黑體】 */
      }

      html, body 
      {
        margin: 0;
        border: 0;
        padding: 0;
        width: 100%;
        height: 100%;
      }

      a:link, a:visited
      {
        color: blue;
        text-decoration: none;
      }
      
      #divWrapper
      {
        padding: 1em;
      }
      
      .classSmallImage
      {
      }
      
      .classBlock
      {
        display: inline-block;
        padding: 6px;
        margin: 6px;
        background-color: lightpink;
        border-radius: 6px;
      }
      
    </style>
  </head>
  <body>
    
    <div id="divWrapper">
      <div style="color: red;">Firefox, about:config, set security.fileuri.strict_origin_policy to false</div>
      <div style="color: red;">Firefox, 設定存檔時不用詢問,並改到想要的位置。</div>
      請從 excel 把資料貼過來。<br>
      description,secs1,secs2,imageName,begX1,begY1,begX2,begY2,endX1,endY1,endX2,endY2<br>
      <div class="classBlock">畫面大小<input type="text" name="inpScreenWidth" id="inpScreenWidth" value="1920"> x <input type="text" name="inpScreenHeight" id="inpScreenHeight" value="1080"></div>
      <div class="classBlock"><input type="text" name="inpFPS" id="inpFPS" value="24">fps</div>
      <div class="classBlock">total <input type="text" name="inpTotalSecs" id="inpTotalSecs" value="2793.576">secs</div>
      <br>
      <div class="classBlock">繪製frame起訖<input type="text" name="inpFrameBegin" id="inpFrameBegin" value="1"> ~ <input type="text" name="inpFrameEnd" id="inpFrameEnd" value="67200"></div>
      <br>
      <textarea id="taInput" rows="10" cols="120"></textarea>
      <br>
      <button type="button" onclick="goDrawing();">進行繪製</button>
      <br>
      <div>
        <canvas id="myCanvas"></canvas>
      </div>
    </div>
    
    <hr>
    <hr>
    <hr>
    <hr>
    
    <div id="divImages"></div>
    
    <script>
      
      if(!String.prototype.ltrim) { String.prototype.ltrim = function() { return this.replace(/^\s+/,'');    }; }
      if(!String.prototype.rtrim) { String.prototype.rtrim = function() { return this.replace(/\s+$/,'');    }; }
      if(!String.prototype.trim ) { String.prototype.trim = function()  { return this.replace(/^\s+|\s+$/g,''); }; }
      
      function gebi(strId)
      {
        return document.getElementById(strId);
      }
      
      for(var j=0; j<2; j++)
      {
        var strHead="fg";
        if(j==1) { strHead="bg"; }
        
        for(var i=1; i<=15; i++)
        {
          var oImg=document.createElement("img");
          oImg.setAttribute("id",strHead+("00"+i).substr(-2));
          oImg.setAttribute("src",strHead+("00"+i).substr(-2)+".jpg");
          oImg.setAttribute("class","classSmallImage");
          gebi("divImages").appendChild(oImg);
        }
      }
      
      window.addEventListener(
        "load",
        function() 
        {
        }
      );

    </script>
    
    <script>
      
      var iWidth, iHeight, fFPS, fTotalSecs, fTotalFrames;
      
      function getStraData()
      {
        var straLine=gebi("taInput").value.split("\r\n").join("\n").split("\r").join("\n").split("\n");
        var straBuffer=[];
        
        // 去掉空白行以及不符合規格的資料
        for(var i=0; i<straLine.length; i++)
        {
          var straItem=straLine[i].split("\t");
          var iCount=0;
          for(var j=0; j<straItem.length; j++)
          {
            if(straItem[j].trim()!="")
            {
              iCount++;
            }
          }
          if(iCount>=12)
          {
            straBuffer.push(straLine[i]);
          }
        }
        
        return straBuffer;
      }
      
      function getOAnalyzed(str1)
      {
        var straItem=str1.split("\t");
        var strDescription=straItem[0];
        var fSecs1=straItem[1];
        var fSecs2=straItem[2];
        var strImageName=straItem[3];
        var strBGImageId=strImageName.substring(0,strImageName.length-4);
        var strFGImageId=strBGImageId.split("bg").join("fg");
        var fBegX1=straItem[4];
        var fBegY1=straItem[5];
        var fBegX2=straItem[6];
        var fBegY2=straItem[7];
        var fEndX1=straItem[8];
        var fEndY1=straItem[9];
        var fEndX2=straItem[10];
        var fEndY2=straItem[11];
        
        return {
          strDescription:strDescription,
          fSecs1:fSecs1,
          fSecs2:fSecs2,
          strImageName:strImageName,
          strBGImageId:strBGImageId,
          strFGImageId:strFGImageId,
          fBegX1:fBegX1,
          fBegY1:fBegY1,
          fBegX2:fBegX2,
          fBegY2:fBegY2,
          fEndX1:fEndX1,
          fEndY1:fEndY1,
          fEndX2:fEndX2,
          fEndY2:fEndY2};
      }
      
      var iIdxFrame, iIdxFrameBegin, iIdxFrameEnd;
      var straDrawingData, oCanvas, oCtx;
      var oaDrawingData;
      function goDrawing()
      {
        straDrawingData=getStraData();
        oaDrawingData=[];
        for(var i=0; i<straDrawingData.length; i++)
        {
          oaDrawingData.push(getOAnalyzed(straDrawingData[i]));
        }
        
        iWidth=gebi("inpScreenWidth").value*1;
        iHeight=gebi("inpScreenHeight").value*1;
        fFPS=gebi("inpFPS").value*1;
        fTotalSecs=gebi("inpTotalSecs").value*1;
        fTotalFrames=fTotalSecs*fFPS;
        
        oCanvas=gebi("myCanvas");
        oCanvas.setAttribute("width", iWidth);
        oCanvas.setAttribute("height", iHeight);
        oCtx=oCanvas.getContext("2d");
        
        iIdxFrameBegin=gebi("inpFrameBegin").value*1-1; // base0
        //iIdxFrameEnd=fTotalFrames-1; // nmomtf
        iIdxFrameEnd=gebi("inpFrameEnd").value*1-1; // base0
        iIdxFrame=iIdxFrameBegin-1;
        
        goDrawingCore();
      }
      
      function goDrawingCore()
      {
        iIdxFrame++;
        if(iIdxFrame>iIdxFrameEnd) { return; }
        
        {
          //var fCurrentSeconds=((iIdxFrame+1)/fTotalFrames)*fTotalSecs;
          var fCurrentSeconds=(iIdxFrame+1)/fFPS;
          var strCurrentImageName="";
          var iIdxDataBegin=-1;
          var iIdxDataEnd=-1;
          
          for(var iIdxData=(oaDrawingData.length-1); iIdxData>-1; iIdxData--)
          {
            var o1=oaDrawingData[iIdxData];
            
            if(fCurrentSeconds>=o1.fSecs1)
            {
              iIdxDataEnd=iIdxData;
              strCurrentImageName=o1.strImageName;
              break;
            }
          }
          
          for(var iIdxData=iIdxDataEnd; iIdxData>-1; iIdxData--)
          {
            var o1=oaDrawingData[iIdxData];
            
            if(o1.strImageName==strCurrentImageName)
            {
              iIdxDataBegin=iIdxData;
            }
            else
            {
              break;
            }
          }
          
          // 繪製這個 frame
          for(var iIdxCB=iIdxDataBegin; iIdxCB<=iIdxDataEnd; iIdxCB++)
          {
            var o1=oaDrawingData[iIdxCB];
            
            if(o1.fSecs1==o1.fSecs2)
            {
              oCtx.drawImage(gebi(o1.strBGImageId),0,0);
            }
            else
            {
              // void ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
              var x=o1.fBegX1;
              var y=o1.fBegY1;
              var w=o1.fBegX2-o1.fBegX1;
              var h=o1.fEndY1-o1.fBegY1;
              
              if(fCurrentSeconds<o1.fSecs2)
              {
                h=(fCurrentSeconds-o1.fSecs1)/(o1.fSecs2-o1.fSecs1)*h;
              }
              
              oCtx.drawImage(gebi(o1.strFGImageId),x,y,w,h,x,y,w,h);
            }
            
          }
          
          // 儲存
          oCanvas.toBlob(myBlobCallback.bind(null, iIdxFrame),'image/jpeg',1);
        }
      }
      
      function myBlobCallback(iIdxFrame, oBlob)
      {
        var oUrl=URL.createObjectURL(oBlob);
        var oLink=document.createElement('a');
        oLink.innerText='Download';
        oLink.href=oUrl;
        oLink.download='frame'+(iIdxFrame+1)+'.jpg';
        oLink.click();
        
        window.setTimeout(goDrawingCore, 1);
      }
      
    </script>
    
  </body>
</html>