Work towards changing camera resolution

This commit is contained in:
Thomas Farstrike
2025-11-24 22:54:26 +01:00
parent fa80b7ce13
commit 1ab4970dc7
4 changed files with 346 additions and 37 deletions
+116 -24
View File
@@ -30,17 +30,24 @@ typedef struct _webcam_obj_t {
int frame_count;
unsigned char *gray_buffer; // For grayscale
uint16_t *rgb565_buffer; // For RGB565
int input_width; // Webcam capture width (from V4L2)
int input_height; // Webcam capture height (from V4L2)
int output_width; // Configurable output width (default OUTPUT_WIDTH)
int output_height; // Configurable output height (default OUTPUT_HEIGHT)
} webcam_obj_t;
static void yuyv_to_rgb565_240x240(unsigned char *yuyv, uint16_t *rgb565, int in_width, int in_height) {
int crop_size = 480;
static void yuyv_to_rgb565(unsigned char *yuyv, uint16_t *rgb565, int in_width, int in_height, int out_width, int out_height) {
// Crop to largest square that fits in the input frame
int crop_size = (in_width < in_height) ? in_width : in_height;
int crop_x_offset = (in_width - crop_size) / 2;
int crop_y_offset = (in_height - crop_size) / 2;
float x_ratio = (float)crop_size / OUTPUT_WIDTH;
float y_ratio = (float)crop_size / OUTPUT_HEIGHT;
for (int y = 0; y < OUTPUT_HEIGHT; y++) {
for (int x = 0; x < OUTPUT_WIDTH; x++) {
// Calculate scaling ratios
float x_ratio = (float)crop_size / out_width;
float y_ratio = (float)crop_size / out_height;
for (int y = 0; y < out_height; y++) {
for (int x = 0; x < out_width; x++) {
int src_x = (int)(x * x_ratio) + crop_x_offset;
int src_y = (int)(y * y_ratio) + crop_y_offset;
int src_index = (src_y * in_width + src_x) * 2;
@@ -65,24 +72,27 @@ static void yuyv_to_rgb565_240x240(unsigned char *yuyv, uint16_t *rgb565, int in
uint16_t g6 = (g >> 2) & 0x3F;
uint16_t b5 = (b >> 3) & 0x1F;
rgb565[y * OUTPUT_WIDTH + x] = (r5 << 11) | (g6 << 5) | b5;
rgb565[y * out_width + x] = (r5 << 11) | (g6 << 5) | b5;
}
}
}
static void yuyv_to_grayscale_240x240(unsigned char *yuyv, unsigned char *gray, int in_width, int in_height) {
int crop_size = 480;
static void yuyv_to_grayscale(unsigned char *yuyv, unsigned char *gray, int in_width, int in_height, int out_width, int out_height) {
// Crop to largest square that fits in the input frame
int crop_size = (in_width < in_height) ? in_width : in_height;
int crop_x_offset = (in_width - crop_size) / 2;
int crop_y_offset = (in_height - crop_size) / 2;
float x_ratio = (float)crop_size / OUTPUT_WIDTH;
float y_ratio = (float)crop_size / OUTPUT_HEIGHT;
for (int y = 0; y < OUTPUT_HEIGHT; y++) {
for (int x = 0; x < OUTPUT_WIDTH; x++) {
// Calculate scaling ratios
float x_ratio = (float)crop_size / out_width;
float y_ratio = (float)crop_size / out_height;
for (int y = 0; y < out_height; y++) {
for (int x = 0; x < out_width; x++) {
int src_x = (int)(x * x_ratio) + crop_x_offset;
int src_y = (int)(y * y_ratio) + crop_y_offset;
int src_index = (src_y * in_width + src_x) * 2;
gray[y * OUTPUT_WIDTH + x] = yuyv[src_index];
gray[y * out_width + x] = yuyv[src_index];
}
}
}
@@ -174,8 +184,22 @@ static int init_webcam(webcam_obj_t *self, const char *device) {
}
self->frame_count = 0;
self->gray_buffer = (unsigned char *)malloc(OUTPUT_WIDTH * OUTPUT_HEIGHT * sizeof(unsigned char));
self->rgb565_buffer = (uint16_t *)malloc(OUTPUT_WIDTH * OUTPUT_HEIGHT * sizeof(uint16_t));
// Store the input dimensions from V4L2 format
self->input_width = WIDTH;
self->input_height = HEIGHT;
// Initialize output dimensions with defaults if not already set
if (self->output_width == 0) self->output_width = OUTPUT_WIDTH;
if (self->output_height == 0) self->output_height = OUTPUT_HEIGHT;
WEBCAM_DEBUG_PRINT("Webcam initialized: input %dx%d, output %dx%d\n",
self->input_width, self->input_height,
self->output_width, self->output_height);
// Allocate buffers with configured output dimensions
self->gray_buffer = (unsigned char *)malloc(self->output_width * self->output_height * sizeof(unsigned char));
self->rgb565_buffer = (uint16_t *)malloc(self->output_width * self->output_height * sizeof(uint16_t));
if (!self->gray_buffer || !self->rgb565_buffer) {
WEBCAM_DEBUG_PRINT("Cannot allocate buffers: %s\n", strerror(errno));
free(self->gray_buffer);
@@ -227,13 +251,13 @@ static mp_obj_t capture_frame(mp_obj_t self_in, mp_obj_t format) {
}
if (!self->gray_buffer) {
self->gray_buffer = (unsigned char *)malloc(OUTPUT_WIDTH * OUTPUT_HEIGHT * sizeof(unsigned char));
self->gray_buffer = (unsigned char *)malloc(self->output_width * self->output_height * sizeof(unsigned char));
if (!self->gray_buffer) {
mp_raise_OSError(MP_ENOMEM);
}
}
if (!self->rgb565_buffer) {
self->rgb565_buffer = (uint16_t *)malloc(OUTPUT_WIDTH * OUTPUT_HEIGHT * sizeof(uint16_t));
self->rgb565_buffer = (uint16_t *)malloc(self->output_width * self->output_height * sizeof(uint16_t));
if (!self->rgb565_buffer) {
mp_raise_OSError(MP_ENOMEM);
}
@@ -241,22 +265,26 @@ static mp_obj_t capture_frame(mp_obj_t self_in, mp_obj_t format) {
const char *fmt = mp_obj_str_get_str(format);
if (strcmp(fmt, "grayscale") == 0) {
yuyv_to_grayscale_240x240(self->buffers[buf.index], self->gray_buffer, WIDTH, HEIGHT);
yuyv_to_grayscale(self->buffers[buf.index], self->gray_buffer,
self->input_width, self->input_height,
self->output_width, self->output_height);
// char filename[32];
// snprintf(filename, sizeof(filename), "frame_%03d.raw", self->frame_count++);
// save_raw(filename, self->gray_buffer, OUTPUT_WIDTH, OUTPUT_HEIGHT);
mp_obj_t result = mp_obj_new_memoryview('b', OUTPUT_WIDTH * OUTPUT_HEIGHT, self->gray_buffer);
// save_raw(filename, self->gray_buffer, self->output_width, self->output_height);
mp_obj_t result = mp_obj_new_memoryview('b', self->output_width * self->output_height, self->gray_buffer);
res = ioctl(self->fd, VIDIOC_QBUF, &buf);
if (res < 0) {
mp_raise_OSError(-res);
}
return result;
} else {
yuyv_to_rgb565_240x240(self->buffers[buf.index], self->rgb565_buffer, WIDTH, HEIGHT);
yuyv_to_rgb565(self->buffers[buf.index], self->rgb565_buffer,
self->input_width, self->input_height,
self->output_width, self->output_height);
// char filename[32];
// snprintf(filename, sizeof(filename), "frame_%03d.rgb565", self->frame_count++);
// save_raw_rgb565(filename, self->rgb565_buffer, OUTPUT_WIDTH, OUTPUT_HEIGHT);
mp_obj_t result = mp_obj_new_memoryview('b', OUTPUT_WIDTH * OUTPUT_HEIGHT * 2, self->rgb565_buffer);
// save_raw_rgb565(filename, self->rgb565_buffer, self->output_width, self->output_height);
mp_obj_t result = mp_obj_new_memoryview('b', self->output_width * self->output_height * 2, self->rgb565_buffer);
res = ioctl(self->fd, VIDIOC_QBUF, &buf);
if (res < 0) {
mp_raise_OSError(-res);
@@ -277,6 +305,10 @@ static mp_obj_t webcam_init(size_t n_args, const mp_obj_t *args) {
self->fd = -1;
self->gray_buffer = NULL;
self->rgb565_buffer = NULL;
self->input_width = 0; // Will be set from V4L2 format in init_webcam
self->input_height = 0; // Will be set from V4L2 format in init_webcam
self->output_width = 0; // Will use default OUTPUT_WIDTH in init_webcam
self->output_height = 0; // Will use default OUTPUT_HEIGHT in init_webcam
int res = init_webcam(self, device);
if (res < 0) {
@@ -309,6 +341,65 @@ static mp_obj_t webcam_capture_frame(mp_obj_t self_in, mp_obj_t format) {
}
MP_DEFINE_CONST_FUN_OBJ_2(webcam_capture_frame_obj, webcam_capture_frame);
static mp_obj_t webcam_reconfigure(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
// NOTE: This function only changes OUTPUT resolution (what Python receives).
// The INPUT resolution (what the webcam captures from V4L2) remains fixed at 640x480.
// The conversion functions will crop/scale from input to output resolution.
// TODO: Add support for changing input resolution (requires V4L2 reinit)
enum { ARG_self, ARG_output_width, ARG_output_height };
static const mp_arg_t allowed_args[] = {
{ MP_QSTR_self, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} },
{ MP_QSTR_output_width, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} },
{ MP_QSTR_output_height, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 0} },
};
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
webcam_obj_t *self = MP_OBJ_TO_PTR(args[ARG_self].u_obj);
// Get new dimensions (keep current if not specified)
int new_width = args[ARG_output_width].u_int;
int new_height = args[ARG_output_height].u_int;
if (new_width == 0) new_width = self->output_width;
if (new_height == 0) new_height = self->output_height;
// Validate dimensions
if (new_width <= 0 || new_height <= 0 || new_width > 1920 || new_height > 1920) {
mp_raise_ValueError(MP_ERROR_TEXT("Invalid output dimensions"));
}
// If dimensions changed, reallocate buffers
if (new_width != self->output_width || new_height != self->output_height) {
// Free old buffers
free(self->gray_buffer);
free(self->rgb565_buffer);
// Update dimensions
self->output_width = new_width;
self->output_height = new_height;
// Allocate new buffers
self->gray_buffer = (unsigned char *)malloc(self->output_width * self->output_height * sizeof(unsigned char));
self->rgb565_buffer = (uint16_t *)malloc(self->output_width * self->output_height * sizeof(uint16_t));
if (!self->gray_buffer || !self->rgb565_buffer) {
free(self->gray_buffer);
free(self->rgb565_buffer);
self->gray_buffer = NULL;
self->rgb565_buffer = NULL;
mp_raise_OSError(MP_ENOMEM);
}
WEBCAM_DEBUG_PRINT("Webcam reconfigured to %dx%d\n", self->output_width, self->output_height);
}
return mp_const_none;
}
MP_DEFINE_CONST_FUN_OBJ_KW(webcam_reconfigure_obj, 1, webcam_reconfigure);
static const mp_obj_type_t webcam_type = {
{ &mp_type_type },
.name = MP_QSTR_Webcam,
@@ -321,6 +412,7 @@ static const mp_rom_map_elem_t mp_module_webcam_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR_capture_frame), MP_ROM_PTR(&webcam_capture_frame_obj) },
{ MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&webcam_deinit_obj) },
{ MP_ROM_QSTR(MP_QSTR_free_buffer), MP_ROM_PTR(&webcam_free_buffer_obj) },
{ MP_ROM_QSTR(MP_QSTR_reconfigure), MP_ROM_PTR(&webcam_reconfigure_obj) },
};
static MP_DEFINE_CONST_DICT(mp_module_webcam_globals, mp_module_webcam_globals_table);
@@ -22,6 +22,11 @@
"category": "default"
}
]
},
{
"entrypoint": "assets/camera_app.py",
"classname": "CameraSettingsActivity",
"intent_filters": []
}
]
}
@@ -13,6 +13,8 @@ except Exception as e:
print(f"Info: could not import webcam module: {e}")
from mpos.apps import Activity
from mpos.config import SharedPreferences
from mpos.content.intent import Intent
import mpos.time
class CameraApp(Activity):
@@ -20,6 +22,9 @@ class CameraApp(Activity):
width = 240
height = 240
# Resolution preferences
prefs = None
status_label_text = "No camera found."
status_label_text_searching = "Searching QR codes...\n\nHold still and try varying scan distance (10-25cm) and QR size (4-12cm). Ensure proper lighting."
status_label_text_found = "Decoding QR..."
@@ -42,7 +47,24 @@ class CameraApp(Activity):
status_label = None
status_label_cont = None
def load_resolution_preference(self):
"""Load resolution preference from SharedPreferences and update width/height."""
if not self.prefs:
self.prefs = SharedPreferences("com.micropythonos.camera")
resolution_str = self.prefs.get_string("resolution", "240x240")
try:
width_str, height_str = resolution_str.split('x')
self.width = int(width_str)
self.height = int(height_str)
print(f"Camera resolution loaded: {self.width}x{self.height}")
except Exception as e:
print(f"Error parsing resolution '{resolution_str}': {e}, using default 240x240")
self.width = 240
self.height = 240
def onCreate(self):
self.load_resolution_preference()
self.scanqr_mode = self.getIntent().extras.get("scanqr_mode")
main_screen = lv.obj()
main_screen.set_style_pad_all(0, 0)
@@ -56,6 +78,16 @@ class CameraApp(Activity):
close_label.set_text(lv.SYMBOL.CLOSE)
close_label.center()
close_button.add_event_cb(lambda e: self.finish(),lv.EVENT.CLICKED,None)
# Settings button
settings_button = lv.button(main_screen)
settings_button.set_size(60,60)
settings_button.align(lv.ALIGN.TOP_LEFT, 0, 0)
settings_label = lv.label(settings_button)
settings_label.set_text(lv.SYMBOL.SETTINGS)
settings_label.center()
settings_button.add_event_cb(lambda e: self.open_settings(),lv.EVENT.CLICKED,None)
self.snap_button = lv.button(main_screen)
self.snap_button.set_size(60, 60)
self.snap_button.align(lv.ALIGN.RIGHT_MID, 0, 0)
@@ -103,10 +135,10 @@ class CameraApp(Activity):
self.setContentView(main_screen)
def onResume(self, screen):
self.cam = init_internal_cam()
self.cam = init_internal_cam(self.width, self.height)
if not self.cam:
# try again because the manual i2c poweroff leaves it in a bad state
self.cam = init_internal_cam()
self.cam = init_internal_cam(self.width, self.height)
if self.cam:
self.image.set_rotation(900) # internal camera is rotated 90 degrees
else:
@@ -114,6 +146,9 @@ class CameraApp(Activity):
try:
self.cam = webcam.init("/dev/video0")
self.use_webcam = True
# Reconfigure webcam to use saved resolution
print(f"Reconfiguring webcam to {self.width}x{self.height}")
webcam.reconfigure(self.cam, output_width=self.width, output_height=self.height)
except Exception as e:
print(f"camera app: webcam exception: {e}")
if self.cam:
@@ -241,7 +276,42 @@ class CameraApp(Activity):
self.start_qr_decoding()
else:
self.stop_qr_decoding()
def open_settings(self):
"""Launch the camera settings activity."""
intent = Intent(activity_class=CameraSettingsActivity)
self.startActivityForResult(intent, self.handle_settings_result)
def handle_settings_result(self, result):
"""Handle result from settings activity."""
if result.get("result_code") == True:
print("Settings changed, reloading resolution...")
# Reload resolution preference
self.load_resolution_preference()
# Recreate image descriptor with new dimensions
self.image_dsc["header"]["w"] = self.width
self.image_dsc["header"]["h"] = self.height
self.image_dsc["header"]["stride"] = self.width * 2
self.image_dsc["data_size"] = self.width * self.height * 2
# Reconfigure camera if active
if self.cam:
if self.use_webcam:
print(f"Reconfiguring webcam to {self.width}x{self.height}")
webcam.reconfigure(self.cam, output_width=self.width, output_height=self.height)
else:
# For internal camera, need to reinitialize
print(f"Reinitializing internal camera to {self.width}x{self.height}")
if self.capture_timer:
self.capture_timer.delete()
self.cam.deinit()
self.cam = init_internal_cam(self.width, self.height)
if self.cam:
self.capture_timer = lv.timer_create(self.try_capture, 100, None)
self.set_image_size()
def try_capture(self, event):
#print("capturing camera frame")
try:
@@ -262,9 +332,36 @@ class CameraApp(Activity):
# Non-class functions:
def init_internal_cam():
def init_internal_cam(width=240, height=240):
"""Initialize internal camera with specified resolution."""
try:
from camera import Camera, GrabMode, PixelFormat, FrameSize, GainCeiling
# Map resolution to FrameSize enum
# Format: (width, height): FrameSize
resolution_map = {
(96, 96): FrameSize.R96X96,
(160, 120): FrameSize.QQVGA,
(128, 128): FrameSize.R128X128,
(176, 144): FrameSize.QCIF,
(240, 176): FrameSize.HQVGA,
(240, 240): FrameSize.R240X240,
(320, 240): FrameSize.QVGA,
(320, 320): FrameSize.R320X320,
(400, 296): FrameSize.CIF,
(480, 320): FrameSize.HVGA,
(640, 480): FrameSize.VGA,
(800, 600): FrameSize.SVGA,
(1024, 768): FrameSize.XGA,
(1280, 720): FrameSize.HD,
(1280, 1024): FrameSize.SXGA,
(1600, 1200): FrameSize.UXGA,
(1920, 1080): FrameSize.FHD,
}
frame_size = resolution_map.get((width, height), FrameSize.R240X240)
print(f"init_internal_cam: Using FrameSize for {width}x{height}")
cam = Camera(
data_pins=[12,13,15,11,14,10,7,2],
vsync_pin=6,
@@ -277,15 +374,9 @@ def init_internal_cam():
powerdown_pin=-1,
reset_pin=-1,
pixel_format=PixelFormat.RGB565,
#pixel_format=PixelFormat.GRAYSCALE,
frame_size=FrameSize.R240X240,
grab_mode=GrabMode.LATEST
frame_size=frame_size,
grab_mode=GrabMode.LATEST
)
#cam.init() automatically done when creating the Camera()
#cam.reconfigure(frame_size=FrameSize.HVGA)
#frame_size=FrameSize.HVGA, # 480x320
#frame_size=FrameSize.QVGA, # 320x240
#frame_size=FrameSize.QQVGA # 160x120
cam.set_vflip(True)
return cam
except Exception as e:
@@ -311,3 +402,124 @@ def remove_bom(buffer):
if buffer.startswith(bom):
return buffer[3:]
return buffer
class CameraSettingsActivity(Activity):
"""Settings activity for camera resolution configuration."""
# Resolution options for desktop/webcam
WEBCAM_RESOLUTIONS = [
("160x120", "160x120"),
("240x240", "240x240"), # Default
("320x240", "320x240"),
("480x320", "480x320"),
("640x480", "640x480"),
("800x600", "800x600"),
("1024x768", "1024x768"),
("1280x720", "1280x720"),
]
# Resolution options for internal camera (ESP32) - all available FrameSize options
ESP32_RESOLUTIONS = [
("96x96", "96x96"),
("160x120", "160x120"),
("128x128", "128x128"),
("176x144", "176x144"),
("240x176", "240x176"),
("240x240", "240x240"), # Default
("320x240", "320x240"),
("320x320", "320x320"),
("400x296", "400x296"),
("480x320", "480x320"),
("640x480", "640x480"),
("800x600", "800x600"),
("1024x768", "1024x768"),
("1280x720", "1280x720"),
("1280x1024", "1280x1024"),
("1600x1200", "1600x1200"),
("1920x1080", "1920x1080"),
]
dropdown = None
current_resolution = None
def onCreate(self):
# Load preferences
prefs = SharedPreferences("com.micropythonos.camera")
self.current_resolution = prefs.get_string("resolution", "240x240")
# Create main screen
screen = lv.obj()
screen.set_size(lv.pct(100), lv.pct(100))
screen.set_style_pad_all(10, 0)
# Title
title = lv.label(screen)
title.set_text("Camera Settings")
title.align(lv.ALIGN.TOP_MID, 0, 10)
# Resolution label
resolution_label = lv.label(screen)
resolution_label.set_text("Resolution:")
resolution_label.align(lv.ALIGN.TOP_LEFT, 0, 50)
# Detect if we're on desktop or ESP32 based on available modules
try:
import webcam
resolutions = self.WEBCAM_RESOLUTIONS
print("Using webcam resolutions")
except:
resolutions = self.ESP32_RESOLUTIONS
print("Using ESP32 camera resolutions")
# Create dropdown
self.dropdown = lv.dropdown(screen)
self.dropdown.set_size(200, 40)
self.dropdown.align(lv.ALIGN.TOP_LEFT, 0, 80)
# Build dropdown options string
options_str = "\n".join([label for label, _ in resolutions])
self.dropdown.set_options(options_str)
# Set current selection
for idx, (label, value) in enumerate(resolutions):
if value == self.current_resolution:
self.dropdown.set_selected(idx)
break
# Save button
save_button = lv.button(screen)
save_button.set_size(100, 50)
save_button.align(lv.ALIGN.BOTTOM_MID, -60, -10)
save_button.add_event_cb(lambda e: self.save_and_close(resolutions), lv.EVENT.CLICKED, None)
save_label = lv.label(save_button)
save_label.set_text("Save")
save_label.center()
# Cancel button
cancel_button = lv.button(screen)
cancel_button.set_size(100, 50)
cancel_button.align(lv.ALIGN.BOTTOM_MID, 60, -10)
cancel_button.add_event_cb(lambda e: self.finish(), lv.EVENT.CLICKED, None)
cancel_label = lv.label(cancel_button)
cancel_label.set_text("Cancel")
cancel_label.center()
self.setContentView(screen)
def save_and_close(self, resolutions):
"""Save selected resolution and return result."""
selected_idx = self.dropdown.get_selected()
_, new_resolution = resolutions[selected_idx]
# Save to preferences
prefs = SharedPreferences("com.micropythonos.camera")
editor = prefs.edit()
editor.put_string("resolution", new_resolution)
editor.commit()
print(f"Camera resolution saved: {new_resolution}")
# Return success result
self.setResult(True, {"resolution": new_resolution})
self.finish()
@@ -160,7 +160,7 @@ class WiFi(Activity):
def password_page_result_cb(self, result):
print(f"PasswordPage finished, result: {result}")
if result.get("result_code"):
if result.get("result_code") is True:
data = result.get("data")
if data:
self.start_attempt_connecting(data.get("ssid"), data.get("password"))