Color Vectorscope in Davinci Resolve Studio 17

Creating a Vectorscope in Angular 12

Jim Armstrong
ngconf

--

How to work with HTML Canvas natively in Angular applications

Are you just beginning to work with Angular? Are you curious about how to work with Canvas in an Angular application without third-party libraries? If so, then this article was written just for you!

Although there are powerful third-party libraries for working with Canvas such as Pixi JS and Fabric JS, all these libraries have weaknesses. In some cases, a third-party library does not provide the same level of performance that can be achieved through native Canvas methods. This article discusses a type of application that is ideal for native Canvas manipulation. We are going to build a primitive vectorscope in Angular.

What is a Vectorscope?

A vectorscope is one of many ‘scopes’ used in the process of color grading video. If you have ever seen a movie or television commercial and noticed the soft look, vibrant colors, or how the sky stands out from the foreground, then you have witnessed the power of color grading. Practically nothing that we see in film and broadcast video appears as it did ‘straight out of the camera.’

The colorist requires precise, accurate information about the distribution of color throughout an entire video frame. This is facilitated through the use of multiple displays, one of which is a vectorscope. The bottom half of a ‘four-up’ scope display in Davinci Resolve Studio 17 is shown below. The vectorscope appears on the left.

Vectorscope and RGB histogram in Resolve Studio 17

Color Models

A brief discussion of color models is helpful before we dive into how a vectorscope is used during grading. If you are already familiar with RGB, HSV, and HSL, then move onto the next section!

RGB (Red, Green, Blue) is one way to represent color as a combination of ‘basis vectors’ or primary colors. The RGB color space is often modeled as a cube.

RGB color space courtesy of ResearchGate

HSV (Hue, Saturation, Value) is another popular model that is based on a cylindrical coordinate space. It compares to RGB as shown below.

RGB vs HSV courtesy of Neurosapiens

HSL (Hue, Saturation, Lightness) is another cylindrical coordinate system.

HSL model courtesy of ResearchGate

Note the familiar ‘color picker’ style wheel at the left which results from a horizontal ‘slice’ through the middle of the cylinder.

If you want more detail on color models, the following reference is recommended. Note that although they appear similar, mathematical formulas for converting RGB to HSV and HSL result in different values for saturation.

What does a vectorscope display?

A vectorscope plots points that are derived from each pixel in a single image (video frame). The plot is in polar coordinates. Although specifics depend on the particular scope and particular video editing/grading software, typical practice is

  • Polar coordinate angle is derived from the hue (from 0 to 360)
  • Distance from the center of the scope is based on saturation (scaled from 0 to 1)
  • Color or alpha value of a ‘dot’ placed on the scope is derived from the remaining ‘value’ in the underlying color model

The following image shows the relationship between a single video frame and a vectorscope in Resolve Studio (bottom-right).

Vectorscope in Davinci Resolve Studio 17

The vectorscope plots a two-dimensional spread of color information obtained from each pixel in the video frame. In order to interpret this information, we need to know the primary colors and color-wheel representation used in the software. Davinci Resolve uses red, yellow, and blue as primary colors. Here is one representation of a wheel (gamut of colors) generated from those primaries.

Color Wheel courtesy of Draw Paint Academy

Resolve Studio places blue on the horizontal axis at zero degrees as shown in the following screen shot of its primary color adjustment wheels.

Davinci Resolve Studio 17 Primary Wheels

We can interpret the vectorscope display above as indicating that color distribution is not ‘centered.’ It is weighted more toward yellow. So, as a correction step, the colorist would likely eliminate some of the yellow from the entire image. The advantage of scopes and other graphical representations of color distribution is that they display information in a precise, accurate manner that is independent of ‘what our eyes see on a monitor.’

How do we create a vectorscope in Angular?

Displaying a vectorscope is literally a painting process. A ‘dot’ or 1x1 rectangle is displayed at some coordinate (inside the bounds of a circle) for each pixel in an image. An HTML Canvas is the most efficient means for such a display. A Canvas is also convenient to both display an image and extract RGB color information for each pixel. It is customary to display the image (video frame) along with the scope, so this demo employs two Canvases. One Canvas is used for the image previewer and another for the vectorscope.

The vectorscope displayed in this project is derived from the HSV color model. Hue is computed in the interval [0, 360] where zero degrees corresponds to blue, 120 degrees corresponds to green, and 240 degrees corresponds to red.

