Skip to content

Commit

Permalink
chore: add notes on how the multiview label numbers were calculated
Browse files Browse the repository at this point in the history
  • Loading branch information
Julusian committed Mar 21, 2022
1 parent a597571 commit d83948d
Showing 1 changed file with 213 additions and 0 deletions.
213 changes: 213 additions & 0 deletions notes/multiviewLabels.md
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))
```

0 comments on commit d83948d

Please sign in to comment.