mpv/TOOLS/lua/autocrop.lua

299 lines
9.0 KiB
Lua
Raw Normal View History

--[[
2023-10-06 07:00:23 +00:00
This script uses the lavfi cropdetect filter and the video-crop property to
automatically crop the currently playing video with appropriate parameters.
2023-10-06 07:00:23 +00:00
It automatically crops the video when playback starts.
2023-10-06 07:00:23 +00:00
You can also manually crop the video by pressing the "C" (shift+c) key.
Pressing it again undoes the crop.
2023-10-06 07:00:23 +00:00
The workflow is as follows: First, it inserts the cropdetect filter. After
<detect_seconds> (default is 1) seconds, it then sets video-crop based on the
vf-metadata values gathered by cropdetect. The cropdetect filter is removed
after video-crop is set as it is no longer needed.
2023-10-06 07:00:23 +00:00
Since the crop parameters are determined from the 1 second of video between
inserting the cropdetect filter and setting video-crop, 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).
If non-copy-back hardware decoding is in use, hwdec is temporarily disabled for
the duration of cropdetect as the filter would fail otherwise.
2023-10-06 07:00:23 +00:00
These are the default options. They can be overridden by adding
script-opts-append=autocrop-<parameter>=<value> to mpv.conf.
--]]
local options = {
2023-10-06 07:00:23 +00:00
-- Whether to automatically apply crop at the start of playback. If you
-- don't want to crop automatically, add
-- script-opts-append=autocrop-auto=no to mpv.conf.
auto = true,
2023-10-06 07:00:23 +00:00
-- Delay before starting crop in auto mode. You can try to increase this
-- value to avoid dark scenes or fade ins at beginning. Automatic cropping
-- will not occur if the value is larger than the remaining playback time.
auto_delay = 4,
2023-10-06 07:00:23 +00:00
-- Black threshold for cropdetect. Smaller values will generally result in
-- less cropping. See limit of
-- https://ffmpeg.org/ffmpeg-filters.html#cropdetect
detect_limit = "24/255",
2023-10-06 07:00:23 +00:00
-- 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_round = 2,
2023-10-06 07:00:23 +00:00
-- The ratio of the minimum clip size to the original. A number from 0 to
-- 1. If the picture is over cropped, try adjusting this value.
detect_min_ratio = 0.5,
2023-10-06 07:00:23 +00:00
-- How long to gather cropdetect data. Increasing this may be desirable to
-- allow cropdetect more time to collect data.
detect_seconds = 1,
2023-10-06 07:00:23 +00:00
-- Whether the OSD shouldn't be used when cropdetect and video-crop are
-- applied and removed.
suppress_osd = false,
}
2023-10-06 07:00:23 +00:00
require "mp.options".read_options(options)
local cropdetect_label = mp.get_script_name() .. "-cropdetect"
timers = {
auto_delay = nil,
detect_crop = nil
}
local hwdec_backup
local command_prefix = options.suppress_osd and 'no-osd' or ''
function is_enough_time(seconds)
-- Plus 1 second for deviation.
local time_needed = seconds + 1
local playtime_remaining = mp.get_property_native("playtime-remaining")
return playtime_remaining and time_needed < playtime_remaining
end
function is_cropable(time_needed)
if mp.get_property_native('current-tracks/video/image') ~= false then
mp.msg.warn("autocrop only works for videos.")
return false
end
if not is_enough_time(time_needed) then
mp.msg.warn("Not enough time to detect crop.")
return false
end
return true
end
function remove_cropdetect()
for _, filter in pairs(mp.get_property_native("vf")) do
if filter.label == cropdetect_label then
mp.command(
string.format("%s vf remove @%s", command_prefix, filter.label))
return
end
end
end
function restore_hwdec()
if hwdec_backup then
mp.set_property("hwdec", hwdec_backup)
hwdec_backup = nil
end
end
function cleanup()
remove_cropdetect()
-- Kill all timers.
for index, timer in pairs(timers) do
if timer then
timer:kill()
timers[index] = nil
end
end
restore_hwdec()
end
function detect_crop()
local time_needed = options.detect_seconds
if not is_cropable(time_needed) then
return
end
local hwdec_current = mp.get_property("hwdec-current")
if hwdec_current:find("-copy$") == nil and hwdec_current ~= "no" and
hwdec_current ~= "crystalhd" and hwdec_current ~= "rkmpp" then
hwdec_backup = mp.get_property("hwdec")
mp.set_property("hwdec", "no")
end
-- Insert the cropdetect filter.
local limit = options.detect_limit
local round = options.detect_round
mp.command(
string.format(
'%s vf pre @%s:cropdetect=limit=%s:round=%d:reset=0',
command_prefix, cropdetect_label, 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(
"vf-metadata/" .. cropdetect_label)
remove_cropdetect()
-- Remove the timer of detect crop.
if timers.detect_crop then
timers.detect_crop:kill()
timers.detect_crop = nil
end
restore_hwdec()
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?")
2024-01-19 18:42:17 +00:00
mp.msg.info("Does your version of FFmpeg 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.")
end
apply_crop(meta)
end
function apply_crop(meta)
-- Verify if it is necessary to crop.
local is_effective = meta.w and meta.h and meta.x and meta.y and
(meta.x > 0 or meta.y > 0
or meta.w < meta.max_w or meta.h < meta.max_h)
-- Verify it is not over cropped.
local is_excessive = false
if is_effective and (meta.w < meta.min_w or meta.h < meta.min_h) then
mp.msg.info("The area to be cropped is too large.")
mp.msg.info("You might need to decrease detect_min_ratio.")
is_excessive = true
end
if not is_effective or is_excessive then
-- Clear any existing crop.
mp.command(string.format("%s set file-local-options/video-crop ''", command_prefix))
return
end
-- Apply crop.
mp.command(string.format("%s set file-local-options/video-crop %sx%s+%s+%s",
command_prefix, 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_cropable(time_needed) then
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 mp.get_property("video-crop") ~= "" then
mp.command(string.format("%s set file-local-options/video-crop ''", command_prefix))
return
end
-- Detecting => Leave it.
if timers.detect_crop then
mp.msg.warn("Already cropdetecting!")
return
end
-- Neither => Detect crop.
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)