반응형

0. 들어가기

-. 유튜브 링크를 기반으로 i) 영상 다운로드 ii) 자막이 있는 부분만 캡쳐해서 합성하는 코드를 짜보자.

-. 목적: 유튜브영상으로 짤 자동생성 / 주피터랩으로 작업해봄.

-. moviepy, opencv, PIL 사용함

1. 유튜브 영상 다운로드

-. pytube를 이용한 유튜브 영상 다운로드 코드

import pytube

MOVIE_URL = 'https://www.youtube.com/watch?v=FD5tIIkccHY'

yt = pytube.YouTube(MOVIE_URL,
        use_oauth=False,
        allow_oauth_cache=True
    )

movie_title = yt.title
print('target movie: ' + movie_title)

movie_target = yt.streams.filter(res='720p')

if movie_target.count==0: #no movie with filtering condition
    quit()
else: #if movie is exist
    #download first stream
    print(movie_target[0])
    movie_file = movie_target[0].download('downloads')


print('download 완료: ', movie_file)

-. 위와 같이 코드를 작성하고 실행하면 아래와 같이 파일 저장경로가 출력된다.

target movie: Friends: Rachel Finds Out (Season 1 Clip) | TBS
download 완료:  C:\servers\develope\movie_to_cartoon\Friends Rachel Finds Out (Season 1 Clip)  TBS.mp4

-. 조금 자세하게 살펴보자.

1) 제목 가져오기

-. pytube 패키지의 YouTube() 함수에 url 을 변수로 넣는 것 만으로 간단하게 유튜브 정보를 가져올 수 있다.

-. title 속성을 출력하면 그대로 유튜브 제목이 출력됨.

yt = pytube.YouTube(MOVIE_URL,
        use_oauth=False, # 필요 시 유튜브(구글) 로그인
        allow_oauth_cache=True # oauth 사용 시 로그인정보 캐싱(저장)
    )

print(yt.title)
>> Friends: Rachel Finds Out (Season 1 Clip) | TBS

 

2) 동영상(stream) 정보 필터링

-. 해당 링크에서 연결해주는 동영상의 종류(해상도, 영상 타입 등)에 대한 정보는 stream 속성에서 쉽게 볼 수 있다.  테스트로 사용한 Friends는 TBS에서 매우 다양한 타입의 영상을 제공한다.

for each in yt.streams:
    print(each)
    
----
<Stream: itag="17" mime_type="video/3gpp" res="144p" fps="12fps" vcodec="mp4v.20.3" acodec="mp4a.40.2" progressive="True" type="video">
<Stream: itag="18" mime_type="video/mp4" res="360p" fps="24fps" vcodec="avc1.42001E" acodec="mp4a.40.2" progressive="True" type="video">
<Stream: itag="22" mime_type="video/mp4" res="720p" fps="24fps" vcodec="avc1.64001F" acodec="mp4a.40.2" progressive="True" type="video">
<Stream: itag="137" mime_type="video/mp4" res="1080p" fps="24fps" vcodec="avc1.640028" progressive="False" type="video">
<Stream: itag="248" mime_type="video/webm" res="1080p" fps="24fps" vcodec="vp9" progressive="False" type="video">
<Stream: itag="399" mime_type="video/mp4" res="1080p" fps="24fps" vcodec="av01.0.08M.08" progressive="False" type="video">
<Stream: itag="136" mime_type="video/mp4" res="720p" fps="24fps" vcodec="avc1.4d401f" progressive="False" type="video">
<Stream: itag="247" mime_type="video/webm" res="720p" fps="24fps" vcodec="vp9" progressive="False" type="video">
<Stream: itag="398" mime_type="video/mp4" res="720p" fps="24fps" vcodec="av01.0.05M.08" progressive="False" type="video">
<Stream: itag="135" mime_type="video/mp4" res="480p" fps="24fps" vcodec="avc1.4d401e" progressive="False" type="video">
<Stream: itag="244" mime_type="video/webm" res="480p" fps="24fps" vcodec="vp9" progressive="False" type="video">
<Stream: itag="397" mime_type="video/mp4" res="480p" fps="24fps" vcodec="av01.0.04M.08" progressive="False" type="video">
<Stream: itag="134" mime_type="video/mp4" res="360p" fps="24fps" vcodec="avc1.4d401e" progressive="False" type="video">
<Stream: itag="243" mime_type="video/webm" res="360p" fps="24fps" vcodec="vp9" progressive="False" type="video">
<Stream: itag="396" mime_type="video/mp4" res="360p" fps="24fps" vcodec="av01.0.01M.08" progressive="False" type="video">
<Stream: itag="133" mime_type="video/mp4" res="240p" fps="24fps" vcodec="avc1.4d4015" progressive="False" type="video">
<Stream: itag="242" mime_type="video/webm" res="240p" fps="24fps" vcodec="vp9" progressive="False" type="video">
<Stream: itag="395" mime_type="video/mp4" res="240p" fps="24fps" vcodec="av01.0.00M.08" progressive="False" type="video">
<Stream: itag="160" mime_type="video/mp4" res="144p" fps="24fps" vcodec="avc1.4d400c" progressive="False" type="video">
<Stream: itag="278" mime_type="video/webm" res="144p" fps="24fps" vcodec="vp9" progressive="False" type="video">
<Stream: itag="394" mime_type="video/mp4" res="144p" fps="24fps" vcodec="av01.0.00M.08" progressive="False" type="video">
<Stream: itag="139" mime_type="audio/mp4" abr="48kbps" acodec="mp4a.40.5" progressive="False" type="audio">
<Stream: itag="140" mime_type="audio/mp4" abr="128kbps" acodec="mp4a.40.2" progressive="False" type="audio">
<Stream: itag="249" mime_type="audio/webm" abr="50kbps" acodec="opus" progressive="False" type="audio">
<Stream: itag="250" mime_type="audio/webm" abr="70kbps" acodec="opus" progressive="False" type="audio">
<Stream: itag="251" mime_type="audio/webm" abr="160kbps" acodec="opus" progressive="False" type="audio">

