How to censor videos using FFmpeg — masking

Learn to blacken a portion of screen

Back when The Onion was a satire website, it published an article titled CIA Realizes It’s Been Using Black Highlighters All These Years to mock the cocaine import agency's propensity for redacting almost all the documents it declassified. I will show you something similar. FFmpeg has a filter that can draw a box on a video and fill it with colour if necessary. The colour can be specified in RGBA format or using one of the HTML colour names. The filter is aptly named drawbox and here is the output of the documentation command ffmpeg -h filter=drawbox.

Filter drawbox
  Draw a colored box on the input video.
    Inputs:
       #0: default (video)
    Outputs:
       #0: default (video)
drawbox AVOptions:
   x                 <string>     ..FV.....T. set horizontal position of the left box edge (default "0")
   y                 <string>     ..FV.....T. set vertical position of the top box edge (default "0")
   width             <string>     ..FV.....T. set width of the box (default "0")
   w                 <string>     ..FV.....T. set width of the box (default "0")
   height            <string>     ..FV.....T. set height of the box (default "0")
   h                 <string>     ..FV.....T. set height of the box (default "0")
   color             <string>     ..FV.....T. set color of the box (default "black")
   c                 <string>     ..FV.....T. set color of the box (default "black")
   thickness         <string>     ..FV.....T. set the box thickness (default "3")
   t                 <string>     ..FV.....T. set the box thickness (default "3")
   replace           <boolean>    ..FV.....T. replace color & alpha (default false)
   box_source        <string>     ..FV.....T. use data from bounding box in side data

What is not mentioned above but is available in the full HTML documentation is that the thickness option can be set to the value fill and the filter will not just draw the outline but also fill it with black or the colour specified by the color option.

Background

When I bought my latest TV, I had no choice but to choose the dreaded SMART TV option. There were no dumb TVs at that size. However, I made sure that it did not use Google Android spyware or an Android-based OS such as the AOSP. On my old TV, I used a media box (WD TV) for streaming over the Internets. On this new TV, wired (Ethernet) networking was built-in. I do not leave the TV connected to the Internet but I did test its media streaming abilities for a while. [I continue to download and play online videos offline because someone seems to throttle bandwidth intermittently.] One app played the Hindi movie song Dhoom Machale Dhoom. The fireworks in the beginning of the song intrigued me … they seemed too real. Outside the app, I had downloaded the song and I could not get the same effect. Anyway, the video file is in my collection of cool Hindi movie songs that I like to watch once in a while. But, there was something very very annoying in this song. It is a guy in the audience whom the director had forced to overact. This guy's sequences irritated me so much I had long felt the need to censor him off the video. Recently, I wrote a script to perform such censorships. Here is the effect.

Screenshot of FFplay video player

For each black box sequence, the script prompts for

  • starting time (in seconds)
  • ending time (in seconds)
  • coordinates of the top-left corner of the box in x:y format
  • width and height of the box in w:h format

To obtain the values, I had to pause the video, take a screenshot and paste from clipboard in GIMP. I would then use the Rectangle Select Tool to mark the region that needs to be masked and this gave me the x:Y and w:h values. I recorded these values in a text file (doomed.txt) …

Screenshot of text editor

… and then input it to the script.

bash mask-video.txt DhoomMachaleDhoom.mkv < doomed.txt

Video Masking FFmpeg Script

#!/bin/bash

set -u

sFile=$(readlink -f "$*")
sFileDir=$(dirname "$sFile")
sFileFullName=$(basename "$sFile")
sFileName="${sFileFullName%.*}"
sFileExt="${sFileFullName##*.}"

if [ -f "$sFile" ]; then
   echo "Masking $sFile"
else
  echo -f "ERROR: Not found $sFile"
  exit
fi

i=0
sFilter=""
echo "Press Enter twice to exit data-entry loop."
while true; do

    read -p "Enter starting time: " iStart
    read -p "Enter ending time: " iEnd

    if [ -z $iStart ] || [ -z $iEnd ]; then
        break
    fi

    if [ $iStart -ge 0 ] 2> /dev/null   && 
         [ $iEnd -ge 0 ] 2> /dev/null ; then
        if [ $iStart -ge $iEnd ]; then
            echo "ERROR: Starting time cannot be >= to ending time."
            echo "--------------------"
            continue
        else
          read -p "Enter mask location (x:y) values: " sMaskCoordinates
          read -p "Enter mask dimension (w:h) values: " sMaskDimensions

      if [ -z "$sMaskCoordinates" ] || [ -z "$sMaskDimensions" ]; then
        echo -e "ERROR: Invalid mask value(s)\n--------------------"
            continue
      else
        let i=i+1
        if [ -z "$sFilter" ]; then
          sFilter="[0:v:0]drawbox=${sMaskCoordinates}:${sMaskDimensions}:color=0x000000F0:thickness=fill:enable='between(t,${iStart},${iEnd})'"
        else
          sFilter="${sFilter},drawbox=${sMaskCoordinates}:${sMaskDimensions}:color=0x000000F0:thickness=fill:enable='between(t,${iStart},${iEnd})'"
        fi
      fi
        fi
    else
        echo -e "ERROR: Invalid number(s)\n--------------------"
        continue
    fi
done

