mirror of https://github.com/mpv-player/mpv
TOOLS/autocrop.lua: automatically crop at startup
This commit is contained in:
parent
c5158b057c
commit
376aea36eb
|
@ -1,114 +1,322 @@
|
|||
-- This script uses the lavfi cropdetect filter to automatically
|
||||
-- insert a crop filter with appropriate parameters for the currently
|
||||
-- playing video.
|
||||
--
|
||||
-- It registers the key-binding "C" (shift+c), which when pressed,
|
||||
-- inserts the filter vf=lavfi=cropdetect. After 1 second, it then
|
||||
-- inserts the filter vf=crop=w:h:x:y, where w,h,x,y are determined
|
||||
-- from the vf-metadata gathered by cropdetect. The cropdetect filter
|
||||
-- is removed immediately after the crop filter is inserted as it is
|
||||
-- no longer needed.
|
||||
--
|
||||
-- If the "C" key is pressed again, the crop filter is removed
|
||||
-- restoring playback to its original state.
|
||||
--
|
||||
-- Since the crop parameters are determined from the 1 second of video
|
||||
-- between inserting the cropdetect and crop filters, the "C" key
|
||||
-- should be pressed at a position in the video where the crop region
|
||||
-- is unambiguous (i.e., not a black frame, black background title
|
||||
-- card, or dark scene).
|
||||
--
|
||||
-- The default delay between insertion of the cropdetect and
|
||||
-- crop filters may be overridden by adding
|
||||
--
|
||||
-- --script-opts=autocrop.detect_seconds=<number of seconds>
|
||||
--
|
||||
-- to mpv's arguments. This may be desirable to allow cropdetect more
|
||||
-- time to collect data.
|
||||
--[[
|
||||
This script uses the lavfi cropdetect filter to automatically
|
||||
insert a crop filter with appropriate parameters for the
|
||||
currently playing video.
|
||||
|
||||
It will automatically crop the video, when playback starts.
|
||||
|
||||
Also It registers the key-binding "C" (shift+c). You can manually
|
||||
crop the video by pressing the "C" (shift+c) key.
|
||||
|
||||
If the "C" key is pressed again, the crop filter is removed
|
||||
restoring playback to its original state.
|
||||
|
||||
The workflow is as follows: First, it inserts the filter
|
||||
vf=lavfi=cropdetect. After <detect_seconds> (default is 1)
|
||||
seconds, it then inserts the filter vf=crop=w:h:x:y, where
|
||||
w,h,x,y are determined from the vf-metadata gathered by
|
||||
cropdetect. The cropdetect filter is removed immediately after
|
||||
the crop filter is inserted as it is no longer needed.
|
||||
|
||||
Since the crop parameters are determined from the 1 second of
|
||||
video between inserting the cropdetect and crop filters, the "C"
|
||||
key should be pressed at a position in the video where the crop
|
||||
region is unambiguous (i.e., not a black frame, black background
|
||||
title card, or dark scene).
|
||||
|
||||
The default options can be overridden by adding
|
||||
script-opts-append=autocrop-<parameter>=<value> into mpv.conf
|
||||
|
||||
List of available parameters (For default values, see <options>):
|
||||
|
||||
auto: bool - Whether to automatically apply crop at the start of
|
||||
playback. If you don't want to crop automatically, set it to
|
||||
false or add "script-opts-append=autocrop-auto=no" into
|
||||
mpv.conf.
|
||||
|
||||
auto_delay: seconds - Delay before starting crop in auto mode.
|
||||
You can try to increase this value to avoid dark scene or
|
||||
fade in at beginning. Automatic cropping will not occur if
|
||||
the value is larger than the remaining playback time.
|
||||
|
||||
detect_limit: number[0-255] - Black threshold for cropdetect.
|
||||
Smaller values will generally result in less cropping.
|
||||
See limit of https://ffmpeg.org/ffmpeg-filters.html#cropdetect
|
||||
|
||||
detect_round: number[2^n] - The value which the width/height
|
||||
should be divisible by. Smaller values have better detection
|
||||
accuracy. If you have problems with other filters,
|
||||
you can try to set it to 4 or 16.
|
||||
See round of https://ffmpeg.org/ffmpeg-filters.html#cropdetect
|
||||
|
||||
detect_min_ratio: number[0.0-1.0] - The ratio of the minimum clip
|
||||
size to the original. If the picture is over cropped or under
|
||||
cropped, try adjusting this value.
|
||||
|
||||
detect_seconds: seconds - How long to gather cropdetect data.
|
||||
Increasing this may be desirable to allow cropdetect more
|
||||
time to collect data.
|
||||
--]]
|
||||
|
||||
require "mp.msg"
|
||||
require 'mp.options'
|
||||
|
||||
script_name = mp.get_script_name()
|
||||
cropdetect_label = string.format("%s-cropdetect", script_name)
|
||||
crop_label = string.format("%s-crop", script_name)
|
||||
|
||||
-- number of seconds to gather cropdetect data
|
||||
detect_seconds = tonumber(mp.get_opt(string.format("%s.detect_seconds", script_name)))
|
||||
if not detect_seconds then
|
||||
local options = {
|
||||
auto = true,
|
||||
auto_delay = 4,
|
||||
detect_limit = "24/255",
|
||||
detect_round = 2,
|
||||
detect_min_ratio = 0.5,
|
||||
detect_seconds = 1
|
||||
end
|
||||
}
|
||||
read_options(options)
|
||||
|
||||
function del_filter_if_present(label)
|
||||
-- necessary because mp.command('vf del @label:filter') raises an
|
||||
-- error if the filter doesn't exist
|
||||
local vfs = mp.get_property_native("vf")
|
||||
for i,vf in pairs(vfs) do
|
||||
if vf["label"] == label then
|
||||
table.remove(vfs, i)
|
||||
mp.set_property_native("vf", vfs)
|
||||
local label_prefix = mp.get_script_name()
|
||||
local labels = {
|
||||
crop = string.format("%s-crop", label_prefix),
|
||||
cropdetect = string.format("%s-cropdetect", label_prefix)
|
||||
}
|
||||
|
||||
timers = {
|
||||
auto_delay = nil,
|
||||
detect_crop = nil
|
||||
}
|
||||
|
||||
function is_filter_present(label)
|
||||
local filters = mp.get_property_native("vf")
|
||||
for index, filter in pairs(filters) do
|
||||
if filter["label"] == label then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function autocrop_start()
|
||||
-- exit if cropdetection is already in progress
|
||||
if timer then
|
||||
mp.msg.warn("already cropdetecting!")
|
||||
return
|
||||
end
|
||||
function is_enough_time(seconds)
|
||||
|
||||
-- if there's a crop filter, remove it and exit
|
||||
if del_filter_if_present(crop_label) then
|
||||
return
|
||||
end
|
||||
-- Plus 1 second for deviation.
|
||||
local time_needed = seconds + 1
|
||||
local playtime_remaining = mp.get_property_native("playtime-remaining")
|
||||
|
||||
-- insert the cropdetect filter
|
||||
ret=mp.command(
|
||||
string.format(
|
||||
'vf add @%s:cropdetect=limit=%f:round=2:reset=0',
|
||||
cropdetect_label, 24/255
|
||||
)
|
||||
)
|
||||
-- wait to gather data
|
||||
timer=mp.add_timeout(detect_seconds, do_crop)
|
||||
return playtime_remaining and time_needed < playtime_remaining
|
||||
end
|
||||
|
||||
function do_crop()
|
||||
-- get the metadata
|
||||
local cropdetect_metadata = mp.get_property_native(
|
||||
string.format("vf-metadata/%s", cropdetect_label)
|
||||
)
|
||||
function is_cropable()
|
||||
local vid = mp.get_property_native("vid")
|
||||
local is_album = vid and mp.get_property_native(
|
||||
string.format("track-list/%d/albumart", vid)
|
||||
) or false
|
||||
|
||||
-- use it to crop if its valid
|
||||
if cropdetect_metadata then
|
||||
if cropdetect_metadata["lavfi.cropdetect.w"]
|
||||
and cropdetect_metadata["lavfi.cropdetect.h"]
|
||||
and cropdetect_metadata["lavfi.cropdetect.x"]
|
||||
and cropdetect_metadata["lavfi.cropdetect.y"]
|
||||
then
|
||||
mp.command(string.format("vf add @%s:lavfi-crop=w=%s:h=%s:x=%s:y=%s",
|
||||
crop_label,
|
||||
cropdetect_metadata["lavfi.cropdetect.w"],
|
||||
cropdetect_metadata["lavfi.cropdetect.h"],
|
||||
cropdetect_metadata["lavfi.cropdetect.x"],
|
||||
cropdetect_metadata["lavfi.cropdetect.y"]))
|
||||
else
|
||||
mp.msg.error(
|
||||
"Got empty crop data. You might need to increase detect_seconds."
|
||||
)
|
||||
return vid and not is_album
|
||||
end
|
||||
|
||||
function remove_filter(label)
|
||||
if is_filter_present(label) then
|
||||
mp.command(string.format('vf remove @%s', label))
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function cleanup()
|
||||
|
||||
-- Remove all existing filters.
|
||||
for key, value in pairs(labels) do
|
||||
remove_filter(value)
|
||||
end
|
||||
|
||||
-- Kill all timers.
|
||||
for index, timer in pairs(timers) do
|
||||
if timer then
|
||||
timer:kill()
|
||||
timer = nil
|
||||
end
|
||||
else
|
||||
mp.msg.error(
|
||||
"No crop data. Was the cropdetect filter successfully inserted?"
|
||||
)
|
||||
mp.msg.error(
|
||||
"Does your version of ffmpeg/libav support AVFrame metadata?"
|
||||
)
|
||||
end
|
||||
-- remove the cropdetect filter
|
||||
del_filter_if_present(cropdetect_label)
|
||||
timer=nil
|
||||
end
|
||||
|
||||
mp.add_key_binding("C", "auto_crop", autocrop_start)
|
||||
function detect_crop()
|
||||
|
||||
-- If it's not cropable, exit.
|
||||
if not is_cropable() then
|
||||
mp.msg.warn("autocrop only works for videos.")
|
||||
return
|
||||
end
|
||||
|
||||
-- Verify if there is enough time to detect crop.
|
||||
local time_needed = options.detect_seconds
|
||||
|
||||
if not is_enough_time(time_needed) then
|
||||
mp.msg.warn("Not enough time to detect crop.")
|
||||
return
|
||||
end
|
||||
|
||||
-- Insert the cropdetect filter.
|
||||
local limit = options.detect_limit
|
||||
local round = options.detect_round
|
||||
|
||||
mp.command(
|
||||
string.format(
|
||||
'vf pre @%s:cropdetect=limit=%s:round=%d:reset=0',
|
||||
labels.cropdetect, limit, round
|
||||
)
|
||||
)
|
||||
|
||||
-- Wait to gather data.
|
||||
timers.detect_crop = mp.add_timeout(time_needed, detect_end)
|
||||
end
|
||||
|
||||
function detect_end()
|
||||
|
||||
-- Get the metadata and remove the cropdetect filter.
|
||||
local cropdetect_metadata =
|
||||
mp.get_property_native(
|
||||
string.format("vf-metadata/%s",
|
||||
labels.cropdetect
|
||||
)
|
||||
)
|
||||
remove_filter(labels.cropdetect)
|
||||
|
||||
-- Remove the timer of detect crop.
|
||||
if timers.detect_crop then
|
||||
timers.detect_crop:kill()
|
||||
timers.detect_crop = nil
|
||||
end
|
||||
|
||||
local meta = {}
|
||||
|
||||
-- Verify the existence of metadata.
|
||||
if cropdetect_metadata then
|
||||
meta = {
|
||||
w = cropdetect_metadata["lavfi.cropdetect.w"],
|
||||
h = cropdetect_metadata["lavfi.cropdetect.h"],
|
||||
x = cropdetect_metadata["lavfi.cropdetect.x"],
|
||||
y = cropdetect_metadata["lavfi.cropdetect.y"],
|
||||
}
|
||||
else
|
||||
mp.msg.error("No crop data.")
|
||||
mp.msg.info("Was the cropdetect filter successfully inserted?")
|
||||
mp.msg.info("Does your version of ffmpeg/libav support AVFrame metadata?")
|
||||
return
|
||||
end
|
||||
|
||||
-- Verify that the metadata meets the requirements and convert it.
|
||||
if meta.w and meta.h and meta.x and meta.y then
|
||||
local width = mp.get_property_native("width")
|
||||
local height = mp.get_property_native("height")
|
||||
|
||||
meta = {
|
||||
w = tonumber(meta.w),
|
||||
h = tonumber(meta.h),
|
||||
x = tonumber(meta.x),
|
||||
y = tonumber(meta.y),
|
||||
min_w = width * options.detect_min_ratio,
|
||||
min_h = height * options.detect_min_ratio,
|
||||
max_w = width,
|
||||
max_h = height
|
||||
}
|
||||
else
|
||||
mp.msg.error("Got empty crop data.")
|
||||
mp.msg.info("You might need to increase detect_seconds.")
|
||||
return
|
||||
end
|
||||
|
||||
apply_crop(meta)
|
||||
end
|
||||
|
||||
function apply_crop(meta)
|
||||
|
||||
-- Verify if it is necessary to crop.
|
||||
local is_effective = meta.x > 0 or meta.y > 0
|
||||
or meta.w < meta.max_w or meta.h < meta.max_h
|
||||
|
||||
if not is_effective then
|
||||
mp.msg.info("No area detected for cropping.")
|
||||
return
|
||||
end
|
||||
|
||||
-- Verify it is not over cropped.
|
||||
local is_excessive = meta.w < meta.min_w and meta.h < meta.min_h
|
||||
|
||||
if is_excessive then
|
||||
mp.msg.info("The area to be cropped is too large.")
|
||||
mp.msg.info("You might need to decrease detect_min_ratio.")
|
||||
return
|
||||
end
|
||||
|
||||
-- Remove existing crop.
|
||||
remove_filter(labels.crop)
|
||||
|
||||
-- Apply crop.
|
||||
mp.command(
|
||||
string.format("vf pre @%s:lavfi-crop=w=%s:h=%s:x=%s:y=%s",
|
||||
labels.crop, meta.w, meta.h, meta.x, meta.y
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
function on_start()
|
||||
|
||||
-- Clean up at the beginning.
|
||||
cleanup()
|
||||
|
||||
-- If auto is not true, exit.
|
||||
if not options.auto then
|
||||
return
|
||||
end
|
||||
|
||||
-- If it is the beginning, wait for detect_crop
|
||||
-- after auto_delay seconds, otherwise immediately.
|
||||
local playback_time = mp.get_property_native("playback-time")
|
||||
local is_delay_needed = playback_time
|
||||
and options.auto_delay > playback_time
|
||||
|
||||
if is_delay_needed then
|
||||
|
||||
-- Verify if there is enough time for autocrop.
|
||||
local time_needed = options.auto_delay + options.detect_seconds
|
||||
|
||||
if not is_enough_time(time_needed) then
|
||||
mp.msg.warn("Not enough time for autocrop.")
|
||||
return
|
||||
end
|
||||
|
||||
timers.auto_delay = mp.add_timeout(time_needed,
|
||||
function()
|
||||
detect_crop()
|
||||
|
||||
-- Remove the timer of auto delay.
|
||||
timers.auto_delay:kill()
|
||||
timers.auto_delay = nil
|
||||
end
|
||||
)
|
||||
else
|
||||
detect_crop()
|
||||
end
|
||||
end
|
||||
|
||||
function on_toggle()
|
||||
|
||||
-- If it is during auto_delay, kill the timer.
|
||||
if timers.auto_delay then
|
||||
timers.auto_delay:kill()
|
||||
timers.auto_delay = nil
|
||||
end
|
||||
|
||||
-- Cropped => Remove it.
|
||||
if remove_filter(labels.crop) then
|
||||
return
|
||||
end
|
||||
|
||||
-- Detecting => Leave it.
|
||||
if timers.detect_crop then
|
||||
mp.msg.warn("Already cropdetecting!")
|
||||
return
|
||||
end
|
||||
|
||||
-- Neither => Do delectcrop.
|
||||
detect_crop()
|
||||
end
|
||||
|
||||
mp.add_key_binding("C", "toggle_crop", on_toggle)
|
||||
mp.register_event("end-file", cleanup)
|
||||
mp.register_event("file-loaded", on_start)
|
||||
|
|
Loading…
Reference in New Issue