Merge pull request #1569 from pypeclub/feature/crop_overscan_extract_review

This commit is contained in:
Milan Kolar 2021-05-27 21:01:46 +02:00 committed by GitHub
commit 9040817b0b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 371 additions and 10 deletions

View file

@ -3,6 +3,9 @@ import re
import copy
import json
from abc import ABCMeta, abstractmethod
import six
import clique
import pyblish.api
@ -873,12 +876,6 @@ class ExtractReview(pyblish.api.InstancePlugin):
"""
filters = []
letter_box_def = output_def["letter_box"]
letter_box_enabled = letter_box_def["enabled"]
# Get instance data
pixel_aspect = temp_data["pixel_aspect"]
# NOTE Skipped using instance's resolution
full_input_path_single_file = temp_data["full_input_path_single_file"]
input_data = ffprobe_streams(
@ -887,6 +884,33 @@ class ExtractReview(pyblish.api.InstancePlugin):
input_width = int(input_data["width"])
input_height = int(input_data["height"])
# NOTE Setting only one of `width` or `heigth` is not allowed
# - settings value can't have None but has value of 0
output_width = output_def.get("width") or None
output_height = output_def.get("height") or None
# Convert overscan value video filters
overscan_crop = output_def.get("overscan_crop")
overscan = OverscanCrop(input_width, input_height, overscan_crop)
overscan_crop_filters = overscan.video_filters()
# Add overscan filters to filters if are any and modify input
# resolution by it's values
if overscan_crop_filters:
filters.extend(overscan_crop_filters)
input_width = overscan.width()
input_height = overscan.height()
# Use output resolution as inputs after cropping to skip usage of
# instance data resolution
if output_width is None or output_height is None:
output_width = input_width
output_height = input_height
letter_box_def = output_def["letter_box"]
letter_box_enabled = letter_box_def["enabled"]
# Get instance data
pixel_aspect = temp_data["pixel_aspect"]
# Make sure input width and height is not an odd number
input_width_is_odd = bool(input_width % 2 != 0)
input_height_is_odd = bool(input_height % 2 != 0)
@ -911,10 +935,6 @@ class ExtractReview(pyblish.api.InstancePlugin):
self.log.debug("input_width: `{}`".format(input_width))
self.log.debug("input_height: `{}`".format(input_height))
# NOTE Setting only one of `width` or `heigth` is not allowed
# - settings value can't have None but has value of 0
output_width = output_def.get("width") or None
output_height = output_def.get("height") or None
# Use instance resolution if output definition has not set it.
if output_width is None or output_height is None:
output_width = temp_data["resolution_width"]
@ -1438,3 +1458,291 @@ class ExtractReview(pyblish.api.InstancePlugin):
vf_back = "-vf " + ",".join(vf_fixed)
return vf_back
@six.add_metaclass(ABCMeta)
class _OverscanValue:
def __repr__(self):
return "<{}> {}".format(self.__class__.__name__, str(self))
@abstractmethod
def copy(self):
"""Create a copy of object."""
pass
@abstractmethod
def size_for(self, value):
"""Calculate new value for passed value."""
pass
class PixValueExplicit(_OverscanValue):
def __init__(self, value):
self._value = int(value)
def __str__(self):
return "{}px".format(self._value)
def copy(self):
return PixValueExplicit(self._value)
def size_for(self, value):
if self._value == 0:
return value
return self._value
class PercentValueExplicit(_OverscanValue):
def __init__(self, value):
self._value = float(value)
def __str__(self):
return "{}%".format(abs(self._value))
def copy(self):
return PercentValueExplicit(self._value)
def size_for(self, value):
if self._value == 0:
return value
return int((value / 100) * self._value)
class PixValueRelative(_OverscanValue):
def __init__(self, value):
self._value = int(value)
def __str__(self):
sign = "-" if self._value < 0 else "+"
return "{}{}px".format(sign, abs(self._value))
def copy(self):
return PixValueRelative(self._value)
def size_for(self, value):
return value + self._value
class PercentValueRelative(_OverscanValue):
def __init__(self, value):
self._value = float(value)
def __str__(self):
return "{}%".format(self._value)
def copy(self):
return PercentValueRelative(self._value)
def size_for(self, value):
if self._value == 0:
return value
offset = int((value / 100) * self._value)
return value + offset
class PercentValueRelativeSource(_OverscanValue):
def __init__(self, value, source_sign):
self._value = float(value)
if source_sign not in ("-", "+"):
raise ValueError(
"Invalid sign value \"{}\" expected \"-\" or \"+\"".format(
source_sign
)
)
self._source_sign = source_sign
def __str__(self):
return "{}%{}".format(self._value, self._source_sign)
def copy(self):
return PercentValueRelativeSource(self._value, self._source_sign)
def size_for(self, value):
if self._value == 0:
return value
return int((value * 100) / (100 - self._value))
class OverscanCrop:
"""Helper class to read overscan string and calculate output resolution.
It is possible to enter single value for both width and heigh or two values
for width and height. Overscan string may have a few variants. Each variant
define output size for input size.
### Example
For input size: 2200px
| String | Output | Description |
|----------|--------|-------------------------------------------------|
| "" | 2200px | Empty string does nothing. |
| "10%" | 220px | Explicit percent size. |
| "-10%" | 1980px | Relative percent size (decrease). |
| "+10%" | 2420px | Relative percent size (increase). |
| "-10%+" | 2000px | Relative percent size to output size. |
| "300px" | 300px | Explicit output size cropped or expanded. |
| "-300px" | 1900px | Relative pixel size (decrease). |
| "+300px" | 2500px | Relative pixel size (increase). |
| "300" | 300px | Value without "%" and "px" is used as has "px". |
Value without sign (+/-) in is always explicit and value with sign is
relative. Output size for "200px" and "+200px" are not the same.
Values "0", "0px" or "0%" are ignored.
All values that cause output resolution smaller than 1 pixel are invalid.
Value "-10%+" is a special case which says that input's resolution is
bigger by 10% than expected output.
It is possible to combine these variants to define different output for
width and height.
Resolution: 2000px 1000px
| String | Output |
|---------------|---------------|
| "100px 120px" | 2100px 1120px |
| "-10% -200px" | 1800px 800px |
"""
item_regex = re.compile(r"([\+\-])?([0-9]+)(.+)?")
relative_source_regex = re.compile(r"%([\+\-])")
def __init__(self, input_width, input_height, string_value):
# Make sure that is not None
string_value = string_value or ""
self.input_width = input_width
self.input_height = input_height
width, height = self._convert_string_to_values(string_value)
self._width_value = width
self._height_value = height
self._string_value = string_value
def __str__(self):
return "{}".format(self._string_value)
def __repr__(self):
return "<{}>".format(self.__class__.__name__)
def width(self):
"""Calculated width."""
return self._width_value.size_for(self.input_width)
def height(self):
"""Calculated height."""
return self._height_value.size_for(self.input_height)
def video_filters(self):
"""FFmpeg video filters to achieve expected result.
Filter may be empty, use "crop" filter, "pad" filter or combination of
"crop" and "pad".
Returns:
list: FFmpeg video filters.
"""
# crop=width:height:x:y - explicit start x, y position
# crop=width:height - x, y are related to center by width/height
# pad=width:heigth:x:y - explicit start x, y position
# pad=width:heigth - x, y are set to 0 by default
width = self.width()
height = self.height()
output = []
if self.input_width == width and self.input_height == height:
return output
# Make sure resolution has odd numbers
if width % 2 == 1:
width -= 1
if height % 2 == 1:
height -= 1
if width <= self.input_width and height <= self.input_height:
output.append("crop={}:{}".format(width, height))
elif width >= self.input_width and height >= self.input_height:
output.append(
"pad={}:{}:(iw-ow)/2:(ih-oh)/2".format(width, height)
)
elif width > self.input_width and height < self.input_height:
output.append("crop=iw:{}".format(height))
output.append("pad={}:ih:(iw-ow)/2:(ih-oh)/2".format(width))
elif width < self.input_width and height > self.input_height:
output.append("crop={}:ih".format(width))
output.append("pad=iw:{}:(iw-ow)/2:(ih-oh)/2".format(height))
return output
def _convert_string_to_values(self, orig_string_value):
string_value = orig_string_value.strip().lower()
if not string_value:
return
# Replace "px" (and spaces before) with single space
string_value = re.sub(r"([ ]+)?px", " ", string_value)
string_value = re.sub(r"([ ]+)%", "%", string_value)
# Make sure +/- sign at the beggining of string is next to number
string_value = re.sub(r"^([\+\-])[ ]+", "\g<1>", string_value)
# Make sure +/- sign in the middle has zero spaces before number under
# which belongs
string_value = re.sub(
r"[ ]([\+\-])[ ]+([0-9])",
r" \g<1>\g<2>",
string_value
)
string_parts = [
part
for part in string_value.split(" ")
if part
]
error_msg = "Invalid string for rescaling \"{}\"".format(
orig_string_value
)
if 1 > len(string_parts) > 2:
raise ValueError(error_msg)
output = []
for item in string_parts:
groups = self.item_regex.findall(item)
if not groups:
raise ValueError(error_msg)
relative_sign, value, ending = groups[0]
if not relative_sign:
if not ending:
output.append(PixValueExplicit(value))
else:
output.append(PercentValueExplicit(value))
else:
source_sign_group = self.relative_source_regex.findall(ending)
if not ending:
output.append(PixValueRelative(int(relative_sign + value)))
elif source_sign_group:
source_sign = source_sign_group[0]
output.append(PercentValueRelativeSource(
float(relative_sign + value), source_sign
))
else:
output.append(
PercentValueRelative(float(relative_sign + value))
)
if len(output) == 1:
width = output.pop(0)
height = width.copy()
else:
width, height = output
return width, height

View file

@ -55,6 +55,7 @@
"ftrack"
]
},
"overscan_crop": "",
"width": 0,
"height": 0,
"bg_color": [

View file

@ -173,6 +173,15 @@
{
"type": "separator"
},
{
"type": "label",
"label": "Crop input overscan. See the documentation for more information."
},
{
"type": "text",
"key": "overscan_crop",
"label": "Overscan crop"
},
{
"type": "label",
"label": "Width and Height must be both set to higher value than 0 else source resolution is used."

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Before After
Before After

View file

@ -69,6 +69,49 @@ Profile may generate multiple outputs from a single input. Each output must defi
- it is possible to rescale output to specified resolution and keep aspect ratio.
- If value is set to 0, source resolution will be used.
- **`Overscan crop`**
- Crop input resolution before rescaling.
- Value is text may have a few variants. Each variant define output size for input size.
- All values that cause output resolution smaller than 1 pixel are invalid.
- Value without sign (+/-) in is always explicit and value with sign is
relative. Output size for values "200px" and "+200px" are not the same "+200px" will add 200 pixels to source and "200px" will keep only 200px from source. Value of "0", "0px" or "0%" are automatically converted to "+0px" as 0px is invalid ouput.
- Cropped value is related to center. It is better to avoid odd numbers if
possible.
**Example outputs for input size: 2200px**
| String | Output | Description |
|---|---|---|
| ` ` | 2200px | Empty string keep resolution unchanged. |
| `50%` | 1100px | Crop 25% of input width on left and right side. |
| `300px` | 300px | Keep 300px in center of input and crop rest on left adn right. |
| `300` | 300px | Values without units are used as pixels (`px`). |
| `+0px` | 2200px | Keep resolution unchanged. |
| `0px` | 2200px | Same as `+0px`. |
| `+300px` | 2500px | Add black pillars of 150px width on left and right side. |
| `-300px` | 1900px | Crop 150px on left and right side |
| `+10%` | 2420px | Add black pillars of 5% size of input on left and right side. |
| `-10%` | 1980px | Crop 5% of input size by on left and right side. |
| `-10%+` | 2000px | Input width is 110% of output width. |
**Value "-10%+" is a special case which says that input's resolution is
bigger by 10% than expected output.**
- It is possible to enter single value for both width and height or
combination of two variants for width and height separated with space.
**Example for resolution: 2000px 1000px**
| String | Output |
|---------------|---------------|
| "100px 120px" | 2100px 1120px |
| "-10% -200px" | 1800px 800px |
| "-10% -0px" | 1800px 1000px |
- **`Letter Box`**
- **Enabled** - Enable letter boxes
- **Ratio** - Ratio of letter boxes