The CRT filter that I used in my "what is that editor" video

Bisqwit’s CRT filter

This is the CRT filter that I used in my ”What is That Editor” video, at https://www.youtube.com/watch?v=ZMBQmhO8KqI.

It received some accolades, but I forgot to publish it. Here it is finally.

To build

Run this command to build the filter:

g++ -o crt-filter crt-filter.cc -fopenmp -Ofast -march=native -Wall -Wextra -std=c++17

Usage

The filter takes BGRA (RGB32) video (RAW!) from stdin, and produces BGRA video (RAW!) into stdout.

The filter takes five commandline parameters:

./crt-filter <sourcewidth> <sourceheight> <outputwidth> <outputheight> <scanlines>

The sourcewidth and sourceheight denote the size of the original video. The outputwidth and outputheight denote the size that you want to produce. Generally speaking you want to produce as high quality as possible. Vertical resolution is more important than horizontal resolution.

Scanlines is the number of scanlines you wish to simulate. Generally that would be the same as the vertical resolution of the source video, but that is not a requirement.

For best quality, the number of scanlines should be chosen such that the intermediate height (see Constants) is its integer multiple. The intermediate width should ideally also be an integer multiple of the source width. None of this is required though.

IMPORTANT: This filter does not decode or produce video formats like avi/mp4/mkv/whatever. It only deals with raw video frames. You need to use an external program, like ffmpeg, to perform the conversions. See make-reencoded.sh and reencode.sh for a practical example.

Screenshots

(Click to enlarge the filtered pictures)

Original1 Filtered1

Original2 Filtered2

How it works

Constants

These constants specify the pixel grid (shadow mask) used by the simulated CRT monitor.

Currently they are hardcoded in the program, but they are easy to find if you want to tweak the source code.

width

The cell widths and heights and staggering specify the geometry of the shadow mask. See Filtering, below, for an example of what it looks like.

NB: This page uses GitHub’s own LaTeX math renderer to show equations. Unfortunately, this renderer produces transparent pictures with black text, and has very poor usability on dark mode. I am aware of this problem, but there is very little I can do about it, until GitHub itself fixes it! Sorry. Please view this site on desktop with non-dark mode.

Hashing

The filter is designed for DOS videos, and specifically for sessions involving the text mode. Because chances are that successive frames are often identical, the filter calculates a hash of every source frame.

If the hash is found to be identical to some previous frame, the filtered result of the previous frame is sent. Otherwise, the new frame is processed, and saved into a cache with the hash of the input image.

Four previous unique frames are cached. This accounts e.g. for blinking cursors.

Converting into linear colors

First, the image is un-gammacorrected.

1/gamma

Rescaling to scanline count

Then, the image is rescaled to the height of number of given scanlines using a Lanczos filter. Kernel size 2 was was selected for the Lanczos filter.

If your source height is greater than the number of scanlines you specified, you will lose detail.

Rescaling to intermediate size

Next, the image is rescaled to the intermediate width and height using a nearest-neighbor filter.

The scaling is performed first vertically and then horizontally. Before horizontal scaling, the brightness of each row of pixels is adjusted by a constant factor that is calculated by

formula

This formula produces a figure that sort of looks like a hill. It peaks in the middle and fades smoothly to the sides. This hill represents the brightness of each scanline, as a function of distance from its beginning. Plotted in a graphing calculator, it looks like this. The c constant controls how steep that hill is. A small value like 0.1 produces a very narrow hill with very sharp and narrow scanlines, and bigger values produce flatter hills and less pronounced scanlines. 0.3 looked like a good compromise.

This simulates the electron gun passing through in horizontal lines called scanlines, as it renders the picture line by line.

Gaussian Copper bars

You can download the source code of the right-hand-side illustration in img/coppers.php.

Filtering

Each color channel and each pixel of the picture — now intermediate width and height — is multiplied by a mask that is either one or zero, depending on whether that pixel belongs inside a cell of that color according to the hardcoded cell geometry.

The mask is a repeating pattern that essentially looks like this:

Mask

Red pixels denote 1 for red channel, green pixels denote 1 for green channel, blue pixels denote 1 for blue channel, and everything else for everyone is 0.

This simulates the shadow mask in front of the cathode ray tube.

The mask is generated procedurally from the cell parameters (see Constants).

Rescaling to target size

Then the image is rescaled to the target picture width and target picture height using a Lanczos filter. The scaling is performed first vertically and the horizontally.

A Lanczos filter was chosen because it is generally deemed the best compromise between blurring and fringing among several simple filters (Wikipedia). I have been using it for years for interpolating all sorts of signals from pictures to sounds.

