├── .dockerignore ├── .gitignore ├── Dockerfile ├── Dockerfile.centos ├── LICENSE ├── Makefile ├── README.md ├── TODO ├── VERSION ├── bin ├── create_quality_report.py ├── decode ├── encode.py ├── include │ └── vs_example.py ├── merge_quality_reports.py ├── readme.md └── results.py ├── ffmpeg4_modifications.diff ├── ffmpeg_modifications.diff ├── ffmpeg_phqm.diff ├── reference.cpp ├── scripts ├── clip_videos.sh ├── gen_cmd.sh ├── helper-scripts │ ├── build-per-title-ladder-csv.js │ ├── generate-objective-perceptual-analysis-tiers.js │ ├── package-lock.json │ ├── package.json │ └── readme.md ├── readme.md ├── run_example.sh └── run_per_title_analysis.sh ├── setup.sh ├── setupArch.sh ├── setupCentOS7.sh ├── setupGCC_540.sh ├── setupMacOSX.sh ├── stats.gp └── tests └── .keep /.dockerignore: -------------------------------------------------------------------------------- 1 | tests/* 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tests/* 2 | build.log 3 | id_rsa* 4 | bin/include/* 5 | reference 6 | */**/node_modules 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Christopher Kennedy - Objective Perceptual Analysis Encoders Dockerfile 2 | # 3 | ## create docker container 4 | # docker build --rm -t opaencoder . 5 | # 6 | ## execute scripts on files via docker 7 | # docker run --rm -v `pwd`/tests:/opaencoder/tests opaencoder sh scripts/run_example.sh 8 | # 9 | ## open a shell in docker container 10 | # docker run --rm -it opaencoder /bin/bash 11 | FROM archlinux 12 | 13 | COPY . /opaencoder 14 | RUN pacman --noconfirm -Syu && pacman --noconfirm -S make 15 | 16 | # build opaencoder 17 | WORKDIR /opaencoder 18 | RUN /bin/sh setupArch.sh 19 | 20 | # cleanup 21 | # RUN rm -R /root/_opaencoder_deps 22 | RUN chmod +x /opaencoder/scripts/*.sh 23 | 24 | LABEL version="0.2" 25 | LABEL description="Objective Perceptual Analysis Encoder" 26 | 27 | # Runtime 28 | ENV PATH=/opaencoder/bin:/opaencoder:$PATH 29 | RUN ldconfig 30 | RUN ffmpeg -version 31 | 32 | CMD ["ffmpeg", "-h", "full"] 33 | -------------------------------------------------------------------------------- /Dockerfile.centos: -------------------------------------------------------------------------------- 1 | # Christopher Kennedy - Objective Perceptual Analysis Encoders Dockerfile 2 | # 3 | # Access to Git ORG using SSH KEY CERT 4 | # by setting ARG SSH_PRIVATE_KEY= 5 | # must put clear public key (no passphrase) into objective_perceptual_analysis/id_rsa 6 | # during building the docker to allow github access for building ffmpeg. 7 | # 8 | ## create docker container 9 | # docker build --rm --build-arg SSH_PRIVATE_KEY=id_rsa -t opaencoder . 10 | # 11 | ## execute scripts on files via docker 12 | # docker run --rm -v `pwd`/tests:/opaencoder/tests opaencoder sh scripts/run_example.sh 13 | # 14 | ## open a shell in docker container 15 | # docker run --rm -it opaencoder /bin/bash 16 | FROM centos:7 17 | 18 | ARG SSH_PRIVATE_KEY 19 | WORKDIR /root/ 20 | 21 | # Dev 22 | RUN yum install -y -q https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm 23 | RUN yum -y update 24 | RUN yum -y -q group install "Development Tools" 25 | RUN yum install -y -q \ 26 | wget \ 27 | git \ 28 | clang \ 29 | cargo \ 30 | rust \ 31 | cmake3 \ 32 | sudo \ 33 | gnuplot \ 34 | mediainfo \ 35 | freetype-devel \ 36 | libass-devel \ 37 | fontconfig-devel \ 38 | meson \ 39 | ninja-build \ 40 | zlib-devel \ 41 | libpng-devel \ 42 | bzip2 \ 43 | which \ 44 | yasm \ 45 | python \ 46 | mediainfo \ 47 | openssl-devel 48 | 49 | # Setup GitHub Credentials 50 | RUN mkdir /root/.ssh/ 51 | COPY ${SSH_PRIVATE_KEY} /root/.ssh/id_rsa 52 | RUN touch /root/.ssh/known_hosts 53 | RUN chmod 600 /root/.ssh/* 54 | RUN ssh-keyscan github.com >> /root/.ssh/known_hosts 55 | 56 | # Get opaencoder 57 | RUN git clone git@github.com:crunchyroll/objective_perceptual_analysis.git opaencoder 58 | 59 | ENV PATH=/root/opaencoder/bin:/root/opaencoder:$PATH 60 | 61 | COPY ./setupCentOS7.sh /root/opaencoder/setupCentOS7.sh 62 | COPY ./Makefile /root/opaencoder/Makefile 63 | 64 | # Build system 65 | RUN cd opaencoder && make 66 | 67 | WORKDIR /root/opaencoder 68 | 69 | 70 | RUN yum clean all 71 | RUN rm -rf /var/cache/yum 72 | RUN rm -rf /var/lib/rpm 73 | 74 | # Clean up 75 | RUN cp /root/opaencoder/FFmpeg/ffmpeg /root/opaencoder/bin/ffmpeg 76 | RUN cp /root/opaencoder/FFmpeg/ffprobe /root/opaencoder/bin/ffprobe 77 | RUN rm -rf /root/opaencoder/FFmpeg 78 | RUN rm -rf /root/opaencoder/x264 79 | RUN rm -rf /root/opaencoder/aom 80 | RUN rm -rf /root/opaencoder/rav1e 81 | RUN rm -rf /root/opaencoder/SVT-AV1 82 | RUN rm -rf /root/opaencoder/opencv* 83 | RUN rm -rf /root/opaencoder/libvpx 84 | RUN rm -rf /root/opaencoder/dav1d 85 | RUN rm -rf /root/opaencoder/vmaf 86 | RUN rm -rf /root/opaencoder/nasm* 87 | RUN rm -rf /root/opaencoder/.git 88 | RUN rm -rf /usr/local/lib/*.a 89 | RUN rm -rf /usr/local/include 90 | 91 | RUN mv /root/opaencoder / 92 | RUN sudo chmod +x /opaencoder/scripts/*.sh 93 | 94 | WORKDIR /opaencoder 95 | 96 | LABEL version="0.2" 97 | LABEL description="Objective Perceptual Analysis Encoder" 98 | 99 | # Runtime 100 | ENV PATH=/opaencoder/bin:/opaencoder:/usr/local/bin:$PATH 101 | ENV LD_LIBRARY_PATH=/usr/local/lib64:$LD_LIBRARY_PATH 102 | RUN ldconfig 103 | RUN ffmpeg -version 104 | 105 | CMD ["ffmpeg", "-h", "full"] 106 | 107 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Chris Kennedy - Perceptual Encoder Makefile 3 | # 4 | # This works on CentOS 7 and Mac OS X 5 | # 6 | 7 | UNAME_S := $(shell uname -s) 8 | 9 | all: setup reference 10 | 11 | setup: 12 | ./setup.sh 13 | 14 | reference: 15 | g++ reference.cpp -std=c++11 -lopencv_core -lopencv_highgui -lopencv_img_hash -lopencv_imgproc -lopencv_imgcodecs -o reference 16 | 17 | x264lib: 18 | cd x264 && \ 19 | ./configure --prefix=/usr --disable-lavf --enable-static --enable-shared && \ 20 | make clean && \ 21 | make -j$(nproc) && \ 22 | sudo make install && \ 23 | sudo ldconfig 24 | 25 | vpxlib: 26 | cd libvpx/build/ && \ 27 | ../configure --prefix=/usr && \ 28 | make -j$(nproc) && \ 29 | sudo make install 30 | 31 | aomlib: 32 | rm -rf aom/aombuild && \ 33 | mkdir aom/aombuild && \ 34 | cd aom/aombuild/ && \ 35 | cmake3 \ 36 | -DCMAKE_INSTALL_PREFIX=/usr \ 37 | -DCMAKE_INSTALL_LIBDIR=lib \ 38 | -DBUILD_SHARED_LIBS=True \ 39 | -DCMAKE_BUILD_TYPE=Release ../ && \ 40 | make -j$(nproc) && \ 41 | sudo make install 42 | 43 | svtav1libmac: 44 | cd SVT-AV1/Build && \ 45 | cmake3 .. -G"Unix Makefiles" \ 46 | -DCMAKE_BUILD_TYPE=Release && \ 47 | make -j8 && \ 48 | sudo make install 49 | 50 | svtvp9libmac: 51 | cd SVT-VP9/Build && \ 52 | cmake3 .. -G"Unix Makefiles" \ 53 | -DCMAKE_BUILD_TYPE=Release && \ 54 | make -j8 && \ 55 | sudo make install 56 | 57 | svtav1lib: 58 | cd SVT-AV1/Build && \ 59 | cmake3 .. -G"Unix Makefiles" \ 60 | -DCMAKE_BUILD_TYPE=Release \ 61 | -DCMAKE_CXX_FLAGS="-I/usr/local/include -L/usr/local/lib" \ 62 | -DCMAKE_C_FLAGS="-I/usr/local/include -L/usr/local/lib" \ 63 | -DCMAKE_CXX_COMPILER=$(which g++) \ 64 | -DCMAKE_CC_COMPILER=$(which gcc) \ 65 | -DCMAKE_C_COMPILER=$(which gcc) && \ 66 | make -j$(nproc) && \ 67 | sudo make install 68 | 69 | svtvp9lib: 70 | cd SVT-VP9/Build && \ 71 | cmake3 .. -G"Unix Makefiles" \ 72 | -DCMAKE_BUILD_TYPE=Release \ 73 | -DCMAKE_CXX_FLAGS="-I/usr/local/include -L/usr/local/lib" \ 74 | -DCMAKE_C_FLAGS="-I/usr/local/include -L/usr/local/lib" \ 75 | -DCMAKE_CXX_COMPILER=$(which g++) \ 76 | -DCMAKE_CC_COMPILER=$(which gcc) \ 77 | -DCMAKE_C_COMPILER=$(which gcc) && \ 78 | make -j$(nproc) && \ 79 | sudo make install 80 | 81 | dav1dlib: 82 | cd dav1d && \ 83 | meson build --buildtype release && \ 84 | ninja-build -C build && \ 85 | cd build && \ 86 | sudo meson install 87 | 88 | rav1elib: 89 | cd rav1e && \ 90 | sudo cargo clean && \ 91 | sudo cargo build --release && \ 92 | sudo cargo cinstall --release 93 | 94 | vmaflib: 95 | cd vmaf && \ 96 | make -j$(nproc) && \ 97 | sudo make install 98 | 99 | ffmpegbin: 100 | cd FFmpeg && \ 101 | ./configure --prefix=/usr --enable-libx264 --enable-libvpx --enable-gpl --enable-libopencv --enable-version3 --enable-libvmaf --enable-libfreetype --enable-fontconfig --enable-libass --enable-libaom --enable-libsvtav1 && \ 102 | make clean && \ 103 | make -j$(nproc) 104 | 105 | ffmpegbinmac: 106 | cd FFmpeg && \ 107 | ./configure --prefix=/usr --enable-libx264 --enable-libvpx --enable-gpl --enable-libopencv --enable-version3 --enable-libvmaf --enable-libfreetype --enable-fontconfig --enable-libass --enable-libaom --enable-librav1e --enable-libdav1d && \ 108 | make clean && \ 109 | make -j8 110 | 111 | install: 112 | cd FFmpeg && \ 113 | sudo make install 114 | 115 | docker: 116 | docker build --rm -t opaencoder . 117 | 118 | docker_centos_deprecated: 119 | docker build -f Dockerfile.centos --rm --build-arg SSH_PRIVATE_KEY=id_rsa -t opaencoder_centos . 120 | 121 | docker_clean: 122 | docker system prune 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Objective Perceptual Analysis - Video Karma Predictor 2 | 3 | This is a kit for testing codecs objectively through FFmpeg. 4 | It employs VMAF, SSIM, PSNR and also a Perceptual Hash metric. 5 | Multiple encoding tests can be ran comparing the encoders and 6 | the encodings, encoding techniques. The metrics and test harness 7 | allow quick feedback to test theories and new code in FFmpeg. 8 | There's objective metrics, graphs and easy comparisons historically. 9 | 10 | Use OpenCV img_hash frame comparisons in FFmpeg libavfilter for per title encoding / Perceptual Quality comparisons 11 | 12 | Documentation on OpenCV img_hash: https://docs.opencv.org/trunk/d4/d93/group__img__hash.html 13 | 14 | This will use perceptual hashes from OpenCV's img_hash module which includes PHash 15 | and it the main algorithm used. Each video frame is compared to the last video frame 16 | then a hamming distance is derived from the two hashes. This values shows the perceptual 17 | similarity of the two images. The hamming distance is used to vary the encoders bitrate 18 | or CRF level. Currently only X264 is supported in this implementation. 19 | 20 | Also research via bin/encode.py and bin/results.py script: 21 | 22 | - Parallel Encoding / can set per test for comparisons 23 | - Quality both Objective and setup for Subjective tests 24 | - Easy encoding tests for H.265, VP9 and AV1 25 | - Perceptual Hash research with encoding decisions and metrics 26 | - Simple Objective metrics calculated 27 | - Frame images and metrics burn in per frame via SRT 28 | - Scene segmentation and analysis with objective metrics 29 | 30 | Everything can be easily setup via setup.sh, it will install what is necessary 31 | for the most part. Please report back any issues so this can be improved for edge cases. 32 | 33 | See the bin/readme.md file for information on bin/encode.py and bin/results.py. 34 | See the scripts/readme.md file for information on setting up tests. 35 | 36 | *Currenty works on Arch Linux (recommended), CentOS 7 (deprecated) and Mac OS X* 37 | *VMAF, libVPX, libAOM, libRav1e, svt-av1, libx264, libOpenCV build of FFmpeg* 38 | - rav1e support based off of work by Derek Buitenhuis 39 | https://github.com/dwbuiten/FFmpeg 40 | 41 | ## Setup 42 | 43 | ### Dockerfile setup: Easy and safest 44 | 45 | ``` 46 | type: make docker 47 | 48 | Example using the docker image: 49 | - sudo docker run --rm -v `pwd`/tests:/opaencoder/tests opaencoder sh scripts/run_example.sh 50 | - sudo docker run --rm -v `pwd`/tests:/opaencoder/tests opaencoder bin/encode.py -m vmaf,psnr,phqm,cambi\ 51 | -n tests/test000 -p 2 -t "01000X264H264|ffmpeg|twopass|S|mp4||1080|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|1000k|-maxrate:v|1500k|-bufsize:v|3000k|-minrate:v|1000k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats" 52 | 53 | ``` 54 | 55 | Makefile will run the proper setup script and install mediainfo, opencv, libx264, libvmaf, nasm 56 | git, wget, freetype-devel... Everything should be done for you, although if not report it as a bug. 57 | *Warning: Scripts will install / alter system packages via Sudo. Please keep this in mind* 58 | 59 | ### type: ```make``` 60 | 61 | This uses an FFmpeg with an extra video filter which uses OpenCV to 62 | compute hamming distance values from each frames hash vs. the previous 63 | frames hash. 64 | 65 | There is a ffmpeg_modifications.diff patch included... 66 | 67 | (this is done for you via the make command which runs the proper setup* script) 68 | 69 | ``` 70 | git clone https://git.ffmpeg.org/ffmpeg.git FFmpeg 71 | cd FFmpeg 72 | git checkout tags/n5.1.2 73 | cat ../ffmpeg_modifications.diff | patch -p1 74 | ``` 75 | 76 | You can run tests using the bin/encode.py script. See the /bin/readme.md for more 77 | details. 78 | 79 | ## FFmpeg Commands 80 | 81 | Perceptual Hash Quality Metric: (output a stats file with psnr/mse/phqm (perceptual hash quality metric) 82 | 83 | ```./FFmpeg/ffmpeg -i -i -filter_complex "[0:v][1:v]phqm=hash_type=phash:stats_file=stats.log" -f null -``` 84 | ``` 85 | phqm AVOptions: 86 | stats_file ..FV..... Set file where to store per-frame difference information. 87 | f ..FV..... Set file where to store per-frame difference information. 88 | scd_thresh ..FV..... Scene Change Detection Threshold. (from 0 to 1) (default 0.5) 89 | hash_type ..FV..... Type of Image Hash to use from OpenCV. (from 0 to 6) (default phash) 90 | average ..FV..... Average Hash 91 | blockmean1 ..FV..... Block Mean Hash 1 92 | blockmean2 ..FV..... Block Mean Hash 2 93 | colormoment ..FV..... Color Moment Hash 94 | marrhildreth ..FV..... Marr Hildreth Hash 95 | phash ..FV..... Perceptual Hash (PHash) 96 | radialvariance ..FV..... Radial Variance Hash 97 | ``` 98 | 99 | PHQM Scene Detection, frame ranges for each segmented scene with an avg hamming distance score per scene. 100 | 101 | ``` 102 | # (./FFmpeg/ffmpeg -loglevel warning -i encode.mp4 -i reference.mov -nostats -nostdin \ 103 | -threads 12 -filter_complex [0:v][1:v]phqm=stats_file=phqm.data -f null -) 104 | 105 | [phqm @ 0x40def00] ImgHashScene: n:1-231 hd_avg:0.861 hd_min:0.000 hd_max:6.000 scd:0.80 106 | [phqm @ 0x40def00] ImgHashScene: n:232-491 hd_avg:0.265 hd_min:0.000 hd_max:2.000 scd:0.57 107 | [phqm @ 0x40def00] ImgHashScene: n:492-541 hd_avg:0.340 hd_min:0.000 hd_max:2.000 scd:0.57 108 | [phqm @ 0x40def00] ImgHashScene: n:542-658 hd_avg:0.350 hd_min:0.000 hd_max:2.000 scd:0.82 109 | [phqm @ 0x40def00] ImgHashScene: n:659-708 hd_avg:0.420 hd_min:0.000 hd_max:2.000 scd:0.92 110 | [phqm @ 0x40def00] ImgHashScene: n:709-1057 hd_avg:1.009 hd_min:0.000 hd_max:6.000 scd:0.51 111 | [phqm @ 0x40def00] ImgHashScene: n:1058-1266 hd_avg:0.708 hd_min:0.000 hd_max:4.000 scd:0.59 112 | [Parsed_phqm_0 @ 0x40f1340] PHQM average:0.601282 min:0.000000 max:6.000000 113 | ``` 114 | 115 | This is implementing a Patent by Christopher Kennedy @ Ellation / Crunchyroll: 116 | 117 | Patent for https://patents.justia.com/patent/10244234 118 | Adaptive compression rate control 119 | 120 | ## VapourSynth support 121 | 122 | OPA can run VapourSynth filter chains on the mezzanines which can be associated to test labels. When using VapourSynth, new filtered mezzanines are being created which will be used as source files for the test encodes tagged with the same label. Metrics can either be calculated against the original mezzanine or the filtered mezzanine. 123 | 124 | ### Usage: 125 | 126 | - Make sure that the filters you want to use are installed on your system. On Arch Linux distributions you can simply run `setupArch.sh` to setup a VapourSynth environment with most relevant filter libraries and wrapper scripts. 127 | - Create wrapper scripts in the `bin/include` directory with your filter chains. They must have `src` and `args` arguments. They also need `return video` (or whatever your clip object is called) at the bottom. See `bin/include/vs_example.py` for an example. 128 | - Pass the `-vs` parameter to `bin/encode.py` with the following structure: `Label1,Label2,...|wrapper_script.wrapper_function|arg1,arg2,...;Label3|wrapper_script.wrapper_function|arg1,arg2,...;...` 129 | - Pass the `-vp` parameter to `bin/encode.py` to calculate encode metrics against the filtered mezzanines. 130 | 131 | Docker example: 132 | 133 | ``` 134 | docker run --rm -v `pwd`/tests:/opaencoder/tests opaencoder bin/encode.py -m vmaf,psnr \ 135 | -n tests/test000 -p 2 \ 136 | -t "01000X264H264|ffmpeg|twopass|S|mp4||1080|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|1000k|-maxrate:v|1500k|-bufsize:v|3000k|-minrate:v|1000k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats" \ 137 | -vs "01000X264H264|vs_example.example_wrapper|640,360" \ 138 | -vp 139 | ``` 140 | 141 | 142 | ## Notes 143 | 144 | Nov 18, 2016 145 | 146 | Disclosed by way of example embodiments are a system and a computer implemented 147 | method for adaptively encoding a video by changing compression rates for 148 | different frames of the video. In one aspect, two frames of a video are 149 | compared to determine a compression rate for compressing one of the two frames. 150 | Hash images may be generated for corresponding frames for the comparison. 151 | By comparing two hash images, a number of stationary objects and a number of 152 | moving objects in the two frames may be determined. Moreover, a compression rate 153 | may be determined according to the number of stationary objects and 154 | the number of moving objects. 155 | 156 | Patent number: 10244234 Filed: Nov 18, 2016 Date of Patent: Mar 26, 2019 Patent Publication Number: 20170150145 157 | Assignee: Ellation, Inc. (San Francisco, CA) Inventor: Chris Kennedy (Alameda, CA) Primary Examiner: Dramos Kalapodas 158 | Application Number: 15/356,510 159 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | TODO 2 | 3 | Dockerfile - alpine linux 4 | - FFmpeg APK + patch 5 | - simple and clean setup 6 | 7 | Quality Finder: 8 | - run low bitrate and high bitrate encodings 9 | - find sweet spot where VMAF gain and bitrate are best 10 | - rate scene complexity and machine learn patterns 11 | 12 | encode.py: 13 | - subtitle hardsub tests 14 | 15 | results.py: 16 | obj metrics 17 | - more graphs 18 | - more ways to analyze video encoding easier 19 | - setup screen shot comparisons 20 | - perceptual hash scene change / motion research 21 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.0.1 2 | -------------------------------------------------------------------------------- /bin/create_quality_report.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | from os import getcwd 5 | from os import listdir 6 | from os import environ 7 | from os import path 8 | import subprocess 9 | import json 10 | 11 | VIVICT_URLBASE = "http://svt.github.io/vivict/" 12 | STORAGE_URLBASE = "http://your-http-storage.com" 13 | REFERENCE_LABEL = "08000X264H264" 14 | CURRENT_DIR = getcwd() 15 | RESULTS_SCRIPT = "results.py" 16 | environ["PATH"] = "bin/:%s" % (environ["PATH"]) 17 | 18 | 19 | ap = argparse.ArgumentParser() 20 | ap.add_argument('-n', '--directory', dest='directory', required=True, help="Name of the tests base directory") 21 | ap.add_argument('-v', '--vivict_urlbase', dest='vivict_urlbase', default=VIVICT_URLBASE, required=False, help="Web server running vivict player") 22 | ap.add_argument('-s', '--storage_urlbase', dest='storage_urlbase', default=STORAGE_URLBASE, required=False, help="Web server with videos accessible") 23 | ap.add_argument('-r', '--reference_label', dest='reference_label', default=REFERENCE_LABEL, required=False, help="Reference label to use for comparisons") 24 | ap.add_argument('-d', '--debug', dest='debug', required=False, action='store_true', help="Debug") 25 | ap.add_argument('-q', '--quality', dest='quality', required=False, default=95.0, help="VMAF Quality Percentage minimum, 95.0 is the default") 26 | ap.add_argument('-e', '--exclude', dest='exclude', required=False, action='store_true', help="Exclude low quality, only show ones that pass minimum") 27 | ap.add_argument('-is', '--ignore_scenes', dest='ignore_scenes', required=False, action='store_true', help="Ignore scene scores when judging quality for bitrate ladder inclusion") 28 | args = vars(ap.parse_args()) 29 | 30 | base_directory = args['directory'] 31 | vivict_urlbase = args['vivict_urlbase'] 32 | storage_urlbase = args['storage_urlbase'] 33 | reference_label = args['reference_label'] 34 | debug = args['debug'] 35 | minimum_quality = float(args['quality']) 36 | exclude_quality = args['exclude'] 37 | ignore_scenes = not args['ignore_scenes'] 38 | 39 | encode_dir = "%s/encodes" % base_directory 40 | encodes = [f for f in listdir(encode_dir)] 41 | results = subprocess.check_output(['results.py', '-n', base_directory]) 42 | encode = None 43 | label = None 44 | stats = None 45 | metrics = None 46 | encode_list = {} 47 | reference_encode = None 48 | for r in results.splitlines(): 49 | if r[1] != ' ': 50 | encode = r[1:].replace(':', '') 51 | if debug: 52 | print("Encode: %s" % encode) 53 | encode_list[encode] = {} 54 | elif "Encode [" in r: 55 | label = r[11:].replace("]", "").replace("[", "").replace(":", "") 56 | if debug: 57 | print("Label: %s" % label) 58 | encode_list[encode]["label"] = label 59 | 60 | # get encode 61 | encode_file = None 62 | for e in sorted(encodes): 63 | if e[0] == '.' or e.split(".")[1] != "mp4": 64 | # skip .dotfiles 65 | continue 66 | if label in e and encode[0:len(encode)-(len(label))] in e: 67 | encode_file = e 68 | # get mezzanine 69 | for e in sorted(encodes): 70 | if e[0] == '.' or e.split(".")[1] != "mp4": 71 | # skip .dotfiles 72 | continue 73 | #print "encode: %s label: %s" % (encode, label) 74 | if reference_label in e and encode[0:len(encode)-(len(label))] in e and e[len(e)-4:] == ".mp4": 75 | reference_encode = e 76 | encode_list[encode]["reference"] = e 77 | encode_list[encode]["encode_file"] = encode_file 78 | 79 | elif "Stats: " in r: 80 | stats = r.replace(" Stats: ", "") 81 | if debug: 82 | print("Stats: %s" % (stats)) 83 | encode_list[encode]["stats"] = stats 84 | elif "Metrics: " in r: 85 | metrics = r.replace(" Metrics: ", "") 86 | if debug: 87 | print("Metrics: %s" % (metrics)) 88 | encode_list[encode]["metrics"] = metrics 89 | else: 90 | if debug: 91 | print("Scene: %s" % r) 92 | if "scenes" not in encode_list[encode]: 93 | encode_list[encode]["scenes"] = [] 94 | encode_list[encode]["scenes"].append(r[10:]) 95 | 96 | if debug: 97 | print("Encode List: %s" % json.dumps(encode_list, indent=4)) 98 | 99 | quality_good = {} 100 | 101 | print("Encode Quality Comparison %s" % base_directory) 102 | print("") 103 | print("") 104 | print("" % (storage_urlbase, base_directory, storage_urlbase, base_directory)) 107 | print("") 108 | for encode, data in sorted(encode_list.items()): 109 | metadata_file = "%s/encodes/%s.mp4_data.json" % (base_directory, encode) 110 | metadata = "None" 111 | if path.isfile(metadata_file): 112 | with open(metadata_file, 'r') as mf: 113 | metadata = mf.read() 114 | metadata_json = json.loads(metadata) 115 | metrics_json = json.loads(data["metrics"]) 116 | total_vmaf_score = float(metrics_json["vmaf"]) 117 | total_pdiff_score = float(metrics_json["hamm"]) 118 | total_psnr_score = float(metrics_json["psnr"]) 119 | data["pdiff"] = total_pdiff_score 120 | data["vmaf"] = total_vmaf_score 121 | data["psnr"] = total_psnr_score 122 | # check if this matches our minimum quality expectations 123 | if exclude_quality and total_vmaf_score < minimum_quality: 124 | continue 125 | if debug: 126 | print("Encode: %s" % encode) 127 | print("Mezzanine: %s" % data["reference"]) 128 | # {bitrate: 778, filesize: 10649765, duration: 109.48} 129 | print("Stats: %s" % data["stats"]) 130 | # {speed: 416.00, pfhd: 1.97 hamm: 1.66 phqm: 66.79 vmaf: 96.08, ssim: 1.00, psnr: 47.33} 131 | print("Metrics: %s" % data["metrics"]) 132 | 133 | print("") 134 | if "reference" not in data: 135 | data["reference"] = "Not-Finished-Yet" 136 | 137 | lowest_vmaf = total_vmaf_score 138 | if "scenes" not in data: 139 | data["scenes"] = [] 140 | else: 141 | for scene in data["scenes"]: 142 | vmaf_score = float(scene.split(" ")[5].split(":")[1]) 143 | # get lowest VMAF scene 144 | if vmaf_score < lowest_vmaf: 145 | lowest_vmaf = vmaf_score 146 | bcolor = "green" 147 | fcolor = "black" 148 | if total_vmaf_score < (minimum_quality - 10): 149 | bcolor = "purple" 150 | fcolor = "white" 151 | elif total_vmaf_score < (minimum_quality - 10): 152 | bcolor = "red" 153 | fcolor = "white" 154 | elif total_vmaf_score < (minimum_quality - 5): 155 | bcolor = "orange" 156 | elif total_vmaf_score < minimum_quality: 157 | bcolor = "yellow" 158 | elif lowest_vmaf < minimum_quality: 159 | # avg total score ok but individual scenes not ok 160 | bcolor = "yellow" 161 | print("") 207 | if data["reference"] not in quality_good: 208 | quality_good[data["reference"]] = [] 209 | if (vmaf_good or ignore_scenes) and data["vmaf"] >= minimum_quality: 210 | quality_good[data["reference"]].append(data["label"] + "_VMAF_[%0.2f:%0.2f:%0.2f]" % (data["vmaf"], data["pdiff"], data["psnr"])) 211 | 212 | print("

