diff --git a/README.md b/README.md
index 799e7c0..08cdbdd 100644
--- a/README.md
+++ b/README.md
@@ -1,19 +1,26 @@
# Per-Title Analysis
-*This a python package providing tools for optimizing your over-the-top bitrate ladder per each video you need to encode.*
+*This a python package providing tools for optimizing your over-the-top (OTT) bitrate ladder per each video you need to encode.*
+
+
+
+
## How does it work?
-You can configure a template encoding ladder with constraints (min/max bitrate) that will be respected for the output optimal ladder.
+You can configure a template encoding ladder with constraints (min/max bitrate) that will be respected for the output optimal ladder and comparing it with the default bitrate.
You also have the control over analysis parameters (based on CRF encoding or multiple bitrate encodings with video quality metric assessments).
The CRF Analyzer
-This analyzer calculates an optimal bitrate for the higher profile.
-Other profiles are declined top to bottom from the initial gap between each profiles of the template ladder.
+This analyzer calculates an optimal bitrate for the higher profile for a given CRF value.
+Other profiles are declined top to bottom from the initial gap between each profiles of the template ladder (only if you use linear model).
+Otherwise every optimal bitrates are calculated for each profil in "for_each" model.
The Metric Analyzer
This analyzer encodes multiple bitrates for each profile in the template ladder (from min to max, respecting a bitrate step defined by the user)
-It then calculates video quality metrics for each of these encodings (only ssim or psnr for now).
+It then calculates video quality metrics for each of these encodings (only SSIM or PSNR for now).
The final optimized ladder will be constructed choosing for the best quality/bitrate ratio (similar to Netflix).
+You can then use a super graph to analyze your results !
+
### The template encoding ladder
It is composed of multiple encoding profile object.
@@ -24,21 +31,27 @@ Each encoding profile is defined by those attributes:
- __bitrate_min__ (int): This is the minimal bitrate you set for this profile in the output optimized encoding ladder
- __bitrate_max__ (int): This is the maximal bitrate you set for this profile in the output optimized encoding ladder
- __required__ (bool): Indicates if you authorize the script to remove this profile if considered not useful after optimization (conditions for this to happen are explained after)
-- __bitrate_factor__ (float): this is a private attribute calculated after initialization of the template encoding ladder
+- __bitrate_steps_individual__ (int): This is the bitrate step used for metric_analyzer only if you want to configure one step for each profile
+- __bitrate_factor__ (float): this is a private attribute calculated after initialization of the template encoding ladder
##### See this template example
-| width | height | bitrate_default | bitrate_min | bitrate_max | required |
-| --- | --- | --- | --- | --- | --- |
-| *in pixels* | *in pixels* | *in bits per second* | *in bits per second* | *in bits per second* | *bool* |
-| 1920 | 1080 | 4500000 | 2000000 | 6000000 | True |
-| 1280 | 720 | 3400000 | 1300000 | 4500000 | True |
-| 960 | 540 | 2100000 | 700000 | 3000000 | True |
-| 640 | 360 | 1100000 | 300000 | 2000000 | True |
-| 480 | 270 | 750000 | 300000 | 900000 | False |
-| 480 | 270 | 300000 | 150000 | 500000 | True |
-##### What does it imply? *(soon)*
+| width | height | bitrate_default | bitrate_min | bitrate_max | required | bitrate_steps_individual |
+| --- | --- | --- | --- | --- | --- | --- |
+| *in pixels* | *in pixels* | *in bits per second* | *in bits per second* | *in bits per second* | *bool* | *int bits per second* |
+| 1920 | 1080 | 4500000 | 1000000 | 6000000 | True | 100000 |
+| 1280 | 720 | 3400000 | 800000 | 5000000 | True | 100000 |
+| 960 | 540 | 2100000 | 600000 | 4000000 | True | 100000 |
+| 640 | 360 | 1100000 | 300000 | 3000000 | True | 100000 |
+| 480 | 270 | 750000 | 200000 | 2000000 | False | 100000 |
+| 480 | 270 | 300000 | 150000 | 2000000 | True | 100000 |
+##### How configure it ?
+- You can now play with this values.
+- For example set the bitrate_default with your actual bitrate.
+- Then set the Min and Max considering your network constraints or storage capacity
+- If you need to keep one profile whatever happens set required to True.
+
-#### In depth: *(soon)*
+#### In depth: (SOON!)
- How to choose the analysis parameters
- What is the multiple part analysis
- How is the weighted average bitrate calculated
@@ -47,40 +60,37 @@ Each encoding profile is defined by those attributes:
___
## Installation:
-This is package requires at least Python 3.4.
+This is package requires at least Python 3.4
-You need to have ffmpeg and ffprobe installed on the host running the script.
+- You need to have ffmpeg and ffprobe installed on the host running the script.
+- You need to have matplotlib and pylab to create the graphs
## Example:
-This is an example using the CRF Analyzer method.
+Two examples using the CRF Analyzer method and the Metric Analyzer one are included in the per_title_analysis folder.
-##### Code:
-```python
-# -*- coding: utf8 -*-
+You can now use these scripts with the following command:
-from pertitleanalysis import per_title_analysis as pta
+### command for CrfAnalyzer:
+```bash
+python3 crf_analyzer.py [path/my_movie.mxf] [CRF_value] [number_of_parts] [model]
+example : python3 crf_analyzer.py /home/xxxx/Documents/pertitleanalysis/Sources/my_movie.mxf 23 1 1
-# create your template encoding ladder
-PROFILE_LIST = []
-PROFILE_LIST.append(pta.EncodingProfile(1920, 1080, 4500000, 2000000, 6000000, True))
-PROFILE_LIST.append(pta.EncodingProfile(1280, 720, 3400000, 1300000, 4500000, True))
-PROFILE_LIST.append(pta.EncodingProfile(960, 540, 2100000, 700000, 300000, True))
-PROFILE_LIST.append(pta.EncodingProfile(640, 360, 1100000, 300000, 2000000, True))
-PROFILE_LIST.append(pta.EncodingProfile(480, 270, 750000, 300000, 900000, False))
-PROFILE_LIST.append(pta.EncodingProfile(480, 270, 300000, 150000, 500000, True))
-LADDER = pta.EncodingLadder(PROFILE_LIST)
+#model: 1 (linear mode, only one profile is encoded, the higher) or 0 (for_each mode, each profile is encoded)
+```
-# Create a new CRF analysis provider
-ANALYSIS = pta.CrfAnalyzer("{{ your_input_file_path }}", LADDER)
-# Launch various analysis
-ANALYSIS.process(1, 1920, 1080, 23, 2)
-ANALYSIS.process(10, 1920, 1080, 23, 2)
+### command for MetricAnalyzer:
+```bash
+python3 metric_analyzer.py [path/my_movie.mxf] [metric] [limit_metric_value]
+example : python3 metric_analyzer.py /home/xxxx/Documents/pertitleanalysis/Sources/my_movie.mxf psnr 0.095
-# Print results
-print(ANALYSIS.get_json())
+#metric: psnr or ssim
+#limit_metric_value: To find the optimal bitrate we need to fix a limit of quality/bitrate_step ratio.
+#we advise you to use for PSNR a limit of 0.09 (with bitrate_step = 100 kbps) and for SSIM a limit of 0.005 (with bitrate_step = 50 kbps)
```
+The JSON file and the Graphics are saved in your current working directory as /results/[my_movie.mxf]/ .json .png .png
+
##### JSON ouput:
```json
{
@@ -273,4 +283,4 @@ print(ANALYSIS.get_json())
},
"input_file_path": "{{ your_input_file_path }}"
}
-```
\ No newline at end of file
+```
diff --git a/my_movie.mxf-PSNR-0.09-Per_Title.png b/my_movie.mxf-PSNR-0.09-Per_Title.png
new file mode 100644
index 0000000..d8d3316
Binary files /dev/null and b/my_movie.mxf-PSNR-0.09-Per_Title.png differ
diff --git a/my_movie.mxf-PSNR-0.09-Per_Title_Histogram.png b/my_movie.mxf-PSNR-0.09-Per_Title_Histogram.png
new file mode 100644
index 0000000..491aa05
Binary files /dev/null and b/my_movie.mxf-PSNR-0.09-Per_Title_Histogram.png differ
diff --git a/pertitleanalysis/__init__.py b/per_title_analysis/__init__.py
old mode 100755
new mode 100644
similarity index 66%
rename from pertitleanalysis/__init__.py
rename to per_title_analysis/__init__.py
index 1ca3e57..415656c
--- a/pertitleanalysis/__init__.py
+++ b/per_title_analysis/__init__.py
@@ -3,10 +3,10 @@
pertitleanalysis
-----
A smart and simple Per-Title video analysis tool for optimizing your over-the-top encoding ladder
- :copyright: (c) 2017 by Antoine Henning.
+ :copyright: (c) 2018 by Antoine Henning & Thom Marin.
:license: MIT, see LICENSE for more details.
"""
__title__ = 'pertitleanalysis'
-__author__ = 'Antoine Henning'
-__version__ = '0.1-dev'
+__author__ = 'Antoine Henning, Thom Marin'
+__version__ = '0.2-dev'
diff --git a/per_title_analysis/crf_analyzer.py b/per_title_analysis/crf_analyzer.py
new file mode 100644
index 0000000..6e3cc2f
--- /dev/null
+++ b/per_title_analysis/crf_analyzer.py
@@ -0,0 +1,46 @@
+# -*- coding: utf8 -*-
+import per_title_analysis as pta
+import sys
+import os
+import json
+
+path=str(sys.argv[1])
+print ("\nfile=",path)
+crf_value=str(sys.argv[2])
+print("crf =",crf_value)
+number_of_parts=int(sys.argv[3])
+print("number_of_parts =",number_of_parts)
+model=int(sys.argv[4])
+print("model value 1 for True (linear), 0 for False (for each):", model, "\n\n")
+
+# create your template encoding ladder
+PROFILE_LIST = [] #(self, width, height, bitrate_default, bitrate_min, bitrate_max, required, bitrate_steps_individual)
+PROFILE_LIST.append(pta.EncodingProfile(1920, 1080, 4500000, 1000000, 6000000, True, 100000))
+PROFILE_LIST.append(pta.EncodingProfile(1280, 720, 3400000, 800000, 5000000, True, 100000))
+PROFILE_LIST.append(pta.EncodingProfile(960, 540, 2100000, 600000, 4000000, True, 100000))
+PROFILE_LIST.append(pta.EncodingProfile(640, 360, 1100000, 300000, 3000000, True, 100000))
+PROFILE_LIST.append(pta.EncodingProfile(480, 270, 750000, 200000, 2000000, False, 100000))
+#PROFILE_LIST.append(pta.EncodingProfile(480, 270, 300000, 200000, 2000000, True, 100000))
+
+LADDER = pta.EncodingLadder(PROFILE_LIST)
+
+
+# Create a new Metric analysis provider
+ANALYSIS = pta.CrfAnalyzer(path, LADDER)
+
+# Launch various analysis (here crf)
+if model == 1: #model = linear (True) or for each (False)
+ ANALYSIS.process(number_of_parts, 1920, 1080, crf_value, 2, True)
+if model == 0:
+ ANALYSIS.process(number_of_parts, 1920, 1080, crf_value, 2, None)
+ ANALYSIS.process(number_of_parts, 1280, 720, crf_value, 2, None)
+ ANALYSIS.process(number_of_parts, 960, 540, crf_value, 2, None)
+ ANALYSIS.process(number_of_parts, 640, 360, crf_value, 2, None)
+ ANALYSIS.process(number_of_parts, 480, 270, crf_value, 2, None)
+
+
+# Save JSON results
+name=str(os.path.basename(path))
+filePathNameWExt = str(os.getcwd())+"/results/%s/%s-CRF-nbr_parts:%s-%s-Per_Title.json" % (name, name, number_of_parts , str(crf_value))
+with open(filePathNameWExt, 'w') as fp:
+ print(ANALYSIS.get_json(), file=fp)
diff --git a/per_title_analysis/metric_analyzer.py b/per_title_analysis/metric_analyzer.py
new file mode 100644
index 0000000..e978f0e
--- /dev/null
+++ b/per_title_analysis/metric_analyzer.py
@@ -0,0 +1,36 @@
+# -*- coding: utf8 -*-
+import per_title_analysis as pta
+import sys
+import os
+import json
+
+path=str(sys.argv[1])
+metric=str(sys.argv[2])
+limit_metric_value=float(sys.argv[3])
+print('\nmetric:', metric)
+print('limit metric =', limit_metric_value)
+
+# create your template encoding ladder
+PROFILE_LIST = [] #(self, width, height, bitrate_default, bitrate_min, bitrate_max, required, bitrate_steps_individual)
+PROFILE_LIST.append(pta.EncodingProfile(1920, 1080, 4500000, 1000000, 6000000, True, 100000))
+PROFILE_LIST.append(pta.EncodingProfile(1280, 720, 3400000, 800000, 5000000, True, 100000))
+PROFILE_LIST.append(pta.EncodingProfile(960, 540, 2100000, 600000, 4000000, True, 100000))
+PROFILE_LIST.append(pta.EncodingProfile(640, 360, 1100000, 300000, 3000000, True, 100000))
+PROFILE_LIST.append(pta.EncodingProfile(480, 270, 750000, 200000, 2000000, False, 100000))
+#PROFILE_LIST.append(pta.EncodingProfile(480, 270, 300000, 200000, 2000000, True, 100000))
+
+LADDER = pta.EncodingLadder(PROFILE_LIST)
+
+
+# Create a new Metric analysis provider
+ANALYSIS = pta.MetricAnalyzer(path, LADDER)
+
+# Launch various analysis (here ssim or psnr)
+ #(self, metric, limit_metric, bitrate_steps_by_default, idr_interval, steps_individual_bitrate_required)
+ANALYSIS.process(metric, limit_metric_value, 200000, 2, False)
+
+# Save JSON results
+name=str(os.path.basename(path))
+filePathNameWExt = str(os.getcwd())+"/results/%s/%s-METRIC-%s-%s-Per_Title.json" % (name, name, (metric).strip().upper(), str(limit_metric_value))
+with open(filePathNameWExt, 'w') as fp:
+ print(ANALYSIS.get_json(), file=fp)
diff --git a/pertitleanalysis/per_title_analysis.py b/per_title_analysis/per_title_analysis.py
old mode 100755
new mode 100644
similarity index 51%
rename from pertitleanalysis/per_title_analysis.py
rename to per_title_analysis/per_title_analysis.py
index 7673b1a..79544eb
--- a/pertitleanalysis/per_title_analysis.py
+++ b/per_title_analysis/per_title_analysis.py
@@ -1,18 +1,26 @@
# -*- coding: utf-8 -*-
+#importation
+
from __future__ import division
+from pylab import *
+import sys
import os
import json
import datetime
import statistics
+import matplotlib.pyplot as plt
+import matplotlib.animation as animation
+
+from task_providers import Probe, CrfEncode, CbrEncode, Metric
-from .task_providers import Probe, CrfEncode, CbrEncode, Metric
class EncodingProfile(object):
"""This class defines an encoding profile"""
- def __init__(self, width, height, bitrate_default, bitrate_min, bitrate_max, required):
+
+ def __init__(self, width, height, bitrate_default, bitrate_min, bitrate_max, required, bitrate_steps_individual):
"""EncodingProfile initialization
:param width: Video profile width
@@ -27,7 +35,11 @@ def __init__(self, width, height, bitrate_default, bitrate_min, bitrate_max, req
:type bitrate_max: int
:param required: The video profile is required and cannot be removed from the optimized encoding ladder
:type required: bool
+ :param bitrate_steps_individual: Step Bitrate Range defined for each Video profile (in bits per second)
+ :type bitrate_steps_individual: int
"""
+ #call: PROFILE_LIST.append(pta.EncodingProfile(480, 270, 300000, 150000, 500000, True, 150000))
+
if width is None:
raise ValueError('The EncodingProfile.width value is required')
@@ -54,6 +66,11 @@ def __init__(self, width, height, bitrate_default, bitrate_min, bitrate_max, req
else:
self.bitrate_max = self.bitrate_default
+ if bitrate_steps_individual is None:
+ self.bitrate_steps_individual = None
+ else:
+ self.bitrate_steps_individual = int(bitrate_steps_individual)
+
if required is not None:
self.required = required
else:
@@ -61,51 +78,57 @@ def __init__(self, width, height, bitrate_default, bitrate_min, bitrate_max, req
self.bitrate_factor = None
+
def __str__(self):
"""Display the encoding profile informations
-
:return: human readable string describing an encoding profil object
:rtype: str
"""
- return "{}x{}, bitrate_default={}, bitrate_min={}, bitrate_max={}, bitrate_factor={}, required={}".format(self.width, self.height, self.bitrate_default, self.bitrate_min, self.bitrate_max, self.bitrate_factor, self.required)
+ return "{}x{}, bitrate_default={}, bitrate_min={}, bitrate_max={}, bitrate_steps_individual{}, bitrate_factor={}, required={}".format(self.width, self.height, self.bitrate_default, self.bitrate_min, self.bitrate_max, self.bitrate_steps_individual, self.bitrate_factor, self.required)
+
def get_json(self):
- """Return object details in json
+ """Return object details in json
+ :return: json object describing the encoding profile and the configured constraints
+ :rtype: str
+ """
+ profile = {}
+ profile['width'] = self.width
+ profile['height'] = self.height
+ profile['bitrate'] = self.bitrate_default
+ profile['constraints'] = {}
+ profile['constraints']['bitrate_min'] = self.bitrate_min
+ profile['constraints']['bitrate_max'] = self.bitrate_max
+ profile['constraints']['bitrate_factor'] = self.bitrate_factor
+ profile['constraints']['required'] = self.required
+ return json.dumps(profile)
- :return: json object describing the encoding profile and the configured constraints
- :rtype: str
- """
- profile = {}
- profile['width'] = self.width
- profile['height'] = self.height
- profile['bitrate'] = self.bitrate_default
- profile['constraints'] = {}
- profile['constraints']['bitrate_min'] = self.bitrate_min
- profile['constraints']['bitrate_max'] = self.bitrate_max
- profile['constraints']['bitrate_factor'] = self.bitrate_factor
- profile['constraints']['required'] = self.required
- return json.dumps(profile)
def set_bitrate_factor(self, ladder_max_bitrate):
"""Set the bitrate factor from the max bitrate in the encoding ladder"""
self.bitrate_factor = ladder_max_bitrate/self.bitrate_default
+
+
class EncodingLadder(object):
"""This class defines an over-the-top encoding ladder template"""
+
def __init__(self, encoding_profile_list):
"""EncodingLadder initialization
:param encoding_profile_list: A list of multiple encoding profiles
:type encoding_profile_list: per_title.EncodingProfile[]
"""
+ #call: LADDER = pta.EncodingLadder(PROFILE_LIST)
+
self.encoding_profile_list = encoding_profile_list
self.calculate_bitrate_factors()
+
def __str__(self):
"""Display the encoding ladder informations
-
:return: human readable string describing the encoding ladder template
:rtype: str
"""
@@ -114,9 +137,9 @@ def __str__(self):
string += str(encoding_profile) + "\n"
return string
+
def get_json(self):
"""Return object details in json
-
:return: json object describing the encoding ladder template
:rtype: str
"""
@@ -127,9 +150,9 @@ def get_json(self):
ladder['encoding_profiles'].append(json.loads(encoding_profile.get_json()))
return json.dumps(ladder)
+
def get_max_bitrate(self):
"""Get the max bitrate in the ladder
-
:return: The maximum bitrate into the encoding laddder template
:rtype: int
"""
@@ -141,7 +164,6 @@ def get_max_bitrate(self):
def get_overall_bitrate(self):
"""Get the overall bitrate for the ladder
-
:return: The sum of all bitrate profiles into the encoding laddder template
:rtype: int
"""
@@ -150,19 +172,19 @@ def get_overall_bitrate(self):
ladder_overall_bitrate += encoding_profile.bitrate_default
return ladder_overall_bitrate
- def calculate_bitrate_factors(self):
+ def calculate_bitrate_factors(self): #cf plus haut !
"""Calculate the bitrate factor for each profile"""
ladder_max_bitrate = self.get_max_bitrate()
for encoding_profile in self.encoding_profile_list:
encoding_profile.set_bitrate_factor(ladder_max_bitrate)
+
class Analyzer(object):
"""This class defines a Per-Title Analyzer"""
def __init__(self, input_file_path, encoding_ladder):
"""Analyzer initialization
-
:param input_file_path: The input video file path
:type input_file_path: str
:param encoding_ladder: An EncodingLadder object
@@ -182,9 +204,9 @@ def __init__(self, input_file_path, encoding_ladder):
self.json['template_encoding_ladder'] = json.loads(self.encoding_ladder.get_json())
self.json['analyses'] = []
+
def __str__(self):
"""Display the per title analysis informations
-
:return: human readable string describing all analyzer configuration
:rtype: str
"""
@@ -194,19 +216,43 @@ def __str__(self):
def get_json(self):
"""Return object details in json
-
:return: json object describing all inputs configuration and output analyses
:rtype: str
"""
return json.dumps(self.json, indent=4, sort_keys=True)
+
class CrfAnalyzer(Analyzer):
"""This class defines a Per-Title Analyzer based on calculating the top bitrate wit CRF, then deducting the ladder"""
- def process(self, number_of_parts, width, height, crf_value, idr_interval):
- """Do the necessary crf encodings and assessments
+ def set_bitrate(self,number_of_parts):
+ """In linear mode, optimal_bitrates are defined from the first analysis thanks to the bitrate_factor
+ : print results in linear mode for CRF analyzer
+ """
+
+ overall_bitrate_optimal = 0
+ for encoding_profile in self.encoding_ladder.encoding_profile_list:
+ target_bitrate = int(self.optimal_bitrate/encoding_profile.bitrate_factor)
+ remove_profile = False
+ if target_bitrate < encoding_profile.bitrate_min and encoding_profile.required is False:
+ remove_profile = True
+
+ if target_bitrate < encoding_profile.bitrate_min:
+ target_bitrate = encoding_profile.bitrate_min
+
+ if target_bitrate > encoding_profile.bitrate_max:
+ target_bitrate = encoding_profile.bitrate_max
+
+ if remove_profile is False:
+ overall_bitrate_optimal += target_bitrate
+
+ print(' ',encoding_profile.width,'x',encoding_profile.height,' ',target_bitrate*1e-3,'kbps linear',' / nbr part:',number_of_parts,' ')
+
+
+ def process(self, number_of_parts, width, height, crf_value, idr_interval, model):
+ """Do the necessary crf encodings and assessments
:param number_of_parts: Number of part/segment for the analysis
:type number_of_parts: int
:param width: Width of the CRF encode
@@ -217,17 +263,22 @@ def process(self, number_of_parts, width, height, crf_value, idr_interval):
:type crf_value: int
:param idr_interval: IDR interval in seconds
:type idr_interval: int
+ :param model: linear (True) or for each (False)
+ :type model: bool
"""
+
# Start by probing the input video file
input_probe = Probe(self.input_file_path)
input_probe.execute()
crf_bitrate_list = []
part_duration = input_probe.duration/number_of_parts
- idr_interval_frames = idr_interval*input_probe.framerate
+ idr_interval_frames = idr_interval*input_probe.framerate #rcl: An IDR frame is a special type of I-frame in H.264. An IDR frame specifies that no frame after the IDR frame can reference any frame before it. This makes seeking the H.264 file easier and more responsive in the player.
+ #As I have an IDR_FRAME every 2 seconds, I can find out the number of frame between two IDR using framerate !
+ # Start Analysis
for i in range(0,number_of_parts):
- part_start_time = i*part_duration
+ part_start_time = i*part_duration #select extracts to encode
# Do a CRF encode for the input file
crf_encode = CrfEncode(self.input_file_path, width, height, crf_value, idr_interval_frames, part_start_time, part_duration)
@@ -255,6 +306,7 @@ def process(self, number_of_parts, width, height, crf_value, idr_interval):
weighted_bitrate_sum = 0
weighted_bitrate_len = 0
+ # Giving weight for each bitrate based on the standard deviation
for bitrate in crf_bitrate_list:
if bitrate > (self.average_bitrate + self.standard_deviation):
weight = 4
@@ -270,12 +322,20 @@ def process(self, number_of_parts, width, height, crf_value, idr_interval):
weighted_bitrate_sum += weight*bitrate
weighted_bitrate_len += weight
+ # Set the optimal bitrate from the weighted bitrate of all crf encoded parts
self.optimal_bitrate = weighted_bitrate_sum/weighted_bitrate_len
else:
- # Set the optimal bitrate from the average of all crf encoded parts
+ # Set the optimal bitrate from the only one crf result
self.optimal_bitrate = self.average_bitrate
+ if not model:
+ print(' ',width,'x',height,' ',self.optimal_bitrate*1e-3,'kbps encode_for_each','/ nbr part:',number_of_parts,' ')
+
+ if model:
+ # We calculate optimal bitrate of the the remaining profiles using bitrate factor
+ self.set_bitrate(number_of_parts)
+
# Adding results to json
result = {}
result['processing_date'] = str(datetime.datetime.now())
@@ -293,49 +353,29 @@ def process(self, number_of_parts, width, height, crf_value, idr_interval):
result['bitrate']['peak'] = self.average_bitrate
result['bitrate']['standard_deviation'] = self.standard_deviation
result['optimized_encoding_ladder'] = {}
- result['optimized_encoding_ladder']['encoding_profiles'] = []
-
- overall_bitrate_optimal = 0
- for encoding_profile in self.encoding_ladder.encoding_profile_list:
-
- target_bitrate = int(self.optimal_bitrate/encoding_profile.bitrate_factor)
-
- remove_profile = False
- if target_bitrate < encoding_profile.bitrate_min and encoding_profile.required is False:
- remove_profile = True
-
- if target_bitrate < encoding_profile.bitrate_min:
- target_bitrate = encoding_profile.bitrate_min
-
- if target_bitrate > encoding_profile.bitrate_max:
- target_bitrate = encoding_profile.bitrate_max
+ if model == "True":
+ result['optimized_encoding_ladder']['model'] = "linear"
+ if model == "False":
+ result['optimized_encoding_ladder']['model'] = "encode_for_each"
- if remove_profile is False:
- overall_bitrate_optimal += target_bitrate
- profile = {}
- profile['width'] = encoding_profile.width
- profile['height'] = encoding_profile.height
- profile['bitrate'] = target_bitrate
- profile['bitrate_savings'] = encoding_profile.bitrate_default - target_bitrate
- result['optimized_encoding_ladder']['encoding_profiles'].append(profile)
-
- result['optimized_encoding_ladder']['overall_bitrate_ladder'] = overall_bitrate_optimal
- result['optimized_encoding_ladder']['overall_bitrate_savings'] = self.encoding_ladder.get_overall_bitrate() - overall_bitrate_optimal
self.json['analyses'].append(result)
class MetricAnalyzer(Analyzer):
"""This class defines a Per-Title Analyzer based on VQ Metric and Multiple bitrate encodes"""
- def process(self, metric, bitrate_steps, idr_interval):
+ def process(self, metric, limit_metric, bitrate_steps_by_default, idr_interval, steps_individual_bitrate_required):
"""Do the necessary encodings and quality metric assessments
-
:param metric: Supporting "ssim" or "psnr"
:type metric: string
- :param bitrate_steps: Bitrate gap between every encoding
- :type bitrate_steps: int
+ :param limit_metric: limit value of "ssim" or "psnr" use to find optimal bitrate
+ :type limit_metric: int
+ :param bitrate_steps_by_default: Bitrate gap between every encoding, only use if steps_individual_bitrate_required is False
+ :type bitrate_steps_by_default: int
:param idr_interval: IDR interval in seconds
:type idr_interval: int
+ :param steps_individual_bitrate_required: The step is the same for each profile and cannot be set individually if False
+ :type steps_individual_bitrate_required: bool
"""
# Start by probing the input video file
@@ -347,19 +387,28 @@ def process(self, metric, bitrate_steps, idr_interval):
idr_interval_frames = idr_interval*input_probe.framerate
metric = str(metric).strip().lower()
+ #Create two lists for GRAPH 2
+ optimal_bitrate_array = []
+ default_bitrate_array = []
+
+ print('\n********************************\n********Encoding Started********\n********************************\n')
+ print('File Selected: ', os.path.basename(self.input_file_path))
+
# Adding results to json
json_ouput = {}
json_ouput['processing_date'] = str(datetime.datetime.now())
json_ouput['parameters'] = {}
json_ouput['parameters']['method'] = "Metric"
json_ouput['parameters']['metric'] = metric
- json_ouput['parameters']['bitrate_steps'] = bitrate_steps
+ json_ouput['parameters']['bitrate_steps'] = bitrate_steps_by_default
json_ouput['parameters']['idr_interval'] = idr_interval
json_ouput['parameters']['number_of_parts'] = 1
json_ouput['parameters']['part_duration'] = part_duration
json_ouput['optimized_encoding_ladder'] = {}
json_ouput['optimized_encoding_ladder']['encoding_profiles'] = []
+
+ # Start Analysis
for encoding_profile in self.encoding_ladder.encoding_profile_list:
profile = {}
@@ -368,50 +417,176 @@ def process(self, metric, bitrate_steps, idr_interval):
profile['cbr_encodings'] = []
profile['optimal_bitrate'] = None
+ default_bitrate_array.append(encoding_profile.bitrate_default)
+
+ if steps_individual_bitrate_required:
+ bitrate_steps_by_default = encoding_profile.bitrate_steps_individual
+ print('\n\n __________________________________________')
+ print(' The bitrate_step is: ',bitrate_steps_by_default*10**(-3),'kbps')
+ print('\n |||',encoding_profile.width, 'x', encoding_profile.height,'|||\n')
+
last_metric_value = 0
last_quality_step_ratio = 0
+ bitrate_array = []
+ quality_array = []
- for bitrate in range(encoding_profile.bitrate_min, (encoding_profile.bitrate_max + bitrate_steps), bitrate_steps):
- # Do a CRF encode for the input file
+ for bitrate in range(encoding_profile.bitrate_min, (encoding_profile.bitrate_max + bitrate_steps_by_default), bitrate_steps_by_default):
+ # Do a CBR encode for the input file
cbr_encode = CbrEncode(self.input_file_path, encoding_profile.width, encoding_profile.height, bitrate, idr_interval_frames, part_start_time, part_duration)
cbr_encode.execute()
+ print('cbr_encode -> in progress -> ->')
- # Get the Bitrate from the CRF encoded file
+ # Get the Bitrate from the CBR encoded file
metric_assessment = Metric(metric, cbr_encode.output_file_path, self.input_file_path, input_probe.width, input_probe.height)
metric_assessment.execute()
+ print('-> -> probe |>', bitrate*10**(-3),'kbps |>',metric,' = ',metric_assessment.output_value, '\n')
- # Remove temporary CRF encoded file
+ # Remove temporary CBR encoded file
os.remove(cbr_encode.output_file_path)
- if last_metric_value is 0 :
- # for first value, you cannot calculate acurate jump in quality from nothing
- last_metric_value = metric_assessment.output_value
- profile['optimal_bitrate'] = bitrate
- quality_step_ratio = (metric_assessment.output_value)/bitrate # frist step from null to the starting bitrate
- else:
- quality_step_ratio = (metric_assessment.output_value - last_metric_value)/bitrate_steps
- if quality_step_ratio >= (last_quality_step_ratio/2):
- profile['optimal_bitrate'] = bitrate
+ # OLD method to find optimal bitrate_min
+
+ # if last_metric_value is 0 :
+ # # for first value, you cannot calculate acurate jump in quality from nothing
+ # last_metric_value = metric_assessment.output_value
+ # profile['optimal_bitrate'] = bitrate
+ # quality_step_ratio = (metric_assessment.output_value)/bitrate # first step from null to the starting bitrate
+ # else:
+ # quality_step_ratio = (metric_assessment.output_value - last_metric_value)/bitrate_steps_by_default
+ #
+ # if quality_step_ratio >= (last_quality_step_ratio/2):
+ # profile['optimal_bitrate'] = bitrate
+
+ # if 'ssim' in metric:
+ # if metric_assessment.output_value >= (last_metric_value + 0.01):
+ # profile['optimal_bitrate'] = bitrate
+ # elif 'psnr' in metric:
+ # if metric_assessment.output_value > last_metric_value:
+ # profile['optimal_bitrate'] = bitrate
+
+ # last_metric_value = metric_assessment.output_value
+ # last_quality_step_ratio = quality_step_ratio
+
+ # New method
+ bitrate_array.append(bitrate) # All bitrate for one profile
+ print(bitrate_array)
+ quality_array.append(metric_assessment.output_value) #pour un profile on a toutes les qualités
+ print(quality_array)
- #if 'ssim' in metric:
- # if metric_assessment.output_value >= (last_metric_value + 0.01):
- # profile['optimal_bitrate'] = bitrate
- #elif 'psnr' in metric:
- # if metric_assessment.output_value > last_metric_value:
- # profile['optimal_bitrate'] = bitrate
- last_metric_value = metric_assessment.output_value
- last_quality_step_ratio = quality_step_ratio
+ #**************GRAPH 1 matplotlib*************
+ # Initialize
+ diff_bitrate_array=1 # X
+ diff_quality_array=0 # Y
+ taux_accroissement=1
+
+ #Curve
+ figure(1)
+ plot(bitrate_array, quality_array, label=str(encoding_profile.width)+'x'+str(encoding_profile.height))
+ xlabel('bitrate (bps)')
+ ylabel("quality: "+str(metric).upper())
+ title(str(self.input_file_path))
+
+ # Rate of change and find out the optimal bitrate in the array
+ for j in range(0, len(quality_array)-1):
+ diff_quality_array=quality_array[j+1]-quality_array[j]
+ diff_bitrate_array=bitrate_array[j+1]-bitrate_array[j]
+
+ #limited_evolution_metric=0.005 -> indication: set arround 0.1 for psnr with a 100000 bps bitrate step and 0.05 with a 50000 bitrate step for ssim
+ limited_evolution_metric=limit_metric
+
+ taux_accroissement = diff_quality_array/diff_bitrate_array
encoding = {}
- encoding['bitrate'] = bitrate
- encoding['metric_value'] = metric_assessment.output_value
- encoding['quality_step_ratio'] = quality_step_ratio
+ encoding['bitrate'] = bitrate_array[j]
+ encoding['metric_value'] = quality_array[j]
+ encoding['quality_step_ratio'] = taux_accroissement
profile['cbr_encodings'].append(encoding)
+ if taux_accroissement <= limited_evolution_metric/bitrate_steps_by_default:
+ #scatter(bitrate_array[j], quality_array[j]) # I found out the good point
+ break
+
+ # Display good values !
+ print ('\nI found the best values for ||--- ', str(encoding_profile.width)+'x'+str(encoding_profile.height),' ---|| >> ',metric,':',quality_array[j],'| bitrate = ',bitrate_array[j]*10**(-3),'kbps')
+ optimal_bitrate_array.append(bitrate_array[j]) # use in GRAPH 2
+ profile['optimal_bitrate'] = bitrate_array[j]
profile['bitrate_savings'] = encoding_profile.bitrate_default - profile['optimal_bitrate']
- json_ouput['optimized_encoding_ladder']['encoding_profiles'].append(profile)
- self.json['analyses'].append(json_ouput)
+ # Graph annotations
+ annotation=str(bitrate_array[j]*1e-3)+' kbps'
+ #plot([bitrate_array[j],bitrate_array[j]], [0, quality_array[j]], linestyle='--' )
+ annotate(annotation, xy=(bitrate_array[j], quality_array[j]), xycoords='data', xytext=(+1, +20), textcoords='offset points', fontsize=8, arrowprops=dict(arrowstyle="->", connectionstyle="arc,rad=0.2"))
+ #plot([0, bitrate_array[j]], [quality_array[j], quality_array[j]], linestyle='--' )
+ scatter(bitrate_array[j], quality_array[j], s=7)
+ grid()
+ legend()
+ draw()
+ show(block=False)
+ pause(0.001)
+
+
+ #save graph1 and plot graph2
+ name=str(os.path.basename(self.input_file_path))
+ input("\n\n\nPress [enter] to continue, This will close the graphic and save the figure as ''file_name_metric_limit_metric.png'' !")
+ newpath = str(os.getcwd())+"/results/%s" % (name)
+ #newpath = '/home/labo/Documents/per_title_analysis/results/%s' % (name)
+ if not os.path.exists(newpath):
+ os.makedirs(newpath)
+ plt.savefig(newpath+"/%s-%s-%s-Per_Title.png" % (name, str(metric).strip().upper(), str(limited_evolution_metric)))
+
+ bitrate_data = [list(i) for i in zip(optimal_bitrate_array, default_bitrate_array)]
+
+ # GRAH 2 Computation
+ figure(2)
+ columns = ('Dynamic (kbps)', 'Fix (kbps)')
+ rows = ['%s' % resolution for resolution in ('1920 x 1080', '1280 x 720', '960 x 540', '640 x 360', '480 x 270')]
+
+ ylabel("bitrate (bps)")
+ title(str(self.input_file_path))
+
+ # Get some pastel shades for the colors
+ colors = plt.cm.YlOrBr(np.linspace(0.35, 0.8, len(rows)))
+ #size and positions
+ n_rows = len(bitrate_data)-1
+ index = np.arange(len(columns)) + 0.3
+ bar_width = 0.5
+
+ # Initialize the vertical-offset for the stacked bar chart.
+ y_offset = np.zeros(len(columns))
+
+ # Plot bars and create text labels for the table
+ cell_text = []
+ for row in range(n_rows+1): # until n_rows
+ plt.bar(index, bitrate_data[n_rows-row], bar_width, bottom=y_offset, color=colors[row])
+ y_offset = y_offset + bitrate_data[n_rows-row]
+ print('this is y_offset',y_offset)
+ cell_text.append(['%1.1f' % (x / 1000.0) for x in bitrate_data[n_rows-row]])
+ # Reverse colors and text labels to display the last value at the top.
+ colors = colors[::-1]
+ cell_text.reverse()
+
+
+ # Add a table at the bottom of the axes
+ the_table = plt.table(cellText=cell_text,
+ rowLabels=rows,
+ rowColours=colors,
+ colLabels=columns,
+ loc='bottom')
+
+ # Adjust layout to make room for the table:
+ plt.subplots_adjust(left=0.5, bottom=0.2)
+
+ #plt.ylabel("Loss in ${0}'s".format(value_increment))
+ #plt.yticks(values * value_increment, ['%d' % val for val in values])
+ plt.xticks([])
+ #plt.title('Loss by Disaster')
+
+ show(block=False)
+ pause(0.001)
+ print('\n\n->->\nloading graphic Histogram\n->->\n\n')
+ input("Press [enter] to continue, This will close the graphic and save the figure as ''file_name_metric_limit_metric.png'' ")
+ plt.savefig(newpath+"/%s-%s-%s-Per_Title_Histogram.png" % (name, str(metric).strip().upper(), str(limited_evolution_metric)))
+ print('\n\n\n************ALL DONE********** !\n\n')
diff --git a/pertitleanalysis/task_providers.py b/per_title_analysis/task_providers.py
old mode 100755
new mode 100644
similarity index 93%
rename from pertitleanalysis/task_providers.py
rename to per_title_analysis/task_providers.py
index 1943d88..cfa63db
--- a/pertitleanalysis/task_providers.py
+++ b/per_title_analysis/task_providers.py
@@ -73,12 +73,16 @@ def execute(self):
for stream in data['streams']:
if stream['codec_type'] == 'video':
self.width = int(stream['width'])
+ #print('the probe',self.width)
self.height = int(stream['height'])
- self.bitrate = int(stream['bit_rate'])
self.duration = float(stream['duration'])
self.video_codec = stream['codec_name']
- self.framerate = int(stream['r_frame_rate'].replace('/1',''))
+ self.framerate = int(stream['r_frame_rate'].replace('/1','')) #c'est quoi ce replace
+ self.bitrate = float(stream['bit_rate']) #error
+ self.video_codec = stream['codec_name']
+ self.framerate = int(stream['r_frame_rate'].replace('/1','')) #c'est quoi ce replace
except:
+
# TODO: error management
pass
@@ -115,13 +119,13 @@ def __init__(self, input_file_path, width, height, crf_value, idr_interval, part
# Generate a temporary file name for the task output
self.output_file_path = os.path.join(os.path.dirname(self.input_file_path),
os.path.splitext(os.path.basename(self.input_file_path))[0] + "_"+uuid.uuid4().hex+".mp4")
- print(self.output_file_path)
- print(self.part_start_time)
- print(self.input_file_path)
- print(self.part_duration)
- print(self.crf_value)
- print(self.definition)
- print(self.idr_interval)
+ # print(self.output_file_path)
+ # print(self.part_start_time)
+ # print(self.input_file_path)
+ # print(self.part_duration)
+ # print(self.crf_value)
+ # print(self.definition)
+ # print(self.idr_interval)
def execute(self):
"""Using FFmpeg to CRF Encode a file or part of a file"""