Bloom

First, the brightness of each pixel is normalized so that the sum of masks and scanline magnitudes does not change the overall brightness of the picture.

Then, a copy is created of the picture. This copy is gamma-corrected and amplified with a significant factor, to promote bloom.

gamma

This copy is 2D-gaussian-blurred using a three-step box filter, where the blur width is set as output-width / 640. The blur algorithm is very fast and works in linear time, adapted from http://blog.ivank.net/fastest-gaussian-blur.html .

Then, the actual picture is gamma-corrected, this time without a brightening factor.

gamma

Then, the blurry copy is merged into the picture, by literally adding its pixel values into the target pixel values.

gamma

Because of the combination of amplification and blurring, if there are isolated bright pixels in the scene, their power is spread out on big area and thus do not contribute much to the final picture, but if there is a large cluster of bright pixels closeby, they remain bright even after blurring, and will influence the final picture a lot. This produces a bloom effect.

Clamping

Finally, before quantizing the floating-point colors and sending the frame to output, each pixel is clamped to the target range using a desaturation formula.

The desaturation formula

The desaturation formula first calculates a luminosity value from the input R,G,B components using ITU coefficients (see sRGB on Wikipedia):

luma calculation

  • If the luminosity is less than 0, black is returned.
  • If the luminosity is more than 1, white is returned.
  • Otherwise, a saturation value is initialized as 1, and then adjusted by inspecting each color channel value separately:

adjust

After analyzing all color channels, if the saturation still remains as 1, the input color is returned verbatim. Otherwise each color channel is readjusted as:

adjust

The readjusted color channel values are then joined together to form the returned color.

The advantage of desaturation-aware clamping over naïve clamping is that it does a much better job at preserving energy. To illustrate, here is a picture with two color ramps. The brightness of the color ramp increases linearly along the Y axis. That is, top is darkest (0) and bottom is brightest (1, i.e. full). Every pixel on each scanline should be approximately same brightness.

The brightness scaling in this illustration is done by simply multiplying the RGB color with the brightness value. At high brightness values, this produces colors that are impossible to show on the screen.

Rainbow illustration

In the leftside picture with naïve clamping (i.e. if x>255, then set x to 255), you can see that the further down you go in the picture, the more different the color brightnesses are. The blue stripe is much, much darker than anything else in the picture, even though it is fully saturated and as bright as your screen can make it.*

However, on the right side, with the desaturation aware clamping formula, every scanline remains at perfectly even brightness, even when you exceed the maximum possible brightness of the screen colors.

In the desaturation-aware algorithm, colors that are impossible to show on screen due to excess brightness are approximated with desaturated versions, that preserve the brightness perception at the cost of color saturation.

(Note: “Perfectly” was a hyperbole. The colors are not quite the same brightness, because of differences in screen calibration and because of differences in human individual eyes. This is more of an illustration.) You can download the source code of this illustration in img/rainbow.php.

Note that this does not mean that all colors become more washed out. You may come to this mistaken conclusion, because this illustration is fixed for perceptual brightness. The only colors that will be desaturated are those that are have out-of-range values (i.e. individual channel values are greater than 255 or smaller than 0); marked with crosshatch pattern in the below picture. Everything else is kept unchanged.

Rainbow with crosshatch

*) Note that #0000FF is not blue at brightness 1. While it is maximally bright fully saturated blue, its brightness is only about 10 % of the brightness of #00FF00, maximally bright fully saturated green, and only about 7 % of the brightness of #FFFFFF, a maximally bright white pixel (which does have brightness level of 1).

This is trivial to prove: #FFFFFF is a color where you light up all the LEDs that comprise color #0000FF, but you also light up all the LEDs that comprise #FF0000 and all the LEDs that comprise #00FF00. Because there are three times as many LEDs shining as when just #0000FF is shown, the brightness of #FFFFFF cannot be the same, but has to be much higher. Therefore, #0000FF cannot have brightness level of 1.

It is also worth noting that brightness is not the same as radiant energy. This has nothing to do with energy. The human eye is simply differently sensitive to different wavelengths of visible light; least of them to blue (see V(λ)). Brightness is a perception phenomenon.

Owner
Joel Yliluoma
The Bisqwit. Free software author. YouTuber. Founder of #TASVideos. ROM hacker. Coach drⅳer. Teacher of #IsraeliFolkDance. Speaker of Hebraic Roots apologetics.
Joel Yliluoma
Similar Resources

Extended kalman filter implementation.

Extended kalman filter implementation.

EKF (Extended Kalman Filter) This project is a C++ implementation of EKF.For the related principles of EKF, please check this tutorial (TODO). Project