Encode Quality Comparsion (%s)

" % (storage_urlbase, base_directory, base_directory)) 105 | print("Bitrate Ladders Raw Json Data

") 106 | print("
Encodescenes
") 162 | print("" % (bcolor, fcolor, total_vmaf_score, data["label"])) 163 | print("" % ("%s?leftVideoUrl=%s&rightVideoUrl=%s&hideSourceSelector=1&hideHelp=1&score=0&quality=" % (vivict_urlbase, "%s/%s/%s" % (storage_urlbase, "%s/encodes" % base_directory, data["reference"]), "%s/%s/%s" % (storage_urlbase, "%s/encodes" % base_directory, "%s.mp4" % encode)), encode)) 164 | print("" % (reference_label, data["reference"])) 165 | # {"video": {"framerate": 23.976, "vbitrate": 102, "height": 240, "width": 427, "filesize": 1397598, "duration": 109.485}} 166 | print("" % (json.dumps(json.loads(metadata), indent=4).replace("\n", "
").replace(" ", " "))) 167 | print("
[%0.2f%%] Encode: (%s)%s
Reference: (%s)%s
Mediainfo: %s
Stats:%s
Metrics:%s
" % (json.dumps(json.loads(data["stats"]), indent=4).replace("\n", "
").replace(" ", " "), json.dumps(json.loads(data["metrics"]), indent=4).replace("\n", "
").replace(" ", " "))) 168 | vmaf_good = True 169 | for scene in data["scenes"]: 170 | start_frame, end_frame = scene.split(" ")[0].split("-") 171 | framerate = float(metadata_json["video"]["framerate"]) 172 | position = int(float(start_frame) / framerate) 173 | duration = int(float(int(end_frame) - int(start_frame)) / framerate) 174 | vmaf_score = float(scene.split(" ")[5].split(":")[1]) 175 | 176 | # %s?leftVideoUrl=%s&rightVideoUrl=%s&hideSourceSelector=1&hideHelp=1&position=10&duration=10 177 | if debug: 178 | print("Scene: %s" % scene) 179 | url = "%s?leftVideoUrl=%s&rightVideoUrl=%s&hideSourceSelector=1&hideHelp=1&position=%d&duration=%d&score=0&quality=" % (vivict_urlbase, 180 | "%s/%s/%s" % (storage_urlbase, "%s/encodes" % base_directory, data["reference"]), 181 | "%s/%s/%s" % (storage_urlbase, "%s/encodes" % base_directory, "%s.mp4" % encode), 182 | position, duration) 183 | if debug: 184 | print("Url: %s" % url) 185 | bcolor = "green" 186 | fcolor = "black" 187 | lcolor = "blue" 188 | if vmaf_score < (minimum_quality - 10): 189 | bcolor = "purple" 190 | fcolor = "white" 191 | lcolor = "yellow" 192 | elif vmaf_score < (minimum_quality - 10): 193 | bcolor = "red" 194 | fcolor = "white" 195 | lcolor = "yellow" 196 | elif vmaf_score < (minimum_quality - 5): 197 | bcolor = "orange" 198 | elif vmaf_score < minimum_quality: 199 | bcolor = "yellow" 200 | lcolor = "red" 201 | if vmaf_score < minimum_quality: 202 | vmaf_good = False 203 | print("" % (bcolor, 204 | fcolor, position, position+duration, 205 | vmaf_score, lcolor, lcolor, url, ": ".join(scene.split(" ")[1:8]))) 206 | print("
%d-%ds [%0.2f%%]: %s
") 213 | 214 | print("
") 215 | print("") 216 | print("") 217 | levels = {} 218 | ladder_file = "%s/ladders.json" % base_directory 219 | ladder_json = {} 220 | for k, v in sorted(iter(quality_good.items()), reverse = True): 221 | if k not in ladder_json: 222 | # mezzanine indexed bitrate ladder in json 223 | ladder_json[k.rsplit("_", 1)[0]] = {} 224 | print("

Encodes that pass Quality levels

" % k) 225 | for q in sorted(v, reverse = False): 226 | if k + ":" + q[:3] not in levels and reference_label not in q: 227 | print("" % q) 228 | levels[k + ":" + q[:3]] = q 229 | # bitrate ladder levels 230 | res = int(q[0:4]) # resolution 231 | br = int(q[5:10]) # bitrate 232 | codec = q[11:].split("_VMAF_")[0].split("_")[0] 233 | # mezzanine index, dict of resolutions 234 | ladder_json[k.rsplit("_", 1)[0]][res] = {} 235 | codec_key = "%s_%s" % (codec, base_directory[18:].replace("/","")) 236 | # use codec and test label to differentiate each codec/test's bitrate ladder and vmaf score 237 | ladder_json[k.rsplit("_", 1)[0]][res][codec_key] = {} 238 | ladder_json[k.rsplit("_", 1)[0]][res][codec_key]["bitrate"] = br 239 | ladder_json[k.rsplit("_", 1)[0]][res][codec_key]["vmaf"] = float(q.split("_VMAF_")[1].replace("[","").replace("]","").split(":")[0]) 240 | ladder_json[k.rsplit("_", 1)[0]][res][codec_key]["pdiff"] = float(q.split("_VMAF_")[1].replace("[","").replace("]","").split(":")[1]) 241 | ladder_json[k.rsplit("_", 1)[0]][res][codec_key]["psnr"] = float(q.split("_VMAF_")[1].replace("[","").replace("]","").split(":")[2]) 242 | print("

Mezzanine: %s

PASS: %s

