Skip to main content

FFmpeg and Imagick

Ghép mặt người vào 1 video cụ thể.

Có nhiều trang như http://www.jibjab.com/ cho phép người dùng đưa mặt mình vào một số video ngộ nghĩnh như rockstar, dance video ... Thậm chí bạn có thể thêm nhiều mặt người vào video để thành cặp hay nhóm.

Kỹ thuật làm trang này chủ yếu dựa trên Flash, chi tiết có thể tra GG.

Mình muốn giới thiệu một cách khác không dùng Flash và có thể chạy trên mọi thiết bị thông qua web.
 Về kỹ thuật video ghép từ nhiều khung hình, thường là 25fps (frames per second, hay video nhanh hơn là 30fps. Do vậy, chỉ cần ghép mặt mình vào từng khung hình hoặc đè lên theo từng thời điểm là OK. Công cụ đã có sẵn FFmpeg ghép ảnh thành video, ImageMagick chỉnh sửa ảnh cơ bản. Và một Tool quan trọng đó là  Adobe AfterEffect (AE) trong bộ Adobe Suites. Tool này chuyên để chỉnh sửa video. Nó có 1 chức năng là Motion Tracking detect vật thể di chuyển theo khung hình.

Như vậy là về kỹ thuật là có thể làm được, kết quả vd:

https://drive.google.com/file/d/0B9XwFe7bHCQ0RE94Qm1yeGJKRGs/view?usp=sharing

Video này không phải video chuẩn (mình cũng không nhớ để đâu) nhưng nếu lúc bạn crop face chuẩn thì video sẽ đẹp hơn. Lúc chọn mặt sẽ có bước xoay, zoom sao cho mặt vừa với khung mẫu.

Đại khái là vậy, chi tiết mình sẽ cập nhật sau.

dữ liệu test.
https://drive.google.com/folderview?id=0B9XwFe7bHCQ0clB5RHJ2NDVTMjQ&usp=sharing


Source code (mình để nhiều chỗ nên sẽ tổng hợp sau) đại khái như sau:

<?php

    $source = 'Transform_data.xml';



    $xmlstr = file_get_contents($source);

    $xmlcont = @simplexml_load_string($xmlstr); 



    $bg = new ImagickPixel('none');

    $face_name = $_GET['face_name'];

    $face_dir = basename($face_name, ".png"); 

    new_working_dir($face_dir);



    $face = new Imagick();

    $face->readImage("./faces/".$face_name);

    $face->resizeImage(191,232,imagick::FILTER_LANCZOS,1);

    $face_w = $face->getimagewidth();

    $face_h = $face->getimageHeight();





    $start = time();

    $quality = "low_png_frm"; // high frame quality, low_png_frm for low quality

    

    

    foreach($xmlcont as $frame) 

    {

         if((intval($frame->time) >= 44) && (intval($frame->time) <= 79)) { // part1

            $imgFace = clone $face;

            $imgFace = transform(-2, 2.42*$face_w, 2.42*$face_h, $imgFace);

            // $face = new Imagick("RESULT/frame_0".$frame->time. ".png");

            $frm = new Imagick("low_png_frm/frame_0".$frame->time. ".png");

            $frm->compositeImage($imgFace, imagick::COMPOSITE_DSTOVER, intval($frame->x +92.4 -241), intval($frame->y -20.8 -277)); 

            $img_outp = $face_dir."/frame_0". $frame->time. ".png";

            $frm->setImageCompressionQuality(40);

            $frm->stripImage();    

            $frm->writeImage(''.$img_outp); 

        } 

        

        else if((intval($frame->time) >= 80) && (intval($frame->time) <= 125)) { // part2

            $imgFace = clone $face;

            $imgFace = transform(-2, 2.42*$face_w, 2.42*$face_h, $imgFace);

            $frm = new Imagick("low_png_frm/frame_0".$frame->time. ".png");

            // $face = new Imagick("RESULT/frame_0".$frame->time. ".png");

            $frm->compositeImage($imgFace, imagick::COMPOSITE_DSTOVER, intval($frame->x +62 -241), intval($frame->y -32.3 -277)); 

            $img_outp = $face_dir."/frame_0". $frame->time. ".png";

            $frm->setImageCompressionQuality(40);

            $frm->stripImage();    

            $frm->writeImage(''.$img_outp); 

        } else if((intval($frame->time) >= 132) && (intval($frame->time) <= 196)) { // part3  132 (196)

            $anchor_x = 43.7;

            $anchor_y = 56.6;



            $x_zoom = $frame->scl_x/100;

            $y_zoom = $frame->scl_y/100;

            $scl_x = floatval($x_zoom)*36.2/100 * $face_w;

            $scl_y = floatval($y_zoom)*39.5/100 * $face_h;

            $scl_d = sqrt($scl_x*$scl_x/4 + $scl_y*$scl_y/4);



            $scl_D = sqrt($anchor_x*$anchor_x*$x_zoom*$x_zoom + $anchor_y*$anchor_y*$y_zoom*$y_zoom);

            $diameter = abs($scl_D - $scl_d);   // 



            $base_angle = atan($scl_y/$scl_x);

            $base_img_rotate = 0/180 *pi(); // Caused by when generate data from AE

            $rt = floatval($frame->rt)/180 *pi();

            $delta_rot = $base_angle + $rt + $base_img_rotate;



            $delta_x = $diameter * cos($delta_rot);

            $delta_y = $diameter * sin($delta_rot);



            $new_anchor_x = $frame->x + $delta_x;  // anchor point is in center of face image

            $new_anchor_y = $frame->y + $delta_y;  





            $imgFace = clone $face;

            $imgFace = transform(floatval($frame->rt), $scl_x, $scl_y, $imgFace);

            $shift_x = sin($frame->rt/180 * pi()) *$scl_y;  // X pos shift caused by rotate

            $shift_y = sin($frame->rt/180 * pi()) *$scl_x;



            $imgBG = new Imagick("low_png_frm/frame_0".$frame->time. ".png");

            if($frame->rt > 0) {

                $imgBG->compositeImage($imgFace, imagick::COMPOSITE_DSTOVER, intval($new_anchor_x-$shift_x), intval($new_anchor_y)); 

            } else {

                $imgBG->compositeImage($imgFace, imagick::COMPOSITE_DSTOVER, intval($new_anchor_x), intval($new_anchor_y +$shift_y)); 

            }

            $img_outp = $face_dir."/frame_0". $frame->time. ".png";

            $imgBG->setImageCompressionQuality(40);

            $imgBG->stripImage();    

            $imgBG->writeImage(''.$img_outp); 

        } else if((intval($frame->time) >= 377) && (intval($frame->time) <= 455)) { // part4

            $x_zoom = $frame->scl_x/100;

            $y_zoom = $frame->scl_y/100;

            $scl_x = floatval($x_zoom)*36.2/100 * $face_w;

            $scl_y = floatval($y_zoom)*39.5/100 * $face_h;

            $scl_d = sqrt($scl_x*$scl_x/4 + $scl_y*$scl_y/4);

            $diameter = $scl_d;



            $base_angle = atan($scl_y/$scl_x);

            $rt = floatval($frame->rt)/180 *pi();

            $delta_rot = $base_angle + $rt;



            $delta_x = $diameter * cos($delta_rot);

            $delta_y = $diameter * sin($delta_rot);



            $new_anchor_x = $frame->x - $delta_x;  // anchor point is in center of face image

            $new_anchor_y = $frame->y - $delta_y;  



            $imgFace = clone $face;

            $imgFace = transform(floatval($frame->rt), $scl_x, $scl_y, $imgFace); //floatval($frame->rt)

            $shift_x = sin($frame->rt/180 * pi()) *$scl_y;  // X pos shift caused by rotate

            $shift_y = sin($frame->rt/180 * pi()) *$scl_x;



            $imgBG = new Imagick("low_png_frm/frame_0".$frame->time. ".png");



            if($frame->rt > 0) {

                $imgBG->compositeImage($imgFace, imagick::COMPOSITE_DSTOVER, intval($new_anchor_x-$shift_x+5), intval($new_anchor_y-5)); 

            } else {

                $imgBG->compositeImage($imgFace, imagick::COMPOSITE_DSTOVER, intval($new_anchor_x+5), intval($new_anchor_y +$shift_y-5)); 

            }

            $img_outp = $face_dir."/frame_0". $frame->time. ".png";

            $imgBG->setImageCompressionQuality(40);

            $imgBG->stripImage();

            $imgBG->writeImage(''.$img_outp); 

        }

        



    }

    

    

    // compose($face_dir);

    // exec("/opt/local/bin/ffmpeg -framerate 30 -i RST/frame_0%03d.png -s:v 1280x720 -c:v libx264 -profile:v high -crf 23 -pix_fmt yuv420p -r 30 -vb 12M  RST/movies_2.mp4 2>&1", $o, $v);

    $end = time();

    echo " <br/> Time: ". ($end -$start). "<br/>";

    echo $end;



    function transform($rotate, $scale_x, $scale_y, $face) {

        $scale_x = intval($scale_x);          

        $scale_y = intval($scale_y);



        $bg = new ImagickPixel('none');

        // $imgFace = $face;

        $face->scaleImage($scale_x, $scale_y, false);

        $face->rotateImage($bg, $rotate); 

        return $face;

    }

    function compose($face_dir) {

        $ffmpeg = "/usr/bin/ffmpeg";

        $name = rand(1, 99999);

        exec("cp -r ./low_buffer/*.png $face_dir/ 2>&1", $out_cp, $log_cp);

        print_r($out_cp[0]);

        echo exec("chmod 777 $face_dir/*");

        exec("$ffmpeg -framerate 30 -i $face_dir/frame_0%03d.png -preset ultrafast -s:v 1280x720 -c:v libx264 -profile:v high -crf 23 -pix_fmt yuv420p -r 30 -vb 12M  $face_dir/movies_$name.mp4 1>&2", $o, $v);

        print_r($o[0]);

        exec("$ffmpeg -i $face_dir/movies_$name.mp4 -c copy -bsf:v h264_mp4toannexb -f mpegts $face_dir.ts", $o2, $v2);

        print_r($o2);

        exec("$ffmpeg -i \"concat:intermediate1.ts|$face_dir.ts|intermediate3.ts\" -c copy -bsf:a aac_adtstoasc -preset ultrafast output_$name.mp4", $o3, $v3);

        print_r($o3[0]);

        exec("rm -rf $face_dir/");

        exec("$ffmpeg -i output_$name.mp4 -i audio.mp3  -strict -2 -preset ultrafast Final_$name.mp4", $out_aud, $v_aud);

        print_r($out_aud);

        print_r($o3);



        echo "xong !";

        echo "<a href='Final_$name.mp4'>Movie</a>";

    }

    function add_buffer_frame($face_dir) {

        exec("cp -r low_buffer/* $face_dir 2>&1", $o, $v);

    }

    function new_working_dir($dir_name) {

        exec("mkdir -p $dir_name 2>&1", $o, $v);

        print_r($o);

    }



?>



Dữ liệu Transform data XML từ Adobe After Effect lưu vị trí mặt tại mỗi khung hình:

<?xml version="1.0" encoding="UTF-8"?>
<frames>
    <frame><time>044</time><x>591.562</x><y>334.75</y><scl_x>100</scl_x><scl_y>100</scl_y><rt>0</rt></frame>
    <frame><time>045</time><x>591.562</x><y>334.75</y><scl_x>100</scl_x><scl_y>100</scl_y><rt>0</rt></frame>
    <frame><time>046</time><x>591.562</x><y>334.75</y><scl_x>100</scl_x><scl_y>100</scl_y><rt>0</rt></frame>
    <frame><time>047</time><x>591.562</x><y>334.75</y><scl_x>100</scl_x><scl_y>100</scl_y><rt>0</rt></frame>
    <frame><time>048</time><x>591.562</x><y>334.75</y><scl_x>100</scl_x><scl_y>100</scl_y><rt>0</rt></frame>
    <frame><time>049</time><x>590.6</x><y>334.75</y><scl_x>100</scl_x><scl_y>100</scl_y><rt>0</rt></frame>
    <frame><time>050</time><x>590.6</x><y>334.75</y><scl_x>100</scl_x><scl_y>100</scl_y><rt>0</rt></frame>
    <frame><time>051</time><x>590.6</x><y>334.75</y><scl_x>100</scl_x><scl_y>100</scl_y><rt>0</rt></frame>
    <frame><time>052</time><x>590.6</x><y>334.75</y><scl_x>100</scl_x><scl_y>100</scl_y><rt>0</rt></frame>
    <frame><time>053</time><x>590.6</x><y>334.75</y><scl_x>100</scl_x><scl_y>100</scl_y><rt>0</rt></frame>
    <frame><time>054</time><x>589.6</x><y>334.75</y><scl_x>100</scl_x><scl_y>100</scl_y><rt>0</rt></frame>
    <frame><time>055</time><x>589.6</x><y>334.75</y><scl_x>100</scl_x><scl_y>100</scl_y><rt>0</rt></frame>
    <frame><time>056</time><x>589.6</x><y>334.75</y><scl_x>100</scl_x><scl_y>100</scl_y><rt>0</rt></frame> ....




File:  https://drive.google.com/file/d/0B9XwFe7bHCQ0YVZ6eUJmWE1Mc1k/view?usp=sharing

Update 1 Mar 17, 2018:

I update source code to https://github.com/FIF/cropzoom_laravel_jibjab
The web part have problem with update laravel from 4.1 to 5.6, I will fix and update later.
For sample video processing, you can clone source code and try run this URL:
http://localhost/Hannibal/cropzoom_laravel_jibjab2/resources/material/compose.php?face_name=AT4.png
Since I use Apache and put source code at /var/www/Hannibal/...
face_name is GET variable for sample face to process video, it accept Face_1.png to Face_11.png
Please see processing video file (compose.php) in resources folder for more detail.

I will update remain documents about source code and Adobe After Effect latter.

Comments

  1. ffmpeg -i music.wav -f mp2 music.mp3

    wget http://littlewing.wc.lt/sounds/music.wav

    ReplyDelete
  2. # first convert an image sequence to a movie
    ffmpeg -sameq -i %03d.jpg output.mp4

    # ... and then convert the movie to a GIF animation
    ffmpeg -i output.mp4 -pix_fmt rgb24 -s qcif -loop_output 0 output.gif

    ReplyDelete
  3. convert $(for ((a=0; a<700; a++)); do printf -- "-delay 10 name%s.png " $a; done;) result.gif

    ReplyDelete
  4. http://www.imagemagick.org/script/import.php

    ReplyDelete
  5. Hi, anyone have this example working? thanks!

    ReplyDelete
    Replies
    1. Wait me a bit. I will rebuild example and reply to you soon.
      Currently I am quite busy.

      Delete
    2. Thanks a lot Nick! that will really help me!
      have a great day!
      Rod

      Delete
    3. Hi Rodrigo, here is the PHP file for processing video: https://github.com/dungnv53/Laratype/blob/master/app/lib/LaravelPrototype/Common/VideoProcessing.php
      The whole simple website is here https://github.com/dungnv53/Laratype
      Because of there are some config, setup in order to make this site work, so I only focus on video processing path.

      Delete
    4. This comment has been removed by the author.

      Delete
    5. @Rodrigo I will update this post and try to rebuild project then provide you detail guide.
      The basic idea is simple. We have a face image, we have a short video.
      Then firstly, we cut video to frame (ie. 30s x 30fps = 900 frame).
      Second, we merge (compose) a face to each frame and then combined to a video.
      The position of face in each frame is stored in an array or an XML file. The position value is created using Adobe After Effect tracking tool or something like this.
      I will provide detail for archive this task latter. There are may be another way to done this ie. Fake Deep (but require a lot of sample data and computing power).
      I code in bash or PHP. PHP is only for Laravel to connect with other feature like cut user face from facebook/uploaded image, post video to youtube, facebook...

      Delete
    6. Hello Nick!!
      Thank you very much for your answers! they really help me a lot !! Thank you for your selfless help! I will investigate a little more with the new info that you send me, and I will be looking at the updates that you send.
      Thank you!
      PS: can I contact you privately?

      Delete
    7. Please add my Skype live: vandung_17
      Phone 841205029223

      Delete
    8. My skype name: dzung nguyen
      Quote: 925 trap

      Delete
    9. hi, i will sent you and email and also i already add you to skype
      thanks!

      Delete

Post a Comment

Popular posts from this blog

AWS Elasticache Memcached connection

https://docs.aws.amazon.com/AmazonElastiCache/latest/mem-ug/accessing-elasticache.html#access-from-outside-aws http://hourlyapps.blogspot.com/2010/06/examples-of-memcached-commands.html Access memcached https://docs.aws.amazon.com/AmazonElastiCache/latest/mem-ug/GettingStarted.AuthorizeAccess.html Zip include hidden file https://stackoverflow.com/questions/12493206/zip-including-hidden-files phpmemcachedadmin ~ phpMyAdmin or phpPgAdmin ... telnet mycachecluster.eaogs8.0001.usw2.cache.amazonaws.com 11211 stats items stats cachedump 27 100 https://docs.aws.amazon.com/AmazonElastiCache/latest/mem-ug/VPCs.EC.html https://lzone.de/cheat-sheet/memcached VPC ID Security Group ID (sg-...) Cluster: The identifier for the cluster memcached1 Creation Time: The time (UTC) when the cluster was created January 9, 2019 at 11:47:16 AM UTC+7 Configuration Endpoint: The configuration endpoint of the cluster memcached1.ahgofe.cfg.usw1.cache.amazonaws.com:11211 St...

Notes Windows 10 Virtualbox config, PHP Storm Japanese, custom PHP, Apache build, Postgresql

 cmd => Ctrl + Shift + Enter mklink "C:\Users\HauNT\Videos\host3" "C:\Windows\System32\drivers\etc\hosts" https://www.quora.com/How-to-create-a-router-in-php https://serverfault.com/questions/225155/virtualbox-how-to-set-up-networking-so-both-host-and-guest-can-access-internet 1 NAT + 1 host only config https://unix.stackexchange.com/questions/115464/how-to-properly-set-up-2-network-interfaces-in-centos-running-in-virtualbox DEVICE=eth0 TYPE=Ethernet #BOOTPROTO=dhcp BOOTPROTO=none #IPADDR=10.9.11.246 #PREFIX=24 #GATEWAY=10.9.11.1 #IPV4_FAILURE_FATAL=yes #HWADDR=08:00:27:CC:AC:AC ONBOOT=yes NAME="System eth0" [root@localhost www]# cat /etc/sysconfig/network-scripts/ifcfg-eth1 # Advanced Micro Devices, Inc. [AMD] 79c970 [PCnet32 LANCE] DEVICE=eth1 IPADDR=192.168.56.28 <= no eff => auto like DHCP #GATEWAY=192.168.56.1 #BOOTPROTO=dhcp BOOTPROTO=static <= no eff ONBOOT=yes HWADDR=08:00:27:b4:20:10 [root@localhost www]# ...

Rocket.Chat DB schema

_raix_push_notifications avatars.chunks avatars.files instances meteor_accounts_loginServiceConfiguration meteor_oauth_pendingCredentials meteor_oauth_pendingRequestTokens migrations rocketchat__trash rocketchat_cron_history rocketchat_custom_emoji rocketchat_custom_sounds rocketchat_import rocketchat_integration_history rocketchat_integrations rocketchat_livechat_custom_field rocketchat_livechat_department rocketchat_livechat_department_agents rocketchat_livechat_external_message rocketchat_livechat_inquiry rocketchat_livechat_office_hour rocketchat_livechat_page_visited rocketchat_livechat_trigger rocketchat_message rocketchat_oauth_apps rocketchat_oembed_cache rocketchat_permissions rocketchat_raw_imports rocketchat_reports rocketchat_roles rocketchat_room rocketchat_settings rocketchat_smarsh_history rocketchat_statistics rocketchat_subscription rocketchat_uploads system.indexes users usersSessions https://rocket.chat/docs/developer-guides/sc...