Jun 26, 2022

Kalman Filter Implementation for MPU6050 with STM32-Nucleo. Via CubeIDE

Kalman Filter Implementation for MPU6050 with STM32-Nucleo. Via CubeIDE

Kalman Filter Implementation for MPU6050 on STM32 Nucleo Board Kalman Filter Implementation for MPU6050 with STM32-Nucleo I implemented a Kalman Filte

Jul 8, 2022

Port of Adafruit / NXP Sensor Fusion filter

AHRS Fusion Port of Adafruit NXP sensor fusion algorithms based on Kalman filters for rust. Resources https://github.com/adafruit/Adafruit_AHRS https:

May 14, 2022

A toolkit for pointcloud processing, including: filter, bounding box, ground segmentation, cluster

A toolkit for pointcloud processing, including: filter, bounding box, ground segmentation, cluster. And implemented by different algorithms(some with pcl wrapper). c++17 supported

Jun 23, 2022

Cg shader version of the HQx pixel art upscaling filter

HQx-shader Cg shader version of the HQx pixel art upscaling filter. How to use Load the preset files for the desired upscale factor in an emulator tha

Jul 26, 2022

AVX2-vectorized box filter

vs-boxblur AVX2-vectorized box filter. For integer input, it favors architectures with fast cross lane shuffle (e.g. haswell or later architectures of

Apr 7, 2022

Gradation Curves filter for VirtualDub and AviSynth+

Gradation Curves filter for VirtualDub and AviSynth+

Gradation Curves An updated version of Alexander Nagiller's Gradation Curves filter for VirtualDub. Additional documentation can be found in the filte

May 11, 2022

Fast glsl deNoise spatial filter, with circular gaussian kernel, full configurable

Fast glsl deNoise spatial filter, with circular gaussian kernel, full configurable

glslSmartDeNoise Fast glsl spatial deNoise filter, with circular gaussian kernel and smart/flexible/adaptable - full configurable: Standard Deviation

Aug 10, 2022

AviSynthPlus color correction filter.

Description A color constancy filter that applies color correction based on the grayworld assumption. For more info. This is a port of the FFmpeg filt

Aug 7, 2022
The pico can be used to program other devices. Raspberry pi made such an effort. However there is no board yet, that is open-source and can be used with OpenOCD as a general-purpose programmer
The pico can be used to program other devices. Raspberry pi made such an effort. However there is no board yet, that is open-source and can be used with OpenOCD as a general-purpose programmer

pico-probe-programmer The pico can be used to program other devices. Raspberry pi made such an effort. However there is no board yet, that is open-sou

Jul 20, 2022
GLSL optimizer based on Mesa's GLSL compiler. Used to be used in Unity for mobile shader optimization.

GLSL optimizer ⚠️ As of mid-2016, the project is unlikely to have any significant developments. At Unity we are moving to a different shader compilati

Aug 11, 2022
This is a simple filter that will block any attempt to access streams beginning with

Triggering the notification only requires that you visit a particular path on an NTFS volume.

Jul 11, 2022
Comparing the performance of Wave Digital Filter implementations

WDF Bakeoff Comparing performance between Wave Digital Filters implemented in C++ and Faust. Building First clone the repository and submodules: git c

Jun 19, 2022
Simple sensor filter chain nodes and nodelets

sensor_filters This package is a collection of nodes and nodelets that service a filters::FilterChain for message types from sensor_msgs package. Each

Jun 30, 2022
High Quality DeNoise 3D is an AviSynth port of the MPlayer filter of the same name

High Quality DeNoise 3D is an AviSynth port of the MPlayer filter of the same name. It performs a 3-way low-pass filter, which can completely remove high-frequency noise while minimizing blending artifacts.

Jun 7, 2022
Fuses IMU readings with a complementary filter to achieve accurate pitch and roll readings.
Fuses IMU readings with a complementary filter to achieve accurate pitch and roll readings.

SimpleFusion A library that fuses accelerometer and gyroscope readings quickly and easily with a complementary filter. Overview This library combines

Aug 12, 2022
Filter and launch links in a browser of your choice!

Link Launcher Filter links with regular expressions and launch them into your favourite browsers Have you ever wanted to open a youtube link from othe

Aug 20, 2021
usb to 5 din midi converter-filter-router, sound generator
usb to 5 din midi converter-filter-router, sound generator

multi What is multi? It's a PCB (shield/hat) hosting a seeeduino Xiao. It has 6 potentiometers, 2 pushbuttons and a 1/8" audio out connected to the Xi

Aug 11, 2022