") 243 | print("") 244 | 245 | good_qualities = "" 246 | last_mezz = "" 247 | for k, l in sorted(iter(levels.items()), reverse = True): 248 | mezz, br = k.split(":") 249 | if last_mezz != mezz: 250 | last_mezz = mezz 251 | good_qualities = "%s\\nMezzanine: %s" % (good_qualities, last_mezz) 252 | good_qualities = "%s\\n - %s" % (good_qualities, l) 253 | 254 | with open(ladder_file, 'w') as f: 255 | f.write(json.dumps(ladder_json, sort_keys=True)) 256 | 257 | print("\n") 258 | 259 | -------------------------------------------------------------------------------- /bin/decode: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | PATH=$(pwd)/bin:$PATH 4 | 5 | ffmpeg="ffmpeg" 6 | 7 | if [ -z $1 -o -z $2 ]; then 8 | echo "Usage: $0 [duration] [start_offset]" 9 | exit 1 10 | fi 11 | fpath="$1" 12 | dpath="$2" 13 | max_duration="" 14 | start_offset="" 15 | if [ "$3" = "" ]; then 16 | max_duration="" 17 | else 18 | max_duration="-t $3" 19 | fi 20 | 21 | if [ "$4" = "" ]; then 22 | start_offset="" 23 | else 24 | start_offset="-ss $4" 25 | fi 26 | 27 | # decode entire video to raw video in an avi format 28 | $ffmpeg -hide_banner -y -nostdin $start_offset \ 29 | -i "$fpath" -f avi -vcodec rawvideo -pix_fmt yuv420p \ 30 | -dn -sn -acodec copy \ 31 | $max_duration \ 32 | "$dpath" 33 | 34 | if [ ! -f "$dpath" ]; then 35 | echo "ERROR: $dpath doesn't exist, failed to decode" 36 | exit 1 37 | fi 38 | 39 | 40 | -------------------------------------------------------------------------------- /bin/include/vs_example.py: -------------------------------------------------------------------------------- 1 | import vapoursynth as vs 2 | core = vs.core 3 | 4 | def example_wrapper(src, args): 5 | video = core.resize.Spline36(src, width=args[0], height=args[1], format=vs.YUV420P8) 6 | return video 7 | -------------------------------------------------------------------------------- /bin/merge_quality_reports.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import json 5 | from os import path 6 | 7 | 8 | ap = argparse.ArgumentParser() 9 | ap.add_argument('-n', '--directories', dest='directories', required=True, help="Comma delimited list of test directories to merge") 10 | 11 | args = vars(ap.parse_args()) 12 | 13 | base_directories = args['directories'] 14 | 15 | directories = base_directories.split(",") 16 | 17 | ladders = {} 18 | for d in directories: 19 | ladder_file = "%s/ladders.json" % d 20 | label = d[6:].replace("/","") 21 | #print "Label: %s" % label 22 | if path.isfile(ladder_file): 23 | ladder_json = None 24 | with open(ladder_file, 'r') as lf: 25 | ladder_json = json.loads(lf.read()) 26 | # mezzanines 27 | for m, v in sorted(ladder_json.items()): 28 | if m not in ladders: 29 | ladders[m] = {} 30 | # resolutions 31 | for r, d in sorted(v.items()): 32 | if r not in ladders[m]: 33 | ladders[m][r] = {} 34 | # codecs 35 | for c, i in sorted(d.items()): 36 | if c not in ladders[m][r]: 37 | # codec information 38 | # - bitrate, vmaf score 39 | ladders[m][r][c] = i 40 | ladders[m][r][c]["mezzanine"] = "%s_%s_%s" % (m, c, r) 41 | ladders[m][r][c]["codec"] = "%s_%s" % (c, r) 42 | ladders[m][r][c]["resolution"] = "%s" % (r) 43 | else: 44 | print("Error, missing %s file" % ladder_file) 45 | 46 | print(json.dumps(ladders, indent = True, sort_keys = True)) 47 | -------------------------------------------------------------------------------- /bin/readme.md: -------------------------------------------------------------------------------- 1 | Encoding subjective and objective test harness 2 | 3 | bin/encode.py: contains logic to run encoding tests using FFmpeg/ffmpeg 4 | bin/results.py: gather stats and produce reports comparing tests 5 | bin/decode: produce decoded output for test metrics 6 | 7 | scripts/run_example.sh: example of a command line with many tests 8 | scripts/clip_videos.sh: clipping video helper script to create in/out points 9 | scripts/gen_cmd.sh: help create multiple command lines of incrementing bitrates 10 | 11 | General usage: 12 | - setup FFmpeg for perceptual hash per title encoding 13 | - copy mezzanines to test into ./[test_dir]/mezzanines/ directory 14 | - execute ./bin/encode.py -n test_dir ... (-h for arguments) 15 | - execute ./bin/results.py -n test_dir 16 | 17 | Directories: 18 | * [test_dir]/mezzanines/ put video files in this directory to use for analysis and testing. 19 | * [test_dir]/encodes/ contains encoding variants and .json stats files with 20 | encoding parameters. 21 | * [test_dir]/results/ contains metric output (vmaf, msssim, psnr). 22 | 23 | Example steps: 24 | 1. build ffmpeg following instructions in base readme file 25 | 2. copy mezzanines to ./[test_dir]/mezzanines/ 26 | 3. execute bin/encode.py (see bin/encode.py -h for Help Output) 27 | example: (multiple tests can be separated by commas) 28 | 29 | ```bin/encode.py -m psnr,vmaf -n tests/test001 -p 4 \ 30 | -t "test1|ffmpeg|twopass|S|mp4|-vcodec|libx264|-vf|perceptual|-b:v|4000k|-maxrate:v|4000k|-bufsize|6000k" -d -o 31 | ``` 32 | 33 | The PATH environment is set to include ./FFmpeg/ so it will use the custom one automatically. 34 | Format - ```Label|FFmpegBin|RateControl|Flags|Format|arg|arg;Label|FFmpegBin|RC|Flgs|Fmt|arg|arg;...``` 35 | - multiple sets of encode tests separted by semi colons with each FFmpeg 36 | arg separated by pipes, with a label, binary path, and rate control method. 37 | - FLAGS: 38 | - S=segmented/parallel encoding mode 39 | - F=force mezzanine fps (use -r FPS when encoding) 40 | - Encoders: 41 | - ffmpeg - supports FFmpeg/ffmpeg build via 'ffmpeg' 42 | - SvtAv1EncApp - if given as an encoder then it is used instead of ffmpeg 43 | 44 | format output supported: 45 | 46 | - webm 47 | - mp4 48 | 49 | ratecontrol options: 50 | 51 | - twopass (implemented) 52 | - crf_NN 53 | - perceptual_crf_NN 54 | - perceptual_abr 55 | - abr 56 | 57 | Encoding Features / Research items: 58 | 59 | - Parallel / Segmented encoding (especially for AV1) 60 | - Image Hash x264 Perceptual encode optimization (WIP, help if you want) 61 | - Perceptual Hash metric for analysis 62 | - AV1 Encoding 63 | - VP9 Encoding 64 | - H.264 Encoding 65 | 66 | 4. execute ```bin/results.py -n tests/test001``` to get results 67 | 68 | Results Json ```stats.json``` output is CSV compatible in any converter. 69 | Also there is a graph ```stats.jpg``` created using gnuplot and ```stats.csv```. 70 | 71 | -------------------------------------------------------------------------------- /bin/results.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import datetime 5 | import json 6 | import subprocess 7 | from os import getcwd 8 | from os import listdir 9 | from os import mkdir 10 | from os.path import isfile, isdir, join, getsize 11 | from os.path import basename 12 | from os.path import splitext 13 | from os import environ 14 | 15 | environ["GDFONTPATH"] = "/usr/share/fonts/msttcorefonts/" 16 | environ["PATH"] = "%s/FFmpeg:%s" % (getcwd(), environ["PATH"]) 17 | 18 | # results list 19 | results = [] 20 | 21 | debug = False 22 | preview = False 23 | base_directory = None 24 | 25 | ap = argparse.ArgumentParser() 26 | ap.add_argument('-n', '--directory', dest='directory', required=True, help="Name of the tests base directory") 27 | ap.add_argument('-d', '--debug', dest='debug', required=False, action='store_true', help="Debug") 28 | ap.add_argument('-p', '--preview', dest='preview', required=False, action='store_true', help="Create preview videos with metrics burned in") 29 | args = vars(ap.parse_args()) 30 | 31 | base_directory = args['directory'] 32 | debug = args['debug'] 33 | preview = args['preview'] 34 | 35 | mezz_dir = "%s/mezzanines" % base_directory 36 | encode_dir = "%s/encodes" % base_directory 37 | result_dir = "%s/results" % base_directory 38 | preview_dir = "%s/preview" % base_directory 39 | 40 | # return video timecode from seconds value 41 | def secs2time(s): 42 | ms = int((s - int(s)) * 1000000) 43 | s = int(s) 44 | # Get rid of this line if s will never exceed 86400 45 | while s >= 24*60*60: s -= 24*60*60 46 | h = s / (60*60) 47 | s -= h*60*60 48 | m = s / 60 49 | s -= m*60 50 | timecode_microseconds = datetime.time(h, m, s, ms).isoformat() 51 | if '.' in timecode_microseconds: 52 | base_time, microseconds = timecode_microseconds.split('.') 53 | else: 54 | base_time = timecode_microseconds 55 | microseconds = 0 56 | return "%s,%03d" % (base_time, int(microseconds) / 1000) 57 | 58 | 59 | # get list of mezzanines 60 | mezzanines = [f for f in listdir(mezz_dir)] 61 | 62 | for m in mezzanines: 63 | if m[0] == '.': 64 | # skip .dotfiles 65 | continue 66 | # this specific mezzanines result dictionary 67 | result = {} 68 | # remove mezzanine file extension for base name of test files 69 | mbase = "%s" % splitext(m)[0] 70 | if debug: 71 | print("\nMezzanine %s:" % mbase) 72 | 73 | fkey = mbase 74 | result_key_spacer = "%s-1000" % (fkey) 75 | result[result_key_spacer] = {} 76 | 77 | # grab encode stats list for this mezzanine 78 | encode_stats = [f for f in listdir(encode_dir) if f.startswith(mbase) if f.endswith(".json")] 79 | for es in sorted(encode_stats): 80 | # remove extensions, both .json and _data 81 | ebase = "%s" % splitext(splitext(es)[0])[0] 82 | elabel = ebase[len(mbase):] 83 | if len(elabel[1:].split('_')) > 2 or (len(mbase) > len(ebase)) or elabel[0] != '_': 84 | if debug: 85 | print("Warning: Wrong encode status file for %s: %s" % (mbase, es)) 86 | print("\t- %s, %s, %s, %s" % (m, ebase, mbase, elabel[1:])) 87 | continue 88 | n, l = elabel[1:].split('_') 89 | # turn alphabet character into an index number for human readable label 90 | if len(l) == 3: 91 | hindex = (ord(l[0].lower()) - 96) - 1 92 | hindex += (ord(l[1].lower()) - 96) - 1 93 | hindex += (ord(l[2].lower()) - 96) - 1 94 | elif len(l) == 2: 95 | hindex = (ord(l[0].lower()) - 96) - 1 96 | hindex += (ord(l[1].lower()) - 96) - 1 97 | elif len(l) == 1: 98 | hindex = (ord(l.lower()) - 96) - 1 99 | else: 100 | print("ERROR: Invalid index letter %s" % l) 101 | continue 102 | # test label as setup in encode.py 103 | hlabel = n 104 | 105 | # get encode stats from encode json data 106 | bitrate = 0 107 | filesize = 0 108 | duration = 0.0 109 | height = 0 110 | width = 0 111 | framerate = 0.0 112 | with open("%s/%s" % (encode_dir, es)) as encode_stats_json: 113 | try: 114 | ed = json.load(encode_stats_json) 115 | # case of identify job output w/out encoding 116 | if 'video' in ed: 117 | ed = ed['video'] 118 | if 'vbitrate' in ed: 119 | bitrate = int(ed['vbitrate']) 120 | if 'filesize' in ed: 121 | filesize = int(ed['filesize']) 122 | if 'duration' in ed: 123 | duration = float(ed['duration']) 124 | if 'framerate' in ed: 125 | framerate= float(ed['framerate']) 126 | if 'height' in ed: 127 | height = float(ed['height']) 128 | if 'width' in ed: 129 | width = float(ed['width']) 130 | except Exception as e: 131 | if debug: 132 | print("error: %s %s" % (es, e)) 133 | 134 | # Combine PHQM segment scores with VMAF 135 | result_base = result_dir + '/' + ebase 136 | phqm_stats = result_base + "_phqm.data" 137 | phqm_stdout = result_base + "_phqm.stdout" 138 | vmaf_data = result_base + "_vmaf.data" 139 | phqm_scd = result_base + "_phqm.scd" 140 | preview_srt = result_base + "_preview.srt" 141 | 142 | sections = [] 143 | try: 144 | # scene change segments avg score calc 145 | vd = {} 146 | if isfile(vmaf_data): 147 | with open(vmaf_data) as vmaf_json: 148 | # get vmaf data 149 | vd = json.load(vmaf_json) 150 | 151 | # create srt file for metrics OSD 152 | if isfile(phqm_stats) and (not isfile(preview_srt) or getsize(preview_srt) < 0): 153 | psnr_metrics = None 154 | with open(phqm_stats, "r") as f: 155 | psnr_metrics = f.readlines() 156 | psnr_metrics = [x.strip() for x in psnr_metrics] 157 | with open(preview_srt, "w") as f: 158 | for l in psnr_metrics: 159 | parts = l.split(' ') 160 | frame = int(parts[0].split(':')[1]) 161 | phqm_avg = float(parts[1].split(':')[1]) 162 | scd = float(parts[4].split(':')[1]) 163 | #psnr_avg = parts[9].split(':')[1] 164 | start_seconds = (1.0/float(framerate)) * float(frame) 165 | end_seconds = (1.0/float(framerate)) * (float(frame) + .9) 166 | start_time = secs2time(start_seconds) 167 | end_time = secs2time(end_seconds) 168 | vmaf = float(vd["frames"][frame-1]["metrics"]["vmaf"]) 169 | msssim = float(vd["frames"][frame-1]["metrics"]["ms_ssim"]) 170 | psnr = float(vd["frames"][frame-1]["metrics"]["psnr"]) 171 | srt_line = "%08d\n%s --> %s\nTIMECODE[%s] SCD[%0.1f] PHQM[%0.3f] VMAF[%0.1f] PSNR[%0.1f] SSIM[%0.3f]\n\n" % (frame, 172 | start_time, end_time, start_time, scd, phqm_avg, vmaf, psnr, msssim) 173 | f.write(srt_line) 174 | 175 | if not isfile(phqm_scd) and isfile(phqm_stdout): 176 | # read stdout with scenes segmented into frame ranges 177 | with open(phqm_stdout) as phqm_data: 178 | for i, line in enumerate(phqm_data): 179 | if "ImgHashScene:" in line: 180 | parts = line.split(' ') 181 | start_frame, end_frame = parts[4].split(':')[1].split('-') 182 | start_frame = int(start_frame) 183 | end_frame = int(end_frame) 184 | phqm_avg = float(parts[5].split(':')[1]) 185 | phqm_min = 0.0 186 | phqm_max = 0.0 187 | sft = 0.0 188 | hft = 0.0 189 | if len(parts) >= 8: 190 | phqm_min = float(parts[6].split(':')[1]) 191 | phqm_max = float(parts[7].split(':')[1]) 192 | if len(parts) >= 11: 193 | hft = float(parts[9].split(':')[1]) 194 | sft = float(parts[10].split(':')[1]) 195 | vmaf_total = 0.0 196 | psnr_total = 0.0 197 | ms_ssim_total = 0.0 198 | for n, frame in enumerate(vd["frames"][start_frame-1:end_frame-1]): 199 | vmaf_total += float(frame["metrics"]["vmaf"]) 200 | psnr_total += float(frame["metrics"]["psnr"]) 201 | ms_ssim_total += float(frame["metrics"]["ms_ssim"]) 202 | #print "VMAF end_frame: %d start_frame: %d" % (start_frame, end_frame) 203 | vmaf_avg = vmaf_total 204 | psnr_avg = psnr_total 205 | ms_ssim_avg = ms_ssim_total 206 | if end_frame > start_frame: 207 | # if last frame was a scene change we may have only 1 frame in a section 208 | vmaf_avg = vmaf_total / (end_frame - start_frame) 209 | psnr_avg = psnr_total / (end_frame - start_frame) 210 | ms_ssim_avg = ms_ssim_total / (end_frame - start_frame) 211 | section = {} 212 | section["number"] = i 213 | section["nframes"] = end_frame - start_frame 214 | section["start_frame"] = start_frame 215 | section["end_frame"] = end_frame 216 | section["hamm_avg"] = phqm_avg 217 | section["hamm_min"] = phqm_min 218 | section["hamm_max"] = phqm_max 219 | section["phqm_avg"] = min(100.0, 100.0 - (20.0 * min(phqm_avg, 5.0))) 220 | section["vmaf_avg"] = vmaf_avg 221 | section["ssim_avg"] = ms_ssim_avg 222 | section["psnr_avg"] = psnr_avg 223 | section["sft"] = sft 224 | section["hft"] = hft 225 | sections.append(section) 226 | # write combined metrics to a json file for scd 227 | with open(phqm_scd, "w") as f: 228 | f.write("%s" % json.dumps(sections, sort_keys=True)) 229 | except Exception as e: 230 | print("Error opening %s: %s" % (vmaf_data, e)) 231 | 232 | # read scd file if it was created 233 | scenes = [] 234 | if isfile(phqm_scd): 235 | with open(phqm_scd, "r") as scd_data: 236 | sections = json.load(scd_data) 237 | video_files = [] 238 | for i, s in enumerate(sections): 239 | mdetail = "%03d). %06d-%06d hamm:%0.3f min:%0.2f max:%0.2f phqm:%0.2f vmaf:%0.2f psnr:%0.2f ssim:%0.2f sft:%0.3f hft:%0.3f" % (i, 240 | s["start_frame"], s["end_frame"], 241 | s["hamm_avg"], s["hamm_min"], s["hamm_max"], s["phqm_avg"], s["vmaf_avg"], s["psnr_avg"], s["ssim_avg"], s["sft"], s["hft"]) 242 | scenes.append(mdetail) 243 | image_dir_base = preview_dir + "/" + ebase + "_" + "%06d-%06d" % (s["start_frame"], s["end_frame"]) 244 | image_dir_period = image_dir_base + "/" + "images" 245 | video_dir_base = preview_dir + "/" + ebase + "/" + "videos" 246 | video_dir_period = video_dir_base + "/" + "%03d_%06d-%06d" % (i+1, s["start_frame"], s["end_frame"]) 247 | if not isdir(preview_dir): 248 | mkdir(preview_dir) 249 | if not isdir(image_dir_base): 250 | mkdir(image_dir_base) 251 | if not isdir("%s/%s" % (preview_dir, ebase)): 252 | mkdir("%s/%s" % (preview_dir, ebase)) 253 | mkdir("%s/%s/videos" % (preview_dir, ebase)) 254 | 255 | # create a directory for the images in the frame range 256 | if preview and not isdir(image_dir_period): 257 | mkdir(image_dir_period) 258 | 259 | # ffmpeg -i mezzanine -vf select='between(n\,%d\,%d),setpts=PTS-STARTPTS' image_dir_period/%08d.jpg 260 | # drawtext=text='Test Text':fontcolor=white:fontsize=75:x=1002:y=100: 261 | # box=1:boxcolor=black@0.7 262 | subprocess.call(['ffmpeg', '-hide_banner', '-y', '-nostdin', '-nostats', '-loglevel', 'error', 263 | '-i', "%s/%s" % (mezz_dir, m), '-vf', 264 | "select='between(n\,%d\,%d)',setpts=PTS-STARTPTS,drawtext=text='%s':fontcolor=white:box=1:boxcolor=black@0.7:fontsize=28:x=5:y=5" % (s["start_frame"], s["end_frame"], mdetail.replace(':', ' ').replace(')', '')), 265 | '-pix_fmt', 'yuv420p', 266 | "%s/%%08d.jpg" % image_dir_period]) 267 | # create video segment with stats burned in 268 | if preview and not isfile("%s.mp4" % video_dir_period): 269 | subprocess.call(['ffmpeg', '-hide_banner', '-y', '-nostdin', '-nostats', '-loglevel', 'error', 270 | '-i', "%s/%s" % (mezz_dir, m), '-vf', 271 | "select='between(n\,%d\,%d)',setpts=PTS-STARTPTS,drawtext=text='%s':fontcolor=white:box=1:boxcolor=black@0.7:fontsize=28:x=5:y=5" % (s["start_frame"], s["end_frame"], mdetail.replace(':', ' ').replace(')', '')), 272 | '-pix_fmt', 'yuv420p', '-an', 273 | "%s.mp4" % video_dir_period]) 274 | 275 | # save video segment for concatenation later 276 | video_files.append("%s.mp4" % video_dir_period) 277 | 278 | video_concat = preview_dir + "/" + ebase + ".mp4" 279 | if preview and not isfile(video_concat) and i == (len(sections)-1): 280 | # last segment, concatenate them all 281 | # 282 | # ffmpeg -i segment[0] -i segment[1] -i segment[2] -filter_complex \ 283 | # '[0:0] [1:0] [2:0] concat=n=3:v=1:a=0 [v]' \ 284 | # -map '[v]' output.mp4 285 | cmd = ['ffmpeg', '-hide_banner', '-y', '-nostdin', '-nostats', 286 | '-loglevel', 'error'] 287 | for sfile in video_files: 288 | # input files 289 | cmd.append('-i') 290 | cmd.append(sfile) 291 | cmd.append('-filter_complex') 292 | filter_str = "" 293 | for order, sfile in enumerate(video_files): 294 | # input streams per file 295 | filter_str = filter_str + "[%d:0] " % (order) 296 | filter_str = filter_str + "concat=n=%d:v=1:a=0,subtitles=%s [v]" % (len(sections), preview_srt) 297 | cmd.append(filter_str) 298 | cmd.append('-map') 299 | cmd.append('[v]') 300 | cmd.append(video_concat) 301 | 302 | if debug: 303 | print("Running cmd: %r" % cmd) 304 | subprocess.call(cmd) 305 | 306 | # grab MSU results list for this mezzanine 307 | metrics = [f for f in listdir(result_dir) if f.startswith(ebase) if f.endswith(".json")] 308 | phqm = 0.0 309 | vmaf = 0.0 310 | psnr = 0.0 311 | ssim = 0.0 312 | speed = 0.0 313 | pfhd = 0.0 314 | for ms in metrics: 315 | # base filename per metric type 316 | rbase = "%s" % splitext(ms)[0] 317 | # metric label for type of score 318 | label = rbase[len(ebase):][1:] 319 | score = None 320 | with open("%s/%s" % (result_dir, ms)) as result_stats_json: 321 | try: 322 | rd = json.load(result_stats_json) 323 | # get avg score from MSU results 324 | if 'avg' in rd: 325 | score = rd['avg'][0] 326 | # encoding speed (system dependent) 327 | if label == 'speed' and 'speed' in rd: 328 | speed = float(rd['speed']) 329 | except Exception as e: 330 | if debug: 331 | print("Bad stats file: %s" % result_stats_json) 332 | continue # skip this, probably truncated in progress writing 333 | # save score as type 334 | if score: 335 | if label == 'phqm': 336 | phqm = float(score) 337 | if label == 'pfhd': 338 | pfhd = float(score) 339 | if label == 'vmaf': 340 | vmaf = float(score) 341 | elif label == 'msssim': 342 | ssim = float(score) 343 | elif label == 'psnr': 344 | psnr = float(score) 345 | #result_key = "%s-%s_%s" % (fkey, str("%0.3f" % vmaf).replace('.','-'), elabel[1:]) 346 | result_key = "%s_%s" % (fkey, elabel[1:]) 347 | result[result_key] = {} 348 | 349 | print(" %s:" % result_key) 350 | print(" Encode [%s]:" % elabel) 351 | print(" Stats: {\"bitrate\": %d, \"filesize\": %d, \"duration\": %0.2f}" % (bitrate, filesize, duration)) 352 | # pick out specific codec values we are testing 353 | result[result_key]['bitrate'] = bitrate 354 | result[result_key]['filesize'] = filesize 355 | result[result_key]['duration'] = duration 356 | result[result_key]['mezzanine'] = fkey 357 | result[result_key]['label'] = hlabel 358 | 359 | phqm_normalized = min(100, (100 - (min(phqm, 5) * 20.0))) 360 | print(" Metrics: {\"speed\": %0.2f, \"pfhd\": %0.2f, \"hamm\": %0.2f, \"phqm\": %0.2f, \"vmaf\": %0.2f, \"ssim\": %0.2f, \"psnr\": %0.2f}" % (speed, 361 | pfhd, phqm, phqm_normalized, vmaf, ssim, psnr)) 362 | for s in scenes: 363 | print(" %s" % s) 364 | 365 | result[result_key]['speed'] = speed 366 | result[result_key]['phqm'] = "%0.3f" % phqm_normalized 367 | result[result_key]['hamm'] = "%0.3f" % phqm 368 | result[result_key]['vmaf'] = "%0.3f" % vmaf 369 | result[result_key]['ssim'] = "%0.3f" % ssim 370 | result[result_key]['psnr'] = "%0.3f" % psnr 371 | result[result_key]['pfhd'] = "%0.3f" % pfhd 372 | 373 | # append result to total results for all mezzanines 374 | if float(vmaf) > 0 and float(ssim) > 0 and float(psnr) > 0 and float(phqm) >= 0: 375 | results.append(result) 376 | elif debug: 377 | print("Skipping: %s" % m) 378 | 379 | with open("%s/stats.json" % base_directory, "w") as f: 380 | f.write("%s" % json.dumps(results, sort_keys=True)) 381 | 382 | results_avg = {} 383 | for result in sorted(results): 384 | for label, data in sorted(result.items()): 385 | bitrate = 0 386 | phqm = 0.0 387 | vmaf = 0.0 388 | ssim = 0.0 389 | psnr = 0.0 390 | pfhd = 0.0 391 | test_label = label 392 | speed = 0 393 | for key, value in data.items(): 394 | if key == "bitrate": 395 | bitrate = value 396 | elif key == "phqm": 397 | phqm = float(value) 398 | elif key == "pfhd": 399 | pfhd = float(value) 400 | elif key == "vmaf": 401 | vmaf = float(value) 402 | elif key == "psnr": 403 | psnr = float(value) 404 | elif key == "ssim": 405 | ssim = float(value) 406 | elif key == "label": 407 | test_label = value 408 | elif key == "speed": 409 | speed = int(value) 410 | 411 | # setup test label if not in the dict yet 412 | if not test_label in results_avg: 413 | results_avg[test_label] = {} 414 | results_avg[test_label]['psnr'] = [] 415 | results_avg[test_label]['phqm'] = [] 416 | results_avg[test_label]['pfhd'] = [] 417 | results_avg[test_label]['vmaf'] = [] 418 | results_avg[test_label]['ssim'] = [] 419 | results_avg[test_label]['bitrate'] = [] 420 | results_avg[test_label]['speed'] = [] 421 | 422 | # collect values into lists 423 | results_avg[test_label]['psnr'].append(psnr) 424 | results_avg[test_label]['pfhd'].append(pfhd) 425 | results_avg[test_label]['phqm'].append(phqm) 426 | results_avg[test_label]['vmaf'].append(vmaf) 427 | results_avg[test_label]['ssim'].append(ssim) 428 | results_avg[test_label]['bitrate'].append(bitrate) 429 | results_avg[test_label]['speed'].append(speed) 430 | 431 | # create dat file for CSV or GNUPlot 432 | body = "# test\tpfhd\tphqm\tvmaf\tssim\tpsnr\tbitrate\tspeed\n" 433 | for label, data in sorted(results_avg.items()): 434 | bitrate = 0 435 | vmaf = 0.0 436 | phqm = 0.0 437 | pfhd = 0.0 438 | ssim = 0.0 439 | psnr = 0.0 440 | speed = 0 441 | for key, value in data.items(): 442 | if key == "bitrate": 443 | for b in value: 444 | bitrate += int(b) 445 | bitrate = int(bitrate / len(value)) 446 | elif key == "speed": 447 | for b in value: 448 | speed += int(b) 449 | speed = int(speed / len(value)) 450 | elif key == "phqm": 451 | for b in value: 452 | phqm += float(b) 453 | phqm = float(phqm/ len(value)) 454 | elif key == "pfhd": 455 | for b in value: 456 | pfhd += float(b) 457 | pfhd = float(pfhd/ len(value)) 458 | elif key == "vmaf": 459 | for b in value: 460 | vmaf += float(b) 461 | vmaf = float(vmaf / len(value)) 462 | elif key == "psnr": 463 | for b in value: 464 | psnr += float(b) 465 | psnr = float(psnr / len(value)) 466 | elif key == "ssim": 467 | for b in value: 468 | ssim += float(b) 469 | ssim = float(ssim / len(value)) 470 | if vmaf > 0 and ssim > 0 and psnr > 0 and phqm >= 0: 471 | body = "%s%s\t%0.3f\t%0.3f\t%0.3f\t%0.3f\t%0.3f\t%d\t%d\n" % (body, label, pfhd, phqm, vmaf, ssim, psnr, bitrate, speed) 472 | 473 | with open("%s/stats.csv" % base_directory, "w") as f: 474 | f.write("%s" % body) 475 | 476 | # copy gnuplot config template into the test base directory 477 | gpdata = [] 478 | with open("stats.gp", "r") as f: 479 | gpdata = f.readlines() 480 | with open("%s/stats.gp" % base_directory, "w") as f: 481 | for l in gpdata: 482 | l = l.replace("__TITLE__", "%s" % base_directory) 483 | f.write("%s" % l) 484 | 485 | if len(results) > 0: 486 | subprocess.call(['gnuplot', '--persist', "stats.gp"], cwd="%s" % base_directory) 487 | 488 | -------------------------------------------------------------------------------- /ffmpeg_modifications.diff: -------------------------------------------------------------------------------- 1 | diff --git a/configure b/configure 2 | index ba5793b2ff..54b1b88239 100755 3 | --- a/configure 4 | +++ b/configure 5 | @@ -3692,6 +3692,8 @@ nnedi_filter_deps="gpl" 6 | ocr_filter_deps="libtesseract" 7 | ocv_filter_deps="libopencv" 8 | openclsrc_filter_deps="opencl" 9 | +phqm_filter_deps="libopencv" 10 | +phqm_filter_extralibs="-lstdc++ -lopencv_img_hash" 11 | overlay_opencl_filter_deps="opencl" 12 | overlay_qsv_filter_deps="libmfx" 13 | overlay_qsv_filter_select="qsvvpp" 14 | diff --git a/libavcodec/libx264.c b/libavcodec/libx264.c 15 | index 98ec030865..57d3d831ec 100644 16 | --- a/libavcodec/libx264.c 17 | +++ b/libavcodec/libx264.c 18 | @@ -217,24 +217,76 @@ static void reconfig_encoder(AVCodecContext *ctx, const AVFrame *frame) 19 | x264_encoder_reconfig(x4->enc, &x4->params); 20 | } 21 | 22 | - if (x4->params.rc.i_vbv_buffer_size != ctx->rc_buffer_size / 1000 || 23 | - x4->params.rc.i_vbv_max_bitrate != ctx->rc_max_rate / 1000) { 24 | - x4->params.rc.i_vbv_buffer_size = ctx->rc_buffer_size / 1000; 25 | - x4->params.rc.i_vbv_max_bitrate = ctx->rc_max_rate / 1000; 26 | - x264_encoder_reconfig(x4->enc, &x4->params); 27 | + if (frame->perceptual_score == -1) { 28 | + if (x4->params.rc.i_vbv_buffer_size != ctx->rc_buffer_size / 1000 || 29 | + x4->params.rc.i_vbv_max_bitrate != ctx->rc_max_rate / 1000) { 30 | + x4->params.rc.i_vbv_buffer_size = ctx->rc_buffer_size / 1000; 31 | + x4->params.rc.i_vbv_max_bitrate = ctx->rc_max_rate / 1000; 32 | + x264_encoder_reconfig(x4->enc, &x4->params); 33 | + } 34 | } 35 | 36 | if (x4->params.rc.i_rc_method == X264_RC_ABR && 37 | - x4->params.rc.i_bitrate != ctx->bit_rate / 1000) { 38 | - x4->params.rc.i_bitrate = ctx->bit_rate / 1000; 39 | - x264_encoder_reconfig(x4->enc, &x4->params); 40 | + (frame->perceptual_score > -1 || 41 | + x4->params.rc.i_bitrate != ctx->bit_rate / 1000)) { 42 | + if (frame->perceptual_score > -1) { 43 | + int bitrate = 0; 44 | + /* set ABR bitrate value from perceptual score */ 45 | + /* decrease compression by raising the avg bitrate up to N times */ 46 | + bitrate = (ctx->bit_rate / 1000) + ((frame->perceptual_score * frame->perceptual_score_factor) * (ctx->bit_rate / 1000.0)); 47 | + x4->params.rc.i_bitrate = bitrate; 48 | + x4->params.rc.i_vbv_max_bitrate = bitrate * 1.5; 49 | + x4->params.rc.i_vbv_buffer_size = bitrate * 1.5 * 1.5; 50 | + av_log(ctx, AV_LOG_DEBUG, 51 | + "Perceptual: [%0.2f] bitrate %d maxbitrate %d from %"PRIu64"\n", 52 | + frame->perceptual_score, 53 | + x4->params.rc.i_bitrate, 54 | + x4->params.rc.i_vbv_max_bitrate, 55 | + ctx->bit_rate / 1000); 56 | + 57 | + /* tag this frame with this specific config */ 58 | + x4->pic.param = &x4->params; 59 | + x264_encoder_reconfig(x4->enc, &x4->params); 60 | + } else { 61 | + x4->params.rc.i_bitrate = ctx->bit_rate / 1000; 62 | + x264_encoder_reconfig(x4->enc, &x4->params); 63 | + } 64 | } 65 | 66 | if (x4->crf >= 0 && 67 | x4->params.rc.i_rc_method == X264_RC_CRF && 68 | - x4->params.rc.f_rf_constant != x4->crf) { 69 | - x4->params.rc.f_rf_constant = x4->crf; 70 | - x264_encoder_reconfig(x4->enc, &x4->params); 71 | + (frame->perceptual_score > -1 || 72 | + x4->params.rc.f_rf_constant != x4->crf)) { 73 | + if (frame->perceptual_score > -1) { 74 | + float crf_value = 0.0; 75 | + 76 | + /* set crf value from perceptual score */ 77 | + /* decrease compression by lowering the score by up to N CRF points */ 78 | + crf_value = x4->crf - ((frame->perceptual_score * 100.0) / (frame->perceptual_score_factor * 2.0)); 79 | + x4->params.rc.f_rf_constant = crf_value; 80 | + 81 | + if (ctx->rc_max_rate) { 82 | + int bitrate = 0; 83 | + /* set ABR bitrate value from perceptual score */ 84 | + /* decrease compression by raising the avg bitrate up to N times */ 85 | + bitrate = (ctx->rc_max_rate / 1000) + ((frame->perceptual_score * frame->perceptual_score_factor) * (ctx->rc_max_rate / 1000.0)); 86 | + x4->params.rc.i_vbv_max_bitrate = bitrate; 87 | + x4->params.rc.i_vbv_buffer_size = bitrate * 1.5 * 1.5; 88 | + } 89 | + av_log(ctx, AV_LOG_DEBUG, 90 | + "Perceptual: [%0.2f] crf: %0.2f bitrate %d maxbitrate %d from %"PRIu64"\n", 91 | + frame->perceptual_score, 92 | + x4->params.rc.f_rf_constant, 93 | + x4->params.rc.i_bitrate, 94 | + x4->params.rc.i_vbv_max_bitrate, 95 | + ctx->rc_max_rate / 1000); 96 | + 97 | + /* tag this frame with this specific config */ 98 | + x4->pic.param = &x4->params; 99 | + } else { 100 | + x4->params.rc.f_rf_constant = x4->crf; 101 | + x264_encoder_reconfig(x4->enc, &x4->params); 102 | + } 103 | } 104 | 105 | if (x4->params.rc.i_rc_method == X264_RC_CQP && 106 | diff --git a/libavfilter/Makefile b/libavfilter/Makefile 107 | index 30cc329fb6..3a4972c55c 100644 108 | --- a/libavfilter/Makefile 109 | +++ b/libavfilter/Makefile 110 | @@ -402,6 +402,7 @@ OBJS-$(CONFIG_PERMS_FILTER) += f_perms.o 111 | OBJS-$(CONFIG_PERSPECTIVE_FILTER) += vf_perspective.o 112 | OBJS-$(CONFIG_PHASE_FILTER) += vf_phase.o 113 | OBJS-$(CONFIG_PHOTOSENSITIVITY_FILTER) += vf_photosensitivity.o 114 | +OBJS-$(CONFIG_PHQM_FILTER) += vf_phqm.o img_hash.o 115 | OBJS-$(CONFIG_PIXDESCTEST_FILTER) += vf_pixdesctest.o 116 | OBJS-$(CONFIG_PIXELIZE_FILTER) += vf_pixelize.o 117 | OBJS-$(CONFIG_PIXSCOPE_FILTER) += vf_datascope.o 118 | @@ -609,6 +610,7 @@ SLIBOBJS-$(HAVE_GNU_WINDRES) += avfilterres.o 119 | SKIPHEADERS-$(CONFIG_LCMS2) += fflcms2.h 120 | SKIPHEADERS-$(CONFIG_LIBVIDSTAB) += vidstabutils.h 121 | 122 | +SKIPHEADERS-$(CONFIG_LIBOPENCV) += img_hash.h 123 | SKIPHEADERS-$(CONFIG_QSVVPP) += qsvvpp.h 124 | SKIPHEADERS-$(CONFIG_OPENCL) += opencl.h 125 | SKIPHEADERS-$(CONFIG_VAAPI) += vaapi_vpp.h 126 | diff --git a/libavfilter/allfilters.c b/libavfilter/allfilters.c 127 | index 5ebacfde27..9c9ebc990e 100644 128 | --- a/libavfilter/allfilters.c 129 | +++ b/libavfilter/allfilters.c 130 | @@ -386,6 +386,7 @@ extern const AVFilter ff_vf_pixelize; 131 | extern const AVFilter ff_vf_pixscope; 132 | extern const AVFilter ff_vf_pp; 133 | extern const AVFilter ff_vf_pp7; 134 | +extern const AVFilter ff_vf_phqm; 135 | extern const AVFilter ff_vf_premultiply; 136 | extern const AVFilter ff_vf_prewitt; 137 | extern const AVFilter ff_vf_prewitt_opencl; 138 | diff --git a/libavfilter/img_hash.cpp b/libavfilter/img_hash.cpp 139 | new file mode 100644 140 | index 0000000000..4d5843da22 141 | --- /dev/null 142 | +++ b/libavfilter/img_hash.cpp 143 | @@ -0,0 +1,98 @@ 144 | +/* 145 | + * Copyright (c) 2019 Christopher Kennedy 146 | + * 147 | + * OpenCV img_hash 148 | + * 149 | + * This file is part of FFmpeg. 150 | + * 151 | + * FFmpeg is free software; you can redistribute it and/or 152 | + * modify it under the terms of the GNU Lesser General Public 153 | + * License as published by the Free Software Foundation; either 154 | + * version 2.1 of the License, or (at your option) any later version. 155 | + * 156 | + * FFmpeg is distributed in the hope that it will be useful, 157 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of 158 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 159 | + * Lesser General Public License for more details. 160 | + * 161 | + * You should have received a copy of the GNU Lesser General Public 162 | + * License along with FFmpeg; if not, write to the Free Software 163 | + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 164 | + */ 165 | + 166 | +#include 167 | +#include 168 | +#include 169 | +#include 170 | +#include 171 | + 172 | +#include 173 | + 174 | +#include "img_hash.h" 175 | +#include "libavutil/pixdesc.h" 176 | +extern "C" { 177 | +#include "avfilter.h" 178 | +} 179 | + 180 | +// convert from avframe to iplimage format 181 | +static int fill_iplimage_from_frame(IplImage *img, const AVFrame *frame, enum AVPixelFormat pixfmt) 182 | +{ 183 | + IplImage *tmpimg; 184 | + int depth = IPL_DEPTH_8U, channels_nb; 185 | + 186 | + switch (pixfmt) { 187 | + case AV_PIX_FMT_GRAY8: channels_nb = 1; break; 188 | + case AV_PIX_FMT_BGRA: channels_nb = 4; break; 189 | + case AV_PIX_FMT_BGR24: channels_nb = 3; break; 190 | + default: return -1; 191 | + } 192 | + 193 | + tmpimg = cvCreateImageHeader((CvSize){frame->width, frame->height}, depth, channels_nb); 194 | + *img = *tmpimg; 195 | + img->imageData = img->imageDataOrigin = (char *) frame->data[0]; 196 | + img->dataOrder = IPL_DATA_ORDER_PIXEL; 197 | + img->origin = IPL_ORIGIN_TL; 198 | + img->widthStep = frame->linesize[0]; 199 | + 200 | + return 0; 201 | +} 202 | + 203 | +// Get the score of two Video Frames by comparing the perceptual hashes and deriving a hamming distance 204 | +// showing how similar they are or different. lower score is better for most algorithms 205 | +extern "C" double getScore(const AVFrame *frame1, const AVFrame *frame2, enum AVPixelFormat pixfmt, int hash_type) { 206 | + cv::Ptr algo; 207 | + IplImage ipl1, ipl2; 208 | + cv::Mat h1; 209 | + cv::Mat h2; 210 | + cv::Mat m1; 211 | + cv::Mat m2; 212 | + 213 | + // Take FFmpeg video frame and convert into an IplImage for OpenCV 214 | + if (fill_iplimage_from_frame(&ipl1, frame1, pixfmt) != 0 || 215 | + fill_iplimage_from_frame(&ipl2, frame2, pixfmt) != 0) 216 | + return DBL_MAX; // Return an invalid value if either fails 217 | + 218 | + // Convert an IplImage to an Mat Image for OpenCV (newer format) 219 | + m1 = cv::cvarrToMat(&ipl1); 220 | + m2 = cv::cvarrToMat(&ipl2); 221 | + 222 | + // substantiate the hash type algorithm 223 | + switch (hash_type) { 224 | + case PHASH: algo = cv::img_hash::PHash::create(); break; 225 | + case AVERAGE: algo = cv::img_hash::AverageHash::create(); break; 226 | + case MARRHILDRETH: algo = cv::img_hash::MarrHildrethHash::create(); break; 227 | + case RADIALVARIANCE: algo = cv::img_hash::RadialVarianceHash::create(); break; 228 | + // BlockMeanHash support mode 0 and mode 1, they associate to 229 | + // mode 1 and mode 2 of PHash library 230 | + case BLOCKMEAN1: algo = cv::img_hash::BlockMeanHash::create(0); break; 231 | + case BLOCKMEAN2: algo = cv::img_hash::BlockMeanHash::create(1); break; 232 | + case COLORMOMENT: algo = cv::img_hash::ColorMomentHash::create(); break; 233 | + } 234 | + 235 | + // Compute the hash 236 | + algo->compute(m1, h1); 237 | + algo->compute(m2, h2); 238 | + 239 | + // Compare the hashes and return the hamming distance 240 | + return algo->compare(h1, h2); 241 | +} 242 | diff --git a/libavfilter/img_hash.h b/libavfilter/img_hash.h 243 | new file mode 100644 244 | index 0000000000..76f55c3013 245 | --- /dev/null 246 | +++ b/libavfilter/img_hash.h 247 | @@ -0,0 +1,46 @@ 248 | +/* 249 | + * Copyright (c) 2019 Christopher Kennedy 250 | + * 251 | + * PHQM Perceptual Hash Quality Metric 252 | + * 253 | + * This file is part of FFmpeg. 254 | + * 255 | + * FFmpeg is free software; you can redistribute it and/or 256 | + * modify it under the terms of the GNU Lesser General Public 257 | + * License as published by the Free Software Foundation; either 258 | + * version 2.1 of the License, or (at your option) any later version. 259 | + * 260 | + * FFmpeg is distributed in the hope that it will be useful, 261 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of 262 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 263 | + * Lesser General Public License for more details. 264 | + * 265 | + * You should have received a copy of the GNU Lesser General Public 266 | + * License along with FFmpeg; if not, write to the Free Software 267 | + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 268 | + */ 269 | + 270 | +#ifndef AVFILTER_IMG_HASH_H 271 | +#define AVFILTER_IMG_HASH_H 272 | + 273 | +#include "avfilter.h" 274 | + 275 | +#if defined(__cplusplus) 276 | +extern "C" 277 | +{ 278 | +#endif 279 | + 280 | +#define AVERAGE 0 281 | +#define BLOCKMEAN1 1 282 | +#define BLOCKMEAN2 2 283 | +#define COLORMOMENT 3 284 | +#define MARRHILDRETH 4 285 | +#define PHASH 5 286 | +#define RADIALVARIANCE 6 287 | + 288 | +double getScore(const AVFrame *frame1, const AVFrame *frame2, enum AVPixelFormat pixfmt, int hash_type); 289 | +#if defined(__cplusplus) 290 | +} 291 | +#endif 292 | + 293 | +#endif 294 | diff --git a/libavfilter/vf_phqm.c b/libavfilter/vf_phqm.c 295 | new file mode 100644 296 | index 0000000000..af644dbdf5 297 | --- /dev/null 298 | +++ b/libavfilter/vf_phqm.c 299 | @@ -0,0 +1,380 @@ 300 | +/* 301 | + * Copyright (c) 2019 Christopher Kennedy 302 | + * 303 | + * PHQM Perceptual Hash Quality Metric 304 | + * 305 | + * This file is part of FFmpeg. 306 | + * 307 | + * FFmpeg is free software; you can redistribute it and/or 308 | + * modify it under the terms of the GNU Lesser General Public 309 | + * License as published by the Free Software Foundation; either 310 | + * version 2.1 of the License, or (at your option) any later version. 311 | + * 312 | + * FFmpeg is distributed in the hope that it will be useful, 313 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of 314 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 315 | + * Lesser General Public License for more details. 316 | + * 317 | + * You should have received a copy of the GNU Lesser General Public 318 | + * License along with FFmpeg; if not, write to the Free Software 319 | + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 320 | + */ 321 | + 322 | +/** 323 | + * @file 324 | + * PHQM: Calculate the Image Hash Hamming Difference between two input videos. 325 | + */ 326 | + 327 | +#include 328 | +#include "libavutil/avstring.h" 329 | +#include "libavutil/opt.h" 330 | +#include "libavutil/pixdesc.h" 331 | +#include "avfilter.h" 332 | +#include "drawutils.h" 333 | +#include "formats.h" 334 | +#include "framesync.h" 335 | +#include "internal.h" 336 | +#include "video.h" 337 | + 338 | +#include "img_hash.h" 339 | +#include "scene_sad.h" 340 | + 341 | +typedef struct PHQMContext { 342 | + const AVClass *class; 343 | + FFFrameSync fs; 344 | + double shd, hd, min_hd, max_hd, smin_hd, smax_hd; 345 | + double hft, sft, phd, psad; 346 | + uint64_t nb_shd; 347 | + uint64_t nb_frames; 348 | + FILE *stats_file; 349 | + char *stats_file_str; 350 | + int hash_type; 351 | + ff_scene_sad_fn sad_ref; ///< Sum of the absolute difference function (scene detect only) 352 | + ff_scene_sad_fn sad_enc; ///< Sum of the absolute difference function (scene detect only) 353 | + double prev_mafd_ref; ///< previous MAFD (scene detect only) 354 | + double prev_mafd_enc; ///< previous MAFD (scene detect only) 355 | + AVFrame *prev_pic_ref; ///< ref previous frame (scene detect only) 356 | + AVFrame *prev_pic_enc; ///< enc previous frame (scene detect only) 357 | + double scd_thresh; 358 | + double scene_score_ref; 359 | + double scene_score_enc; 360 | + double prev_hamm_ref; 361 | + double prev_hamm_enc; 362 | +} PHQMContext; 363 | + 364 | +#define OFFSET(x) offsetof(PHQMContext, x) 365 | +#define FLAGS AV_OPT_FLAG_FILTERING_PARAM|AV_OPT_FLAG_VIDEO_PARAM 366 | + 367 | +static const AVOption phqm_options[] = { 368 | + { "stats_file", "Set file where to store per-frame difference information.", OFFSET(stats_file_str), AV_OPT_TYPE_STRING, {.str=NULL}, 0, 0, FLAGS }, 369 | + { "f", "Set file where to store per-frame difference information.", OFFSET(stats_file_str), AV_OPT_TYPE_STRING, {.str=NULL}, 0, 0, FLAGS }, 370 | + { "scd_thresh", "Scene Change Detection Threshold.", OFFSET(scd_thresh), AV_OPT_TYPE_DOUBLE, {.dbl=0.5}, 0, 1, FLAGS }, 371 | + { "hash_type", "Type of Image Hash to use from OpenCV.", OFFSET(hash_type), AV_OPT_TYPE_INT, {.i64 = PHASH}, 0, 6, FLAGS, "hash_type" }, 372 | + { "average", "Average Hash", 0, AV_OPT_TYPE_CONST, {.i64 = AVERAGE}, 0, 0, FLAGS, "hash_type" }, 373 | + { "blockmean1", "Block Mean Hash 1", 0, AV_OPT_TYPE_CONST, {.i64 = BLOCKMEAN1}, 0, 0, FLAGS, "hash_type" }, 374 | + { "blockmean2", "Block Mean Hash 2", 0, AV_OPT_TYPE_CONST, {.i64 = BLOCKMEAN2}, 0, 0, FLAGS, "hash_type" }, 375 | + { "colormoment", "Color Moment Hash", 0, AV_OPT_TYPE_CONST, {.i64 = COLORMOMENT}, 0, 0, FLAGS, "hash_type" }, 376 | + { "marrhildreth", "Marr Hildreth Hash", 0, AV_OPT_TYPE_CONST, {.i64 = MARRHILDRETH}, 0, 0, FLAGS, "hash_type" }, 377 | + { "phash", "Perceptual Hash (PHash)", 0, AV_OPT_TYPE_CONST, {.i64 = PHASH}, 0, 0, FLAGS, "hash_type" }, 378 | + { "radialvariance", "Radial Variance Hash", 0, AV_OPT_TYPE_CONST, {.i64 = RADIALVARIANCE}, 0, 0, FLAGS, "hash_type" }, 379 | + { NULL } 380 | +}; 381 | + 382 | +FRAMESYNC_DEFINE_CLASS(phqm, PHQMContext, fs); 383 | + 384 | +static void set_meta(AVDictionary **metadata, const char *key, char comp, float d) 385 | +{ 386 | + char value[128]; 387 | + snprintf(value, sizeof(value), "%0.2f", d); 388 | + if (comp) { 389 | + char key2[128]; 390 | + snprintf(key2, sizeof(key2), "%s%c", key, comp); 391 | + av_dict_set(metadata, key2, value, 0); 392 | + } else { 393 | + av_dict_set(metadata, key, value, 0); 394 | + } 395 | +} 396 | + 397 | +static void get_scene_score(AVFilterContext *ctx, AVFrame *ref, AVFrame *enc) 398 | +{ 399 | + PHQMContext *s = ctx->priv; 400 | + AVFrame *prev_pic_ref = s->prev_pic_ref; 401 | + AVFrame *prev_pic_enc = s->prev_pic_enc; 402 | + 403 | + /* reference */ 404 | + if (prev_pic_ref && 405 | + ref->height == prev_pic_ref->height && 406 | + ref->width == prev_pic_ref->width) { 407 | + uint64_t sad; 408 | + double mafd, diff; 409 | + 410 | + /* scene change sad score */ 411 | + s->sad_ref(prev_pic_ref->data[0], prev_pic_ref->linesize[0], ref->data[0], ref->linesize[0], ref->width * 3, ref->height, &sad); 412 | + emms_c(); 413 | + mafd = (double)sad / (ref->width * 3 * ref->height); 414 | + diff = fabs(mafd - s->prev_mafd_ref); 415 | + s->scene_score_ref = av_clipf(FFMIN(mafd, diff) / 100., 0, 1); 416 | + s->prev_mafd_ref = mafd; 417 | + 418 | + /* get prev/current frame hamming difference */ 419 | + s->prev_hamm_ref = getScore(s->prev_pic_ref, ref, ref->format, s->hash_type); 420 | + 421 | + av_frame_free(&prev_pic_ref); 422 | + } 423 | + s->prev_pic_ref = av_frame_clone(ref); 424 | + 425 | + if (prev_pic_enc && 426 | + enc->height == prev_pic_enc->height && 427 | + enc->width == prev_pic_enc->width) { 428 | + uint64_t sad; 429 | + double mafd, diff; 430 | + 431 | + /* scene change sad score */ 432 | + s->sad_enc(prev_pic_enc->data[0], prev_pic_enc->linesize[0], enc->data[0], enc->linesize[0], enc->width * 3, enc->height, &sad); 433 | + emms_c(); 434 | + mafd = (double)sad / (enc->width * 3 * enc->height); 435 | + diff = fabs(mafd - s->prev_mafd_enc); 436 | + s->scene_score_enc = av_clipf(FFMIN(mafd, diff) / 100., 0, 1); 437 | + s->prev_mafd_enc = mafd; 438 | + 439 | + /* get prev/current frame hamming difference */ 440 | + s->prev_hamm_enc = getScore(s->prev_pic_enc, enc, enc->format, s->hash_type); 441 | + 442 | + av_frame_free(&prev_pic_enc); 443 | + } 444 | + s->prev_pic_enc = av_frame_clone(enc); 445 | +} 446 | + 447 | +static int do_phqm(FFFrameSync *fs) 448 | +{ 449 | + AVFilterContext *ctx = fs->parent; 450 | + PHQMContext *s = ctx->priv; 451 | + AVFrame *master, *ref; 452 | + double hd = 0.; 453 | + int ret; 454 | + double hd_limit = 1000000.; 455 | + AVDictionary **metadata; 456 | + 457 | + ret = ff_framesync_dualinput_get(fs, &master, &ref); 458 | + if (ret < 0) 459 | + return ret; 460 | + if (!ref) 461 | + return ff_filter_frame(ctx->outputs[0], master); 462 | + metadata = &master->metadata; 463 | + 464 | + s->nb_frames++; 465 | + 466 | + /* scene change detection score */ 467 | + get_scene_score(ctx, ref, master); 468 | + if (s->scene_score_ref >= s->scd_thresh && s->nb_shd >= 48) { 469 | + av_log(s, AV_LOG_WARNING, "ImgHashScene: n:%"PRId64"-%"PRId64" hd_avg:%0.3lf hd_min:%0.3lf hd_max:%0.3lf scd:%0.2lf hft:%0.3lf sft:%0.3lf\n", 470 | + (s->nb_frames - s->nb_shd), s->nb_frames - 1, (s->shd / s->nb_shd), s->smin_hd, s->smax_hd, s->scene_score_ref, (s->hft / s->nb_shd), (s->sft / s->nb_shd)); 471 | + s->shd = 0.; 472 | + s->sft = 0.; 473 | + s->hft = 0.; 474 | + s->nb_shd = 0; 475 | + s->smin_hd = 0.; 476 | + s->smax_hd = 0.; 477 | + } 478 | + 479 | + /* frame perceptual score, normalize to percentage, read by x264 for crf/vbr */ 480 | + master->perceptual_score = ref->perceptual_score = .01 * FFMIN((s->prev_hamm_ref * 2.0), 100); 481 | + master->perceptual_score_factor = ref->perceptual_score_factor = 2.0; 482 | + set_meta(metadata, "lavfi.phqm.hamm", 0, s->prev_hamm_ref); 483 | + 484 | + /* limit the highest value so we cut off at perceptual difference match */ 485 | + switch (s->hash_type) { 486 | + case PHASH: 487 | + case AVERAGE: hd_limit = 5; break; 488 | + case MARRHILDRETH: hd_limit = 30; break; 489 | + case RADIALVARIANCE: hd_limit = 0.9; break; 490 | + case BLOCKMEAN1: hd_limit = 12; break; 491 | + case BLOCKMEAN2: hd_limit = 48; break; 492 | + case COLORMOMENT: hd_limit = 8; break; 493 | + } 494 | + 495 | + /* get ref / enc perceptual hashes and calc hamming distance difference value */ 496 | + hd = getScore(ref, master, ref->format, s->hash_type); 497 | + if (hd == DBL_MAX) { 498 | + av_log(s, AV_LOG_ERROR, "Failure with handling pix_fmt of AVFrame for conversion to IPLimage.\n"); 499 | + return AVERROR(EINVAL); 500 | + } 501 | + s->hd += FFMIN(hd, hd_limit); 502 | + s->phd += FFMIN(s->prev_hamm_ref, hd_limit); 503 | + s->psad += FFMIN(s->scene_score_ref, hd_limit); 504 | + set_meta(metadata, "lavfi.phqm.phqm", 0, hd); 505 | + 506 | + /* scene hamming distance avg */ 507 | + s->shd += FFMIN(hd, hd_limit); 508 | + s->hft += s->prev_hamm_ref; 509 | + s->sft += s->scene_score_ref; 510 | + s->nb_shd++; 511 | + av_log(s, AV_LOG_DEBUG, "ImgHashFrame: hd:%0.3lf sad:%0.2lf hamm:%0.3lf\n", hd, s->scene_score_ref, s->prev_hamm_ref); 512 | + 513 | + s->min_hd = FFMIN(s->min_hd, hd); 514 | + s->max_hd = FFMAX(s->max_hd, hd); 515 | + s->smin_hd = FFMIN(s->smin_hd, hd); 516 | + s->smax_hd = FFMAX(s->smax_hd, hd); 517 | + 518 | + if (s->stats_file) { 519 | + fprintf(s->stats_file, 520 | + "n:%"PRId64" phqm:%0.3f phqm_min:%0.3f phqm_max:%0.3f sad:%0.2f ref_hamm:%0.2f enc_hamm:%0.2f", 521 | + s->nb_frames, hd, s->min_hd, s->max_hd, s->scene_score_ref, s->prev_hamm_ref, s->prev_hamm_enc); 522 | + fprintf(s->stats_file, "\n"); 523 | + } 524 | + 525 | + return ff_filter_frame(ctx->outputs[0], master); 526 | +} 527 | + 528 | +static av_cold int init(AVFilterContext *ctx) 529 | +{ 530 | + PHQMContext *s = ctx->priv; 531 | + 532 | + if (s->stats_file_str) { 533 | + if (!strcmp(s->stats_file_str, "-")) { 534 | + s->stats_file = stdout; 535 | + } else { 536 | + s->stats_file = fopen(s->stats_file_str, "w"); 537 | + if (!s->stats_file) { 538 | + int err = AVERROR(errno); 539 | + char buf[128]; 540 | + av_strerror(err, buf, sizeof(buf)); 541 | + av_log(ctx, AV_LOG_ERROR, "Could not open stats file %s: %s\n", 542 | + s->stats_file_str, buf); 543 | + return err; 544 | + } 545 | + } 546 | + } 547 | + 548 | + s->sad_ref = ff_scene_sad_get_fn(8); 549 | + if (!s->sad_ref) 550 | + return AVERROR(EINVAL); 551 | + s->sad_enc = ff_scene_sad_get_fn(8); 552 | + if (!s->sad_enc) 553 | + return AVERROR(EINVAL); 554 | + 555 | + s->fs.on_event = do_phqm; 556 | + return 0; 557 | +} 558 | + 559 | +static int query_formats(AVFilterContext *ctx) 560 | +{ 561 | + PHQMContext *s = ctx->priv; 562 | + AVFilterFormats *fmts_list = NULL; 563 | + static const enum AVPixelFormat gray8_pix_fmts[] = { 564 | + AV_PIX_FMT_GRAY8, 565 | + AV_PIX_FMT_NONE 566 | + }; 567 | + static const enum AVPixelFormat bgr24_pix_fmts[] = { 568 | + AV_PIX_FMT_BGR24, 569 | + AV_PIX_FMT_NONE 570 | + }; 571 | + static const enum AVPixelFormat bgra_pix_fmts[] = { 572 | + AV_PIX_FMT_BGRA, 573 | + AV_PIX_FMT_NONE 574 | + }; 575 | + 576 | + switch (s->hash_type) { 577 | + case COLORMOMENT: fmts_list = ff_make_format_list(bgr24_pix_fmts); break; 578 | + case MARRHILDRETH: fmts_list = ff_make_format_list(bgra_pix_fmts); break; 579 | + /* all other hashes take the gray8 format */ 580 | + default: fmts_list = ff_make_format_list(gray8_pix_fmts); break; 581 | + } 582 | + if (!fmts_list) 583 | + return AVERROR(ENOMEM); 584 | + return ff_set_common_formats(ctx, fmts_list); 585 | +} 586 | + 587 | +static int config_input_ref(AVFilterLink *inlink) 588 | +{ 589 | + AVFilterContext *ctx = inlink->dst; 590 | + 591 | + if (ctx->inputs[0]->w != ctx->inputs[1]->w || 592 | + ctx->inputs[0]->h != ctx->inputs[1]->h) { 593 | + av_log(ctx, AV_LOG_ERROR, "Width and height of input videos must be same.\n"); 594 | + return AVERROR(EINVAL); 595 | + } 596 | + if (ctx->inputs[0]->format != ctx->inputs[1]->format) { 597 | + av_log(ctx, AV_LOG_ERROR, "Inputs must be of same pixel format.\n"); 598 | + return AVERROR(EINVAL); 599 | + } 600 | + 601 | + return 0; 602 | +} 603 | + 604 | +static int config_output(AVFilterLink *outlink) 605 | +{ 606 | + AVFilterContext *ctx = outlink->src; 607 | + PHQMContext *s = ctx->priv; 608 | + AVFilterLink *mainlink = ctx->inputs[0]; 609 | + int ret; 610 | + 611 | + ret = ff_framesync_init_dualinput(&s->fs, ctx); 612 | + if (ret < 0) 613 | + return ret; 614 | + outlink->w = mainlink->w; 615 | + outlink->h = mainlink->h; 616 | + outlink->time_base = mainlink->time_base; 617 | + outlink->sample_aspect_ratio = mainlink->sample_aspect_ratio; 618 | + outlink->frame_rate = mainlink->frame_rate; 619 | + if ((ret = ff_framesync_configure(&s->fs)) < 0) 620 | + return ret; 621 | + 622 | + return 0; 623 | +} 624 | + 625 | +static int activate(AVFilterContext *ctx) 626 | +{ 627 | + PHQMContext *s = ctx->priv; 628 | + return ff_framesync_activate(&s->fs); 629 | +} 630 | + 631 | +static av_cold void uninit(AVFilterContext *ctx) 632 | +{ 633 | + PHQMContext *s = ctx->priv; 634 | + 635 | + if (s->nb_frames > 0) 636 | + av_log(ctx, AV_LOG_WARNING, "PHQM average:%f min:%f max:%f hamm:%f sad:%f\n", 637 | + s->hd / s->nb_frames, s->min_hd, s->max_hd, 638 | + s->phd / s->nb_frames, s->psad / s->nb_frames); 639 | + 640 | + ff_framesync_uninit(&s->fs); 641 | + 642 | + if (s->stats_file && s->stats_file != stdout) 643 | + fclose(s->stats_file); 644 | + av_frame_free(&s->prev_pic_ref); 645 | + av_frame_free(&s->prev_pic_enc); 646 | +} 647 | + 648 | +static const AVFilterPad phqm_inputs[] = { 649 | + { 650 | + .name = "main", 651 | + .type = AVMEDIA_TYPE_VIDEO, 652 | + },{ 653 | + .name = "reference", 654 | + .type = AVMEDIA_TYPE_VIDEO, 655 | + .config_props = config_input_ref, 656 | + } 657 | +}; 658 | + 659 | +static const AVFilterPad phqm_outputs[] = { 660 | + { 661 | + .name = "default", 662 | + .type = AVMEDIA_TYPE_VIDEO, 663 | + .config_props = config_output, 664 | + } 665 | +}; 666 | + 667 | +AVFilter ff_vf_phqm = { 668 | + .name = "phqm", 669 | + .description = NULL_IF_CONFIG_SMALL("PHQM: Calculate the Perceptual Hash Hamming Difference between two video streams."), 670 | + .preinit = phqm_framesync_preinit, 671 | + .init = init, 672 | + .uninit = uninit, 673 | + .activate = activate, 674 | + .priv_size = sizeof(PHQMContext), 675 | + .priv_class = &phqm_class, 676 | + FILTER_INPUTS(phqm_inputs), 677 | + FILTER_OUTPUTS(phqm_outputs), 678 | + FILTER_QUERY_FUNC(query_formats), 679 | +}; 680 | diff --git a/libavformat/mpegenc.c b/libavformat/mpegenc.c 681 | index 3ab4bd3f9b..e33ba5135e 100644 682 | --- a/libavformat/mpegenc.c 683 | +++ b/libavformat/mpegenc.c 684 | @@ -979,7 +979,7 @@ static int remove_decoded_packets(AVFormatContext *ctx, int64_t scr) 685 | scr > pkt_desc->dts) { // FIXME: > vs >= 686 | if (stream->buffer_index < pkt_desc->size || 687 | stream->predecode_packet == stream->premux_packet) { 688 | - av_log(ctx, AV_LOG_ERROR, 689 | + av_log(ctx, AV_LOG_WARNING, 690 | "buffer underflow st=%d bufi=%d size=%d\n", 691 | i, stream->buffer_index, pkt_desc->size); 692 | break; 693 | @@ -1060,7 +1060,7 @@ retry: 694 | scr / 90000.0, best_dts / 90000.0); 695 | 696 | if (scr >= best_dts + 1 && !ignore_constraints) { 697 | - av_log(ctx, AV_LOG_ERROR, 698 | + av_log(ctx, AV_LOG_WARNING, 699 | "packet too large, ignoring buffer limits to mux it\n"); 700 | ignore_constraints = 1; 701 | } 702 | diff --git a/libavutil/frame.c b/libavutil/frame.c 703 | index 4c16488c66..5d19742dcc 100644 704 | --- a/libavutil/frame.c 705 | +++ b/libavutil/frame.c 706 | @@ -73,6 +73,8 @@ static void get_frame_defaults(AVFrame *frame) 707 | frame->color_range = AVCOL_RANGE_UNSPECIFIED; 708 | frame->chroma_location = AVCHROMA_LOC_UNSPECIFIED; 709 | frame->flags = 0; 710 | + frame->perceptual_score = -1; 711 | + frame->perceptual_score_factor = 2.0; 712 | } 713 | 714 | static void free_side_data(AVFrameSideData **ptr_sd) 715 | @@ -306,6 +308,8 @@ static int frame_copy_props(AVFrame *dst, const AVFrame *src, int force_copy) 716 | dst->colorspace = src->colorspace; 717 | dst->color_range = src->color_range; 718 | dst->chroma_location = src->chroma_location; 719 | + dst->perceptual_score = src->perceptual_score; 720 | + dst->perceptual_score_factor = src->perceptual_score_factor; 721 | 722 | av_dict_copy(&dst->metadata, src->metadata, 0); 723 | 724 | @@ -357,6 +361,8 @@ FF_ENABLE_DEPRECATION_WARNINGS 725 | dst->width = src->width; 726 | dst->height = src->height; 727 | dst->nb_samples = src->nb_samples; 728 | + dst->perceptual_score = src->perceptual_score; 729 | + dst->perceptual_score_factor = src->perceptual_score_factor; 730 | #if FF_API_OLD_CHANNEL_LAYOUT 731 | FF_DISABLE_DEPRECATION_WARNINGS 732 | dst->channels = src->channels; 733 | diff --git a/libavutil/frame.h b/libavutil/frame.h 734 | index 33fac2054c..a748045daa 100644 735 | --- a/libavutil/frame.h 736 | +++ b/libavutil/frame.h 737 | @@ -702,6 +702,13 @@ typedef struct AVFrame { 738 | * Channel layout of the audio data. 739 | */ 740 | AVChannelLayout ch_layout; 741 | + 742 | + /** 743 | + * perceptual score 744 | + * 0.00 - 1.00 percentage of perceptual match to the previous frame 745 | + */ 746 | + float perceptual_score; 747 | + float perceptual_score_factor; 748 | } AVFrame; 749 | 750 | 751 | -------------------------------------------------------------------------------- /ffmpeg_phqm.diff: -------------------------------------------------------------------------------- 1 | @@ -3487,6 +3496,8 @@ nlmeans_opencl_filter_deps="opencl" 2 | nnedi_filter_deps="gpl" 3 | ocr_filter_deps="libtesseract" 4 | ocv_filter_deps="libopencv" 5 | +phqm_filter_deps="libopencv" 6 | +phqm_filter_extralibs="-lstdc++ -lopencv_img_hash" 7 | openclsrc_filter_deps="opencl" 8 | overlay_opencl_filter_deps="opencl" 9 | overlay_qsv_filter_deps="libmfx" 10 | diff --git a/libavcodec/libx264.c b/libavcodec/libx264.c 11 | index dc4b4b100d..5527ffab67 100644 12 | --- a/libavcodec/libx264.c 13 | +++ b/libavcodec/libx264.c 14 | @@ -195,24 +195,76 @@ static void reconfig_encoder(AVCodecContext *ctx, const AVFrame *frame) 15 | x264_encoder_reconfig(x4->enc, &x4->params); 16 | } 17 | 18 | - if (x4->params.rc.i_vbv_buffer_size != ctx->rc_buffer_size / 1000 || 19 | - x4->params.rc.i_vbv_max_bitrate != ctx->rc_max_rate / 1000) { 20 | - x4->params.rc.i_vbv_buffer_size = ctx->rc_buffer_size / 1000; 21 | - x4->params.rc.i_vbv_max_bitrate = ctx->rc_max_rate / 1000; 22 | - x264_encoder_reconfig(x4->enc, &x4->params); 23 | + if (frame->perceptual_score == -1) { 24 | + if (x4->params.rc.i_vbv_buffer_size != ctx->rc_buffer_size / 1000 || 25 | + x4->params.rc.i_vbv_max_bitrate != ctx->rc_max_rate / 1000) { 26 | + x4->params.rc.i_vbv_buffer_size = ctx->rc_buffer_size / 1000; 27 | + x4->params.rc.i_vbv_max_bitrate = ctx->rc_max_rate / 1000; 28 | + x264_encoder_reconfig(x4->enc, &x4->params); 29 | + } 30 | } 31 | 32 | if (x4->params.rc.i_rc_method == X264_RC_ABR && 33 | - x4->params.rc.i_bitrate != ctx->bit_rate / 1000) { 34 | - x4->params.rc.i_bitrate = ctx->bit_rate / 1000; 35 | - x264_encoder_reconfig(x4->enc, &x4->params); 36 | + (frame->perceptual_score > -1 || 37 | + x4->params.rc.i_bitrate != ctx->bit_rate / 1000)) { 38 | + if (frame->perceptual_score > -1) { 39 | + int bitrate = 0; 40 | + /* set ABR bitrate value from perceptual score */ 41 | + /* decrease compression by raising the avg bitrate up to N times */ 42 | + bitrate = (ctx->bit_rate / 1000) + ((frame->perceptual_score * frame->perceptual_score_factor) * (ctx->bit_rate / 1000.0)); 43 | + x4->params.rc.i_bitrate = bitrate; 44 | + x4->params.rc.i_vbv_max_bitrate = bitrate * 1.5; 45 | + x4->params.rc.i_vbv_buffer_size = bitrate * 1.5 * 1.5; 46 | + av_log(ctx, AV_LOG_DEBUG, 47 | + "Perceptual: [%0.2f] bitrate %d maxbitrate %d from %"PRIu64"\n", 48 | + frame->perceptual_score, 49 | + x4->params.rc.i_bitrate, 50 | + x4->params.rc.i_vbv_max_bitrate, 51 | + ctx->bit_rate / 1000); 52 | + 53 | + /* tag this frame with this specific config */ 54 | + x4->pic.param = &x4->params; 55 | + x264_encoder_reconfig(x4->enc, &x4->params); 56 | + } else { 57 | + x4->params.rc.i_bitrate = ctx->bit_rate / 1000; 58 | + x264_encoder_reconfig(x4->enc, &x4->params); 59 | + } 60 | } 61 | 62 | if (x4->crf >= 0 && 63 | x4->params.rc.i_rc_method == X264_RC_CRF && 64 | - x4->params.rc.f_rf_constant != x4->crf) { 65 | - x4->params.rc.f_rf_constant = x4->crf; 66 | - x264_encoder_reconfig(x4->enc, &x4->params); 67 | + (frame->perceptual_score > -1 || 68 | + x4->params.rc.f_rf_constant != x4->crf)) { 69 | + if (frame->perceptual_score > -1) { 70 | + float crf_value = 0.0; 71 | + 72 | + /* set crf value from perceptual score */ 73 | + /* decrease compression by lowering the score by up to N CRF points */ 74 | + crf_value = x4->crf - ((frame->perceptual_score * 100.0) / (frame->perceptual_score_factor * 2.0)); 75 | + x4->params.rc.f_rf_constant = crf_value; 76 | + 77 | + if (ctx->rc_max_rate) { 78 | + int bitrate = 0; 79 | + /* set ABR bitrate value from perceptual score */ 80 | + /* decrease compression by raising the avg bitrate up to N times */ 81 | + bitrate = (ctx->rc_max_rate / 1000) + ((frame->perceptual_score * frame->perceptual_score_factor) * (ctx->rc_max_rate / 1000.0)); 82 | + x4->params.rc.i_vbv_max_bitrate = bitrate; 83 | + x4->params.rc.i_vbv_buffer_size = bitrate * 1.5 * 1.5; 84 | + } 85 | + av_log(ctx, AV_LOG_DEBUG, 86 | + "Perceptual: [%0.2f] crf: %0.2f bitrate %d maxbitrate %d from %"PRIu64"\n", 87 | + frame->perceptual_score, 88 | + x4->params.rc.f_rf_constant, 89 | + x4->params.rc.i_bitrate, 90 | + x4->params.rc.i_vbv_max_bitrate, 91 | + ctx->rc_max_rate / 1000); 92 | + 93 | + /* tag this frame with this specific config */ 94 | + x4->pic.param = &x4->params; 95 | + } else { 96 | + x4->params.rc.f_rf_constant = x4->crf; 97 | + x264_encoder_reconfig(x4->enc, &x4->params); 98 | + } 99 | } 100 | 101 | if (x4->params.rc.i_rc_method == X264_RC_CQP && 102 | diff --git a/libavfilter/Makefile b/libavfilter/Makefile 103 | index 455c809b15..c819ad672a 100644 104 | --- a/libavfilter/Makefile 105 | +++ b/libavfilter/Makefile 106 | @@ -319,6 +319,7 @@ OBJS-$(CONFIG_PALETTEUSE_FILTER) += vf_paletteuse.o framesync.o 107 | OBJS-$(CONFIG_PERMS_FILTER) += f_perms.o 108 | OBJS-$(CONFIG_PERSPECTIVE_FILTER) += vf_perspective.o 109 | OBJS-$(CONFIG_PHASE_FILTER) += vf_phase.o 110 | +OBJS-$(CONFIG_PHQM_FILTER) += vf_phqm.o img_hash.o 111 | OBJS-$(CONFIG_PIXDESCTEST_FILTER) += vf_pixdesctest.o 112 | OBJS-$(CONFIG_PIXSCOPE_FILTER) += vf_datascope.o 113 | OBJS-$(CONFIG_PP_FILTER) += vf_pp.o 114 | @@ -489,6 +490,7 @@ OBJS-$(CONFIG_SHARED) += log2_tab.o 115 | SKIPHEADERS-$(CONFIG_QSVVPP) += qsvvpp.h 116 | SKIPHEADERS-$(CONFIG_OPENCL) += opencl.h 117 | SKIPHEADERS-$(CONFIG_VAAPI) += vaapi_vpp.h 118 | +SKIPHEADERS-$(CONFIG_LIBOPENCV) += img_hash.h 119 | 120 | TOOLS = graph2dot 121 | TESTPROGS = drawutils filtfmts formats integral 122 | diff --git a/libavfilter/allfilters.c b/libavfilter/allfilters.c 123 | index 04a3df7d56..378c553d66 100644 124 | --- a/libavfilter/allfilters.c 125 | +++ b/libavfilter/allfilters.c 126 | @@ -307,6 +307,7 @@ extern AVFilter ff_vf_pixdesctest; 127 | extern AVFilter ff_vf_pixscope; 128 | extern AVFilter ff_vf_pp; 129 | extern AVFilter ff_vf_pp7; 130 | +extern AVFilter ff_vf_phqm; 131 | extern AVFilter ff_vf_premultiply; 132 | extern AVFilter ff_vf_prewitt; 133 | extern AVFilter ff_vf_prewitt_opencl; 134 | diff --git a/libavfilter/img_hash.cpp b/libavfilter/img_hash.cpp 135 | new file mode 100644 136 | index 0000000000..4d5843da22 137 | --- /dev/null 138 | +++ b/libavfilter/img_hash.cpp 139 | @@ -0,0 +1,98 @@ 140 | +/* 141 | + * Copyright (c) 2019 Christopher Kennedy 142 | + * 143 | + * OpenCV img_hash 144 | + * 145 | + * This file is part of FFmpeg. 146 | + * 147 | + * FFmpeg is free software; you can redistribute it and/or 148 | + * modify it under the terms of the GNU Lesser General Public 149 | + * License as published by the Free Software Foundation; either 150 | + * version 2.1 of the License, or (at your option) any later version. 151 | + * 152 | + * FFmpeg is distributed in the hope that it will be useful, 153 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of 154 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 155 | + * Lesser General Public License for more details. 156 | + * 157 | + * You should have received a copy of the GNU Lesser General Public 158 | + * License along with FFmpeg; if not, write to the Free Software 159 | + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 160 | + */ 161 | + 162 | +#include 163 | +#include 164 | +#include 165 | +#include 166 | +#include 167 | + 168 | +#include 169 | + 170 | +#include "img_hash.h" 171 | +#include "libavutil/pixdesc.h" 172 | +extern "C" { 173 | +#include "avfilter.h" 174 | +} 175 | + 176 | +// convert from avframe to iplimage format 177 | +static int fill_iplimage_from_frame(IplImage *img, const AVFrame *frame, enum AVPixelFormat pixfmt) 178 | +{ 179 | + IplImage *tmpimg; 180 | + int depth = IPL_DEPTH_8U, channels_nb; 181 | + 182 | + switch (pixfmt) { 183 | + case AV_PIX_FMT_GRAY8: channels_nb = 1; break; 184 | + case AV_PIX_FMT_BGRA: channels_nb = 4; break; 185 | + case AV_PIX_FMT_BGR24: channels_nb = 3; break; 186 | + default: return -1; 187 | + } 188 | + 189 | + tmpimg = cvCreateImageHeader((CvSize){frame->width, frame->height}, depth, channels_nb); 190 | + *img = *tmpimg; 191 | + img->imageData = img->imageDataOrigin = (char *) frame->data[0]; 192 | + img->dataOrder = IPL_DATA_ORDER_PIXEL; 193 | + img->origin = IPL_ORIGIN_TL; 194 | + img->widthStep = frame->linesize[0]; 195 | + 196 | + return 0; 197 | +} 198 | + 199 | +// Get the score of two Video Frames by comparing the perceptual hashes and deriving a hamming distance 200 | +// showing how similar they are or different. lower score is better for most algorithms 201 | +extern "C" double getScore(const AVFrame *frame1, const AVFrame *frame2, enum AVPixelFormat pixfmt, int hash_type) { 202 | + cv::Ptr algo; 203 | + IplImage ipl1, ipl2; 204 | + cv::Mat h1; 205 | + cv::Mat h2; 206 | + cv::Mat m1; 207 | + cv::Mat m2; 208 | + 209 | + // Take FFmpeg video frame and convert into an IplImage for OpenCV 210 | + if (fill_iplimage_from_frame(&ipl1, frame1, pixfmt) != 0 || 211 | + fill_iplimage_from_frame(&ipl2, frame2, pixfmt) != 0) 212 | + return DBL_MAX; // Return an invalid value if either fails 213 | + 214 | + // Convert an IplImage to an Mat Image for OpenCV (newer format) 215 | + m1 = cv::cvarrToMat(&ipl1); 216 | + m2 = cv::cvarrToMat(&ipl2); 217 | + 218 | + // substantiate the hash type algorithm 219 | + switch (hash_type) { 220 | + case PHASH: algo = cv::img_hash::PHash::create(); break; 221 | + case AVERAGE: algo = cv::img_hash::AverageHash::create(); break; 222 | + case MARRHILDRETH: algo = cv::img_hash::MarrHildrethHash::create(); break; 223 | + case RADIALVARIANCE: algo = cv::img_hash::RadialVarianceHash::create(); break; 224 | + // BlockMeanHash support mode 0 and mode 1, they associate to 225 | + // mode 1 and mode 2 of PHash library 226 | + case BLOCKMEAN1: algo = cv::img_hash::BlockMeanHash::create(0); break; 227 | + case BLOCKMEAN2: algo = cv::img_hash::BlockMeanHash::create(1); break; 228 | + case COLORMOMENT: algo = cv::img_hash::ColorMomentHash::create(); break; 229 | + } 230 | + 231 | + // Compute the hash 232 | + algo->compute(m1, h1); 233 | + algo->compute(m2, h2); 234 | + 235 | + // Compare the hashes and return the hamming distance 236 | + return algo->compare(h1, h2); 237 | +} 238 | diff --git a/libavfilter/img_hash.h b/libavfilter/img_hash.h 239 | new file mode 100644 240 | index 0000000000..76f55c3013 241 | --- /dev/null 242 | +++ b/libavfilter/img_hash.h 243 | @@ -0,0 +1,46 @@ 244 | +/* 245 | + * Copyright (c) 2019 Christopher Kennedy 246 | + * 247 | + * PHQM Perceptual Hash Quality Metric 248 | + * 249 | + * This file is part of FFmpeg. 250 | + * 251 | + * FFmpeg is free software; you can redistribute it and/or 252 | + * modify it under the terms of the GNU Lesser General Public 253 | + * License as published by the Free Software Foundation; either 254 | + * version 2.1 of the License, or (at your option) any later version. 255 | + * 256 | + * FFmpeg is distributed in the hope that it will be useful, 257 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of 258 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 259 | + * Lesser General Public License for more details. 260 | + * 261 | + * You should have received a copy of the GNU Lesser General Public 262 | + * License along with FFmpeg; if not, write to the Free Software 263 | + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 264 | + */ 265 | + 266 | +#ifndef AVFILTER_IMG_HASH_H 267 | +#define AVFILTER_IMG_HASH_H 268 | + 269 | +#include "avfilter.h" 270 | + 271 | +#if defined(__cplusplus) 272 | +extern "C" 273 | +{ 274 | +#endif 275 | + 276 | +#define AVERAGE 0 277 | +#define BLOCKMEAN1 1 278 | +#define BLOCKMEAN2 2 279 | +#define COLORMOMENT 3 280 | +#define MARRHILDRETH 4 281 | +#define PHASH 5 282 | +#define RADIALVARIANCE 6 283 | + 284 | +double getScore(const AVFrame *frame1, const AVFrame *frame2, enum AVPixelFormat pixfmt, int hash_type); 285 | +#if defined(__cplusplus) 286 | +} 287 | +#endif 288 | + 289 | +#endif 290 | diff --git a/libavfilter/vf_phqm.c b/libavfilter/vf_phqm.c 291 | new file mode 100644 292 | index 0000000000..a7b73cf061 293 | --- /dev/null 294 | +++ b/libavfilter/vf_phqm.c 295 | @@ -0,0 +1,382 @@ 296 | +/* 297 | + * Copyright (c) 2019 Christopher Kennedy 298 | + * 299 | + * PHQM Perceptual Hash Quality Metric 300 | + * 301 | + * This file is part of FFmpeg. 302 | + * 303 | + * FFmpeg is free software; you can redistribute it and/or 304 | + * modify it under the terms of the GNU Lesser General Public 305 | + * License as published by the Free Software Foundation; either 306 | + * version 2.1 of the License, or (at your option) any later version. 307 | + * 308 | + * FFmpeg is distributed in the hope that it will be useful, 309 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of 310 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 311 | + * Lesser General Public License for more details. 312 | + * 313 | + * You should have received a copy of the GNU Lesser General Public 314 | + * License along with FFmpeg; if not, write to the Free Software 315 | + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 316 | + */ 317 | + 318 | +/** 319 | + * @file 320 | + * PHQM: Calculate the Image Hash Hamming Difference between two input videos. 321 | + */ 322 | + 323 | +#include 324 | +#include "libavutil/avstring.h" 325 | +#include "libavutil/opt.h" 326 | +#include "libavutil/pixdesc.h" 327 | +#include "avfilter.h" 328 | +#include "drawutils.h" 329 | +#include "formats.h" 330 | +#include "framesync.h" 331 | +#include "internal.h" 332 | +#include "video.h" 333 | + 334 | +#include "img_hash.h" 335 | +#include "scene_sad.h" 336 | + 337 | +typedef struct PHQMContext { 338 | + const AVClass *class; 339 | + FFFrameSync fs; 340 | + double shd, hd, min_hd, max_hd, smin_hd, smax_hd; 341 | + double hft, sft, phd, psad; 342 | + uint64_t nb_shd; 343 | + uint64_t nb_frames; 344 | + FILE *stats_file; 345 | + char *stats_file_str; 346 | + int hash_type; 347 | + ff_scene_sad_fn sad_ref; ///< Sum of the absolute difference function (scene detect only) 348 | + ff_scene_sad_fn sad_enc; ///< Sum of the absolute difference function (scene detect only) 349 | + double prev_mafd_ref; ///< previous MAFD (scene detect only) 350 | + double prev_mafd_enc; ///< previous MAFD (scene detect only) 351 | + AVFrame *prev_pic_ref; ///< ref previous frame (scene detect only) 352 | + AVFrame *prev_pic_enc; ///< enc previous frame (scene detect only) 353 | + double scd_thresh; 354 | + double scene_score_ref; 355 | + double scene_score_enc; 356 | + double prev_hamm_ref; 357 | + double prev_hamm_enc; 358 | +} PHQMContext; 359 | + 360 | +#define OFFSET(x) offsetof(PHQMContext, x) 361 | +#define FLAGS AV_OPT_FLAG_FILTERING_PARAM|AV_OPT_FLAG_VIDEO_PARAM 362 | + 363 | +static const AVOption phqm_options[] = { 364 | + { "stats_file", "Set file where to store per-frame difference information.", OFFSET(stats_file_str), AV_OPT_TYPE_STRING, {.str=NULL}, 0, 0, FLAGS }, 365 | + { "f", "Set file where to store per-frame difference information.", OFFSET(stats_file_str), AV_OPT_TYPE_STRING, {.str=NULL}, 0, 0, FLAGS }, 366 | + { "scd_thresh", "Scene Change Detection Threshold.", OFFSET(scd_thresh), AV_OPT_TYPE_DOUBLE, {.dbl=0.5}, 0, 1, FLAGS }, 367 | + { "hash_type", "Type of Image Hash to use from OpenCV.", OFFSET(hash_type), AV_OPT_TYPE_INT, {.i64 = PHASH}, 0, 6, FLAGS, "hash_type" }, 368 | + { "average", "Average Hash", 0, AV_OPT_TYPE_CONST, {.i64 = AVERAGE}, 0, 0, FLAGS, "hash_type" }, 369 | + { "blockmean1", "Block Mean Hash 1", 0, AV_OPT_TYPE_CONST, {.i64 = BLOCKMEAN1}, 0, 0, FLAGS, "hash_type" }, 370 | + { "blockmean2", "Block Mean Hash 2", 0, AV_OPT_TYPE_CONST, {.i64 = BLOCKMEAN2}, 0, 0, FLAGS, "hash_type" }, 371 | + { "colormoment", "Color Moment Hash", 0, AV_OPT_TYPE_CONST, {.i64 = COLORMOMENT}, 0, 0, FLAGS, "hash_type" }, 372 | + { "marrhildreth", "Marr Hildreth Hash", 0, AV_OPT_TYPE_CONST, {.i64 = MARRHILDRETH}, 0, 0, FLAGS, "hash_type" }, 373 | + { "phash", "Perceptual Hash (PHash)", 0, AV_OPT_TYPE_CONST, {.i64 = PHASH}, 0, 0, FLAGS, "hash_type" }, 374 | + { "radialvariance", "Radial Variance Hash", 0, AV_OPT_TYPE_CONST, {.i64 = RADIALVARIANCE}, 0, 0, FLAGS, "hash_type" }, 375 | + { NULL } 376 | +}; 377 | + 378 | +FRAMESYNC_DEFINE_CLASS(phqm, PHQMContext, fs); 379 | + 380 | +static void set_meta(AVDictionary **metadata, const char *key, char comp, float d) 381 | +{ 382 | + char value[128]; 383 | + snprintf(value, sizeof(value), "%0.2f", d); 384 | + if (comp) { 385 | + char key2[128]; 386 | + snprintf(key2, sizeof(key2), "%s%c", key, comp); 387 | + av_dict_set(metadata, key2, value, 0); 388 | + } else { 389 | + av_dict_set(metadata, key, value, 0); 390 | + } 391 | +} 392 | + 393 | +static void get_scene_score(AVFilterContext *ctx, AVFrame *ref, AVFrame *enc) 394 | +{ 395 | + PHQMContext *s = ctx->priv; 396 | + AVFrame *prev_pic_ref = s->prev_pic_ref; 397 | + AVFrame *prev_pic_enc = s->prev_pic_enc; 398 | + 399 | + /* reference */ 400 | + if (prev_pic_ref && 401 | + ref->height == prev_pic_ref->height && 402 | + ref->width == prev_pic_ref->width) { 403 | + uint64_t sad; 404 | + double mafd, diff; 405 | + 406 | + /* scene change sad score */ 407 | + s->sad_ref(prev_pic_ref->data[0], prev_pic_ref->linesize[0], ref->data[0], ref->linesize[0], ref->width * 3, ref->height, &sad); 408 | + emms_c(); 409 | + mafd = (double)sad / (ref->width * 3 * ref->height); 410 | + diff = fabs(mafd - s->prev_mafd_ref); 411 | + s->scene_score_ref = av_clipf(FFMIN(mafd, diff) / 100., 0, 1); 412 | + s->prev_mafd_ref = mafd; 413 | + 414 | + /* get prev/current frame hamming difference */ 415 | + s->prev_hamm_ref = getScore(s->prev_pic_ref, ref, ref->format, s->hash_type); 416 | + 417 | + av_frame_free(&prev_pic_ref); 418 | + } 419 | + s->prev_pic_ref = av_frame_clone(ref); 420 | + 421 | + if (prev_pic_enc && 422 | + enc->height == prev_pic_enc->height && 423 | + enc->width == prev_pic_enc->width) { 424 | + uint64_t sad; 425 | + double mafd, diff; 426 | + 427 | + /* scene change sad score */ 428 | + s->sad_enc(prev_pic_enc->data[0], prev_pic_enc->linesize[0], enc->data[0], enc->linesize[0], enc->width * 3, enc->height, &sad); 429 | + emms_c(); 430 | + mafd = (double)sad / (enc->width * 3 * enc->height); 431 | + diff = fabs(mafd - s->prev_mafd_enc); 432 | + s->scene_score_enc = av_clipf(FFMIN(mafd, diff) / 100., 0, 1); 433 | + s->prev_mafd_enc = mafd; 434 | + 435 | + /* get prev/current frame hamming difference */ 436 | + s->prev_hamm_enc = getScore(s->prev_pic_enc, enc, enc->format, s->hash_type); 437 | + 438 | + av_frame_free(&prev_pic_enc); 439 | + } 440 | + s->prev_pic_enc = av_frame_clone(enc); 441 | +} 442 | + 443 | +static int do_phqm(FFFrameSync *fs) 444 | +{ 445 | + AVFilterContext *ctx = fs->parent; 446 | + PHQMContext *s = ctx->priv; 447 | + AVFrame *master, *ref; 448 | + double hd = 0.; 449 | + int ret; 450 | + double hd_limit = 1000000.; 451 | + AVDictionary **metadata; 452 | + 453 | + ret = ff_framesync_dualinput_get(fs, &master, &ref); 454 | + if (ret < 0) 455 | + return ret; 456 | + if (!ref) 457 | + return ff_filter_frame(ctx->outputs[0], master); 458 | + metadata = &master->metadata; 459 | + 460 | + s->nb_frames++; 461 | + 462 | + /* scene change detection score */ 463 | + get_scene_score(ctx, ref, master); 464 | + if (s->scene_score_ref >= s->scd_thresh && s->nb_shd >= 48) { 465 | + av_log(s, AV_LOG_WARNING, "ImgHashScene: n:%"PRId64"-%"PRId64" hd_avg:%0.3lf hd_min:%0.3lf hd_max:%0.3lf scd:%0.2lf hft:%0.3lf sft:%0.3lf\n", 466 | + (s->nb_frames - s->nb_shd), s->nb_frames - 1, (s->shd / s->nb_shd), s->smin_hd, s->smax_hd, s->scene_score_ref, (s->hft / s->nb_shd), (s->sft / s->nb_shd)); 467 | + s->shd = 0.; 468 | + s->sft = 0.; 469 | + s->hft = 0.; 470 | + s->nb_shd = 0; 471 | + s->smin_hd = 0.; 472 | + s->smax_hd = 0.; 473 | + } 474 | + 475 | + /* frame perceptual score, normalize to percentage, read by x264 for crf/vbr */ 476 | + master->perceptual_score = ref->perceptual_score = .01 * FFMIN((s->prev_hamm_ref * 2.0), 100); 477 | + master->perceptual_score_factor = ref->perceptual_score_factor = 2.0; 478 | + set_meta(metadata, "lavfi.phqm.hamm", 0, s->prev_hamm_ref); 479 | + 480 | + /* limit the highest value so we cut off at perceptual difference match */ 481 | + switch (s->hash_type) { 482 | + case PHASH: 483 | + case AVERAGE: hd_limit = 5; break; 484 | + case MARRHILDRETH: hd_limit = 30; break; 485 | + case RADIALVARIANCE: hd_limit = 0.9; break; 486 | + case BLOCKMEAN1: hd_limit = 12; break; 487 | + case BLOCKMEAN2: hd_limit = 48; break; 488 | + case COLORMOMENT: hd_limit = 8; break; 489 | + } 490 | + 491 | + /* get ref / enc perceptual hashes and calc hamming distance difference value */ 492 | + hd = getScore(ref, master, ref->format, s->hash_type); 493 | + if (hd == DBL_MAX) { 494 | + av_log(s, AV_LOG_ERROR, "Failure with handling pix_fmt of AVFrame for conversion to IPLimage.\n"); 495 | + return AVERROR(EINVAL); 496 | + } 497 | + s->hd += FFMIN(hd, hd_limit); 498 | + s->phd += FFMIN(s->prev_hamm_ref, hd_limit); 499 | + s->psad += FFMIN(s->scene_score_ref, hd_limit); 500 | + set_meta(metadata, "lavfi.phqm.phqm", 0, hd); 501 | + 502 | + /* scene hamming distance avg */ 503 | + s->shd += FFMIN(hd, hd_limit); 504 | + s->hft += s->prev_hamm_ref; 505 | + s->sft += s->scene_score_ref; 506 | + s->nb_shd++; 507 | + av_log(s, AV_LOG_DEBUG, "ImgHashFrame: hd:%0.3lf sad:%0.2lf hamm:%0.3lf\n", hd, s->scene_score_ref, s->prev_hamm_ref); 508 | + 509 | + s->min_hd = FFMIN(s->min_hd, hd); 510 | + s->max_hd = FFMAX(s->max_hd, hd); 511 | + s->smin_hd = FFMIN(s->smin_hd, hd); 512 | + s->smax_hd = FFMAX(s->smax_hd, hd); 513 | + 514 | + if (s->stats_file) { 515 | + fprintf(s->stats_file, 516 | + "n:%"PRId64" phqm:%0.3f phqm_min:%0.3f phqm_max:%0.3f sad:%0.2f ref_hamm:%0.2f enc_hamm:%0.2f", 517 | + s->nb_frames, hd, s->min_hd, s->max_hd, s->scene_score_ref, s->prev_hamm_ref, s->prev_hamm_enc); 518 | + fprintf(s->stats_file, "\n"); 519 | + } 520 | + 521 | + return ff_filter_frame(ctx->outputs[0], master); 522 | +} 523 | + 524 | +static av_cold int init(AVFilterContext *ctx) 525 | +{ 526 | + PHQMContext *s = ctx->priv; 527 | + 528 | + if (s->stats_file_str) { 529 | + if (!strcmp(s->stats_file_str, "-")) { 530 | + s->stats_file = stdout; 531 | + } else { 532 | + s->stats_file = fopen(s->stats_file_str, "w"); 533 | + if (!s->stats_file) { 534 | + int err = AVERROR(errno); 535 | + char buf[128]; 536 | + av_strerror(err, buf, sizeof(buf)); 537 | + av_log(ctx, AV_LOG_ERROR, "Could not open stats file %s: %s\n", 538 | + s->stats_file_str, buf); 539 | + return err; 540 | + } 541 | + } 542 | + } 543 | + 544 | + s->sad_ref = ff_scene_sad_get_fn(8); 545 | + if (!s->sad_ref) 546 | + return AVERROR(EINVAL); 547 | + s->sad_enc = ff_scene_sad_get_fn(8); 548 | + if (!s->sad_enc) 549 | + return AVERROR(EINVAL); 550 | + 551 | + s->fs.on_event = do_phqm; 552 | + return 0; 553 | +} 554 | + 555 | +static int query_formats(AVFilterContext *ctx) 556 | +{ 557 | + PHQMContext *s = ctx->priv; 558 | + AVFilterFormats *fmts_list = NULL; 559 | + static const enum AVPixelFormat gray8_pix_fmts[] = { 560 | + AV_PIX_FMT_GRAY8, 561 | + AV_PIX_FMT_NONE 562 | + }; 563 | + static const enum AVPixelFormat bgr24_pix_fmts[] = { 564 | + AV_PIX_FMT_BGR24, 565 | + AV_PIX_FMT_NONE 566 | + }; 567 | + static const enum AVPixelFormat bgra_pix_fmts[] = { 568 | + AV_PIX_FMT_BGRA, 569 | + AV_PIX_FMT_NONE 570 | + }; 571 | + 572 | + switch (s->hash_type) { 573 | + case COLORMOMENT: fmts_list = ff_make_format_list(bgr24_pix_fmts); break; 574 | + case MARRHILDRETH: fmts_list = ff_make_format_list(bgra_pix_fmts); break; 575 | + /* all other hashes take the gray8 format */ 576 | + default: fmts_list = ff_make_format_list(gray8_pix_fmts); break; 577 | + } 578 | + if (!fmts_list) 579 | + return AVERROR(ENOMEM); 580 | + return ff_set_common_formats(ctx, fmts_list); 581 | +} 582 | + 583 | +static int config_input_ref(AVFilterLink *inlink) 584 | +{ 585 | + AVFilterContext *ctx = inlink->dst; 586 | + 587 | + if (ctx->inputs[0]->w != ctx->inputs[1]->w || 588 | + ctx->inputs[0]->h != ctx->inputs[1]->h) { 589 | + av_log(ctx, AV_LOG_ERROR, "Width and height of input videos must be same.\n"); 590 | + return AVERROR(EINVAL); 591 | + } 592 | + if (ctx->inputs[0]->format != ctx->inputs[1]->format) { 593 | + av_log(ctx, AV_LOG_ERROR, "Inputs must be of same pixel format.\n"); 594 | + return AVERROR(EINVAL); 595 | + } 596 | + 597 | + return 0; 598 | +} 599 | + 600 | +static int config_output(AVFilterLink *outlink) 601 | +{ 602 | + AVFilterContext *ctx = outlink->src; 603 | + PHQMContext *s = ctx->priv; 604 | + AVFilterLink *mainlink = ctx->inputs[0]; 605 | + int ret; 606 | + 607 | + ret = ff_framesync_init_dualinput(&s->fs, ctx); 608 | + if (ret < 0) 609 | + return ret; 610 | + outlink->w = mainlink->w; 611 | + outlink->h = mainlink->h; 612 | + outlink->time_base = mainlink->time_base; 613 | + outlink->sample_aspect_ratio = mainlink->sample_aspect_ratio; 614 | + outlink->frame_rate = mainlink->frame_rate; 615 | + if ((ret = ff_framesync_configure(&s->fs)) < 0) 616 | + return ret; 617 | + 618 | + return 0; 619 | +} 620 | + 621 | +static int activate(AVFilterContext *ctx) 622 | +{ 623 | + PHQMContext *s = ctx->priv; 624 | + return ff_framesync_activate(&s->fs); 625 | +} 626 | + 627 | +static av_cold void uninit(AVFilterContext *ctx) 628 | +{ 629 | + PHQMContext *s = ctx->priv; 630 | + 631 | + if (s->nb_frames > 0) 632 | + av_log(ctx, AV_LOG_WARNING, "PHQM average:%f min:%f max:%f hamm:%f sad:%f\n", 633 | + s->hd / s->nb_frames, s->min_hd, s->max_hd, 634 | + s->phd / s->nb_frames, s->psad / s->nb_frames); 635 | + 636 | + ff_framesync_uninit(&s->fs); 637 | + 638 | + if (s->stats_file && s->stats_file != stdout) 639 | + fclose(s->stats_file); 640 | + av_frame_free(&s->prev_pic_ref); 641 | + av_frame_free(&s->prev_pic_enc); 642 | +} 643 | + 644 | +static const AVFilterPad phqm_inputs[] = { 645 | + { 646 | + .name = "main", 647 | + .type = AVMEDIA_TYPE_VIDEO, 648 | + },{ 649 | + .name = "reference", 650 | + .type = AVMEDIA_TYPE_VIDEO, 651 | + .config_props = config_input_ref, 652 | + }, 653 | + { NULL } 654 | +}; 655 | + 656 | +static const AVFilterPad phqm_outputs[] = { 657 | + { 658 | + .name = "default", 659 | + .type = AVMEDIA_TYPE_VIDEO, 660 | + .config_props = config_output, 661 | + }, 662 | + { NULL } 663 | +}; 664 | + 665 | +AVFilter ff_vf_phqm= { 666 | + .name = "phqm", 667 | + .description = NULL_IF_CONFIG_SMALL("PHQM: Calculate the Perceptual Hash Hamming Difference between two video streams."), 668 | + .preinit = phqm_framesync_preinit, 669 | + .init = init, 670 | + .uninit = uninit, 671 | + .query_formats = query_formats, 672 | + .activate = activate, 673 | + .priv_size = sizeof(PHQMContext), 674 | + .priv_class = &phqm_class, 675 | + .inputs = phqm_inputs, 676 | + .outputs = phqm_outputs, 677 | +}; 678 | diff --git a/libavformat/mpegenc.c b/libavformat/mpegenc.c 679 | index 43ebc46e0e..f6aa705e96 100644 680 | --- a/libavformat/mpegenc.c 681 | +++ b/libavformat/mpegenc.c 682 | @@ -984,7 +984,7 @@ static int remove_decoded_packets(AVFormatContext *ctx, int64_t scr) 683 | scr > pkt_desc->dts) { // FIXME: > vs >= 684 | if (stream->buffer_index < pkt_desc->size || 685 | stream->predecode_packet == stream->premux_packet) { 686 | - av_log(ctx, AV_LOG_ERROR, 687 | + av_log(ctx, AV_LOG_WARNING, 688 | "buffer underflow st=%d bufi=%d size=%d\n", 689 | i, stream->buffer_index, pkt_desc->size); 690 | break; 691 | @@ -1063,7 +1063,7 @@ retry: 692 | scr / 90000.0, best_dts / 90000.0); 693 | 694 | if (scr >= best_dts + 1 && !ignore_constraints) { 695 | - av_log(ctx, AV_LOG_ERROR, 696 | + av_log(ctx, AV_LOG_WARNING, 697 | "packet too large, ignoring buffer limits to mux it\n"); 698 | ignore_constraints = 1; 699 | } 700 | diff --git a/libavutil/frame.c b/libavutil/frame.c 701 | index dcf1fc3d17..94e7644d1f 100644 702 | --- a/libavutil/frame.c 703 | +++ b/libavutil/frame.c 704 | @@ -163,6 +163,8 @@ FF_ENABLE_DEPRECATION_WARNINGS 705 | frame->color_range = AVCOL_RANGE_UNSPECIFIED; 706 | frame->chroma_location = AVCHROMA_LOC_UNSPECIFIED; 707 | frame->flags = 0; 708 | + frame->perceptual_score = -1; 709 | + frame->perceptual_score_factor = 2.0; 710 | } 711 | 712 | static void free_side_data(AVFrameSideData **ptr_sd) 713 | @@ -373,6 +375,8 @@ FF_ENABLE_DEPRECATION_WARNINGS 714 | dst->colorspace = src->colorspace; 715 | dst->color_range = src->color_range; 716 | dst->chroma_location = src->chroma_location; 717 | + dst->perceptual_score = src->perceptual_score; 718 | + dst->perceptual_score_factor = src->perceptual_score_factor; 719 | 720 | av_dict_copy(&dst->metadata, src->metadata, 0); 721 | 722 | @@ -453,6 +457,8 @@ int av_frame_ref(AVFrame *dst, const AVFrame *src) 723 | dst->channels = src->channels; 724 | dst->channel_layout = src->channel_layout; 725 | dst->nb_samples = src->nb_samples; 726 | + dst->perceptual_score = src->perceptual_score; 727 | + dst->perceptual_score_factor = src->perceptual_score_factor; 728 | 729 | ret = frame_copy_props(dst, src, 0); 730 | if (ret < 0) 731 | diff --git a/libavutil/frame.h b/libavutil/frame.h 732 | index 5d3231e7bb..c9df011aaa 100644 733 | --- a/libavutil/frame.h 734 | +++ b/libavutil/frame.h 735 | @@ -672,6 +672,13 @@ typedef struct AVFrame { 736 | * for the target frame's private_ref field. 737 | */ 738 | AVBufferRef *private_ref; 739 | + 740 | + /** 741 | + * perceptual score 742 | + * 0.00 - 1.00 percentage of perceptual match to the previous frame 743 | + */ 744 | + float perceptual_score; 745 | + float perceptual_score_factor; 746 | } AVFrame; 747 | 748 | #if FF_API_FRAME_GET_SET 749 | -------------------------------------------------------------------------------- /reference.cpp: -------------------------------------------------------------------------------- 1 | // Tutorial and code from 2 | // https://qtandopencv.blogspot.com/2016/06/introduction-to-image-hash-module-of.html 3 | 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | 13 | using namespace cv; 14 | using namespace cv::img_hash; 15 | using namespace std; 16 | 17 | void computeHash(cv::Ptr algo, char *ref, char *enc) 18 | { 19 | cv::Mat const input = cv::imread(ref); 20 | cv::Mat const target = cv::imread(enc); 21 | 22 | cv::Mat inHash; //hash of input image 23 | cv::Mat targetHash; //hash of target image 24 | 25 | //comupte hash of input and target 26 | algo->compute(input, inHash); 27 | algo->compute(target, targetHash); 28 | //Compare the similarity of inHash and targetHash 29 | //recommended thresholds are written in the header files 30 | //of every classes 31 | double const mismatch = algo->compare(inHash, targetHash); 32 | std::cout< \n"); 43 | exit(1); 44 | } 45 | 46 | std::cout<<"AverageHash:"< d.endsWith('_vmaf.json')); 15 | 16 | console.log(`found ${files.length} vmaf files`); 17 | 18 | // Extract all tiers/scores 19 | let tierScores = []; 20 | files.forEach(f => { 21 | // Filename example: 22 | // {mezz-filename}_240p00120X264H264_ZZD_vmaf.data 23 | let fullPath = path.join(folder, f); 24 | let filenameParts = f.split('_'); 25 | let tier = filenameParts[filenameParts.length - 3]; 26 | 27 | let height = tier.split('p')[0]; 28 | let bitrate = tier.split('p')[1].split('X')[0]; 29 | 30 | let contents = fs.readFileSync(fullPath, { encoding: 'utf-8' }); 31 | let avg = JSON.parse(contents); 32 | 33 | let scoreContents = fs.readFileSync(fullPath.split('.json').join('.data'), { encoding: 'utf-8' }); 34 | let allScores = JSON.parse(scoreContents).frames.map(f => f.metrics.vmaf).sort((a, b) => a - b); 35 | let fifthPercentile = allScores[Math.floor(allScores.length * .05)]; 36 | let numFramesBelow92 = 0; 37 | 38 | let i = 0; 39 | while (i < allScores.length && numFramesBelow92 == 0) { 40 | if (allScores[i] >= 92) { 41 | numFramesBelow92 = i; 42 | } 43 | i++; 44 | } 45 | 46 | tierScores.push({ 47 | height: height, 48 | bitrate: parseInt(bitrate), 49 | vmaf: avg.avg[0], 50 | fifthPercentile: fifthPercentile, 51 | framesBelow92: numFramesBelow92, 52 | numFrames: allScores.length, 53 | file: fullPath 54 | }); 55 | }); 56 | 57 | console.log(`finding ideal ladder from ${tierScores.length} tiers`); 58 | 59 | // Find our top tier to start 60 | let topTier = findLowestBitrateOverVmaf(tierScores, topTierTarget); 61 | 62 | let idealLadder = [topTier]; 63 | let tierFound = true; 64 | let lastBitrateFound = idealLadder[0].bitrate; 65 | 66 | // Loop until we have no more tiers or we have the desired number of tiers 67 | while (tierFound && idealLadder.length < desiredTiers) { 68 | let targetBitrate = lastBitrateFound * .6; 69 | let maxBitrate = targetBitrate + 200; 70 | let minBitrate = targetBitrate - 200; 71 | 72 | let nextTier = findHighestVmafInBitrateRange(tierScores, maxBitrate, minBitrate, idealLadder); 73 | if (nextTier) { 74 | idealLadder.push(nextTier); 75 | lastBitrateFound = nextTier.bitrate; 76 | } else { 77 | tierFound = false; 78 | } 79 | } 80 | 81 | // Organize by bit-rate so we can build an ordered csv for generating graphs 82 | let bitrates = []; 83 | let heights = ['ideal ladder']; 84 | tierScores.forEach(t => { 85 | if (bitrates.indexOf(t.bitrate) < 0){ 86 | bitrates.push(t.bitrate); 87 | } 88 | if (heights.indexOf(t.height) < 0){ 89 | heights.push(t.height); 90 | } 91 | }); 92 | heights = heights.sort((a, b) => a - b); 93 | bitrates = bitrates.sort((a, b) => a - b); 94 | 95 | let results = []; 96 | let firstRow = ['bitrate']; 97 | heights.forEach(h => { 98 | firstRow.push(h); 99 | }); 100 | results.push(firstRow); 101 | 102 | bitrates.forEach(b => { 103 | let row = [b]; 104 | for (let i = 0; i < heights.length; i++){ 105 | if (i == 0) { 106 | // Ideal ladder row 107 | let idealTier = idealLadder.filter(tr => tr.bitrate == b); 108 | if (idealTier.length > 0) { 109 | row.push(idealTier[0].fifthPercentile); 110 | } else { 111 | row.push(''); 112 | } 113 | } else { 114 | let height = heights[i]; 115 | let tier = tierScores.filter(ts => ts.height == height && ts.bitrate == b); 116 | if (tier.length > 0) { 117 | row.push(tier[0].fifthPercentile); 118 | } else { 119 | row.push(''); 120 | } 121 | } 122 | } 123 | results.push(row); 124 | }); 125 | 126 | results.push([]); 127 | results.push(['Top Tier Stats:']); 128 | results.push(['5th Percentile', topTier.fifthPercentile]); 129 | results.push(['Number of frames below 92', topTier.framesBelow92]); 130 | results.push(['Percentage of frames below 92', `%${(topTier.framesBelow92 / topTier.numFrames) * 100}`]) 131 | 132 | console.log(`\nresults:\n\n${results.map(r => r.join(',')).join('\n')}`); 133 | 134 | 135 | function findLowestBitrateOverVmaf(tiers, vmafTarget){ 136 | let filteredTiers = tiers.filter(t => t.fifthPercentile >= vmafTarget); 137 | 138 | if (filteredTiers.length > 0){ 139 | let returning = filteredTiers[0]; 140 | 141 | filteredTiers.forEach(t => { 142 | if (t.bitrate < returning.bitrate || (t.bitrate == returning.bitrate && t.fifthPercentile > returning.fifthPercentile)) { 143 | returning = t; 144 | } 145 | }); 146 | 147 | return returning; 148 | } 149 | 150 | // If we don't have a tier, return the highest vmaf score we found 151 | console.log('No tiers found over target, picking highest scoring tier available'); 152 | return tiers.sort((a, b) => b.fifthPercentile - a.fifthPercentile)[0]; 153 | } 154 | 155 | function findHighestVmafInBitrateRange(tiers, maxBitrate, minBitrate, existingTiers){ 156 | let filtered = tiers.filter(t => { 157 | let fits = t.bitrate >= minBitrate && t.bitrate <= maxBitrate 158 | if (!fits) { 159 | return false; 160 | } else { 161 | let matching = existingTiers.filter(e => e.bitrate == t.bitrate); 162 | if (matching.length > 0){ 163 | return false; 164 | } 165 | 166 | return true; 167 | } 168 | }); 169 | 170 | 171 | if (filtered.length > 0){ 172 | return filtered.sort((a, b) => b.fifthPercentile - a.fifthPercentile)[0]; 173 | } 174 | 175 | return undefined; 176 | } 177 | -------------------------------------------------------------------------------- /scripts/helper-scripts/generate-objective-perceptual-analysis-tiers.js: -------------------------------------------------------------------------------- 1 | let tiers = [ 2 | { 3 | height: 1080, 4 | bitrates: [10000, 9500, 9000, 8500, 8000, 7500, 7000, 6500, 6000, 5500, 5000, 4500, 4000, 3500, 3000, 2500, 2000, 1500, 1000, 500] 5 | }, 6 | { 7 | height: 720, 8 | bitrates: [6000, 5500, 5000, 4500, 4000, 3500, 3000, 2500, 2000, 1500, 1000, 500] 9 | }, 10 | { 11 | height: 480, 12 | bitrates: [3500, 3000, 2500, 2000, 1500, 1000, 500, 300, 200, 120] 13 | }, 14 | { 15 | height: 360, 16 | bitrates: [2500, 2000, 1500, 1000, 500, 300, 200, 120] 17 | }, 18 | { 19 | height: 240, 20 | bitrates: [1500, 1000, 500, 300, 200, 120] 21 | } 22 | ] 23 | 24 | let cmds = []; 25 | 26 | tiers.forEach(t => { 27 | // 01000X264H264|ffmpeg|twopass|S|mp4||1080|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|1000k|-maxrate:v|1500k|-bufsize:v|3000k|-minrate:v|1000k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats; 28 | t.bitrates.forEach(bitrate => { 29 | let maxRate = bitrate * 1.5; 30 | let bufSize = bitrate * 3; 31 | 32 | cmds.push(`${t.height}p${('00000'+bitrate).slice(-5)}X264H264|ffmpeg|twopass|S|mp4||${t.height}|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|${bitrate}k|-maxrate:v|${maxRate}k|-bufsize:v|${bufSize}k|-minrate:v|${bitrate}k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;`); 33 | }); 34 | }); 35 | 36 | console.log(cmds.join('\\\n')); 37 | -------------------------------------------------------------------------------- /scripts/helper-scripts/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "helper-scripts", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "minimist": "^1.2.8" 9 | } 10 | }, 11 | "node_modules/minimist": { 12 | "version": "1.2.8", 13 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", 14 | "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", 15 | "funding": { 16 | "url": "https://github.com/sponsors/ljharb" 17 | } 18 | } 19 | }, 20 | "dependencies": { 21 | "minimist": { 22 | "version": "1.2.8", 23 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", 24 | "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /scripts/helper-scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "minimist": "^1.2.8" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /scripts/helper-scripts/readme.md: -------------------------------------------------------------------------------- 1 | # Helper Scripts 2 | A set of helper scripts designed to work in conjunction with the objective_perceptual_analysis tool 3 | 4 | ## generate-objective-perceptual-analysis-tiers.js 5 | This script generates a list of tiers that can be run through the objective_perceptual_analysis tool to calculate metrics for that tier. 6 | 7 | Example run command: 8 | > node generate-objective-perceptual-analysis-tiers.js 9 | 10 | Results will look like this: 11 | ``` 12 | 240p00200X264H264|ffmpeg|twopass|S|mp4||240|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|200k|-maxrate:v|300k|-bufsize:v|600k|-minrate:v|200k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 13 | 240p00120X264H264|ffmpeg|twopass|S|mp4||240|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|120k|-maxrate:v|180k|-bufsize:v|360k|-minrate:v|120k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 14 | ``` 15 | 16 | ## build-per-title-ladder.js 17 | This script takes in a folder, target vmaf score for top tier, and the desired number of tiers and builds a per-title ladder based off the metrics it finds in that folder. It utiilizes a 5th percentile target for the scoring. So if you input a target of 93 it will find a tier that ensures 95% of frames are at, or above, that score. Using this approach helps alleviate low-quality frames from your asset that using a simple average would not catch. 18 | 19 | In order to run you need to first install the npm packages (will need to install npm if not already installed): 20 | > npm i 21 | 22 | Example run command: 23 | > node build-per-title-ladder-csv.js --folder=../../tests/test000/results/ --target=93 --tiersDesired=5 24 | 25 | Results will look like this: 26 | ``` 27 | bitrate,ideal ladder,240,360,480,720,1080 28 | 120,,5.778297,4.988284,4.063627,, 29 | 200,,10.871521,10.393279,8.083678,, 30 | 300,,17.226468,17.951529,15.693013,, 31 | 500,,26.987996,30.484075,28.621478,21.335097,12.203873 32 | 1000,50.380761,40.999788,49.296895,50.380761,44.335129,32.289643 33 | 1500,,48.386209,59.698441,62.471787,59.289839,47.9577 34 | 2000,70.150978,52.847373,66.243257,70.150978,68.779799,59.610497 35 | 2500,,55.702071,70.794025,75.414619,75.326258,68.026903 36 | 3000,80.039061,57.623165,74.053516,79.230803,80.039061,74.402542 37 | 3500,,59.004372,76.476479,82.114151,83.566198,79.068612 38 | 4000,,59.991882,78.346787,84.352679,86.296918,82.728679 39 | 4500,,,79.789811,86.102682,88.478361,85.598901 40 | 5000,90.185026,,80.920916,87.502313,90.185026,87.90199 41 | 5500,,,,88.634468,91.569519,89.766263 42 | 6000,,,,89.533024,92.667164,91.285771 43 | 6500,,,,,93.537033,92.530484 44 | 7000,,,,,94.244509,93.550486 45 | 7500,,,,,94.799134,94.354476 46 | 8000,95.024027,,,,95.254355,95.024027 47 | 8500,,,,,,95.568996 48 | 9000,,,,,,96.022525 49 | 9500,,,,,,96.410157 50 | 10000,,,,,,96.738205 51 | ``` 52 | -------------------------------------------------------------------------------- /scripts/readme.md: -------------------------------------------------------------------------------- 1 | Test scripts directory 2 | 3 | Create a set of tests: 4 | 5 | ``` 6 | scripts/gen_cmd.sh 000 > scripts/run_test000.sh 7 | ``` 8 | 9 | Create test directories in tests/test000/ directory 10 | 11 | ``` 12 | sh scripts/run_test000.sh 13 | ``` 14 | 15 | Setup mezzanines to test with 16 | 17 | ``` 18 | mv mezzanine.mp4 mezzanine2.mp4 tests/test000/mezzanines/ 19 | ``` 20 | 21 | Run tests 22 | ``` 23 | sh scripts/run_test000.sh 24 | ``` 25 | 26 | Analyze tests 27 | ``` 28 | bin/results.py -n tests/test000 29 | ``` 30 | 31 | View graph stats.jpg 32 | 33 | View json stats.json 34 | 35 | Preview videos w/metrics: previews/*.mp4 36 | 37 | -------------------------------------------------------------------------------- /scripts/run_per_title_analysis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | sudo docker run --rm -v `pwd`/tests:/opaencoder/tests opaencoder bin/encode.py \ 4 | -m vmaf,psnr,ssim,cambi \ 5 | -n tests/test000 \ 6 | -p 8 -t "\ 7 | 1080p10000X264H264|ffmpeg|twopass|S|mp4||1080|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|10000k|-maxrate:v|15000k|-bufsize:v|30000k|-minrate:v|10000k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 8 | 1080p09500X264H264|ffmpeg|twopass|S|mp4||1080|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|9500k|-maxrate:v|14250k|-bufsize:v|28500k|-minrate:v|9500k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 9 | 1080p09000X264H264|ffmpeg|twopass|S|mp4||1080|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|9000k|-maxrate:v|13500k|-bufsize:v|27000k|-minrate:v|9000k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 10 | 1080p08500X264H264|ffmpeg|twopass|S|mp4||1080|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|8500k|-maxrate:v|12750k|-bufsize:v|25500k|-minrate:v|8500k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 11 | 1080p08000X264H264|ffmpeg|twopass|S|mp4||1080|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|8000k|-maxrate:v|12000k|-bufsize:v|24000k|-minrate:v|8000k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 12 | 1080p07500X264H264|ffmpeg|twopass|S|mp4||1080|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|7500k|-maxrate:v|11250k|-bufsize:v|22500k|-minrate:v|7500k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 13 | 1080p07000X264H264|ffmpeg|twopass|S|mp4||1080|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|7000k|-maxrate:v|10500k|-bufsize:v|21000k|-minrate:v|7000k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 14 | 1080p06500X264H264|ffmpeg|twopass|S|mp4||1080|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|6500k|-maxrate:v|9750k|-bufsize:v|19500k|-minrate:v|6500k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 15 | 1080p06000X264H264|ffmpeg|twopass|S|mp4||1080|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|6000k|-maxrate:v|9000k|-bufsize:v|18000k|-minrate:v|6000k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 16 | 1080p05500X264H264|ffmpeg|twopass|S|mp4||1080|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|5500k|-maxrate:v|8250k|-bufsize:v|16500k|-minrate:v|5500k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 17 | 1080p05000X264H264|ffmpeg|twopass|S|mp4||1080|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|5000k|-maxrate:v|7500k|-bufsize:v|15000k|-minrate:v|5000k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 18 | 1080p04500X264H264|ffmpeg|twopass|S|mp4||1080|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|4500k|-maxrate:v|6750k|-bufsize:v|13500k|-minrate:v|4500k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 19 | 1080p04000X264H264|ffmpeg|twopass|S|mp4||1080|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|4000k|-maxrate:v|6000k|-bufsize:v|12000k|-minrate:v|4000k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 20 | 1080p03500X264H264|ffmpeg|twopass|S|mp4||1080|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|3500k|-maxrate:v|5250k|-bufsize:v|10500k|-minrate:v|3500k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 21 | 1080p03000X264H264|ffmpeg|twopass|S|mp4||1080|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|3000k|-maxrate:v|4500k|-bufsize:v|9000k|-minrate:v|3000k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 22 | 1080p02500X264H264|ffmpeg|twopass|S|mp4||1080|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|2500k|-maxrate:v|3750k|-bufsize:v|7500k|-minrate:v|2500k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 23 | 1080p02000X264H264|ffmpeg|twopass|S|mp4||1080|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|2000k|-maxrate:v|3000k|-bufsize:v|6000k|-minrate:v|2000k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 24 | 1080p01500X264H264|ffmpeg|twopass|S|mp4||1080|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|1500k|-maxrate:v|2250k|-bufsize:v|4500k|-minrate:v|1500k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 25 | 1080p01000X264H264|ffmpeg|twopass|S|mp4||1080|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|1000k|-maxrate:v|1500k|-bufsize:v|3000k|-minrate:v|1000k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 26 | 1080p00500X264H264|ffmpeg|twopass|S|mp4||1080|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|500k|-maxrate:v|750k|-bufsize:v|1500k|-minrate:v|500k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 27 | 720p06000X264H264|ffmpeg|twopass|S|mp4||720|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|6000k|-maxrate:v|9000k|-bufsize:v|18000k|-minrate:v|6000k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 28 | 720p05500X264H264|ffmpeg|twopass|S|mp4||720|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|5500k|-maxrate:v|8250k|-bufsize:v|16500k|-minrate:v|5500k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 29 | 720p05000X264H264|ffmpeg|twopass|S|mp4||720|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|5000k|-maxrate:v|7500k|-bufsize:v|15000k|-minrate:v|5000k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 30 | 720p04500X264H264|ffmpeg|twopass|S|mp4||720|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|4500k|-maxrate:v|6750k|-bufsize:v|13500k|-minrate:v|4500k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 31 | 720p04000X264H264|ffmpeg|twopass|S|mp4||720|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|4000k|-maxrate:v|6000k|-bufsize:v|12000k|-minrate:v|4000k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 32 | 720p03500X264H264|ffmpeg|twopass|S|mp4||720|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|3500k|-maxrate:v|5250k|-bufsize:v|10500k|-minrate:v|3500k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 33 | 720p03000X264H264|ffmpeg|twopass|S|mp4||720|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|3000k|-maxrate:v|4500k|-bufsize:v|9000k|-minrate:v|3000k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 34 | 720p02500X264H264|ffmpeg|twopass|S|mp4||720|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|2500k|-maxrate:v|3750k|-bufsize:v|7500k|-minrate:v|2500k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 35 | 720p02000X264H264|ffmpeg|twopass|S|mp4||720|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|2000k|-maxrate:v|3000k|-bufsize:v|6000k|-minrate:v|2000k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 36 | 720p01500X264H264|ffmpeg|twopass|S|mp4||720|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|1500k|-maxrate:v|2250k|-bufsize:v|4500k|-minrate:v|1500k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 37 | 720p01000X264H264|ffmpeg|twopass|S|mp4||720|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|1000k|-maxrate:v|1500k|-bufsize:v|3000k|-minrate:v|1000k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 38 | 720p00500X264H264|ffmpeg|twopass|S|mp4||720|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|500k|-maxrate:v|750k|-bufsize:v|1500k|-minrate:v|500k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 39 | 480p03500X264H264|ffmpeg|twopass|S|mp4||480|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|3500k|-maxrate:v|5250k|-bufsize:v|10500k|-minrate:v|3500k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 40 | 480p03000X264H264|ffmpeg|twopass|S|mp4||480|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|3000k|-maxrate:v|4500k|-bufsize:v|9000k|-minrate:v|3000k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 41 | 480p02500X264H264|ffmpeg|twopass|S|mp4||480|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|2500k|-maxrate:v|3750k|-bufsize:v|7500k|-minrate:v|2500k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 42 | 480p02000X264H264|ffmpeg|twopass|S|mp4||480|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|2000k|-maxrate:v|3000k|-bufsize:v|6000k|-minrate:v|2000k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 43 | 480p01500X264H264|ffmpeg|twopass|S|mp4||480|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|1500k|-maxrate:v|2250k|-bufsize:v|4500k|-minrate:v|1500k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 44 | 480p01000X264H264|ffmpeg|twopass|S|mp4||480|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|1000k|-maxrate:v|1500k|-bufsize:v|3000k|-minrate:v|1000k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 45 | 480p00500X264H264|ffmpeg|twopass|S|mp4||480|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|500k|-maxrate:v|750k|-bufsize:v|1500k|-minrate:v|500k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 46 | 480p00300X264H264|ffmpeg|twopass|S|mp4||480|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|300k|-maxrate:v|450k|-bufsize:v|900k|-minrate:v|300k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 47 | 480p00200X264H264|ffmpeg|twopass|S|mp4||480|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|200k|-maxrate:v|300k|-bufsize:v|600k|-minrate:v|200k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 48 | 480p00120X264H264|ffmpeg|twopass|S|mp4||480|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|120k|-maxrate:v|180k|-bufsize:v|360k|-minrate:v|120k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 49 | 360p02500X264H264|ffmpeg|twopass|S|mp4||360|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|2500k|-maxrate:v|3750k|-bufsize:v|7500k|-minrate:v|2500k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 50 | 360p02000X264H264|ffmpeg|twopass|S|mp4||360|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|2000k|-maxrate:v|3000k|-bufsize:v|6000k|-minrate:v|2000k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 51 | 360p01500X264H264|ffmpeg|twopass|S|mp4||360|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|1500k|-maxrate:v|2250k|-bufsize:v|4500k|-minrate:v|1500k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 52 | 360p01000X264H264|ffmpeg|twopass|S|mp4||360|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|1000k|-maxrate:v|1500k|-bufsize:v|3000k|-minrate:v|1000k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 53 | 360p00500X264H264|ffmpeg|twopass|S|mp4||360|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|500k|-maxrate:v|750k|-bufsize:v|1500k|-minrate:v|500k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 54 | 360p00300X264H264|ffmpeg|twopass|S|mp4||360|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|300k|-maxrate:v|450k|-bufsize:v|900k|-minrate:v|300k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 55 | 360p00200X264H264|ffmpeg|twopass|S|mp4||360|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|200k|-maxrate:v|300k|-bufsize:v|600k|-minrate:v|200k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 56 | 360p00120X264H264|ffmpeg|twopass|S|mp4||360|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|120k|-maxrate:v|180k|-bufsize:v|360k|-minrate:v|120k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 57 | 240p01500X264H264|ffmpeg|twopass|S|mp4||240|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|1500k|-maxrate:v|2250k|-bufsize:v|4500k|-minrate:v|1500k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 58 | 240p01000X264H264|ffmpeg|twopass|S|mp4||240|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|1000k|-maxrate:v|1500k|-bufsize:v|3000k|-minrate:v|1000k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 59 | 240p00500X264H264|ffmpeg|twopass|S|mp4||240|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|500k|-maxrate:v|750k|-bufsize:v|1500k|-minrate:v|500k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 60 | 240p00300X264H264|ffmpeg|twopass|S|mp4||240|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|300k|-maxrate:v|450k|-bufsize:v|900k|-minrate:v|300k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 61 | 240p00200X264H264|ffmpeg|twopass|S|mp4||240|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|200k|-maxrate:v|300k|-bufsize:v|600k|-minrate:v|200k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 62 | 240p00120X264H264|ffmpeg|twopass|S|mp4||240|-pix_fmt|yuv420p|-f|mp4|-movflags|+faststart|-profile:v|high|-preset|slow|-vcodec|libx264|-bf|0|-refs|4|-b:v|120k|-maxrate:v|180k|-bufsize:v|360k|-minrate:v|120k|-tune|animation|-x264opts|rc-lookahead=48:keyint=96|-keyint_min|48|-g|96|-force_key_frames|expr:eq(mod(n,48),0)|-hide_banner|-nostats;\ 63 | " 64 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | OS=$(uname -s) 4 | 5 | echo 6 | if [ "$OS" == "Darwin" ]; then 7 | echo "Setting up for Mac OS X" 8 | echo 9 | ./setupMacOSX.sh 10 | elif [ "$OS" == "Linux" ]; then 11 | if [ -f /etc/redhat-release ]; then 12 | echo "Setting up for Linux CentOS 7" 13 | echo 14 | ./setupCentOS7.sh 15 | elif [ -f /etc/arch-release ]; then 16 | echo "Setting up for ArchLinux" 17 | echo 18 | ./setupArch.sh 19 | else 20 | echo "This Linux OS isn't supported. Try running ./setupCentOS7.sh or ./setupArch.sh manually if brave" 21 | fi 22 | fi 23 | 24 | echo 25 | echo "encode and results tools are in bin/" 26 | echo "scripts to run tests in scripts/" 27 | echo "tests directory is tests/" 28 | -------------------------------------------------------------------------------- /setupArch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir ~/_opaencoder_deps 4 | cd ~/_opaencoder_deps 5 | 6 | pacman --noconfirm -Syu 7 | pacman --noconfirm -S \ 8 | base-devel \ 9 | git \ 10 | cmake \ 11 | nasm \ 12 | x264 \ 13 | dav1d \ 14 | libvpx \ 15 | aom \ 16 | svt-av1 \ 17 | svt-vp9 \ 18 | rav1e \ 19 | ffms2 \ 20 | libmagick \ 21 | tesseract \ 22 | zimg \ 23 | cython \ 24 | meson \ 25 | cargo \ 26 | fftw \ 27 | python-pip \ 28 | gnuplot \ 29 | mediainfo 30 | 31 | # build opencv 32 | git clone https://github.com/opencv/opencv.git 33 | git clone https://github.com/opencv/opencv_contrib.git 34 | cd opencv_contrib 35 | git checkout 3.4 36 | cd .. 37 | cd opencv 38 | git checkout 3.4 39 | mkdir build 40 | cd build 41 | cmake \ 42 | -DCMAKE_INSTALL_PREFIX=/usr \ 43 | -DCMAKE_INSTALL_LIBDIR=lib \ 44 | -DBUILD_SHARED_LIBS=True \ 45 | -DCMAKE_BUILD_TYPE=Release \ 46 | -DCMAKE_CXX_FLAGS="$CXXFLAGS" \ 47 | -DCMAKE_C_FLAGS="$CFLAGS" \ 48 | -DENABLE_PRECOMPILED_HEADERS=OFF \ 49 | -DWITH_OPENMP=OFF \ 50 | -WITH_OPENCL=OFF \ 51 | -DWITH_IPP=OFF \ 52 | -DBUILD_EXAMPLES=OFF \ 53 | -DWITH_FFMPEG=OFF -DWITH_JASPER=OFF -DWITH_PNG=OFF \ 54 | -DBUILD_opencv_python=OFF \ 55 | -DOPENCV_EXTRA_MODULES_PATH=../../opencv_contrib/modules \ 56 | -DOPENCV_GENERATE_PKGCONFIG=True \ 57 | -DBUILD_opencv_core=ON \ 58 | -DBUILD_opencv_imgproc=ON \ 59 | -DBUILD_opencv_img_hash=ON \ 60 | -DBUILD_opencv_imgcodecs=ON \ 61 | -DBUILD_opencv_highgui=ON \ 62 | -DBUILD_opencv_aruco=OFF \ 63 | -DBUILD_opencv_bgsegm=OFF \ 64 | -DBUILD_opencv_bioinspired=OFF \ 65 | -DBUILD_opencv_calib3d=OFF \ 66 | -DBUILD_opencv_ccalib=OFF \ 67 | -DBUILD_opencv_datasets=OFF \ 68 | -DBUILD_opencv_dnn=OFF \ 69 | -DBUILD_opencv_dnn_objdetect=OFF \ 70 | -DBUILD_opencv_dpm=OFF \ 71 | -DBUILD_opencv_face=OFF \ 72 | -DBUILD_opencv_features2d=OFF \ 73 | -DBUILD_opencv_flann=OFF \ 74 | -DBUILD_opencv_fuzzy=OFF \ 75 | -DBUILD_opencv_gapi=OFF \ 76 | -DBUILD_opencv_hfs=OFF \ 77 | -DBUILD_opencv_line_descriptor=OFF \ 78 | -DBUILD_opencv_ml=OFF \ 79 | -DBUILD_opencv_objdetect=OFF \ 80 | -DBUILD_opencv_optflow=OFF \ 81 | -DBUILD_opencv_phase_unwrapping=OFF \ 82 | -DBUILD_opencv_photo=OFF \ 83 | -DBUILD_opencv_plot=OFF \ 84 | -DBUILD_opencv_python2=OFF \ 85 | -DBUILD_opencv_quality=OFF \ 86 | -DBUILD_opencv_reg=OFF \ 87 | -DBUILD_opencv_rgbd=OFF \ 88 | -DBUILD_opencv_saliency=OFF \ 89 | -DBUILD_opencv_shape=OFF \ 90 | -DBUILD_opencv_stereo=OFF \ 91 | -DBUILD_opencv_stitching=OFF \ 92 | -DBUILD_opencv_structured_light=OFF \ 93 | -DBUILD_opencv_superres=OFF \ 94 | -DBUILD_opencv_surface_matching=OFF \ 95 | -DBUILD_opencv_text=OFF \ 96 | -DBUILD_opencv_tracking=OFF \ 97 | -DBUILD_opencv_ts=OFF \ 98 | -DBUILD_opencv_video=OFF \ 99 | -DBUILD_opencv_videoio=OFF \ 100 | -DBUILD_opencv_videostab=OFF \ 101 | -DBUILD_opencv_xfeatures2d=OFF \ 102 | -DBUILD_opencv_ximgproc=OFF \ 103 | -DBUILD_opencv_xobjdetect=OFF \ 104 | -DBUILD_opencv_xphoto=OFF \ 105 | .. && \ 106 | make -j$(nproc) && make install && ldconfig 107 | 108 | # build vmaf 109 | cd ~/_opaencoder_deps 110 | git clone -b v1.3.15 https://github.com/Netflix/vmaf.git vmaf 111 | cd vmaf 112 | make -j$(nproc) && make install && ldconfig 113 | 114 | # build ffmpeg 115 | cd ~/_opaencoder_deps 116 | git clone https://git.ffmpeg.org/ffmpeg.git 117 | cd ffmpeg 118 | git checkout tags/n5.1.2 119 | cat /opaencoder/ffmpeg_modifications.diff | patch -p1 120 | ./configure --prefix=/usr \ 121 | --enable-libx264 \ 122 | --enable-libvpx \ 123 | --enable-gpl \ 124 | --enable-libopencv \ 125 | --enable-version3 \ 126 | --enable-libvmaf \ 127 | --enable-libfreetype \ 128 | --enable-fontconfig \ 129 | --enable-libass \ 130 | --enable-libaom \ 131 | --enable-libsvtav1 && \ 132 | make -j$(nproc) && make install && ldconfig 133 | 134 | # build vapoursynth 135 | cd ~/_opaencoder_deps 136 | git clone https://github.com/vapoursynth/vapoursynth.git 137 | cd vapoursynth 138 | ./autogen.sh && ./configure --prefix=/usr && make -j$(nproc) && make install && ldconfig 139 | 140 | # build vapoursynth plugins 141 | 142 | # ffms2 143 | cd ~/_opaencoder_deps 144 | git clone https://github.com/FFMS/ffms2.git 145 | cd ffms2 146 | ./autogen.sh && ./configure --libdir=/usr/lib/vapoursynth && make -j$(nproc) && make install 147 | 148 | # lsmash 149 | cd ~/_opaencoder_deps 150 | git clone https://github.com/l-smash/l-smash.git 151 | cd l-smash 152 | ./configure --enable-shared && make -j$(nproc) && make install 153 | cd ~/_opaencoder_deps 154 | git clone https://github.com/AkarinVS/L-SMASH-Works.git 155 | cd L-SMASH-Works/VapourSynth 156 | git checkout ffmpeg-4.5 157 | meson build && ninja -C build && ninja -C build install 158 | 159 | # addgrain 160 | cd ~/_opaencoder_deps 161 | git clone https://github.com/HomeOfVapourSynthEvolution/VapourSynth-AddGrain.git 162 | cd VapourSynth-AddGrain 163 | meson build && ninja -C build && ninja -C build install 164 | 165 | # adaptivegrain 166 | cd ~/_opaencoder_deps 167 | git clone https://github.com/Irrational-Encoding-Wizardry/adaptivegrain.git 168 | cd adaptivegrain 169 | cargo build --release && cp target/release/libadaptivegrain_rs.so /usr/lib/vapoursynth/ 170 | 171 | # bm3d 172 | cd ~/_opaencoder_deps 173 | git clone https://github.com/HomeOfVapourSynthEvolution/VapourSynth-BM3D.git 174 | cd VapourSynth-BM3D 175 | meson build && ninja -C build && ninja -C build install 176 | 177 | # continuityfixer 178 | cd ~/_opaencoder_deps 179 | git clone https://github.com/MonoS/VS-ContinuityFixer.git 180 | cd VS-ContinuityFixer 181 | g++ -I /usr/include/vapoursynth -fPIC continuity.cpp -O2 -msse2 -mfpmath=sse -shared -o continuity.so && \ 182 | cp continuity.so /usr/lib/vapoursynth 183 | 184 | # ctmf 185 | cd ~/_opaencoder_deps 186 | git clone https://github.com/HomeOfVapourSynthEvolution/VapourSynth-CTMF.git 187 | cd /root/opaencoder/_deps/VapourSynth-CTMF 188 | meson build && ninja -C build && ninja -C build install 189 | 190 | # dctfilter 191 | cd ~/_opaencoder_deps 192 | git clone https://bitbucket.org/mystery_keeper/vapoursynth-dctfilter 193 | cd vapoursynth-dctfilter/src 194 | gcc -I /usr/lib/vapoursynth -fPIC main.c -O2 -msse2 -shared -o dctfilter.so && cp dctfilter.so /usr/lib/vapoursynth 195 | 196 | # deblock 197 | cd ~/_opaencoder_deps 198 | git clone https://github.com/HomeOfVapourSynthEvolution/VapourSynth-Deblock.git 199 | cd VapourSynth-Deblock 200 | meson build && ninja -C build && ninja -C build install 201 | 202 | # descale 203 | cd ~/_opaencoder_deps 204 | git clone https://github.com/Frechdachs/vapoursynth-descale.git 205 | cd vapoursynth-descale 206 | meson build && ninja -C build && ninja -C build install 207 | 208 | # dfttest 209 | cd ~/_opaencoder_deps 210 | git clone https://github.com/HomeOfVapourSynthEvolution/VapourSynth-DFTTest.git 211 | cd VapourSynth-DFTTest 212 | meson build && ninja -C build && ninja -C build install 213 | 214 | # eedi2 215 | cd ~/_opaencoder_deps 216 | git clone https://github.com/HomeOfVapourSynthEvolution/VapourSynth-EEDI2.git 217 | cd VapourSynth-EEDI2 218 | meson build && ninja -C build && ninja -C build install 219 | 220 | # eedi3 221 | cd ~/_opaencoder_deps 222 | git clone https://github.com/HomeOfVapourSynthEvolution/VapourSynth-EEDI3.git 223 | cd VapourSynth-EEDI3 224 | meson build -Dopencl=false && ninja -C build && ninja -C build install 225 | 226 | # f3kdb 227 | cd ~/_opaencoder_deps 228 | git clone https://github.com/SAPikachu/flash3kyuu_deband.git 229 | cd flash3kyuu_deband 230 | ./waf configure --libdir=/usr/lib/vapoursynth && ./waf build && ./waf install 231 | 232 | # fmtconv 233 | cd ~/_opaencoder_deps 234 | git clone https://github.com/EleonoreMizo/fmtconv.git 235 | cd fmtconv/build/unix 236 | ./autogen.sh && ./configure --libdir=/usr/lib/vapoursynth && make -j$(nproc) && make install 237 | 238 | # mvtools 239 | cd ~/_opaencoder_deps 240 | git clone https://github.com/dubhater/vapoursynth-mvtools.git 241 | cd vapoursynth-mvtools 242 | meson build && ninja -C build && ninja -C build install 243 | 244 | # nnedi3 245 | cd ~/_opaencoder_deps 246 | git clone https://github.com/dubhater/vapoursynth-nnedi3.git 247 | cd vapoursynth-nnedi3 248 | ./autogen.sh && ./configure --libdir=/usr/lib/vapoursynth && make -j$(nproc) && make install 249 | 250 | # retinex 251 | cd ~/_opaencoder_deps 252 | git clone https://github.com/HomeOfVapourSynthEvolution/VapourSynth-Retinex.git 253 | cd VapourSynth-Retinex 254 | meson build && ninja -C build && ninja -C build install 255 | 256 | # sangnom 257 | cd ~/_opaencoder_deps 258 | git clone https://github.com/dubhater/vapoursynth-sangnom.git 259 | cd vapoursynth-sangnom 260 | meson build && ninja -C build && ninja -C build install 261 | 262 | # scxvid 263 | cd ~/_opaencoder_deps 264 | git clone https://github.com/dubhater/vapoursynth-scxvid.git 265 | cd vapoursynth-scxvid 266 | ./autogen.sh && ./configure --libdir=/usr/lib/vapoursynth && make -j$(nproc) && make install 267 | 268 | # tcanny 269 | cd ~/_opaencoder_deps 270 | git clone https://github.com/HomeOfVapourSynthEvolution/VapourSynth-TCanny.git 271 | cd VapourSynth-TCanny 272 | meson build -Dopencl=false && ninja -C build && ninja -C build install 273 | 274 | # tcomb 275 | cd ~/_opaencoder_deps 276 | git clone https://github.com/dubhater/vapoursynth-tcomb.git 277 | cd vapoursynth-tcomb 278 | meson build && ninja -C build && ninja -C build install 279 | 280 | # tdeintmod 281 | cd ~/_opaencoder_deps 282 | git clone https://github.com/HomeOfVapourSynthEvolution/VapourSynth-TDeintMod.git 283 | cd VapourSynth-TDeintMod 284 | meson build && ninja -C build && ninja -C build install 285 | 286 | # add your own vapoursynth plugins here 287 | 288 | # install vapoursynth helper scripts 289 | cd ~/_opaencoder_deps 290 | export PYTHON_SITE_PATH=`python -c 'import site; print(site.getsitepackages()[0])'` 291 | git clone https://github.com/HomeOfVapourSynthEvolution/havsfunc.git 292 | cp ~/_opaencoder_deps/havsfunc/havsfunc.py ${PYTHON_SITE_PATH} 293 | git clone https://github.com/Irrational-Encoding-Wizardry/fvsfunc.git 294 | cp ~/_opaencoder_deps/fvsfunc/fvsfunc.py ${PYTHON_SITE_PATH} 295 | git clone https://github.com/Irrational-Encoding-Wizardry/kagefunc.git 296 | cp ~/_opaencoder_deps/kagefunc/kagefunc.py ${PYTHON_SITE_PATH} 297 | pip3 install git+https://github.com/Irrational-Encoding-Wizardry/lvsfunc.git 298 | -------------------------------------------------------------------------------- /setupCentOS7.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Setup an FFmpeg with the ability to 4 | # per title encode via image hashes 5 | # hamming distance values 6 | 7 | # should run on Linux, other systems untested 8 | # 9 | # requires: 10 | # 11 | # git 12 | # wget 13 | # development tools 14 | 15 | set -e 16 | 17 | # install deps 18 | if [ ! -e /usr/bin/wget ]; then 19 | sudo yum -y -q install wget 20 | fi 21 | if [ ! -e /usr/bin/git ]; then 22 | sudo yum -y -q install git 23 | fi 24 | if [ ! -e /usr/bin/clang ]; then 25 | sudo yum -y -q install clang 26 | fi 27 | if [ ! -e /usr/bin/cargo ]; then 28 | sudo yum -y -q install cargo 29 | fi 30 | if [ ! -e /usr/bin/rustc ]; then 31 | sudo yum -y -q install rust 32 | fi 33 | if [ ! -e /usr/bin/cmake3 ]; then 34 | sudo yum -y -q install cmake3 35 | sudo ln -s /usr/bin/cmake3 /usr/bin/cmake 36 | fi 37 | if [ ! -e /usr/bin/cmake ]; then 38 | sudo ln -s /usr/bin/cmake3 /usr/bin/cmake 39 | fi 40 | if [ ! -e /usr/bin/gnuplot ]; then 41 | sudo yum -y -q install gnuplot 42 | fi 43 | if [ ! -e /usr/bin/mediainfo ]; then 44 | sudo yum -y -q install mediainfo 45 | fi 46 | if [ ! -e /usr/include/freetype2 ]; then 47 | sudo yum -y -q install freetype-devel 48 | fi 49 | if [ ! -e /usr/lib/libass.a ]; then 50 | sudo yum -y -q install libass-devel 51 | fi 52 | if [ ! -e /usr/include/fontconfig ]; then 53 | sudo yum -y -q install fontconfig-devel 54 | fi 55 | # pip for python3 56 | if [ ! -e /usr/bin/pip3 ]; then 57 | sudo yum -y -q install python3-pip 58 | fi 59 | if [ ! -e /usr/bin/meson ]; then 60 | sudo python3 -m pip install meson 61 | sudo yum -y -q install meson 62 | fi 63 | if [ ! -e /usr/local/bin/ninja ]; then 64 | sudo python3 -m pip install ninja 65 | fi 66 | if [ ! -e /usr/bin/openssl ]; then 67 | sudo yum -y -q install openssl-devel 68 | fi 69 | 70 | ## get opencv and opencv_contrib 71 | if [ ! -d "opencv" ]; then 72 | git clone https://github.com/opencv/opencv.git 73 | cd opencv 74 | git checkout 3.4 75 | cd ../ 76 | fi 77 | if [ ! -d "opencv_contrib" ]; then 78 | git clone https://github.com/opencv/opencv_contrib.git 79 | cd opencv_contrib 80 | git checkout 3.4 81 | cd ../ 82 | fi 83 | 84 | if [ ! -d "x264" ]; then 85 | git clone https://code.videolan.org/videolan/x264.git 86 | cd x264 87 | git checkout stable 88 | cd ../ 89 | fi 90 | 91 | if [ ! -d "libvpx" ]; then 92 | git clone https://github.com/webmproject/libvpx.git libvpx 93 | cd libvpx 94 | git checkout v1.8.1 95 | cd ../ 96 | fi 97 | 98 | if [ ! -d "aom" ]; then 99 | git clone https://aomedia.googlesource.com/aom/ 100 | cd aom 101 | # TODO find stable version 102 | cd ../ 103 | fi 104 | 105 | if [ ! -d "SVT-AV1" ]; then 106 | git clone https://gitlab.com/AOMediaCodec/SVT-AV1.git 107 | cd SVT-AV1 108 | # TODO find stable version 109 | cd ../ 110 | fi 111 | 112 | if [ ! -d "SVT-VP9" ]; then 113 | git clone https://github.com/OpenVisualCloud/SVT-VP9.git 114 | cd SVT-VP9 115 | # TODO find stable version 116 | cd ../ 117 | fi 118 | 119 | if [ ! -d "dav1d" ]; then 120 | git clone https://code.videolan.org/videolan/dav1d.git 121 | cd dav1d 122 | # TODO find stable version 123 | cd ../ 124 | fi 125 | 126 | if [ ! -d "rav1e" ]; then 127 | sudo cargo install cargo-c || echo "Already installed cargo-c" 128 | git clone https://github.com/xiph/rav1e.git 129 | cd rav1e 130 | # TODO find stable version 131 | cd ../ 132 | fi 133 | 134 | if [ ! -d "FFmpeg" ]; then 135 | git clone https://git.ffmpeg.org/ffmpeg.git FFmpeg 136 | cd FFmpeg 137 | git checkout d99f3dc6b211509d9f6bbb82bbb59bff86a9e3a5 138 | cat ../ffmpeg4_modifications.diff | patch -p1 139 | cd ../ 140 | fi 141 | 142 | if [ ! -d "vmaf" ]; then 143 | git clone -b v1.3.15 https://github.com/Netflix/vmaf.git vmaf 144 | #if [ ! -f /usr/include/stdatomic.h ]; then 145 | #sudo wget https://gist.githubusercontent.com/nhatminhle/5181506/raw/541482dbc61862bba8a156edaae57faa2995d791/stdatomic.h -O /usr/include/stdatomic.h 146 | #fi 147 | fi 148 | 149 | # GCC 11.x install to /usr/local/ 150 | if [ ! -f "/usr/local/bin/gcc" ]; then 151 | sudo yum install -y -q centos-release-scl 152 | sudo yum install -y -q devtoolset-11-gcc* 153 | fi 154 | 155 | # requirement for x264 156 | if [ ! -f "nasm-2.15.05.tar.bz2" ]; then 157 | wget --no-check-certificate https://www.nasm.us/pub/nasm/releasebuilds/2.15.05/nasm-2.15.05.tar.bz2 158 | tar xvfj nasm-2.15.05.tar.bz2 159 | cd nasm-2.15.05 160 | ./configure --prefix=/usr 161 | make 162 | sudo make install 163 | cd ../ 164 | fi 165 | 166 | if [ ! -d "opencv/build" ]; then 167 | cd opencv 168 | mkdir build 169 | cd build 170 | 171 | # build with only what we need 172 | scl enable devtoolset-11 'cmake3 \ 173 | -DCMAKE_INSTALL_PREFIX=/usr \ 174 | -DCMAKE_INSTALL_LIBDIR=lib \ 175 | -DBUILD_SHARED_LIBS=True \ 176 | -DCMAKE_BUILD_TYPE=Release \ 177 | -DCMAKE_CXX_FLAGS="$CXXFLAGS" \ 178 | -DCMAKE_C_FLAGS="$CFLAGS" \ 179 | -DENABLE_PRECOMPILED_HEADERS=OFF \ 180 | -DWITH_OPENMP=OFF \ 181 | -WITH_OPENCL=OFF \ 182 | -DWITH_IPP=OFF \ 183 | -DBUILD_EXAMPLES=OFF \ 184 | -DWITH_FFMPEG=OFF -DWITH_JASPER=OFF -DWITH_PNG=OFF \ 185 | -DBUILD_opencv_python=OFF \ 186 | -DOPENCV_EXTRA_MODULES_PATH=../../opencv_contrib/modules \ 187 | -DOPENCV_GENERATE_PKGCONFIG=True \ 188 | -DBUILD_opencv_core=ON \ 189 | -DBUILD_opencv_imgproc=ON \ 190 | -DBUILD_opencv_img_hash=ON \ 191 | -DBUILD_opencv_imgcodecs=ON \ 192 | -DBUILD_opencv_highgui=ON \ 193 | -DBUILD_opencv_aruco=OFF \ 194 | -DBUILD_opencv_bgsegm=OFF \ 195 | -DBUILD_opencv_bioinspired=OFF \ 196 | -DBUILD_opencv_calib3d=OFF \ 197 | -DBUILD_opencv_ccalib=OFF \ 198 | -DBUILD_opencv_datasets=OFF \ 199 | -DBUILD_opencv_dnn=OFF \ 200 | -DBUILD_opencv_dnn_objdetect=OFF \ 201 | -DBUILD_opencv_dpm=OFF \ 202 | -DBUILD_opencv_face=OFF \ 203 | -DBUILD_opencv_features2d=OFF \ 204 | -DBUILD_opencv_flann=OFF \ 205 | -DBUILD_opencv_fuzzy=OFF \ 206 | -DBUILD_opencv_gapi=OFF \ 207 | -DBUILD_opencv_hfs=OFF \ 208 | -DBUILD_opencv_line_descriptor=OFF \ 209 | -DBUILD_opencv_ml=OFF \ 210 | -DBUILD_opencv_objdetect=OFF \ 211 | -DBUILD_opencv_optflow=OFF \ 212 | -DBUILD_opencv_phase_unwrapping=OFF \ 213 | -DBUILD_opencv_photo=OFF \ 214 | -DBUILD_opencv_plot=OFF \ 215 | -DBUILD_opencv_python2=OFF \ 216 | -DBUILD_opencv_quality=OFF \ 217 | -DBUILD_opencv_reg=OFF \ 218 | -DBUILD_opencv_rgbd=OFF \ 219 | -DBUILD_opencv_saliency=OFF \ 220 | -DBUILD_opencv_shape=OFF \ 221 | -DBUILD_opencv_stereo=OFF \ 222 | -DBUILD_opencv_stitching=OFF \ 223 | -DBUILD_opencv_structured_light=OFF \ 224 | -DBUILD_opencv_superres=OFF \ 225 | -DBUILD_opencv_surface_matching=OFF \ 226 | -DBUILD_opencv_text=OFF \ 227 | -DBUILD_opencv_tracking=OFF \ 228 | -DBUILD_opencv_ts=OFF \ 229 | -DBUILD_opencv_video=OFF \ 230 | -DBUILD_opencv_videoio=OFF \ 231 | -DBUILD_opencv_videostab=OFF \ 232 | -DBUILD_opencv_xfeatures2d=OFF \ 233 | -DBUILD_opencv_ximgproc=OFF \ 234 | -DBUILD_opencv_xobjdetect=OFF \ 235 | -DBUILD_opencv_xphoto=OFF \ 236 | ..' 237 | 238 | # build opencv 239 | scl enable devtoolset-11 'make -j$(nproc)' 240 | 241 | # install opencv 242 | sudo make install 243 | 244 | # For some reason OpenCV3 doesn't create this link 245 | if [ ! -e /usr/include/opencv2 -a -d /usr/include/opencv4 ]; then 246 | sudo ln -s /usr/include/opencv4/opencv2/ /usr/include/ 247 | fi 248 | 249 | sudo ldconfig 250 | 251 | cd ../../ 252 | fi 253 | 254 | ## Setup x264 255 | if [ ! -f /usr/lib/libx264.a ]; then 256 | scl enable devtoolset-11 'make x264lib' 257 | sudo ldconfig 258 | fi 259 | 260 | ## Setup VMAF 261 | if [ ! -f /usr/local/lib/libvmaf.a ]; then 262 | scl enable devtoolset-11 'make vmaflib' 263 | sudo ln -s /usr/local/lib/pkgconfig/libvmaf.pc /usr/share/pkgconfig/ 264 | fi 265 | 266 | ## setup dav1d 267 | if [ ! -f /usr/local/bin/dav1d ]; then 268 | #make dav1dlib 269 | #sudo ln -s /usr/local/lib64/pkgconfig/dav1d.pc /usr/share/pkgconfig 270 | sudo ldconfig 271 | fi 272 | 273 | ## Setup VPX 274 | if [ ! -f /usr/lib/libvpx.a ]; then 275 | scl enable devtoolset-11 'make vpxlib' 276 | sudo ldconfig 277 | fi 278 | 279 | ## Setup AOM AV1 280 | if [ ! -f /usr/lib/libaom.so ]; then 281 | scl enable devtoolset-11 'make aomlib' 282 | sudo ln -s /usr/lib/pkgconfig/aom.pc /usr/share/pkgconfig/ 283 | sudo ldconfig 284 | fi 285 | 286 | # Setup SVT-AV1 287 | if [ ! -f "/usr/local/lib/pkgconfig/SvtAv1Dec.pc" ]; then 288 | cd SVT-AV1/Build && \ 289 | scl enable devtoolset-11 'cmake3 .. -G"Unix Makefiles" \ 290 | -DCMAKE_BUILD_TYPE=Release \ 291 | -DCMAKE_CXX_FLAGS="-I/usr/local/include -L/usr/local/lib" \ 292 | -DCMAKE_C_FLAGS="-I/usr/local/include -L/usr/local/lib" \ 293 | -DCMAKE_CXX_COMPILER=$(which g++) \ 294 | -DCMAKE_CC_COMPILER=$(which gcc) \ 295 | -DCMAKE_C_COMPILER=$(which gcc)' && \ 296 | scl enable devtoolset-11 'make -j$(nproc)' && \ 297 | sudo make install 298 | cd ../../ 299 | sudo cp -f SVT-AV1/Build/SvtAv1Enc.pc /usr/share/pkgconfig/ 300 | sudo cp -f SVT-AV1/Build/SvtAv1Dec.pc /usr/share/pkgconfig/ 301 | fi 302 | 303 | # Setup SVT-VP9 304 | if [ ! -f "/usr/local/lib/pkgconfig/SvtVp9Enc.pc" ]; then 305 | cd SVT-VP9/Build && \ 306 | scl enable devtoolset-11 'cmake3 .. -G"Unix Makefiles" \ 307 | -DCMAKE_BUILD_TYPE=Release \ 308 | -DCMAKE_CXX_FLAGS="-I/usr/local/include -L/usr/local/lib" \ 309 | -DCMAKE_C_FLAGS="-I/usr/local/include -L/usr/local/lib" \ 310 | -DCMAKE_CXX_COMPILER=$(which g++) \ 311 | -DCMAKE_CC_COMPILER=$(which gcc) \ 312 | -DCMAKE_C_COMPILER=$(which gcc)' && \ 313 | scl enable devtoolset-11 'make -j$(nproc)' && \ 314 | sudo make install 315 | cd ../../ 316 | sudo cp -f SVT-VP9/Build/SvtVp9Enc.pc /usr/share/pkgconfig/ 317 | fi 318 | 319 | ## Setup rav1e AV1 320 | if [ ! -f /usr/local/lib/librav1e.a ]; then 321 | #make rav1elib 322 | #sudo ln -s /usr/local/lib/pkgconfig/rav1e.pc /usr/share/pkgconfig/ 323 | # CentOS doesn't include /usr/local/lib by default 324 | #sudo touch /etc/ld.so.conf.d/local.conf 325 | #sudo echo "/usr/local/lib" >> /etc/ld.so.conf.d/local.conf 326 | #sudo echo "/usr/local/lib64" >> /etc/ld.so.conf.d/local.conf 327 | sudo ldconfig 328 | fi 329 | 330 | ## Setup FFmpeg 331 | if [ ! -f FFmpeg/ffmpeg ]; then 332 | scl enable devtoolset-11 'make ffmpegbin' 333 | fi 334 | 335 | # build tools 336 | scl enable devtoolset-11 'make reference' 337 | 338 | echo 339 | echo "To install FFmpeg into /usr/bin/ffmpeg type: 'make install'" 340 | echo "./FFmpeg/ffmpeg can be copied where you want also" 341 | 342 | -------------------------------------------------------------------------------- /setupGCC_540.sh: -------------------------------------------------------------------------------- 1 | echo "Downloading gcc source files..." 2 | if [ ! -d "gcc-5.4.0-build" ]; then 3 | curl https://ftp.gnu.org/gnu/gcc/gcc-5.4.0/gcc-5.4.0.tar.bz2 -O 4 | echo "extracting files..." 5 | tar xvfj gcc-5.4.0.tar.bz2 6 | 7 | echo "Installing dependencies..." 8 | sudo yum install gcc-c++ gmp-devel mpfr-devel libmpc-devel -y 9 | 10 | echo "Configure and install..." 11 | mkdir gcc-5.4.0-build 12 | cd gcc-5.4.0-build 13 | ../gcc-5.4.0/configure --enable-languages=c,c++ --disable-multilib 14 | make -j$(nproc) && sudo make install 15 | sudo ldconfig 16 | cd ../ 17 | fi 18 | 19 | -------------------------------------------------------------------------------- /setupMacOSX.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Setup an FFmpeg with the ability to 4 | # per title encode via image hashes 5 | # hamming distance values 6 | 7 | # Mac OS X 8 | # 9 | # requires: 10 | # 11 | # development tools 12 | # brew 13 | # nasm 14 | # git 15 | # wget 16 | # cmake3 17 | # opencv@3 18 | # libx264 19 | 20 | set -e 21 | 22 | # install deps 23 | if [ ! -e /usr/local/bin/mediainfo ]; then 24 | brew install mediainfo 25 | fi 26 | if [ ! -e /usr/local/bin/wget ]; then 27 | brew install wget 28 | fi 29 | if [ ! -e /usr/local/bin/nasm ]; then 30 | brew install nasm 31 | fi 32 | if [ ! -e /usr/local/bin/git ]; then 33 | brew install git 34 | fi 35 | if [ ! -e /usr/local/bin/x264 ]; then 36 | brew install x264 37 | fi 38 | if [ ! -e /usr/local/lib/libvpx.a ]; then 39 | brew install libvpx 40 | fi 41 | if [ ! -e /usr/local/bin/cargo ]; then 42 | brew install rust 43 | fi 44 | if [ ! -e /usr/local/include/aom/aom.h ]; then 45 | brew install aom 46 | fi 47 | if [ ! -e /usr/local/lib/libvmaf.a ]; then 48 | brew install libvmaf 49 | fi 50 | if [ ! -e /usr/local/lib/libass.a ]; then 51 | brew install libass 52 | fi 53 | if [ ! -e /usr/local/bin/cmake ]; then 54 | brew install cmake 55 | fi 56 | if [ ! -e /usr/local/bin/cmake3 ]; then 57 | ln -s /usr/local/bin/cmake /usr/local/bin/cmake3 58 | fi 59 | if [ ! -e /usr/local/opt/opencv@3 ]; then 60 | brew install opencv@3 61 | fi 62 | if [ ! -e /usr/local/include/opencv2 ]; then 63 | # necessary to work 64 | brew link --force opencv@3 65 | fi 66 | if [ ! -e /usr/local/bin/gnuplot ]; then 67 | brew install gnuplot 68 | fi 69 | if [ ! -e /usr/local/include/freetype2 ]; then 70 | brew install freetype2 71 | fi 72 | if [ ! -e /usr/local/include/fontconfig ]; then 73 | brew install fontconfig 74 | fi 75 | ## setup dav1d 76 | if [ ! -f /usr/local/bin/dav1d ]; then 77 | brew install dav1d 78 | fi 79 | 80 | # For some reason OpenCV3 doesn't create this link 81 | if [ ! -e /usr/local/include/opencv2 -a -d /usr/local/include/opencv4 ]; then 82 | sudo ln -s /usr/local/include/opencv4/opencv2/ /usr/local/include/ 83 | fi 84 | 85 | if [ ! -d "rav1e" ]; then 86 | git clone https://github.com/xiph/rav1e.git 87 | cd rav1e 88 | # TODO find stable version 89 | cd ../ 90 | fi 91 | 92 | if [ ! -d "SVT-AV1" ]; then 93 | git clone https://github.com/OpenVisualCloud/SVT-AV1.git 94 | cd SVT-AV1 95 | # TODO find stable version 96 | cd ../ 97 | fi 98 | 99 | if [ ! -d "SVT-VP9" ]; then 100 | git clone https://github.com/OpenVisualCloud/SVT-VP9.git 101 | cd SVT-VP9 102 | # TODO find stable version 103 | cd ../ 104 | fi 105 | 106 | ## Setup rav1e AV1 107 | if [ ! -f /usr/local/lib/librav1e.a ]; then 108 | sudo cargo install cargo-c || echo "Already installed cargo-c" 109 | make rav1elib 110 | fi 111 | 112 | ## Setup Intel SVT-AV1 113 | if [ ! -f "/usr/local/lib/pkgconfig/SvtAv1Dec.pc" ]; then 114 | make svtav1libmac 115 | fi 116 | 117 | # Setup SVT-VP9 118 | if [ ! -f "/usr/local/lib/pkgconfig/SvtVp9Enc.pc" ]; then 119 | #make svtvp9libmac 120 | echo "Skipping SVT-VP9, currently doesn't build on MacOS" 121 | fi 122 | 123 | 124 | if [ ! -d "FFmpeg" ]; then 125 | git clone https://git.ffmpeg.org/ffmpeg.git FFmpeg 126 | cd FFmpeg 127 | git checkout remotes/origin/release/4.2 128 | cat ../ffmpeg4_modifications.diff | patch -p1 129 | cd ../ 130 | fi 131 | 132 | 133 | ## Setup FFmpeg 134 | if [ ! -f FFmpeg/ffmpeg ]; then 135 | export PKG_CONFIG_PATH="/usr/local/opt/opencv@3/lib/pkgconfig" 136 | make ffmpegbinmac 137 | fi 138 | 139 | # build tools 140 | g++ reference.cpp -o reference $(PKG_CONFIG_PATH="/usr/local/opt/opencv@3/lib/pkgconfig" pkg-config --cflags --libs opencv) 141 | 142 | echo 143 | echo "To install FFmpeg into /usr/bin/ffmpeg type: 'make install'" 144 | echo "./FFmpeg/ffmpeg can be copied where you want also" 145 | 146 | -------------------------------------------------------------------------------- /stats.gp: -------------------------------------------------------------------------------- 1 | set terminal png size 1920,1080 enhanced font "arial,8" 2 | set output 'stats.jpg' 3 | 4 | set style line 2 lc rgb 'red' lt 1 lw 1 # 5 | set style line 3 lc rgb 'brown' lt 1 lw 1 # 6 | set style line 4 lc rgb 'black' lt 1 lw 1 # 7 | set style line 5 lc rgb 'green' lt 1 lw 3 # 8 | set style line 6 lc rgb 'blue' lt 1 lw 2 # 9 | set style line 7 lc rgb 'orange' lt 1 lw 1 # 10 | set style line 8 lc rgb 'yellow' lt 1 lw 1 # 11 | #set style data histogram 12 | set style data line 13 | set key opaque 14 | set style histogram cluster gap 1 15 | set style fill pattern border -1 16 | set boxwidth 0.9 17 | set yrange [0:100] 18 | set xtics format "" 19 | set xtics rotate 20 | set grid ytics 21 | set grid xtics 22 | set xtics 1 23 | set ytics 1 24 | 25 | # 1 2 3 4 5 6 7 8 26 | # test pfhd phqm vmaf ssim psnr bitrate speed 27 | set title "__TITLE__" 28 | plot "stats.csv" using ($4):xtic(1) title "VMAF" ls 2, "stats.csv" using 3 title "PHQM" ls 6, "stats.csv" using 6 title "PSNR" ls 7, "stats.csv" using ($7/100) title "Bitrate" ls 5, "stats.csv" using ($8/60) title "Speed" ls 4 29 | -------------------------------------------------------------------------------- /tests/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crunchyroll/objective_perceptual_analysis/6ffa0534f9eab6f9492138783381e25c6b057004/tests/.keep --------------------------------------------------------------------------------