[
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# Local test files\ntest_samples/\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 Paul Willot\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "Remove static watermarks from videos with minimal setup.\n\n![example of watermark removal](example_processed_frame.png)\n\nReally basic, but works well enough for simple static watermarks, and can run on a laptop CPU (x3 real-time on a i5-5287U (2015 MacBook Pro), x9 real-time on a i5-8400). You can find brief explanations on how it's done [here](https://paulw.tokyo/post/basic-watermark-removal-in-videos/).\n\nDependencies:\n```sh\n# FFMPEG\ninstaller=$([[ $(uname) == \"Darwin\" ]] && echo brew || echo apt)\n$installer install ffmpeg\n\n# Python libraries\npython3 -m pip install numpy scipy imageio\n\n# Optional, to fetch an example video\n# if already installed, make sure youtube-dl is up to date\n$installer install youtube-dl\n```\n\nUsage:\n```sh\n# The output will default to append \"_cleaned\" to the existing name,\n# and use max 50 keyframes\n./remove_watermark.sh /somewhere/my_video.mp4 [/somewhere/output.mp4] [max_keyframes_to_extract]\n```\n\nTested on MacOS 10.14 (x86), MacOS 14.4 (arm) and Ubuntu 20.04\n"
  },
  {
    "path": "get_watermark.py",
    "content": "#!/usr/bin/env python3\n\nimport sys\nfrom pathlib import Path\n\nimport imageio\nimport numpy as np\nfrom scipy.ndimage import gaussian_filter\n\n\ndef normalize(x):\n    _min = np.min(x)\n    _max = np.max(x)\n    return (x - _min) / (_max - _min)\n\n\nif __name__ == \"__main__\":\n    # Load all images\n    root = Path(sys.argv[1])\n    buff = []\n    for p in root.glob(\"output_*.png\"):\n        buff.append(imageio.imread(p))\n    images = np.array(buff)\n\n    # Compute the gradients\n    dx = np.gradient(images, axis=1).mean(axis=3)\n    dy = np.gradient(images, axis=2).mean(axis=3)\n    mean_dx = np.abs(np.mean(dx, axis=0))\n    mean_dy = np.abs(np.mean(dy, axis=0))\n\n    # Filter at a hand picked threshold\n    threshold = 10\n    salient = ((mean_dx > threshold) | (mean_dy > threshold)).astype(float)\n    salient = normalize(gaussian_filter(salient, sigma=3))\n    mask = ((salient > 0.2) * 255).astype(np.uint8)\n\n    # Saved the computed mask\n    imageio.imsave(root / \"mask.png\", mask)\n"
  },
  {
    "path": "remove_watermark.sh",
    "content": "#!/usr/bin/env bash\n\nset -eo pipefail\n\n# Prepare output name\nfile_no_ext=\"${1%.*}\"\nextension=\"${1##*.}\"\ndef_name=\"$file_no_ext\"\"_cleaned.\"\"$extension\"\noutput_file=\"${2:-$def_name}\"\n\n# Get first few key frames\necho \"Getting key frames...\"\nmax_frames=\"${3:-50}\"\nkeyframes_time=$(ffprobe -hide_banner -loglevel warning -select_streams v -skip_frame nokey -show_frames -show_entries frame=pkt_dts_time \"$1\" | grep \"pkt_dts_time=\" | xargs shuf -n \"$max_frames\" -e | awk -F  \"=\" '{print $2}')\n\n# Save them as images, in a temporary directory\ntmpdir=$(mktemp -d 2>/dev/null || mktemp -d -t 'watermark_remove')\ncounter=0\necho -n \"Extracting frames (up to: $max_frames)... \"\nfor i in $keyframes_time; do\n    if ! [[ \"$i\" =~ ^[0-9]+([.][0-9]+)?$ ]]; then\n        echo \"Skipping unrecognize timing: $i\"\n        continue\n    fi\n    ffmpeg -y -hide_banner -loglevel error -ss \"$i\" -i \"$1\" -vframes 1 \"$tmpdir/output_$counter.png\"\n    echo -n \"$counter \"\n    ((counter=counter+1))\ndone\necho\n\n# Abort if we couldn't extract frames for some reason\nif [[ \"$counter\" -lt 2 ]]; then\n    echo \"$counter frames extracted, need at least 2, aborting.\"\n    exit 1\nfi\n\necho \"Extracting watermark...\"\n./get_watermark.py \"$tmpdir\"\n\necho \"Removing watermark in video...\"\nffmpeg -hide_banner -loglevel warning -y -stats -i \"$1\" -acodec copy -vf \"removelogo=$tmpdir/mask.png\" \"$output_file\"\n\nrm -rf \"$tmpdir\"\n\necho \"Done\"\n\nexit 0\n"
  },
  {
    "path": "test.sh",
    "content": "#!/usr/bin/env bash\n\n\nset -eo pipefail\n\n# Store samples there\nmkdir -p test_samples\n\n# Get the first 3 minutes of Spring (Blender Open Movie) after the opening credits, 720p video only\necho \"Fetching sample movie\"\nURL=$(youtube-dl -g -f136 \"https://youtu.be/WhWc3b3KhnY\")\nffmpeg -hide_banner -loglevel warning -y -stats -ss 00:23 -t 03:00 -i \"$URL\" -c copy ./test_samples/original.mp4\n\n# Add simple text as watermark\necho \"Adding watermark\"\nffmpeg -hide_banner -loglevel warning -y -stats -i ./test_samples/original.mp4 -filter_complex \"[0:v]drawtext='font=sans-serif:fontsize=30:fontcolor=white:x=20:y=20:text=Watermark (TM)'\" ./test_samples/watermarked.mp4\n\n# Test watermark removal\necho \"Tesing:\"\necho\n./remove_watermark.sh test_samples/watermarked.mp4\n"
  }
]