-. 해당 영상들은 list와 유사한 pytube.query.StreamQuery 타입의 변수이고, 원하는 종류의 영상 타입을 찾기 위한 filter 기능을 제공한다.  예를들어 화질 720p 기준으로 필터링을 해보자. 

for each in yt.streams.filter(res='720p'):
    print(each)
    
----
<Stream: itag="22" mime_type="video/mp4" res="720p" fps="24fps" vcodec="avc1.64001F" acodec="mp4a.40.2" progressive="True" type="video">
<Stream: itag="136" mime_type="video/mp4" res="720p" fps="24fps" vcodec="avc1.4d401f" progressive="False" type="video">
<Stream: itag="247" mime_type="video/webm" res="720p" fps="24fps" vcodec="vp9" progressive="False" type="video">
<Stream: itag="398" mime_type="video/mp4" res="720p" fps="24fps" vcodec="av01.0.05M.08" progressive="False" type="video">

3) 동영상 다운로드

-. StreamQuery.download('폴더명') 로 매우 간단하게 가능하다. 폴더명 입력 안할 시 해당 파이썬이 실행되는 위치에 저장됨. 아래 예시코드는 720p 영상이 있을 경우 첫번째(유튜브 링크 접속 시 자동재생 되는 영상) 영상이 다운로드 된다.

movie_target = yt.streams.filter(res='720p')

if movie_target.count==0: #no movie with filtering condition
    quit()
else: #if movie is exist
    #download first stream
    print(movie_target[0])
    movie_file = movie_target[0].download('downloads')


print('download 완료: ', movie_file)

----
<Stream: itag="22" mime_type="video/mp4" res="720p" fps="24fps" vcodec="avc1.64001F" acodec="mp4a.40.2" progressive="True" type="video">
download 완료:  c:\servers\develope\movie_to_cartoon\downloads\Friends Rachel Finds Out (Season 1 Clip)  TBS.mp4

 

2. 자막 다운로드 및 시간, 자막 분할

-. 자막이 있는 장면을 캡쳐하기 위해, 우선 자막이 깔리는 영상의 시간을 확보해야 한다.

import xml, html
def xml_caption_to_caption_with_middle(xml_captions: str) -> str:
    segments = []
    root = xml.etree.ElementTree.fromstring(xml_captions)[1]
    i=0
    for child in list(root):
        if child.tag == 'p':
            caption = child.text
            if caption is not None:
                caption = html.unescape(caption.replace("\n", " ").replace("  ", " "),)
                try:
                    duration = int(child.attrib["d"])
                except KeyError:
                    duration = 0
                start = int(child.attrib["t"])

                middle = int(start + duration/2)

                segments.append((middle,caption))
    return segments

caption = yt.captions.get('en')
targets = xml_caption_to_caption_with_middle(caption.xml_captions)

1) 유튜브 자막 가져오기

-. .captions 속성으로 해당 영상의 자막 리스트 확인이 가능하다.

-. 대강 보면 'en'은 영어자막, 'a.en'은 자동 생성된 영어자막.

print(yt.captions)
----
{'en': <Caption lang="English" code="en">, 'a.en': <Caption lang="English (auto-generated)" code="a.en">}

-. 영상 하나에 여러가지 자막이 있을 수 있는데 (기본적으로 자동 생성되는 자막 포함) 이 중 필요한것을 가져오는건 하위 함수 .get('언어타입')으로 간단하게 실행 가능하다. 아래 예제를 보자.

caption = yt.captions.get('en')
print(caption)
print('----')
print(caption.xml_captions)
----
<Caption lang="English" code="en">

----
<?xml version="1.0" encoding="utf-8" ?><timedtext format="3">
<head>
<pen id="1" i="1"/>
<ws id="1" ju="0"/>
...
</head>
<body>
<p t="1000" d="4405" wp="1" ws="1">Okay, I&#39;m...guessing this is from..</p>
<p t="9442" d="1068" wp="2" ws="1">Well, thank you, Melani.</p>
...
</body>
</timedtext>

