-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: add notes on how the multiview label numbers were calculated
- Loading branch information
Showing
1 changed file
with
213 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,213 @@ | ||
## Multiviewer Labels | ||
|
||
It has been found that the multiviewer labels are stored as 8bit per pixel, with an unknown encoding. | ||
No pattern has been identified, instead we have gone with a lookup table generated by measuring the results. | ||
|
||
The process followed was to upload an image of each colour to the atem, then to use a decklink to grab a frame of the multiviewer. | ||
A pixel was then sampled from each frame, with the resulting colours output. | ||
This was repeated on a couple of different background colours, then equations were 'solved' to calculate the effective colour and alpha values. | ||
From this we can then build our lookup table. | ||
|
||
#### Upload and measure colours | ||
|
||
- Make sure to setup the multiviewer routing correctly or change the source Id. | ||
- A small app will need to grab grames in 10bit yuv from a decklink and serve over http (see below) | ||
|
||
Main script: | ||
|
||
```js | ||
const { Atem } = require('../../dist') | ||
const got = require('got') | ||
|
||
const atem = new Atem({ | ||
// disableMultithreaded: true, | ||
}) | ||
// atem.on('debug', (a) => console.log(a)) | ||
atem.on('connected', () => { | ||
console.log('connected') | ||
|
||
sendLabel() | ||
}) | ||
atem.on('disconnected', () => { | ||
console.log('disconnected') | ||
process.exit(1) | ||
}) | ||
|
||
atem.connect('10.42.13.99') | ||
console.log('connecting') | ||
|
||
function wait(timeout) { | ||
return new Promise((resolve) => setTimeout(resolve, timeout)) | ||
} | ||
|
||
const vals = {} | ||
let nextVal = 0 | ||
|
||
function sendLabel() { | ||
const buffer = Buffer.alloc(320 * 90, ++nextVal) | ||
|
||
if (nextVal >= 233) { | ||
console.log('end!') | ||
console.log(JSON.stringify(vals)) | ||
process.exit(0) | ||
return | ||
} | ||
|
||
const mask = (1 << 10) - 1 | ||
|
||
const manager = atem.dataTransferManager | ||
manager.uploadMultiViewerLabel(10011, buffer).then(async () => { | ||
console.log('label written') | ||
|
||
await wait(2000) | ||
|
||
const body = await got('http://10.42.13.100:3000/frame.data').buffer() | ||
// const body = await fr.buffer() | ||
// const body = Buffer.from(?.body, 'ascii') | ||
console.log('got bytes:', body.length, typeof body) | ||
|
||
const sampleX = 1116 | ||
const sampleY = 1020 | ||
// const sampleY = 250 | ||
|
||
if (sampleX % 6 !== 0) throw new Error('Maths for non first pixel not implemented..') | ||
|
||
const bytesPerRow = (1920 / 48) * 128 | ||
|
||
const firstByte = sampleY * bytesPerRow + Math.floor((16 / 6) * sampleX) | ||
|
||
console.log('offset', firstByte) | ||
|
||
const word1 = body.readUInt32LE(firstByte) | ||
const word2 = body.readUInt32LE(firstByte + 4) | ||
|
||
console.log('raw words', word1, word2) | ||
|
||
const y0 = (word1 >> 10) & mask | ||
const cb0 = (word1 >> 0) & mask | ||
const cr0 = (word1 >> 20) & mask | ||
|
||
console.log('pixel1', y0, cb0, cr0) | ||
|
||
// BT.709 | ||
const KR = 0.2126 | ||
const KB = 0.0722 | ||
const KG = 1 - KR - KB | ||
|
||
const KRi = 1 - KR | ||
const KBi = 1 - KB | ||
|
||
const KBG = KB / KG | ||
const KRG = KR / KG | ||
|
||
const YRange = 219 | ||
const CbCrRange = 224 | ||
const HalfCbCrRange = CbCrRange / 2 | ||
|
||
const YOffset = 16 << 8 | ||
const CbCrOffset = 128 << 8 | ||
|
||
const KBiRange = KBi / HalfCbCrRange | ||
const KRiRange = KRi / HalfCbCrRange | ||
|
||
const cb1 = KBiRange * ((cb0 << 6) - CbCrOffset) | ||
const cr1 = KRiRange * ((cr0 << 6) - CbCrOffset) | ||
|
||
function clamp(v) { | ||
if (v <= 0) return 0 | ||
if (v >= 255) return 255 | ||
return v | ||
} | ||
|
||
const y1 = ((y0 << 6) - YOffset) / YRange | ||
const r1 = clamp(Math.round(y1 + cr1)) | ||
const g1 = clamp(Math.round(y1 - cb1 * KBG - cr1 * KRG)) | ||
const b1 = clamp(Math.round(y1 + cb1)) | ||
|
||
console.log(nextVal, 'r', r1, 'g', g1, 'b', b1) | ||
vals[nextVal] = { r: r1, g: g1, b: b1 } | ||
|
||
setImmediate(() => sendLabel()) | ||
}) | ||
console.log('label queued') | ||
} | ||
``` | ||
|
||
Decklink http script: | ||
|
||
``` | ||
TODO | ||
``` | ||
|
||
#### Convert to csv | ||
|
||
```js | ||
const black = require('./mutliview-on-black.json') | ||
const white = require('./multiview-on-white.json') | ||
const mix100 = require('./multiview-on-100.json') | ||
const mix200 = require('./multiview-on-200.json') | ||
const mix34 = require('./multiview-on-34.json') | ||
const mix176 = require('./multiview-on-176.json') | ||
|
||
for (let i = 1; i < 233; i++) { | ||
const bl = black[i] | ||
const wh = white[i] | ||
const v100 = mix100[i] | ||
const v200 = mix200[i] | ||
const v34 = mix34[i] | ||
const v176 = mix176[i] | ||
|
||
const blV = Math.round((bl.r + bl.g + bl.b) / 3) | ||
const whV = Math.round((wh.r + wh.g + wh.b) / 3) | ||
const v100V = Math.round((v100.r + v100.g + v100.b) / 3) | ||
const v200V = Math.round((v200.r + v200.g + v200.b) / 3) | ||
const v34V = Math.round((v34.r + v34.g + v34.b) / 3) | ||
const v176V = Math.round((v176.r + v176.g + v176.b) / 3) | ||
|
||
console.log(`${i}, ${blV}, ${whV}, ${v100V}, ${v200V}, ${v34V}, ${v176V}`) | ||
} | ||
``` | ||
|
||
#### Solve the equation | ||
|
||
This was done in [the sheet](https://docs.google.com/spreadsheets/d/172R9Utb0au92jILS4q2JNVbueT6ROUnkV8NjKWswm4I/edit?usp=sharing). | ||
It could be rewritten in js if repeated. | ||
|
||
The forumla of interest are: | ||
`a = (((v34 - v176) / (176 - 34) + 1))` | ||
`v = round((v100-(99 * (1 - a))) / a)` | ||
|
||
The rest of the sheet is excessive data, and verifying the formula against the input numbers | ||
|
||
#### Generate lookup table | ||
|
||
This generates a set of tables, for different alpha values. There are not many outside of an alpha of ~0.7, so we ignore those for now as we have no use for them. Some values are duplicated, so we take just one of them. | ||
|
||
```js | ||
const raw = require('./result.json') | ||
|
||
const cols = {} | ||
const alphas = new Set() | ||
|
||
for (const [k, v] of Object.entries(raw)) { | ||
let a = v.a | ||
if (a == 0.04 || a == 0.06) a = 0.05 | ||
if (a >= 0.67 && a <= 0.72) a = 0.7 | ||
if (a >= 0.27 && a <= 0.33) a = 0.3 | ||
if (a >= 0.17 && a <= 0.23) a = 0.2 | ||
if (a >= 0.53 && a <= 0.57) a = 0.55 | ||
if (a >= 0.42 && a <= 0.43) a = 0.42 | ||
if (a >= 0.47 && a <= 0.49) a = 0.48 | ||
|
||
const e = cols[a] || {} | ||
cols[a] = e | ||
|
||
if (e[v.v] !== undefined) console.log(`duplicate for a:${a}, v:${v.v}`) | ||
e[v.v] = Number(k) | ||
|
||
alphas.add(a) | ||
} | ||
|
||
console.log(Array.from(alphas.values()).sort()) | ||
console.log(JSON.stringify(cols)) | ||
``` |