Skip to content

Commit 0107053

Browse files
committed
wip: masking
1 parent 847af16 commit 0107053

18 files changed

Lines changed: 309 additions & 7 deletions

File tree

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,19 @@ Using blur on 60fps footage results in clean motion blur, but occasionally leave
8787

8888
If your footage contains duplicate frames then occasionally blurred frames will look out of place, making the video seem unsmooth at points. The 'deduplicate' option will automatically fill in duplicated frames with interpolated frames to prevent this from happening.
8989

90+
### Masks
91+
92+
Interpolation has no idea that a HUD isn't part of the scene, so it warps static overlays - crosshairs, killfeeds, timers - along with the world behind them. A mask marks those regions so they keep their original pixels instead.
93+
94+
Masks are png files kept in the `masks` folder of your config folder. White means "interpolate this as normal", black means "leave this alone". Anything in between blends the two.
95+
96+
Pick one with the `mask` config setting to apply it to everything by default, and override it per clip from the dropdown in the queue - so a Counter-Strike mask and a Fortnite mask can be used on different clips in the same batch. `blur-cli` takes a `--mask` argument for the same thing (`--mask none` turns off a mask set in the config).
97+
98+
Two things worth knowing:
99+
100+
- Masked areas still get motion blur. Only interpolation skips them, so they blend like the rest of the video.
101+
- The mask is scaled to fit the video, so one mask works across resolutions as long as the aspect ratio matches.
102+
90103
### Frameserver output
91104

92105
Blur supports rendering from frameservers. This means you can avoid having to run blur on your input videos when video editing. When rendering, simply output (make sure your project is high framerate) to the frameserver and then drag the generated AVI into blur. Note that some video editing software might limit the maximum project framerate.
@@ -113,6 +126,7 @@ Blur supports rendering from frameservers. This means you can avoid having to ru
113126