Saturation is normally computed in the range [0, 1]. This value is used to determine how far along the hue angle to plot a single point. The HSV value is also in the range [0,1]. The Angular vectorscope is greyscale, so the value is used to set the alpha for a pure white rectangle.

The remainder of this article deconstructs the code, which you can obtain from this GitHub.

Working with Canvas in Angular

This article illustrates two approaches for working with HTML Canvas in an Angular application. Both are illustrated in app.component.html.

<div class="card-container">
<!-- Simple Img Previewer -->
<div class="preview-container" img-preview (imgError)="onImageError($event)" (imgLoaded)="onImageLoaded($event)"></div>

<!-- VectorScope -->
<div class="canvas-container">
<canvas
#vectorscope
width="400"
height="400"
id="vectorscopeSurface">
</canvas>

</div>
</div>
<div class="mt10">
<button mat-raised-button color="primary" (click)="onLoadImage()">Load Image</button>
</div>

The application’s initial display is shown below.

Vectorscope and image previewer initial display

An Angular attribute directive is used to instantiate a Canvas inside an existing container such as a DIV. Angular Inputs and Output handlers can be added to the parent container and processed as you may have seen before with Angular Components.

<div class="preview-container" img-preview (imgError)="onImageError($event)" (imgLoaded)="onImageLoaded($event)"></div>

This approach is convenient if the image previewer is likely to be re-used and it is desirable to obtain Canvas properties such as width and height from the parent container.

If it is more desirable to define a Canvas and its attributes directly in markup, then a reference may be obtained to that Canvas as a ViewChild. An Angular Service may be injected to use that Canvas as a drawing surface for a specific display such as the vectorscope.

<canvas
#vectorscope
width="400"
height="400"
id="vectorscopeSurface">
</canvas>

Canvas management in this application is handled by the main app component.

import { VectorscopeService } from "./shared/services/vectorscope.service";

import { ImgPreviewDirective } from "./shared/directives/img-preview.directive";
.
.
.
export class AppComponent implements OnInit
{
@ViewChild('vectorscope', {static: true})
private _canvas!: ElementRef<HTMLCanvasElement>;

@ViewChild(ImgPreviewDirective, {static: true})
private _imagePreview!: ImgPreviewDirective;

constructor(private _vectorscope: VectorscopeService){}
.
.
.

ViewChild is used to obtain direct references both to the image previewer (ImagePreviewDirective) and the vectorscope Canvas element. These static assets are available in the ngOnInit() lifecyle method.

Note that non-null assertion (!) is used in the variable declaration as our way of stating to the compiler that by the time we use these variables, they will absolutely not be null or undefined.

public ngOnInit(): void
{
if (this._canvas !== undefined && this._canvas != null) {
this._vectorscope.canvas = this._canvas.nativeElement;
}
}

The vectorscope service is injected into the main app component. When the canvas element is available, the reference to the actual HTML Canvas is assigned to that service. The service’s create() method can be used to render the vectorscope once the complete HSV model is derived from the loaded image.

Image Previewer Directive

The HTML Canvas for the previewer is created when the directive is constructed,

constructor(protected _elRef: ElementRef)
{
this._rgbArr = new Array<RGB>();
this._url = '';

this._onImgError = new EventEmitter<string>();
this._onLoaded = new EventEmitter<string>();

this._container = this._elRef.nativeElement as HTMLDivElement;
this._canvas = document.createElement("CANVAS") as HTMLCanvasElement;


this._canvas.width = this._container.clientWidth;
this._canvas.height = this._container.clientHeight;

this._container.appendChild<HTMLCanvasElement>(this._canvas);
}

The image previewer is designed to work with only one image for demonstration purposes. I suggest that you create a version of this directive that handles previews of multiple images as an exercise.

Click on the ‘Load Image’ button in the application. The ‘click’ handler for that button assigns a url of an image to load,

this._imagePreview.setPreviewImage('/assets/images/drone-photo.jpg');

which is hardcoded for this demo.

In short, the preview image process executes the following steps.

  • Create an HTML image
  • ‘Draw’ the image into the Canvas
  • Get the RGB image data from the Canvas
  • Create an array of RGB color models from that data
  • Emit an output that the image is loaded and processed

The code is provided below

The image is centered horizontally or vertically inside the Canvas depending on which dimension is binding (i.e. largest relative to corresponding dimension in the Canvas). If this is confusing, then don’t worry as the scaling math is already done for you :)

