feat: blurred letterbox, portrait pairs, overlay metadata, button fixes
- Letterbox: extend edge pixels outward with blur + fade to solid color - Portrait: detect and pair portraits side-by-side, fallback to solo - Metadata: add overlay mode (corner badge), time field, custom date/time format strings (strftime), font size 1.5x, region-aware for pairs - Buttons: swap G9/G10 to match physical layout, double-press random button for deep sleep, SLP indicator drawn before sleep - Boot: remove redundant 'Loading photos' screen (saves 17s refresh) Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -123,55 +123,239 @@ static EdgeColor averageEdge(const uint8_t* rgb, uint16_t w, uint16_t h,
|
||||
return {(uint8_t)(sumR / count), (uint8_t)(sumG / count), (uint8_t)(sumB / count)};
|
||||
}
|
||||
|
||||
// Fill letterbox bars in the final buffer, matching each bar to its adjacent photo edge.
|
||||
// fitted image is placed at (offsetX, offsetY) with size (fitW x fitH) inside (targetW x targetH).
|
||||
// In-place horizontal box blur on a rectangular sub-region of a row-major RGB buffer.
|
||||
// Operates on rows from y0..y1-1, columns x0..x1-1, within a buffer of stride `stride` pixels.
|
||||
static void boxBlurH(uint8_t* buf, uint16_t stride,
|
||||
uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, int radius) {
|
||||
int width = x1 - x0;
|
||||
if (width <= 0) return;
|
||||
|
||||
// Temp row buffer for one scanline (3 bytes per pixel)
|
||||
uint8_t* tmp = (uint8_t*)malloc(width * 3);
|
||||
if (tmp == nullptr) return;
|
||||
|
||||
int diam = radius * 2 + 1;
|
||||
|
||||
for (uint16_t y = y0; y < y1; y++) {
|
||||
uint8_t* row = buf + ((size_t)y * stride + x0) * 3;
|
||||
|
||||
// Running sum initialization
|
||||
int sumR = 0, sumG = 0, sumB = 0;
|
||||
for (int i = -radius; i <= radius; i++) {
|
||||
int xi = constrain(i, 0, width - 1);
|
||||
sumR += row[xi * 3];
|
||||
sumG += row[xi * 3 + 1];
|
||||
sumB += row[xi * 3 + 2];
|
||||
}
|
||||
tmp[0] = sumR / diam;
|
||||
tmp[1] = sumG / diam;
|
||||
tmp[2] = sumB / diam;
|
||||
|
||||
for (int x = 1; x < width; x++) {
|
||||
int addIdx = constrain(x + radius, 0, width - 1);
|
||||
int remIdx = constrain(x - radius - 1, 0, width - 1);
|
||||
sumR += row[addIdx * 3] - row[remIdx * 3];
|
||||
sumG += row[addIdx * 3 + 1] - row[remIdx * 3 + 1];
|
||||
sumB += row[addIdx * 3 + 2] - row[remIdx * 3 + 2];
|
||||
tmp[x * 3] = sumR / diam;
|
||||
tmp[x * 3 + 1] = sumG / diam;
|
||||
tmp[x * 3 + 2] = sumB / diam;
|
||||
}
|
||||
|
||||
memcpy(row, tmp, width * 3);
|
||||
}
|
||||
free(tmp);
|
||||
}
|
||||
|
||||
// In-place vertical box blur on a rectangular sub-region.
|
||||
static void boxBlurV(uint8_t* buf, uint16_t stride,
|
||||
uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, int radius) {
|
||||
int height = y1 - y0;
|
||||
if (height <= 0) return;
|
||||
|
||||
uint8_t* tmp = (uint8_t*)malloc(height * 3);
|
||||
if (tmp == nullptr) return;
|
||||
|
||||
int diam = radius * 2 + 1;
|
||||
|
||||
for (uint16_t x = x0; x < x1; x++) {
|
||||
// Running sum initialization
|
||||
int sumR = 0, sumG = 0, sumB = 0;
|
||||
for (int i = -radius; i <= radius; i++) {
|
||||
int yi = y0 + constrain(i, 0, height - 1);
|
||||
size_t idx = ((size_t)yi * stride + x) * 3;
|
||||
sumR += buf[idx];
|
||||
sumG += buf[idx + 1];
|
||||
sumB += buf[idx + 2];
|
||||
}
|
||||
tmp[0] = sumR / diam;
|
||||
tmp[1] = sumG / diam;
|
||||
tmp[2] = sumB / diam;
|
||||
|
||||
for (int y = 1; y < height; y++) {
|
||||
int addY = y0 + constrain(y + radius, 0, height - 1);
|
||||
int remY = y0 + constrain(y - radius - 1, 0, height - 1);
|
||||
size_t addIdx = ((size_t)addY * stride + x) * 3;
|
||||
size_t remIdx = ((size_t)remY * stride + x) * 3;
|
||||
sumR += buf[addIdx] - buf[remIdx];
|
||||
sumG += buf[addIdx + 1] - buf[remIdx + 1];
|
||||
sumB += buf[addIdx + 2] - buf[remIdx + 2];
|
||||
tmp[y * 3] = sumR / diam;
|
||||
tmp[y * 3 + 1] = sumG / diam;
|
||||
tmp[y * 3 + 2] = sumB / diam;
|
||||
}
|
||||
|
||||
// Write back
|
||||
for (int y = 0; y < height; y++) {
|
||||
size_t idx = ((size_t)(y0 + y) * stride + x) * 3;
|
||||
buf[idx] = tmp[y * 3];
|
||||
buf[idx + 1] = tmp[y * 3 + 1];
|
||||
buf[idx + 2] = tmp[y * 3 + 2];
|
||||
}
|
||||
}
|
||||
free(tmp);
|
||||
}
|
||||
|
||||
// 3-pass box blur (approximates Gaussian) on a sub-region
|
||||
static void gaussianBlurRegion(uint8_t* buf, uint16_t stride,
|
||||
uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1,
|
||||
int radius) {
|
||||
for (int pass = 0; pass < 3; pass++) {
|
||||
boxBlurH(buf, stride, x0, y0, x1, y1, radius);
|
||||
boxBlurV(buf, stride, x0, y0, x1, y1, radius);
|
||||
}
|
||||
}
|
||||
|
||||
// Fill letterbox bars with mirrored + blurred + faded content from the photo edge.
|
||||
// The fitted image must already be placed in final_buf at (offsetX, offsetY).
|
||||
static void fillLetterbox(uint8_t* final_buf, uint16_t targetW, uint16_t targetH,
|
||||
const uint8_t* fitted, uint16_t fitW, uint16_t fitH,
|
||||
uint16_t offsetX, uint16_t offsetY) {
|
||||
static constexpr int BLUR_RADIUS = 6;
|
||||
|
||||
if (offsetY > 0) {
|
||||
// Horizontal letterbox (top and bottom bars)
|
||||
EdgeColor top = averageEdge(fitted, fitW, fitH, 0);
|
||||
EdgeColor bot = averageEdge(fitted, fitW, fitH, 1);
|
||||
// Horizontal bars (top and bottom)
|
||||
EdgeColor topEdge = averageEdge(fitted, fitW, fitH, 0);
|
||||
EdgeColor botEdge = averageEdge(fitted, fitW, fitH, 1);
|
||||
|
||||
// Fill top bar
|
||||
for (uint16_t y = 0; y < offsetY; y++)
|
||||
// --- Extend top edge into top bar ---
|
||||
// Every row in the bar copies from the photo's topmost row (row 0)
|
||||
for (uint16_t y = 0; y < offsetY; y++) {
|
||||
for (uint16_t x = 0; x < targetW; x++) {
|
||||
size_t idx = ((size_t)y * targetW + x) * 3;
|
||||
final_buf[idx] = top.r; final_buf[idx+1] = top.g; final_buf[idx+2] = top.b;
|
||||
int srcX = (int)x - (int)offsetX;
|
||||
srcX = constrain(srcX, 0, (int)fitW - 1);
|
||||
size_t srcIdx = ((size_t)0 * fitW + srcX) * 3; // always row 0
|
||||
size_t dstIdx = ((size_t)y * targetW + x) * 3;
|
||||
final_buf[dstIdx] = fitted[srcIdx];
|
||||
final_buf[dstIdx + 1] = fitted[srcIdx + 1];
|
||||
final_buf[dstIdx + 2] = fitted[srcIdx + 2];
|
||||
}
|
||||
// Fill bottom bar
|
||||
}
|
||||
|
||||
// --- Extend bottom edge into bottom bar ---
|
||||
// Every row copies from the photo's bottommost row (fitH - 1)
|
||||
uint16_t botStart = offsetY + fitH;
|
||||
for (uint16_t y = botStart; y < targetH; y++)
|
||||
uint16_t lastRow = fitH - 1;
|
||||
for (uint16_t y = botStart; y < targetH; y++) {
|
||||
for (uint16_t x = 0; x < targetW; x++) {
|
||||
int srcX = (int)x - (int)offsetX;
|
||||
srcX = constrain(srcX, 0, (int)fitW - 1);
|
||||
size_t srcIdx = ((size_t)lastRow * fitW + srcX) * 3;
|
||||
size_t dstIdx = ((size_t)y * targetW + x) * 3;
|
||||
final_buf[dstIdx] = fitted[srcIdx];
|
||||
final_buf[dstIdx + 1] = fitted[srcIdx + 1];
|
||||
final_buf[dstIdx + 2] = fitted[srcIdx + 2];
|
||||
}
|
||||
}
|
||||
|
||||
// --- Blur both bars ---
|
||||
gaussianBlurRegion(final_buf, targetW, 0, 0, targetW, offsetY, BLUR_RADIUS);
|
||||
gaussianBlurRegion(final_buf, targetW, 0, botStart, targetW, targetH, BLUR_RADIUS);
|
||||
|
||||
// --- Fade toward solid edge color ---
|
||||
for (uint16_t y = 0; y < offsetY; y++) {
|
||||
float alpha = (float)(offsetY - 1 - y) / (float)offsetY; // 0 at photo, 1 at screen edge
|
||||
for (uint16_t x = 0; x < targetW; x++) {
|
||||
size_t idx = ((size_t)y * targetW + x) * 3;
|
||||
final_buf[idx] = bot.r; final_buf[idx+1] = bot.g; final_buf[idx+2] = bot.b;
|
||||
final_buf[idx] = (uint8_t)(final_buf[idx] * (1.0f - alpha) + topEdge.r * alpha);
|
||||
final_buf[idx + 1] = (uint8_t)(final_buf[idx + 1] * (1.0f - alpha) + topEdge.g * alpha);
|
||||
final_buf[idx + 2] = (uint8_t)(final_buf[idx + 2] * (1.0f - alpha) + topEdge.b * alpha);
|
||||
}
|
||||
}
|
||||
for (uint16_t y = botStart; y < targetH; y++) {
|
||||
float alpha = (float)(y - botStart) / (float)(targetH - botStart);
|
||||
for (uint16_t x = 0; x < targetW; x++) {
|
||||
size_t idx = ((size_t)y * targetW + x) * 3;
|
||||
final_buf[idx] = (uint8_t)(final_buf[idx] * (1.0f - alpha) + botEdge.r * alpha);
|
||||
final_buf[idx + 1] = (uint8_t)(final_buf[idx + 1] * (1.0f - alpha) + botEdge.g * alpha);
|
||||
final_buf[idx + 2] = (uint8_t)(final_buf[idx + 2] * (1.0f - alpha) + botEdge.b * alpha);
|
||||
}
|
||||
}
|
||||
|
||||
Serial.printf("[pipeline] Letterbox TB: top=#%02X%02X%02X bot=#%02X%02X%02X\n",
|
||||
top.r, top.g, top.b, bot.r, bot.g, bot.b);
|
||||
Serial.printf("[pipeline] Letterbox TB: bars %dpx, top=#%02X%02X%02X bot=#%02X%02X%02X\n",
|
||||
offsetY, topEdge.r, topEdge.g, topEdge.b, botEdge.r, botEdge.g, botEdge.b);
|
||||
}
|
||||
|
||||
if (offsetX > 0) {
|
||||
// Vertical letterbox (left and right bars)
|
||||
EdgeColor left = averageEdge(fitted, fitW, fitH, 2);
|
||||
EdgeColor right = averageEdge(fitted, fitW, fitH, 3);
|
||||
// Vertical bars (left and right)
|
||||
EdgeColor leftEdge = averageEdge(fitted, fitW, fitH, 2);
|
||||
EdgeColor rightEdge = averageEdge(fitted, fitW, fitH, 3);
|
||||
|
||||
// Fill left bar
|
||||
for (uint16_t y = 0; y < targetH; y++)
|
||||
// --- Extend left edge into left bar ---
|
||||
// Every column in the bar copies from the photo's leftmost column (col 0)
|
||||
for (uint16_t y = 0; y < targetH; y++) {
|
||||
int srcY = (int)y - (int)offsetY;
|
||||
srcY = constrain(srcY, 0, (int)fitH - 1);
|
||||
size_t srcIdx = ((size_t)srcY * fitW + 0) * 3; // always column 0
|
||||
for (uint16_t x = 0; x < offsetX; x++) {
|
||||
size_t idx = ((size_t)y * targetW + x) * 3;
|
||||
final_buf[idx] = left.r; final_buf[idx+1] = left.g; final_buf[idx+2] = left.b;
|
||||
}
|
||||
// Fill right bar
|
||||
uint16_t rightStart = offsetX + fitW;
|
||||
for (uint16_t y = 0; y < targetH; y++)
|
||||
for (uint16_t x = rightStart; x < targetW; x++) {
|
||||
size_t idx = ((size_t)y * targetW + x) * 3;
|
||||
final_buf[idx] = right.r; final_buf[idx+1] = right.g; final_buf[idx+2] = right.b;
|
||||
size_t dstIdx = ((size_t)y * targetW + x) * 3;
|
||||
final_buf[dstIdx] = fitted[srcIdx];
|
||||
final_buf[dstIdx + 1] = fitted[srcIdx + 1];
|
||||
final_buf[dstIdx + 2] = fitted[srcIdx + 2];
|
||||
}
|
||||
}
|
||||
|
||||
Serial.printf("[pipeline] Letterbox LR: left=#%02X%02X%02X right=#%02X%02X%02X\n",
|
||||
left.r, left.g, left.b, right.r, right.g, right.b);
|
||||
// --- Extend right edge into right bar ---
|
||||
// Every column copies from the photo's rightmost column (fitW - 1)
|
||||
uint16_t rightStart = offsetX + fitW;
|
||||
uint16_t lastCol = fitW - 1;
|
||||
for (uint16_t y = 0; y < targetH; y++) {
|
||||
int srcY = (int)y - (int)offsetY;
|
||||
srcY = constrain(srcY, 0, (int)fitH - 1);
|
||||
size_t srcIdx = ((size_t)srcY * fitW + lastCol) * 3;
|
||||
for (uint16_t x = rightStart; x < targetW; x++) {
|
||||
size_t dstIdx = ((size_t)y * targetW + x) * 3;
|
||||
final_buf[dstIdx] = fitted[srcIdx];
|
||||
final_buf[dstIdx + 1] = fitted[srcIdx + 1];
|
||||
final_buf[dstIdx + 2] = fitted[srcIdx + 2];
|
||||
}
|
||||
}
|
||||
|
||||
// --- Blur both bars ---
|
||||
gaussianBlurRegion(final_buf, targetW, 0, 0, offsetX, targetH, BLUR_RADIUS);
|
||||
gaussianBlurRegion(final_buf, targetW, rightStart, 0, targetW, targetH, BLUR_RADIUS);
|
||||
|
||||
// --- Fade toward solid edge color ---
|
||||
for (uint16_t y = 0; y < targetH; y++) {
|
||||
for (uint16_t x = 0; x < offsetX; x++) {
|
||||
float alpha = (float)(offsetX - 1 - x) / (float)offsetX;
|
||||
size_t idx = ((size_t)y * targetW + x) * 3;
|
||||
final_buf[idx] = (uint8_t)(final_buf[idx] * (1.0f - alpha) + leftEdge.r * alpha);
|
||||
final_buf[idx + 1] = (uint8_t)(final_buf[idx + 1] * (1.0f - alpha) + leftEdge.g * alpha);
|
||||
final_buf[idx + 2] = (uint8_t)(final_buf[idx + 2] * (1.0f - alpha) + leftEdge.b * alpha);
|
||||
}
|
||||
for (uint16_t x = rightStart; x < targetW; x++) {
|
||||
float alpha = (float)(x - rightStart) / (float)(targetW - rightStart);
|
||||
size_t idx = ((size_t)y * targetW + x) * 3;
|
||||
final_buf[idx] = (uint8_t)(final_buf[idx] * (1.0f - alpha) + rightEdge.r * alpha);
|
||||
final_buf[idx + 1] = (uint8_t)(final_buf[idx + 1] * (1.0f - alpha) + rightEdge.g * alpha);
|
||||
final_buf[idx + 2] = (uint8_t)(final_buf[idx + 2] * (1.0f - alpha) + rightEdge.b * alpha);
|
||||
}
|
||||
}
|
||||
|
||||
Serial.printf("[pipeline] Letterbox LR: bars %dpx, left=#%02X%02X%02X right=#%02X%02X%02X\n",
|
||||
offsetX, leftEdge.r, leftEdge.g, leftEdge.b,
|
||||
rightEdge.r, rightEdge.g, rightEdge.b);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user