114127
- interpolate - whether or not the input video file will be interpolated to a higher fps
115128
- interpolated fps - if interpolate is enabled, this is the fps that the input file will be interpolated to (before blurring). can be a set fps number or a multiplier (append x to end e.g. `5x`)
129+
- mask - mask image used to protect parts of the frame from interpolation, or `none`. [see masks](#masks)
116130
- interpolation method - method used for interpolation:
117131
- Quality: RIFE > svp
118132
- Speed: svp > RIFE

src/cli/cli.cpp

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
#include "cli.h"
22
#include "common/rendering.h"
3+
#include "common/masks.h"
34

45
bool cli::run(
56
std::vector<std::filesystem::path> inputs,
67
std::vector<std::filesystem::path> outputs,
78
std::vector<std::filesystem::path> config_paths,
89
bool preview,
910
bool verbose,
10-
bool disable_update_check
11+
bool disable_update_check,
12+
const std::string& mask
1113
) {
1214
auto init_res = blur.initialise(verbose, preview);
1315
if (!init_res) { // todo: preview in cli
@@ -81,8 +83,21 @@ bool cli::run(
8183
std::filesystem::create_directories(parent);
8284
}
8385

86+
std::optional<std::string> mask_override;
87+
if (!mask.empty()) // "none" turns off a mask the config sets, for this run only
88+
mask_override = mask == masks::NONE_OPTION ? "" : mask;
89+
8490
auto add_res = rendering::video_render_queue.add(
85-
input_path, video_info, config_path, config_app::get_app_config(), output_path
91+
input_path,
92+
video_info,
93+
config_path,
94+
config_app::get_app_config(),
95+
output_path,
96+
0.f,
97+
1.f,
98+
{},
99+
{},
100+
mask_override
86101
);
87102

88103
if (add_res.error) {

src/cli/cli.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ namespace cli {
77
std::vector<std::filesystem::path> config_paths,
88
bool preview,
99
bool verbose,
10-
bool disable_update_check = false
10+
bool disable_update_check = false,
11+
12+
// mask filename to render every input with. empty leaves each config's own mask alone
13+
const std::string& mask = ""
1114
);
1215
}

src/cli/main.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,14 @@ int main(int argc, char* argv[]) {
3232
std::vector<PathStr> config_path_strs;
3333
bool preview = false;
3434
bool verbose = false;
35+
std::string mask;
3536

3637
app.add_option("-i,--input", input_strs, "Input file name(s)")->required();
3738
app.add_option("-o,--output", output_strs, "Output file name(s) (optional)");
3839
app.add_option("-c,--config-path", config_path_strs, "Manual configuration file path(s) (optional)");
40+
app.add_option(
41+
"--mask", mask, "Mask image filename in the masks folder, or 'none' to disable one set in the config (optional)"
42+
);
3943
app.add_flag("-p,--preview", preview, "Enable preview");
4044
app.add_flag("-v,--verbose", verbose, "Verbose mode");
4145

@@ -45,7 +49,7 @@ int main(int argc, char* argv[]) {
4549
auto outputs = to_paths(output_strs);
4650
auto config_paths = to_paths(config_path_strs);
4751

48-
cli::run(inputs, outputs, config_paths, preview, verbose);
52+
cli::run(inputs, outputs, config_paths, preview, verbose, false, mask);
4953

5054
return 0;
5155
}

src/common/blur.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include "config_blur.h"
77
#include "config_app.h"
88
#include "config_presets.h"
9+
#include "masks.h"
910

1011
tl::expected<void, std::string> Blur::initialise(bool _verbose, bool _using_preview) {
1112
resources_path = u::get_resources_path();
@@ -23,6 +24,10 @@ tl::expected<void, std::string> Blur::initialise(bool _verbose, bool _using_prev
2324
if (!std::filesystem::exists(preset_config_path))
2425
config_presets::create(preset_config_path, PresetSettings{});
2526

27+
// so there's somewhere to drop mask images even before one's been used
28+
std::error_code masks_ec;
29+
std::filesystem::create_directories(masks::get_path(), masks_ec);
30+
2631
#if defined(_WIN32)
2732
used_installer = std::filesystem::exists(resources_path / "lib\\vapoursynth\\vspipe.exe") &&
2833
std::filesystem::exists(resources_path / "lib\\ffmpeg\\ffmpeg.exe");

src/common/config_blur.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ std::string config_blur::generate_config_string(const BlurSettings& settings, bo
3838
if (!concise || settings.interpolate) {
3939
output << "interpolated fps: " << settings.interpolated_fps << "\n";
4040
output << "interpolation method: " << settings.interpolation_method << "\n";
41+
if (!concise || !settings.mask.empty()) {
42+
output << "mask: " << settings.mask << "\n";
43+
}
4144
}
4245
}
4346

@@ -300,6 +303,7 @@ BlurSettings config_blur::parse_from_map(const std::map<std::string, std::string
300303
config_base::extract_config_value(config_map, "interpolate", settings.interpolate);
301304
config_base::extract_config_value(config_map, "interpolated fps", settings.interpolated_fps);
302305
config_base::extract_config_value(config_map, "interpolation method", settings.interpolation_method);
306+
config_base::extract_config_value(config_map, "mask", settings.mask);
303307

304308
config_base::extract_config_value(config_map, "pre-interpolate", settings.pre_interpolate);
305309
config_base::extract_config_value(config_map, "pre-interpolated fps", settings.pre_interpolated_fps);
@@ -446,6 +450,7 @@ tl::expected<nlohmann::json, std::string> BlurSettings::to_json() const {
446450
j["interpolate"] = this->interpolate;
447451
j["interpolated_fps"] = this->interpolated_fps;
448452
j["interpolation_method"] = this->interpolation_method;
453+
j["mask"] = this->mask;
449454

450455
j["pre_interpolate"] = this->pre_interpolate;
451456
j["pre_interpolated_fps"] = this->pre_interpolated_fps;

src/common/config_blur.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ struct BlurSettings {
4848
std::string interpolation_method = "svp";
4949
#endif
5050

51+
// filename of a png in <settings path>/masks, or empty for none. masks protect regions from
52+
// interpolation, so this does nothing when interpolate is off
53+
std::string mask;
54+
5155
bool pre_interpolate = false;
5256
std::string pre_interpolated_fps = "360";
5357
std::string pre_interpolation_method = "rife";

src/common/masks.cpp

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#include "masks.h"
2+
3+
std::filesystem::path masks::get_path() {
4+
return blur.settings_path / FOLDER_NAME;
5+
}
6+
7+
std::vector<std::string> masks::list() {
8+
std::vector<std::string> masks;
9+
10+
auto path = get_path();
11+
12+
std::error_code ec; // don't throw if the folder's missing or unreadable, just show nothing
13+
for (const auto& entry : std::filesystem::directory_iterator(path, ec)) {
14+
if (!entry.is_regular_file(ec))
15+
continue;
16+
17+
if (u::to_lower(u::path_to_string(entry.path().extension())) != ".png")
18+
continue;
19+
20+
masks.push_back(u::path_to_string(entry.path().filename()));
21+
}
22+
23+
std::ranges::sort(masks);
24+
25+
return masks;
26+
}
27+
28+
std::vector<std::string> masks::options(const std::string& current) {
29+
auto mask_files = list();
30+
31+
std::vector<std::string> options = { NONE_OPTION };
32+
options.insert(options.end(), mask_files.begin(), mask_files.end());
33+
34+
if (!current.empty() && !u::contains(options, current))
35+
options.push_back(current);
36+
37+
return options;
38+
}

src/common/masks.h

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#pragma once
2+
3+
// Masks mark regions of the frame that should be protected from interpolation. The interpolator has no idea
4+
// that a HUD isn't part of the scene, so it warps it along with the world behind it; a mask lets the renderer
5+
// put the pre-interpolation pixels back over those regions afterwards.
6+
//
7+
// A mask is a png in <settings path>/masks - white where the frame should be interpolated as normal, black
8+
// where it should be left alone. Settings store the bare filename, so a config stays portable between
9+
// machines, and blur.py resolves it against the settings path it's already given.
10+
namespace masks {
11+
inline const std::string FOLDER_NAME = "masks";
12+
13+
// shown in the mask dropdowns, and what an empty setting means
14+
inline const std::string NONE_OPTION = "none";
15+
16+
std::filesystem::path get_path();
17+
18+
// filenames of every png in the masks folder, sorted. empty if the folder doesn't exist
19+
std::vector<std::string> list();
20+
21+
// what a mask dropdown shows: "none", then every mask in the folder. `current` is kept in the list even if
22+
// it's been deleted since it was picked, so that's visible instead of the dropdown silently snapping to
23+
// something else
24+
std::vector<std::string> options(const std::string& current);
25+
}

src/common/rendering/render_queue.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ rendering::QueueAddRes rendering::VideoRenderQueue::add(
5858
const std::function<void(
5959
const VideoRenderDetails& render,
6060
const tl::expected<rendering::RenderResult, std::variant<std::string, RenderError>>& result
61-
)>& finish_callback
61+
)>& finish_callback,
62+
const std::optional<std::string>& mask_override
6263
) {
6364
// parse config file (do it now, not when rendering. nice for batch rendering the same file with different
6465
// settings)
@@ -67,6 +68,9 @@ rendering::QueueAddRes rendering::VideoRenderQueue::add(
6768
!config_path.has_value() // use global only if no config path is specified
6869
);
6970

71+
if (mask_override)
72+
config_res.config.mask = *mask_override;
73+
7074
if (!video_info.audio_sample_rates.empty() && detail::copies_audio(config_res.config, app_settings)) {
7175
if (auto conflict = detail::get_audio_copy_conflict(config_res.config, start != 0.f || end != 1.f)) {
7276
return {

0 commit comments

Comments
 (0)