Drawing the Vectorscope

Finally, we are ready for the star of our show!

Recall from the app.component.html file that an ‘imgLoaded’ Output handler is defined in the app.component.ts file,

<div class="card-container">
<!-- Simple Img Previewer -->
<div class="preview-container" img-preview (imgError)="onImageError($event)" (imgLoaded)="onImageLoaded($event)"></div>
.
.
.
</div>

That handler executes the following steps.

  • Convert RGB to HSV for each pixel in the image
  • Orient the hues to the Resolve Studio color wheel
  • Use the HSV model for each pixel to create the vectorscope display
public onImageLoaded(url: string): void
{
const rgbValues: Array<RGB> = this._imagePreview.rgbValues;
const hsv: Array<HSV> = rgbToHSVArray(rgbValues);

toResolveColorWheelArr(hsv);

this._vectorscope.create(hsv);
}

RGB to HSV conversion is covered in great detail online. Here is a useful resource.

https://www.rapidtables.com/convert/color/rgb-to-hsv.html

It is important, however, to understand which hue is mapped to zero degrees. In most cases, it is red. If we use the y-down Canvas coordinate system with positive angles measured clockwise from the horizontal axis, then red is mapped at 240 degrees in Resolve Studio. That is the reason for this line of code,

toResolveColorWheelArr(hsv);

This step is inserted for consistency since Resolve Studio 17 is used for all the visual examples in this article.

The vectorscope code is in the /shared/services/vectorscope.service.ts file. A circle with horizontal and vertical axes is drawn based on color information provided in a configuration file. The vectorscope pixels are painted based on the HSV model data for each pixel in the preview image. This process uses the HTML Canvas fillRect() method, which is the fastest means (to the author’s knowledge) to perform the painting.

A bit of math is required to convert polar coordinates to x-y coordinates and all the work happens in this loop inside the create() method.

for (i = 0; i < n; ++i)
{
value = values[i];
angle = value.h*DEG_TO_RAD;
x = midX + (value.s*r) * Math.cos(angle);
y = midY + (value.s*r) * Math.sin(angle);

this._context.fillStyle = `rgba(255, 255, 255, ${value.v})`;
this._context.fillRect(x, y, 1, 1);
}

The class variable, _context, is the HTML Canvas 2D rendering context, which is set whenever the canvas element reference is assigned. This happens in the function,

public set canvas(surface: HTMLCanvasElement)
{
if (surface !== undefined && surface != null)
{
this._context = surface.getContext('2d') as CanvasRenderingContext2D;
this._width = surface.width;
this._height = surface.height;
}
}

which was used in the main app component’s ngOnInit() handler as shown previously,

public ngOnInit(): void
{
if (this._canvas !== undefined && this._canvas != null) {
this._vectorscope.canvas = this._canvas.nativeElement;
}
}

The final application display is shown below.

Vectorscope display from a simple HSV model

This is, of course, a very crude scope but it immediately shows there is more reddish-blue in the image than might have been noticed at first glance. This color comes from the bright space behind the drone. The colorist then uses this information along with waveform display to modify the color balance and ‘look’ of the image.

If you wish to follow up on waveforms, scopes, and related information, here is a good resource.

Conclusion

Painting a vectorscope is a computationally intense process, so top performance is critical. Working with native Canvas methods is easier than many readers might think, so I hope this article inspires some readers to explore Canvas more in-depth.

I believe you will find Angular attribute directives and services to be useful in almost every application. This article presented just one way in which they can be used.

Good luck with your Angular efforts!

Now that you’ve read this article and learned a thing or two (or ten!), let’s kick things up another notch!
Take your skills to a whole new level by joining us in person for the world’s first MAJOR Angular conference in over 2 years! Not only will You be hearing from some of the industry’s foremost experts in Angular (including the Angular team themselves!), but you’ll also get access to:

  • Expert panels and Q&A sessions with the speakers
  • A friendly Hallway Track where you can network with 1,500 of your fellow Angular developers, sponsors, and speakers alike.
  • Hands-on workshops
  • Games, prizes, live entertainment, and be able to engage with them and a party you’ll never forget

We’ll see you there this August 29th-Sept 2nd, 2022. Online-only tickets are available as well.
https://2022.ng-conf.org/

--

--

Jim Armstrong
ngconf

Jim Armstrong is an applied mathematician who began his career writing assembly-language math libraries for supercomputers. He now works on FE apps in Angular.