Skip to content

Commit 7e4585e

Browse files
Camera: support more resolutions
It's a bit unstable, as it crashes if the settings button is clicked after startup, but not when closing and then re-opening. Seems to work for 640x480, including QR decoding.
1 parent 1ab4970 commit 7e4585e

File tree

4 files changed

+633
-42
lines changed

4 files changed

+633
-42
lines changed

c_mpos/src/webcam.c

Lines changed: 176 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,25 @@ static void yuyv_to_rgb565(unsigned char *yuyv, uint16_t *rgb565, int in_width,
5050
for (int x = 0; x < out_width; x++) {
5151
int src_x = (int)(x * x_ratio) + crop_x_offset;
5252
int src_y = (int)(y * y_ratio) + crop_y_offset;
53-
int src_index = (src_y * in_width + src_x) * 2;
54-
55-
int y0 = yuyv[src_index];
56-
int u = yuyv[src_index + 1];
57-
int v = yuyv[src_index + 3];
5853

54+
// YUYV format: Y0 U Y1 V (4 bytes for 2 pixels)
55+
// Ensure we're aligned to even pixel boundary
56+
int src_x_even = (src_x / 2) * 2;
57+
int src_base_index = (src_y * in_width + src_x_even) * 2;
58+
59+
// Extract Y, U, V values
60+
int y0;
61+
if (src_x % 2 == 0) {
62+
// Even pixel: use Y0
63+
y0 = yuyv[src_base_index];
64+
} else {
65+
// Odd pixel: use Y1
66+
y0 = yuyv[src_base_index + 2];
67+
}
68+
int u = yuyv[src_base_index + 1];
69+
int v = yuyv[src_base_index + 3];
70+
71+
// YUV to RGB conversion (ITU-R BT.601)
5972
int c = y0 - 16;
6073
int d = u - 128;
6174
int e = v - 128;
@@ -64,10 +77,12 @@ static void yuyv_to_rgb565(unsigned char *yuyv, uint16_t *rgb565, int in_width,
6477
int g = (298 * c - 100 * d - 208 * e + 128) >> 8;
6578
int b = (298 * c + 516 * d + 128) >> 8;
6679

80+
// Clamp to valid range
6781
r = r < 0 ? 0 : (r > 255 ? 255 : r);
6882
g = g < 0 ? 0 : (g > 255 ? 255 : g);
6983
b = b < 0 ? 0 : (b > 255 ? 255 : b);
7084

85+
// Convert to RGB565
7186
uint16_t r5 = (r >> 3) & 0x1F;
7287
uint16_t g6 = (g >> 2) & 0x3F;
7388
uint16_t b5 = (b >> 3) & 0x1F;
@@ -91,8 +106,23 @@ static void yuyv_to_grayscale(unsigned char *yuyv, unsigned char *gray, int in_w
91106
for (int x = 0; x < out_width; x++) {
92107
int src_x = (int)(x * x_ratio) + crop_x_offset;
93108
int src_y = (int)(y * y_ratio) + crop_y_offset;
94-
int src_index = (src_y * in_width + src_x) * 2;
95-
gray[y * out_width + x] = yuyv[src_index];
109+
110+
// YUYV format: Y0 U Y1 V (4 bytes for 2 pixels)
111+
// Ensure we're aligned to even pixel boundary
112+
int src_x_even = (src_x / 2) * 2;
113+
int src_base_index = (src_y * in_width + src_x_even) * 2;
114+
115+
// Extract Y value
116+
unsigned char y_val;
117+
if (src_x % 2 == 0) {
118+
// Even pixel: use Y0
119+
y_val = yuyv[src_base_index];
120+
} else {
121+
// Odd pixel: use Y1
122+
y_val = yuyv[src_base_index + 2];
123+
}
124+
125+
gray[y * out_width + x] = y_val;
96126
}
97127
}
98128
}
@@ -342,14 +372,25 @@ static mp_obj_t webcam_capture_frame(mp_obj_t self_in, mp_obj_t format) {
342372
MP_DEFINE_CONST_FUN_OBJ_2(webcam_capture_frame_obj, webcam_capture_frame);
343373

344374
static mp_obj_t webcam_reconfigure(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
345-
// NOTE: This function only changes OUTPUT resolution (what Python receives).
346-
// The INPUT resolution (what the webcam captures from V4L2) remains fixed at 640x480.
347-
// The conversion functions will crop/scale from input to output resolution.
348-
// TODO: Add support for changing input resolution (requires V4L2 reinit)
349-
350-
enum { ARG_self, ARG_output_width, ARG_output_height };
375+
/*
376+
* Reconfigure webcam resolution.
377+
*
378+
* Supports changing both INPUT resolution (V4L2 capture format) and
379+
* OUTPUT resolution (conversion buffers). If input resolution changes,
380+
* this will stop streaming, reconfigure V4L2, and restart streaming.
381+
*
382+
* Parameters:
383+
* input_width, input_height: V4L2 capture resolution (optional)
384+
* output_width, output_height: Output buffer resolution (optional)
385+
*
386+
* If not specified, dimensions remain unchanged.
387+
*/
388+
389+
enum { ARG_self, ARG_input_width, ARG_input_height, ARG_output_width, ARG_output_height };
351390
static const mp_arg_t allowed_args[] = {
352391
{ MP_QSTR_self, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} },
392+
{ MP_QSTR_input_width, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} },
393+
{ MP_QSTR_input_height, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} },
353394
{ MP_QSTR_output_width, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} },
354395
{ MP_QSTR_output_height, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} },
355396
};
@@ -360,26 +401,135 @@ static mp_obj_t webcam_reconfigure(size_t n_args, const mp_obj_t *pos_args, mp_m
360401
webcam_obj_t *self = MP_OBJ_TO_PTR(args[ARG_self].u_obj);
361402

362403
// Get new dimensions (keep current if not specified)
363-
int new_width = args[ARG_output_width].u_int;
364-
int new_height = args[ARG_output_height].u_int;
404+
int new_input_width = args[ARG_input_width].u_int;
405+
int new_input_height = args[ARG_input_height].u_int;
406+
int new_output_width = args[ARG_output_width].u_int;
407+
int new_output_height = args[ARG_output_height].u_int;
365408

366-
if (new_width == 0) new_width = self->output_width;
367-
if (new_height == 0) new_height = self->output_height;
409+
if (new_input_width == 0) new_input_width = self->input_width;
410+
if (new_input_height == 0) new_input_height = self->input_height;
411+
if (new_output_width == 0) new_output_width = self->output_width;
412+
if (new_output_height == 0) new_output_height = self->output_height;
368413

369414
// Validate dimensions
370-
if (new_width <= 0 || new_height <= 0 || new_width > 1920 || new_height > 1920) {
415+
if (new_input_width <= 0 || new_input_height <= 0 || new_input_width > 1920 || new_input_height > 1920) {
416+
mp_raise_ValueError(MP_ERROR_TEXT("Invalid input dimensions"));
417+
}
418+
if (new_output_width <= 0 || new_output_height <= 0 || new_output_width > 1920 || new_output_height > 1920) {
371419
mp_raise_ValueError(MP_ERROR_TEXT("Invalid output dimensions"));
372420
}
373421

374-
// If dimensions changed, reallocate buffers
375-
if (new_width != self->output_width || new_height != self->output_height) {
422+
bool input_changed = (new_input_width != self->input_width || new_input_height != self->input_height);
423+
bool output_changed = (new_output_width != self->output_width || new_output_height != self->output_height);
424+
425+
// If input resolution changed, need to reconfigure V4L2
426+
if (input_changed) {
427+
WEBCAM_DEBUG_PRINT("Reconfiguring V4L2: %dx%d -> %dx%d\n",
428+
self->input_width, self->input_height,
429+
new_input_width, new_input_height);
430+
431+
// 1. Stop streaming
432+
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
433+
if (ioctl(self->fd, VIDIOC_STREAMOFF, &type) < 0) {
434+
WEBCAM_DEBUG_PRINT("STREAMOFF failed: %s\n", strerror(errno));
435+
mp_raise_OSError(errno);
436+
}
437+
438+
// 2. Unmap old buffers
439+
for (int i = 0; i < NUM_BUFFERS; i++) {
440+
if (self->buffers[i] != MAP_FAILED && self->buffers[i] != NULL) {
441+
munmap(self->buffers[i], self->buffer_length);
442+
self->buffers[i] = MAP_FAILED;
443+
}
444+
}
445+
446+
// 3. Set new V4L2 format
447+
struct v4l2_format fmt = {0};
448+
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
449+
fmt.fmt.pix.width = new_input_width;
450+
fmt.fmt.pix.height = new_input_height;
451+
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
452+
fmt.fmt.pix.field = V4L2_FIELD_ANY;
453+
454+
if (ioctl(self->fd, VIDIOC_S_FMT, &fmt) < 0) {
455+
WEBCAM_DEBUG_PRINT("S_FMT failed: %s\n", strerror(errno));
456+
mp_raise_OSError(errno);
457+
}
458+
459+
// Verify format was set (driver may adjust dimensions)
460+
if (fmt.fmt.pix.width != new_input_width || fmt.fmt.pix.height != new_input_height) {
461+
WEBCAM_DEBUG_PRINT("Warning: Driver adjusted format to %dx%d\n",
462+
fmt.fmt.pix.width, fmt.fmt.pix.height);
463+
new_input_width = fmt.fmt.pix.width;
464+
new_input_height = fmt.fmt.pix.height;
465+
}
466+
467+
// 4. Request new buffers
468+
struct v4l2_requestbuffers req = {0};
469+
req.count = NUM_BUFFERS;
470+
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
471+
req.memory = V4L2_MEMORY_MMAP;
472+
473+
if (ioctl(self->fd, VIDIOC_REQBUFS, &req) < 0) {
474+
WEBCAM_DEBUG_PRINT("REQBUFS failed: %s\n", strerror(errno));
475+
mp_raise_OSError(errno);
476+
}
477+
478+
// 5. Map new buffers
479+
for (int i = 0; i < NUM_BUFFERS; i++) {
480+
struct v4l2_buffer buf = {0};
481+
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
482+
buf.memory = V4L2_MEMORY_MMAP;
483+
buf.index = i;
484+
485+
if (ioctl(self->fd, VIDIOC_QUERYBUF, &buf) < 0) {
486+
WEBCAM_DEBUG_PRINT("QUERYBUF failed: %s\n", strerror(errno));
487+
mp_raise_OSError(errno);
488+
}
489+
490+
self->buffer_length = buf.length;
491+
self->buffers[i] = mmap(NULL, buf.length, PROT_READ | PROT_WRITE,
492+
MAP_SHARED, self->fd, buf.m.offset);
493+
494+
if (self->buffers[i] == MAP_FAILED) {
495+
WEBCAM_DEBUG_PRINT("mmap failed: %s\n", strerror(errno));
496+
mp_raise_OSError(errno);
497+
}
498+
}
499+
500+
// 6. Queue buffers
501+
for (int i = 0; i < NUM_BUFFERS; i++) {
502+
struct v4l2_buffer buf = {0};
503+
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
504+
buf.memory = V4L2_MEMORY_MMAP;
505+
buf.index = i;
506+
507+
if (ioctl(self->fd, VIDIOC_QBUF, &buf) < 0) {
508+
WEBCAM_DEBUG_PRINT("QBUF failed: %s\n", strerror(errno));
509+
mp_raise_OSError(errno);
510+
}
511+
}
512+
513+
// 7. Restart streaming
514+
if (ioctl(self->fd, VIDIOC_STREAMON, &type) < 0) {
515+
WEBCAM_DEBUG_PRINT("STREAMON failed: %s\n", strerror(errno));
516+
mp_raise_OSError(errno);
517+
}
518+
519+
// Update stored input dimensions
520+
self->input_width = new_input_width;
521+
self->input_height = new_input_height;
522+
}
523+
524+
// If output resolution changed (or input changed which may affect output), reallocate output buffers
525+
if (output_changed || input_changed) {
376526
// Free old buffers
377527
free(self->gray_buffer);
378528
free(self->rgb565_buffer);
379529

380530
// Update dimensions
381-
self->output_width = new_width;
382-
self->output_height = new_height;
531+
self->output_width = new_output_width;
532+
self->output_height = new_output_height;
383533

384534
// Allocate new buffers
385535
self->gray_buffer = (unsigned char *)malloc(self->output_width * self->output_height * sizeof(unsigned char));
@@ -392,10 +542,12 @@ static mp_obj_t webcam_reconfigure(size_t n_args, const mp_obj_t *pos_args, mp_m
392542
self->rgb565_buffer = NULL;
393543
mp_raise_OSError(MP_ENOMEM);
394544
}
395-
396-
WEBCAM_DEBUG_PRINT("Webcam reconfigured to %dx%d\n", self->output_width, self->output_height);
397545
}
398546

547+
WEBCAM_DEBUG_PRINT("Webcam reconfigured: input %dx%d, output %dx%d\n",
548+
self->input_width, self->input_height,
549+
self->output_width, self->output_height);
550+
399551
return mp_const_none;
400552
}
401553
MP_DEFINE_CONST_FUN_OBJ_KW(webcam_reconfigure_obj, 1, webcam_reconfigure);

internal_filesystem/apps/com.micropythonos.camera/assets/camera_app.py

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def onCreate(self):
8282
# Settings button
8383
settings_button = lv.button(main_screen)
8484
settings_button.set_size(60,60)
85-
settings_button.align(lv.ALIGN.TOP_LEFT, 0, 0)
85+
settings_button.align(lv.ALIGN.TOP_RIGHT, 0, 60)
8686
settings_label = lv.label(settings_button)
8787
settings_label.set_text(lv.SYMBOL.SETTINGS)
8888
settings_label.center()
@@ -167,7 +167,7 @@ def onResume(self, screen):
167167
self.finish()
168168

169169

170-
def onStop(self, screen):
170+
def onPause(self, screen):
171171
print("camera app backgrounded, cleaning up...")
172172
if self.capture_timer:
173173
self.capture_timer.delete()
@@ -289,26 +289,48 @@ def handle_settings_result(self, result):
289289
# Reload resolution preference
290290
self.load_resolution_preference()
291291

292-
# Recreate image descriptor with new dimensions
293-
self.image_dsc["header"]["w"] = self.width
294-
self.image_dsc["header"]["h"] = self.height
295-
self.image_dsc["header"]["stride"] = self.width * 2
296-
self.image_dsc["data_size"] = self.width * self.height * 2
292+
# CRITICAL: Pause capture timer to prevent race conditions during reconfiguration
293+
if self.capture_timer:
294+
self.capture_timer.delete()
295+
self.capture_timer = None
296+
print("Capture timer paused")
297+
298+
# Clear stale data pointer to prevent segfault during LVGL rendering
299+
self.image_dsc.data = None
300+
self.current_cam_buffer = None
301+
print("Image data cleared")
302+
303+
# Update image descriptor with new dimensions
304+
# Note: image_dsc is an LVGL struct, use attribute access not dictionary access
305+
self.image_dsc.header.w = self.width
306+
self.image_dsc.header.h = self.height
307+
self.image_dsc.header.stride = self.width * 2
308+
self.image_dsc.data_size = self.width * self.height * 2
309+
print(f"Image descriptor updated to {self.width}x{self.height}")
297310

298311
# Reconfigure camera if active
299312
if self.cam:
300313
if self.use_webcam:
301-
print(f"Reconfiguring webcam to {self.width}x{self.height}")
302-
webcam.reconfigure(self.cam, output_width=self.width, output_height=self.height)
314+
print(f"Reconfiguring webcam: input={self.width}x{self.height}, output={self.width}x{self.height}")
315+
# Configure both V4L2 input and output to the same resolution for best quality
316+
webcam.reconfigure(
317+
self.cam,
318+
input_width=self.width,
319+
input_height=self.height,
320+
output_width=self.width,
321+
output_height=self.height
322+
)
323+
# Resume capture timer for webcam
324+
self.capture_timer = lv.timer_create(self.try_capture, 100, None)
325+
print("Webcam reconfigured (V4L2 + output buffers), capture timer resumed")
303326
else:
304327
# For internal camera, need to reinitialize
305328
print(f"Reinitializing internal camera to {self.width}x{self.height}")
306-
if self.capture_timer:
307-
self.capture_timer.delete()
308329
self.cam.deinit()
309330
self.cam = init_internal_cam(self.width, self.height)
310331
if self.cam:
311332
self.capture_timer = lv.timer_create(self.try_capture, 100, None)
333+
print("Internal camera reinitialized, capture timer resumed")
312334

313335
self.set_image_size()
314336

@@ -319,14 +341,23 @@ def try_capture(self, event):
319341
self.current_cam_buffer = webcam.capture_frame(self.cam, "rgb565")
320342
elif self.cam.frame_available():
321343
self.current_cam_buffer = self.cam.capture()
344+
322345
if self.current_cam_buffer and len(self.current_cam_buffer):
323-
self.image_dsc.data = self.current_cam_buffer
324-
#image.invalidate() # does not work so do this:
325-
self.image.set_src(self.image_dsc)
326-
if not self.use_webcam:
327-
self.cam.free_buffer() # Free the old buffer
328-
if self.keepliveqrdecoding:
329-
self.qrdecode_one()
346+
# Defensive check: verify buffer size matches expected dimensions
347+
expected_size = self.width * self.height * 2 # RGB565 = 2 bytes per pixel
348+
actual_size = len(self.current_cam_buffer)
349+
350+
if actual_size == expected_size:
351+
self.image_dsc.data = self.current_cam_buffer
352+
#image.invalidate() # does not work so do this:
353+
self.image.set_src(self.image_dsc)
354+
if not self.use_webcam:
355+
self.cam.free_buffer() # Free the old buffer
356+
if self.keepliveqrdecoding:
357+
self.qrdecode_one()
358+
else:
359+
print(f"Warning: Buffer size mismatch! Expected {expected_size} bytes, got {actual_size} bytes")
360+
print(f" Resolution: {self.width}x{self.height}, discarding frame")
330361
except Exception as e:
331362
print(f"Camera capture exception: {e}")
332363

0 commit comments

Comments
 (0)