Skip to content

Commit 6bf0278

Browse files
authored
Merge pull request #152 from Gohnnyman/feat/usbvideo-frames
feature: the hardware-free half of USB video capture
2 parents 66f7538 + fa8a0ea commit 6bf0278

8 files changed

Lines changed: 503 additions & 46 deletions

File tree

‎src/core/util/AreaSample.h‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#pragma once
2+
3+
/// Sampling a picture's rectangle onto one output cell: the proportional span and its mean in linear light.
4+
5+
#include "core/util/color.h" // RGB
6+
#include "core/util/math16.h" // isqrt: the root the linear-light mean undoes its squares with
7+
8+
#include <cstddef>
9+
#include <cstdint>
10+
11+
namespace mm {
12+
13+
/// The source pixels one output cell covers, `hi` one past the last.
14+
struct Span {
15+
uint16_t lo = 0; ///< the first source pixel the cell covers
16+
uint16_t hi = 0; ///< one past the last, so hi - lo is the count
17+
};
18+
19+
/// Taps an axis of one cell, so a cell's sum stays well inside 32 bits whatever the source size.
20+
constexpr uint32_t kAreaTapsPerAxis = 4;
21+
22+
/// The source span output cell `i` of `out` covers, the proportional split every sampler shares.
23+
inline Span cellSpan(uint32_t i, uint32_t out, uint32_t src) {
24+
uint32_t lo = i * src / out, hi = (i + 1u) * src / out;
25+
if (hi <= lo) hi = lo + 1; // a source smaller than the output: neighboring cells share a pixel
26+
return Span{static_cast<uint16_t>(lo), static_cast<uint16_t>(hi)};
27+
}
28+
29+
/// The rectangle's mean in linear light (square, average, root: gamma 2 for the display curve), from at most four reads per axis.
30+
inline RGB areaMean(const uint8_t* rgb, size_t stride, Span cx, Span ry) {
31+
const uint32_t sx = (cx.hi - cx.lo + kAreaTapsPerAxis - 1u) / kAreaTapsPerAxis;
32+
const uint32_t sy = (ry.hi - ry.lo + kAreaTapsPerAxis - 1u) / kAreaTapsPerAxis;
33+
uint32_t sum[3] = {0, 0, 0}, taps = 0;
34+
for (uint32_t y = ry.lo; y < ry.hi; y += sy) {
35+
const uint8_t* row = rgb + static_cast<size_t>(y) * stride;
36+
for (uint32_t x = cx.lo; x < cx.hi; x += sx) {
37+
const uint8_t* p = row + static_cast<size_t>(x) * 3u;
38+
for (int c = 0; c < 3; c++) sum[c] += static_cast<uint32_t>(p[c]) * p[c];
39+
taps++;
40+
}
41+
}
42+
if (!taps) return RGB{0, 0, 0};
43+
return RGB{static_cast<uint8_t>(isqrt(sum[0] / taps)), static_cast<uint8_t>(isqrt(sum[1] / taps)),
44+
static_cast<uint8_t>(isqrt(sum[2] / taps))};
45+
}
46+
47+
} // namespace mm

‎src/light/effects/PictureEffect.h‎

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#include "light/effects/EffectBase.h"
44
#include "core/services/PictureService.h" // the picture it samples, as audio effects include AudioService
5+
#include "core/util/AreaSample.h" // cellSpan, areaMean: the shrink every sampler shares
56