cd "$sFileDir"
if [ -n "$sFilter" ]; then
  ffmpeg -i "$sFile" \
         -filter_complex "$sFilter" \
         -map 0:a \
         -c:V:0 libx265 -crf 28  \
         -c:a copy \
         -r 25 -pix_fmt yuv420p -y \
         "${sFileDir}/${sFileName}-MASKED.${sFileExt}" && \
     xdg-open "${sFileDir}/${sFileName}-MASKED.${sFileExt}"
fi

The masking black box is not entirely black. I have used a transparency value (F0 in 0x000000F0)

Video demo

Here is the low-res masked video sans audio in consideration of their copyright and my fair use.

Rumble

Next stop: Remove John Abraham from the Dilbar song because he seems to be searching for a bathroom.

Alternatives

There are many ways to censor videos. I use one of three techniques:

  1. Masking: Block a part of the video frame, as described above
  2. Replace a colour with another: In an older blog post, I have mentioned how to censor skin tones to censor gratuitous airing of the cleavage.
  3. Blank the whole video frame and/or silence the audio: I will describe this in an another blog post.

Become an FFmpeg PRO by reading my book Quick Start Guide to FFmpeg.

Book photo

Link: | Magic Link:

Comments are not enabled yet.

How to list only files, not directories, sorted by size in Linux Bash shell

Strange that both the 𝚕𝚜 and the 𝚏𝚒𝚗𝚍 commands cannot do this on their own

In my book Linux Command-Line Tips & Tricks, I had mentioned a feature that was lacking in the ls command. In Microsoft Windoos or rather DOOS, you can type dir /ad and get a list of directories. You cannot do this in bash with just built-in parameters. You need to use a hack in the arguments, i.e., ls -d */. Its output also presents a wrinkle in that the directory names are suffixed by the slash. This makes it difficult to use the output ls command in loops.

The suggested alternative is find -maxdepth 0 -type d. This is obviously not succinct as the DOS command.

This is not the only shortcoming. ls cannot list only files. No sir. For that also, the alternative is find or rather find * -maxdepth 0 -type f.

For one of my scripts, I needed to iterate over a list of files sorted by file size. ls can do this but it will also list directories. find can list files alone but is deficient in the sorting department.

The solution is an indictment of two of the most basic commands in Linux Bash.

#!/bin/bash
set -u

# Directory name is read from the command line
sDir="$*"

if [ ! -d "$sDir" ]; then
  echo -e "ERROR:\tNot a directory — $sDir"
  exit
fi

# Get temporary file to store the list of names of files
sListFile=$(mktemp)

# Get absolute path of directory
sDir=$(readlink -f "$sDir")

pushd . > /dev/null
cd "$sDir"

# Generate the list and store it in the temp file
ls -1pr --sort=size | grep -v "/" > "$sListFile"

# Read the list line-by-line
while IFS= read -u 20 sLine ; do
  sFileName=$(basename "$sLine")
  sFile=$(readlink -f "${sDir}/${sFileName}")

  # Ignore non-file
  if [ ! -f "$sFile" ]; then
    continue 
  fi

  # Do whatever you want with the file name
  echo "$sFile"

done 20< "$sListFile"

# 20 is a randomly chosen number for the file descriptor
# from which the list is read

popd > /dev/null

You need to jump through all these hoops because some files have spaces and brackets that tend to trip ls, find and other bash commands and also wildcards * and ?.


Become a Linux command-line warrior by reading my book Linux Command-Line Tips & Tricks.

Book photo

Link: | Magic Link:

Comments are not enabled yet.

How to choose between AV1, H.266, H.265 and H.264 codecs

AV1 for the browser, HEVC for devices and death to H264

Most online videos use the H.264 video codec. My old WD TV media player box supports H.264 (MPEG-4) codec and I had no reason to convert the downloaded videos when I added them to my offline video library. My new TV supports H.265 codec and switching to this new codec would result in significant space savings.

Screenshot of directory

In the middle of this conversion, I began to wonder if the H.266 (a codec I knew existed) would be more future proof and compressing. Well, the published literature says that H.266 is more compressive than H.265.

There is a competing codec called AV1, which competes with H.266. It is favoured by browser makers as it is not encumbered by software patents. So, which one is better?

I re-compiled FFmpeg today to add the libvvenc encoder and my test showed no improvement by H.266 over H.265. In fact, H.266 performed worse with my test file. AV1 codec provided by FFmpeg project also did not perform well and was in the same league as H.266.

Here is my judgement:

  • H.264 — destined to rule the Web for some more time as it has the widest compatibility with TVs, phones, media player boxes and AV devices.
  • H.265 — will not take over the Web like H.264. Currently, it is supported by most new TVs, camera and phones.
  • AV1 — destined to take over the Web, TVs, phones and AV devices.
  • H.266 — will continue to languish as there is no clarity among patent holder groups. My new TV does not support this format and most new TVs in the near future will not either.

My TV plays AV1 files fine so my future offline video downloads will not need any conversion. AV1's compression ability is comparable to that of H.265.

I will however convert all my old video files from H.264 to H.265 as the space savings are significant. (I will not convert them to AV1 as I do not play them in a browser. I do not use smart phones so I do care even if these devices do not support H.265.)


Become an FFmpeg PRO by reading my book Quick Start Guide to FFmpeg.

Book photo

Link: | Magic Link:

Comments are not enabled yet.

For older posts, check the archives.