2) 자막 분할하기

-. xml 형태의 자막이 제공되는 것을 볼 수 있다. <body> tag 내에 <p t="시작시간" d="지속시간" > 자막내용 </p> 형태인 것을 쉽게 알 수 있고, wp랑 ws는 뭔지 모르겠다. 간단한 함수를 만들어서 이제 (자막의 중간시간, 자막내용)의 tuple list를 출력해보자. python의 xml과 html 패키지를 사용했다.

import xml, html
def xml_caption_to_caption_with_middle(xml_captions: str) -> str:
    segments = []
    root = xml.etree.ElementTree.fromstring(xml_captions)[1]
    i=0
    for child in list(root):
        if child.tag == 'p':
            caption = child.text
            if caption is not None:
                caption = html.unescape(caption.replace("\n", " ").replace("  ", " "),)
                try:
                    duration = int(child.attrib["d"])
                except KeyError:
                    duration = 0
                start = int(child.attrib["t"])

                middle = int(start + duration/2)

                segments.append((middle,caption))
    return segments

caption = yt.captions.get('en')
targets = xml_caption_to_caption_with_middle(caption.xml_captions)

print(targets)

----
[(3202, "Okay, I'm...guessing this is from.."), (9976, 'Well, thank you, Melani.'), ...]

3. 유튜브 영상캡쳐 + 자막합성

-. 이제 마지막으로, 유튜브 영상에서 특정 시간(여기서는 자막이 표시되는 중간지점)의 영상을 캡쳐하고, 자막을 붙여넣는 작업을 해본다.

1) 영상 캡쳐하기

-. opencv를 이용해 영상 캡쳐를 한다. PIL나 moviepy 등을 쓰는게 더 가벼울 수 있는데, opencv의 처리속도가 빠른거같은 느낌때문에 그냥 opencv 쓰기로 했다. (라이브러리가 젤 크다).

-. opencv의 VideoCapture, VideoCapture.read 함수를 사용한다. 마지막 imwrite함수는 이미지 쓰기 (저장)에 성공할 경우 True를 리턴한다. (실패 시 당연히 False)

import cv2
vidcap = cv2.VideoCapture(movie_file)

sec = 10

vidcap.set(cv2.CAP_PROP_POS_MSEC,sec*1000)
hasFrames,image = vidcap.read()
if hasFrames:
    r = cv2.imwrite("./capture/frame"+str(sec)+" sec.png", image)     # save frame as PNG file
    print(r)

--
True

 -. 지정한 디렉토리에 들어가면 아래와 같이 이미지 하나 캡쳐된 것을 확인할 수 있다.

2) 연속 영상 캡쳐

-. 앞에서 자막에서 시간/자막내용을 분리하는 작업을 했으니 이젠 해당 시간들에 대해 반복적으로 캡쳐하는 작업을 해야한다. 단순한 반복문이니 코드만.

def get_movie_frame(movie_file, time):
    vidcap = cv2.VideoCapture(movie_file)

    vidcap.set(cv2.CAP_PROP_POS_MSEC,time)
    hasFrames,image = vidcap.read()
    if hasFrames:
        return image
    else:
        return None

 

3) 영상에 자막 입히기

-. 마지막으로, 영상에 자막을 입히는 작업을 하자. 

-. 여기서는 PIL 패키지를 이용하는데, 원래 opencv만으로도 텍스트 입력은 가능하나, 폰트의 제약이 있어서 어쩔수없이 PIL을 사용하게 됐다. (난 고딕이 좋다.)

def input_text_on_image(img, text):
    cv2_im_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    pil_im = Image.fromarray(cv2_im_rgb)

    draw = ImageDraw.Draw(pil_im)

    # Choose a font
    font = ImageFont.truetype("./kbizgothic.ttf", 50)

    # Draw the text
    textw, texth = draw.textsize(text, font=font)
    h, w, v = img.shape
    topLeftCornerOfText = (int(w/2 - textw/2), int(h*0.95 - texth))
    # draw.text(topLeftCornerOfText, "Your Text Here", font=font)
    draw.text(topLeftCornerOfText, text, font=font)

    # Save the image
    cv2_im_processed = cv2.cvtColor(np.array(pil_im), cv2.COLOR_RGB2BGR)
    return cv2_im_processed

-. 영상 연속캡쳐 함수와 결합하면, 아래와 같이 자막이 있는 장면만 캡쳐해서 자막을 달아주는 결과를 얻을 수 있다.

for each in targets:
    time = each[0]
    text = each[1]
    frame = get_movie_frame(movie_file, time)
    if frame is not None:
        img = input_text_on_image(frame, text)

        cv2.imwrite("./capture/image_with_text"+str(time)+"_miliseconds.png", img)     # save frame as PNG file
    else:
        print(frame)

-. GIF 파일로 편집하면, 아래와 같이 주인공들의 대사가 보이는 짤방이 만들어진다.

728x90
반응형
  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기