67
namespace mm {
78

@@ -105,7 +106,7 @@ class PictureEffect : public EffectBase {
105106

106107
private:
107108
/// One axis of a cell's source rectangle, [lo, hi) in picture pixels; empty outside a fitted picture.
108-
struct Range { uint16_t lo, hi; };
109+
using Range = Span;
109110

110111
/// An edge cell's range at `deep` picture lines in: the first reaches inward from 0, the last back from the end.
111112
static void reachEdge(int32_t k, int32_t used, uint32_t pic, uint32_t deep, uint32_t& lo, uint32_t& hi) {
@@ -126,9 +127,10 @@ class PictureEffect : public EffectBase {
126127
for (uint32_t i = 0; i < n; i++) {
127128
const int32_t k = static_cast<int32_t>(i) - offset;
128129
if (k < 0 || k >= used) { out[i] = {0, 0}; continue; }
129-
uint32_t lo = static_cast<uint32_t>(k) * pic / static_cast<uint32_t>(used), hi = (static_cast<uint32_t>(k) + 1) * pic / static_cast<uint32_t>(used);
130+
const Span base = cellSpan(static_cast<uint32_t>(k), static_cast<uint32_t>(used), pic);
131+
uint32_t lo = base.lo, hi = base.hi;
130132
if (deep) reachEdge(k, used, pic, deep, lo, hi);
131-
if (hi <= lo) hi = lo + 1; // a picture smaller than the grid: neighboring cells share a pixel
133+
if (hi <= lo) hi = lo + 1; // an edge reaching inward can land on its own start
132134
out[i] = {static_cast<uint16_t>(lo), static_cast<uint16_t>(hi)};
133135
}
134136
}
@@ -201,27 +203,7 @@ class PictureEffect : public EffectBase {
201203

202204
/// The rectangle's mean in linear light (square, average, root: gamma 2 for the display curve), from at most four reads per axis.
203205
static RGB average(const PictureFrame& f, Range c, Range r) {
204-
const uint32_t sx = (c.hi - c.lo + 3u) / 4u, sy = (r.hi - r.lo + 3u) / 4u;
205-
uint32_t sum[3] = {0, 0, 0}, n = 0;
206-
for (uint32_t y = r.lo; y < r.hi; y += sy)
207-
for (uint32_t x = c.lo; x < c.hi; x += sx) {
208-
const RGB p = at(f, x, y);
209-
sum[0] += static_cast<uint32_t>(p.r) * p.r;
210-
sum[1] += static_cast<uint32_t>(p.g) * p.g;
211-
sum[2] += static_cast<uint32_t>(p.b) * p.b;
212-
n++;
213-
}
214-
return RGB{isqrt(sum[0] / n), isqrt(sum[1] / n), isqrt(sum[2] / n)};
215-
}
216-
217-
/// The integer square root of a value below 65,536, the bitwise method.
218-
static uint8_t isqrt(uint32_t v) {
219-
uint32_t root = 0;
220-
for (uint32_t bit = 1u << 14; bit; bit >>= 2) {
221-
if (v >= root + bit) { v -= root + bit; root = (root >> 1) + bit; }
222-
else root >>= 1;
223-
}
224-
return static_cast<uint8_t>(root);
206+
return areaMean(f.rgb, static_cast<size_t>(f.width) * 3u, c, r);
225207
}
226208

227209
ScratchBuffer<uint16_t> state_{*this}; ///< 8.8 per channel per cell, only while smoothing

‎src/platform/desktop/platform_desktop.cpp‎

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@
133133
/// A host has no second card, so a subfolder of the one real root stands in. `sdMounted_` gates it as a missing card would, and format is a wipe-and-recreate.
134134

135135
#include "platform/platform.h"
136+
#include "platform/usbvideo_frames.h" // areaAverage: the shrink the device backend also uses
136137
#include "core/util/FirmwareImage.h" // identify/moonBaseRejection: shared image vetting
137138
#include "core/util/H264Bitstream.h" // where the encoder's frames begin and end
138139

@@ -2879,14 +2880,11 @@ struct TestGrabber {
28792880
std::atomic<bool> decodeFails{false};
28802881
std::atomic<bool> open{false};
28812882
std::atomic<uint32_t> decoded{0}, failed{0};
2883+
std::vector<uint8_t> frame; ///< the synthesized source, rebuilt only when its size or color moves
2884+
uint8_t frameRgb[3] = {};
28822885
} g_grabber;
28832886
// How far `have` is from `want`, so the nearest advertised mode can be picked without <cstdlib>.
28842887
long apart(long have, long want) { return have > want ? have - want : want - have; }
2885-
// Halve `w` x `h` until it fits `maxW` x `maxH`, which is the scaling the decode hardware offers; false when even one pixel does not fit.
2886-
bool halveToFit(uint16_t& w, uint16_t& h, uint16_t maxW, uint16_t maxH) {
2887-
while ((w > maxW || h > maxH) && w > 1 && h > 1) { w = uint16_t(w / 2); h = uint16_t(h / 2); }
2888-
return w <= maxW && h <= maxH;
2889-
}
28902888
} // namespace
28912889

28922890
uint32_t videoCaptureFormatGeneration() MM_NONBLOCKING { return g_grabber.generation.load(); }
@@ -2922,16 +2920,19 @@ bool videoCaptureDecodeInto(VideoCaptureHandle& h, uint8_t* dst, uint16_t maxWid
29222920
uint16_t& outWidth, uint16_t& outHeight) {
29232921
if (!h.impl || !g_grabber.open) return false;
29242922
if (g_grabber.decodeFails) { g_grabber.failed++; return false; }
2925-
uint16_t w = g_grabber.openW, hh = g_grabber.openH;
2926-
if (!halveToFit(w, hh, maxWidth, maxHeight)) { g_grabber.failed++; return false; }
2927-
const uint8_t r = g_grabber.r, g = g_grabber.g, b = g_grabber.b;
2928-
for (size_t i = 0, px = size_t(w) * hh; i < px; i++) {
2929-
dst[i * 3 + 0] = r;
2930-
dst[i * 3 + 1] = g;
2931-
dst[i * 3 + 2] = b;
2932-
}
2933-
outWidth = w;
2934-
outHeight = hh;
2923+
if (!maxWidth || !maxHeight) { g_grabber.failed++; return false; }
2924+
// A frame at the mode the device opened, averaged onto the whole picture. It runs the same code the device backend does, so the host proves that path rather than a shortcut past it.
2925+
const uint16_t w = g_grabber.openW, hh = g_grabber.openH;
2926+
const uint8_t rgb[3] = {g_grabber.r, g_grabber.g, g_grabber.b};
2927+
const size_t need = static_cast<size_t>(w) * hh * 3;
2928+
if (g_grabber.frame.size() != need || std::memcmp(g_grabber.frameRgb, rgb, 3) != 0) {
2929+
g_grabber.frame.resize(need);
2930+
for (size_t i = 0; i < need; i += 3) std::memcpy(g_grabber.frame.data() + i, rgb, 3);
2931+
std::memcpy(g_grabber.frameRgb, rgb, 3);
2932+
}
2933+
usbvideo::areaAverage(g_grabber.frame.data(), static_cast<size_t>(w) * 3, w, hh, maxWidth, maxHeight, dst);
2934+
outWidth = maxWidth;
2935+
outHeight = maxHeight;
29352936
g_grabber.decoded++;
29362937
return true;
29372938
}

‎src/platform/usbvideo_frames.h‎

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
#pragma once
2+
3+
/// The hardware-free half of USB video capture: the UVC payload headers, the mode choice and the shrink onto a picture.
4+
5+
#include "core/util/AreaSample.h" // cellSpan, areaMean: the shrink every sampler shares
6+
7+
#include <cstddef>
8+
#include <cstdint>
9+
#include <cstring>
10+
11+
namespace mm::usbvideo {
12+
13+
/// One mode a grabber advertises, as its frame list reports it.
14+
struct Mode {
15+
uint16_t width = 0; ///< frame width in pixels
16+
uint16_t height = 0; ///< frame height in pixels
17+
uint8_t fps = 0; ///< frames a second at that size
18+
};
19+
20+
// --- UVC bulk payload headers (UVC 1.5 2.4.3.3) ---------------------------------------------------
21+
22+
constexpr uint8_t kPts = 0x04, kScr = 0x08, kEoh = 0x80, kErr = 0x40, kReserved = 0x10;
23+
constexpr size_t kBulkMps = 512; ///< high-speed bulk packet size, the stride headers sit on
24+
constexpr size_t kConfirmStrides = 3; ///< a short header carries few bits, so one match proves nothing
25+
26+
/// A header's own length from its flags: 2 bytes, plus 4 for a PTS and 6 for an SCR.
27+
inline size_t headerLenFor(uint8_t flags) {
28+
return 2u + ((flags & kPts) ? 4u : 0u) + ((flags & kScr) ? 6u : 0u);
29+
}
30+
31+
/// Whether the flags are a header's: end of header set, no error, no reserved bit.
32+
inline bool headerFlagsOk(uint8_t flags) { return (flags & kEoh) && !(flags & (kErr | kReserved)); }
33+
34+
/// Whether the SCR's reserved top 5 bits are zero, the strongest single check a header carries.
35+
inline bool headerScrOk(const uint8_t* h, uint8_t flags, uint8_t len) {
36+
return !(flags & kScr) || !(h[len - 1] & 0xF8);
37+
}
38+
39+
/// Whether the PTS matches the frame's first header, which is constant across one frame's payloads.
40+
inline bool headerPtsOk(const uint8_t* h, uint8_t flags, const uint8_t* pts) {
41+
return !pts || !(flags & kPts) || std::memcmp(h + 2, pts, 4) == 0;
42+
}
43+
44+
/// Whether `h` is a payload header of exactly `want` bytes; `pts` pins it to the frame's first one.
45+
inline bool isPayloadHeader(const uint8_t* h, size_t avail, size_t want, const uint8_t* pts) {
46+
if (avail < 2) return false;
47+
const uint8_t len = h[0], flags = h[1];
48+
if (len != want || len != headerLenFor(flags) || avail < len) return false;
49+
return headerFlagsOk(flags) && headerScrOk(h, flags, len) && headerPtsOk(h, flags, pts);
50+
}
51+
52+
/// Whether headers of length `hl` recur after `first`, which is what rules out a chance match.
53+
inline bool confirmedRun(const uint8_t* d, size_t len, size_t first, size_t hl) {
54+
const uint8_t* pts = (d[first + 1] & kPts) ? d + first + 2 : nullptr;
55+
const size_t stride = first + hl;
56+
size_t seen = 0;
57+
for (size_t at = first + stride; at + hl <= len && seen < kConfirmStrides; at += stride, seen++)
58+
if (!isPayloadHeader(d + at, len - at, hl, pts)) return false;
59+
return seen > 0 || first + stride + hl > len; // a frame too short to confirm is taken as it is
60+
}
61+
62+
/// The offset of the first confirmed header of length `hl`, or 0 when the frame carries none.
63+
inline size_t findHeader(const uint8_t* d, size_t len, size_t hl) {
64+
for (size_t i = kBulkMps - hl; i + hl <= len; i += kBulkMps)
65+
if (isPayloadHeader(d + i, len - i, hl, nullptr) && confirmedRun(d, len, i, hl)) return i;
66+
return 0;
67+
}
68+
69+
/// Close the gaps the headers from `hdr` on leave, answering the compacted length.
70+
inline size_t compactFrom(uint8_t* d, size_t len, size_t hdr, size_t hl) {
71+
const size_t payload = hdr + hl;
72+
uint8_t pts[4] = {}; // copied: the compaction overwrites the header it came from
73+
const bool hasPts = (d[hdr + 1] & kPts) != 0;
74+
if (hasPts) std::memcpy(pts, d + hdr + 2, sizeof pts);
75+
size_t w = hdr, r = hdr;
76+
while (isPayloadHeader(d + r, len - r, hl, hasPts ? pts : nullptr)) {
77+
r += hl;
78+
const size_t run = payload - hl < len - r ? payload - hl : len - r;
79+
std::memmove(d + w, d + r, run);
80+
w += run;
81+
r += run;
82+
}
83+
std::memmove(d + w, d + r, len - r); // whatever follows a missing header is left as delivered
84+
return w + (len - r);
85+
}
86+
87+
/// Drops the payload headers a bulk frame still carries, answering the compacted length.
88+
inline size_t stripPayloadHeaders(uint8_t* d, size_t len) {
89+
static constexpr size_t kLens[] = {12, 8, 6, 2}; // longest first: the more fields, the surer the match
90+
for (size_t hl : kLens) {
91+
const size_t hdr = findHeader(d, len, hl);
92+
if (hdr) return compactFrom(d, len, hdr, hl);
93+
}
94+
return len;
95+
}
96+
97+
// --- Choosing a mode ----------------------------------------------------------------------------
98+
99+
/// UVC states a rate as a period in 100 ns ticks. Rounded, so 59.94 fps reads as 60 rather than 59.
100+
inline uint8_t fpsFrom(uint32_t interval) {
101+
constexpr uint32_t kTicksPerSecond = 10000000;
102+
return interval ? static_cast<uint8_t>((kTicksPerSecond + interval / 2) / interval) : 0;
103+
}
104+
105+
/// Of two advertised rates, the one to ask for: the fastest at or below `wanted`, else the slowest.
106+
inline uint8_t betterFps(uint8_t have, uint8_t row, uint8_t wanted) {
107+
if (!have) return row;
108+
const bool haveOk = have <= wanted, rowOk = row <= wanted;
109+
if (haveOk != rowOk) return haveOk ? have : row;
110+
if (haveOk) return row > have ? row : have;
111+
return row < have ? row : have;
112+
}
113+
114+
/// Whether a mode covers the ceiling and is no bigger to decode than the best so far.
115+
inline bool modeFits(const Mode& m, uint16_t width, uint16_t height, uint32_t area, uint32_t bestArea) {
116+
if (m.width < width || m.height < height) return false; // below the ceiling it would be upscaled
117+
return !bestArea || area <= bestArea; // a bigger picture to decode every frame
118+
}
119+
120+
/// The smallest mode at or above the ceiling, at the rate asked for, so decode and shrink stay cheap.
121+
inline bool pickMode(const Mode* modes, size_t count, uint16_t& width, uint16_t& height, uint8_t& fps) {
122+
uint32_t bestArea = 0;
123+
uint16_t bw = 0, bh = 0;
124+
uint8_t bestFps = 0;
125+
for (size_t i = 0; i < count; i++) {
126+
const Mode& m = modes[i];
127+
const uint32_t area = static_cast<uint32_t>(m.width) * m.height;
128+
if (!modeFits(m, width, height, area, bestArea)) continue;
129+
// The rates accumulate per resolution, not per area: two shapes can share an area.
130+
if (m.width != bw || m.height != bh) bestFps = 0;
131+
bestArea = area;
132+
bw = m.width;
133+
bh = m.height;
134+
bestFps = betterFps(bestFps, m.fps, fps);
135+
}
136+
if (!bestArea) return false; // nothing listed, or nothing big enough: ask for what was wanted
137+
width = bw;
138+
height = bh;
139+
if (bestFps) fps = bestFps;
140+
return true;
141+
}
142+
143+
// --- The decoded frame --------------------------------------------------------------------------
144+
145+
/// `v` rounded up to a multiple of `to`, the MCU boundary a decoder pads a frame's axis to.
146+
inline uint32_t alignUp(uint32_t v, uint8_t to) { return (v + to - 1u) / to * to; }
147+
148+
/// The row stride in bytes the decoder wrote, or 0 when `outSize` matches no layout its MCU allows.
149+
inline size_t strideFor(uint16_t w, uint16_t h, uint8_t mcuW, uint8_t mcuH, uint32_t outSize) {
150+
if (!w || !h || !mcuW || !mcuH) return 0;
151+
if (outSize == static_cast<uint32_t>(w) * h * 3u) return static_cast<size_t>(w) * 3u;
152+
const uint32_t padW = alignUp(w, mcuW), padH = alignUp(h, mcuH);
153+
if (outSize == padW * padH * 3u) return static_cast<size_t>(padW) * 3u;
154+
return 0;
155+
}
156+
157+
/// Each output pixel the mean of its rectangle, through the shared sampler so one fix reaches both paths.
158+
inline void areaAverage(const uint8_t* src, size_t stride, uint16_t w, uint16_t h, uint16_t ow,
159+
uint16_t oh, uint8_t* dst) {
160+
for (uint16_t y = 0; y < oh; y++) {
161+
const Span ry = cellSpan(y, oh, h);
162+
for (uint16_t x = 0; x < ow; x++) {
163+
const RGB mean = areaMean(src, stride, cellSpan(x, ow, w), ry);
164+
uint8_t* out = dst + (static_cast<size_t>(y) * ow + x) * 3;
165+
out[0] = mean.r;
166+
out[1] = mean.g;
167+
out[2] = mean.b;
168+
}
169+
}
170+
}
171+
172+
} // namespace mm::usbvideo

‎test/CMakeLists.txt‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ add_executable(mm_tests
1717
unit/core/unit_Color.cpp
1818
unit/core/unit_math8.cpp
1919
unit/core/unit_math16.cpp
20+
unit/core/unit_AreaSample.cpp
21+
unit/core/unit_usbvideo_frames.cpp
2022
unit/core/unit_noise.cpp
2123
unit/core/unit_fields.cpp
2224
unit/core/unit_crc.cpp

0 commit comments

Comments
 (0)