From e8fb50659dc4b46ae4cce755a6ea94fe360376c6 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Tue, 27 May 2025 01:00:55 -0500 Subject: [PATCH] add colorspace support; fix webcam --- source/qjs_sdl.c | 59 +++++++++++----- source/qjs_sdl_surface.c | 28 +++++++- tests/camera_colorspace.js | 72 ++++++++++++++++++++ tests/camera_colorspace_convert.js | 106 +++++++++++++++++++++++++++++ tests/surface_colorspace.js | 63 +++++++++++++++++ tests/webcam.js | 31 ++++++--- 6 files changed, 332 insertions(+), 27 deletions(-) create mode 100644 tests/camera_colorspace.js create mode 100644 tests/camera_colorspace_convert.js create mode 100644 tests/surface_colorspace.js diff --git a/source/qjs_sdl.c b/source/qjs_sdl.c index c47d41aa..5627b024 100644 --- a/source/qjs_sdl.c +++ b/source/qjs_sdl.c @@ -298,11 +298,46 @@ SDL_PixelFormat str2pixelformat(const char *str) { return SDL_PIXELFORMAT_UNKNOWN; } +const char *colorspace2str(SDL_Colorspace colorspace) { + switch(colorspace) { + case SDL_COLORSPACE_UNKNOWN: return "unknown"; + case SDL_COLORSPACE_SRGB: return "srgb"; + case SDL_COLORSPACE_SRGB_LINEAR: return "srgb_linear"; + case SDL_COLORSPACE_HDR10: return "hdr10"; + case SDL_COLORSPACE_JPEG: return "jpeg"; + case SDL_COLORSPACE_BT601_LIMITED: return "bt601_limited"; + case SDL_COLORSPACE_BT601_FULL: return "bt601_full"; + case SDL_COLORSPACE_BT709_LIMITED: return "bt709_limited"; + case SDL_COLORSPACE_BT709_FULL: return "bt709_full"; + case SDL_COLORSPACE_BT2020_LIMITED: return "bt2020_limited"; + case SDL_COLORSPACE_BT2020_FULL: return "bt2020_full"; + default: return "unknown"; + } +} + +SDL_Colorspace str2colorspace(const char *str) { + if (!str) return SDL_COLORSPACE_UNKNOWN; + + if (!strcmp(str, "unknown")) return SDL_COLORSPACE_UNKNOWN; + if (!strcmp(str, "srgb")) return SDL_COLORSPACE_SRGB; + if (!strcmp(str, "srgb_linear")) return SDL_COLORSPACE_SRGB_LINEAR; + if (!strcmp(str, "hdr10")) return SDL_COLORSPACE_HDR10; + if (!strcmp(str, "jpeg")) return SDL_COLORSPACE_JPEG; + if (!strcmp(str, "bt601_limited")) return SDL_COLORSPACE_BT601_LIMITED; + if (!strcmp(str, "bt601_full")) return SDL_COLORSPACE_BT601_FULL; + if (!strcmp(str, "bt709_limited")) return SDL_COLORSPACE_BT709_LIMITED; + if (!strcmp(str, "bt709_full")) return SDL_COLORSPACE_BT709_FULL; + if (!strcmp(str, "bt2020_limited")) return SDL_COLORSPACE_BT2020_LIMITED; + if (!strcmp(str, "bt2020_full")) return SDL_COLORSPACE_BT2020_FULL; + + return SDL_COLORSPACE_UNKNOWN; +} + static JSValue cameraspec2js(JSContext *js, const SDL_CameraSpec *spec) { JSValue obj = JS_NewObject(js); JS_SetPropertyStr(js, obj, "format", JS_NewString(js, pixelformat2str(spec->format))); - JS_SetPropertyStr(js, obj, "colorspace", JS_NewInt32(js, spec->colorspace)); + JS_SetPropertyStr(js, obj, "colorspace", JS_NewString(js, colorspace2str(spec->colorspace))); JS_SetPropertyStr(js, obj, "width", JS_NewInt32(js, spec->width)); JS_SetPropertyStr(js, obj, "height", JS_NewInt32(js, spec->height)); JS_SetPropertyStr(js, obj, "framerate_numerator", JS_NewInt32(js, spec->framerate_numerator)); @@ -325,7 +360,11 @@ static SDL_CameraSpec js2cameraspec(JSContext *js, JSValue obj) { JS_FreeValue(js, v); v = JS_GetPropertyStr(js, obj, "colorspace"); - if (!JS_IsUndefined(v)) JS_ToInt32(js, &spec.colorspace, v); + if (!JS_IsUndefined(v)) { + const char *s = JS_ToCString(js, v); + spec.colorspace = str2colorspace(s); + JS_FreeCString(js, s); + } JS_FreeValue(js, v); v = JS_GetPropertyStr(js, obj, "width"); @@ -438,24 +477,10 @@ JSC_CCALL(camera_capture, } // Create a copy of the surface - SDL_Surface *newsurf = SDL_CreateSurface(surf->w, surf->h, surf->format); + SDL_Surface *newsurf = SDL_DuplicateSurface(surf); - if (!newsurf) { - SDL_ReleaseCameraFrame(cam, surf); - return JS_ThrowReferenceError(js, "Could not create surface: %s", SDL_GetError()); - } - - // Copy the surface data - int result = SDL_BlitSurface(surf, NULL, newsurf, NULL); - - // Release the camera frame SDL_ReleaseCameraFrame(cam, surf); - if (result != 0) { - SDL_DestroySurface(newsurf); - return JS_ThrowReferenceError(js, "Could not blit surface: %s", SDL_GetError()); - } - return SDL_Surface2js(js,newsurf); ) diff --git a/source/qjs_sdl_surface.c b/source/qjs_sdl_surface.c index c54b21ed..356fb76c 100644 --- a/source/qjs_sdl_surface.c +++ b/source/qjs_sdl_surface.c @@ -15,6 +15,8 @@ extern colorf js2color(JSContext *js, JSValue v); extern HMM_Vec2 js2vec2(JSContext *js, JSValue v); extern SDL_PixelFormat str2pixelformat(const char *str); extern const char *pixelformat2str(SDL_PixelFormat fmt); +extern SDL_Colorspace str2colorspace(const char *str); +extern const char *colorspace2str(SDL_Colorspace colorspace); static JSValue pixelformat2js(JSContext *js, SDL_PixelFormat fmt) { @@ -33,6 +35,17 @@ static SDL_PixelFormat js2pixelformat(JSContext *js, JSValue v) return fmt; } +static SDL_Colorspace js2colorspace(JSContext *js, JSValue v) +{ + if (JS_IsUndefined(v)) return SDL_COLORSPACE_UNKNOWN; + const char *s = JS_ToCString(js, v); + if (!s) return SDL_COLORSPACE_UNKNOWN; + + SDL_Colorspace cs = str2colorspace(s); + JS_FreeCString(js,s); + return cs; +} + typedef struct { const char *name; SDL_ScaleMode mode; } scale_entry; static const scale_entry k_scale_table[] = { @@ -139,7 +152,18 @@ JSC_CCALL(surface_rect, JSC_CCALL(surface_convert, SDL_Surface *surf = js2SDL_Surface(js,self); SDL_PixelFormat fmt = js2pixelformat(js, argv[0]); - SDL_Surface *dst = SDL_ConvertSurface(surf, fmt); + + SDL_Surface *dst; + if (argc > 1 && !JS_IsUndefined(argv[1])) { + // Colorspace provided, use SDL_ConvertSurfaceAndColorspace + SDL_Colorspace colorspace = js2colorspace(js, argv[1]); + SDL_PropertiesID props = 0; // No additional properties needed + dst = SDL_ConvertSurfaceAndColorspace(surf, fmt, NULL, colorspace, props); + } else { + // No colorspace, use regular convert + dst = SDL_ConvertSurface(surf, fmt); + } + if (!dst) return JS_ThrowInternalError(js, "Convert failed: %s", SDL_GetError()); return SDL_Surface2js(js, dst); @@ -304,7 +328,7 @@ static const JSCFunctionListEntry js_SDL_Surface_funcs[] = { MIST_FUNC_DEF(surface, rect,2), MIST_FUNC_DEF(surface, dup, 0), MIST_FUNC_DEF(surface, pixels, 0), - MIST_FUNC_DEF(surface, convert, 1), + MIST_FUNC_DEF(surface, convert, 2), MIST_FUNC_DEF(surface, toJSON, 0), JS_CGETSET_DEF("width", js_surface_get_width, NULL), JS_CGETSET_DEF("height", js_surface_get_height, NULL), diff --git a/tests/camera_colorspace.js b/tests/camera_colorspace.js new file mode 100644 index 00000000..b90b3a3c --- /dev/null +++ b/tests/camera_colorspace.js @@ -0,0 +1,72 @@ +// Test camera colorspace functionality +var camera = use('camera'); +var json = use('json'); + +// Get list of cameras +var cameras = camera.list(); +if (cameras.length === 0) { + console.log("No cameras found!"); + $_. stop(); +} + +var cam_id = cameras[0]; +console.log("Testing camera:", camera.name(cam_id)); + +// Get supported formats +var formats = camera.supported_formats(cam_id); +console.log("\nLooking for different colorspaces in supported formats..."); + +// Group formats by colorspace +var colorspaces = {}; +for (var i = 0; i < formats.length; i++) { + var fmt = formats[i]; + if (!colorspaces[fmt.colorspace]) { + colorspaces[fmt.colorspace] = []; + } + colorspaces[fmt.colorspace].push(fmt); +} + +console.log("\nFound colorspaces:"); +for (var cs in colorspaces) { + console.log(" " + cs + ": " + colorspaces[cs].length + " formats"); +} + +// Try opening camera with different colorspaces +console.log("\nTrying to open camera with different colorspaces..."); + +for (var cs in colorspaces) { + // Get first format for this colorspace + var format = colorspaces[cs][0]; + + console.log("\nTrying colorspace '" + cs + "' with format:"); + console.log(" Resolution: " + format.width + "x" + format.height); + console.log(" Pixel format: " + format.format); + + // You can also create a custom format with a specific colorspace + var custom_format = { + format: format.format, + colorspace: cs, // This will be converted from string + width: format.width, + height: format.height, + framerate_numerator: format.framerate_numerator, + framerate_denominator: format.framerate_denominator + }; + + var cam = camera.open(cam_id, custom_format); + if (cam) { + var actual = cam.get_format(); + console.log(" Opened successfully!"); + console.log(" Actual colorspace: " + actual.colorspace); + + // Camera will be closed when object is freed + cam = null; + } else { + console.log(" Failed to open with this colorspace"); + } + + // Just test first 3 colorspaces + if (Object.keys(colorspaces).indexOf(cs) >= 2) break; +} + +console.log("\nColorspace test complete!"); +$_.stop(); \ No newline at end of file diff --git a/tests/camera_colorspace_convert.js b/tests/camera_colorspace_convert.js new file mode 100644 index 00000000..47f6b42d --- /dev/null +++ b/tests/camera_colorspace_convert.js @@ -0,0 +1,106 @@ +// Test camera capture with colorspace conversion +var camera = use('camera'); +var surface = use('surface'); +var json = use('json'); + +// Get first camera +var cameras = camera.list(); +if (cameras.length === 0) { + console.log("No cameras found!"); + $_.stop(); +} + +var cam_id = cameras[0]; +console.log("Using camera:", camera.name(cam_id)); + +// Open camera with default settings +var cam = camera.open(cam_id); +if (!cam) { + console.log("Failed to open camera!"); + $_.stop(); +} + +// Get the format being used +var format = cam.get_format(); +console.log("\nCamera format:"); +console.log(" Resolution:", format.width + "x" + format.height); +console.log(" Pixel format:", format.format); +console.log(" Colorspace:", format.colorspace); + +// Handle camera approval +var approved = false; +$_.receiver(e => { + if (e.type === 'camera_device_approved') { + console.log("\nCamera approved!"); + approved = true; + } else if (e.type === 'camera_device_denied') { + console.error("Camera access denied!"); + $_.stop(); + } +}); + +// Wait for approval then capture +function capture_test() { + if (!approved) { + $_.delay(capture_test, 0.1); + return; + } + + console.log("\nCapturing frame..."); + var surf = cam.capture(); + + if (!surf) { + console.log("No frame captured yet, retrying..."); + $_.delay(capture_test, 0.1); + return; + } + + console.log("\nCaptured surface:"); + console.log(" Size:", surf.width + "x" + surf.height); + console.log(" Format:", surf.format); + + // Test various colorspace conversions + console.log("\nTesting colorspace conversions:"); + + // Convert to sRGB if not already + if (format.colorspace !== "srgb") { + try { + var srgb_surf = surf.convert(surf.format, "srgb"); + console.log(" Converted to sRGB colorspace"); + } catch(e) { + console.log(" sRGB conversion failed:", e.message); + } + } + + // Convert to linear sRGB for processing + try { + var linear_surf = surf.convert("rgba8888", "srgb_linear"); + console.log(" Converted to linear sRGB (RGBA8888) for processing"); + } catch(e) { + console.log(" Linear sRGB conversion failed:", e.message); + } + + // Convert to JPEG colorspace (common for compression) + try { + var jpeg_surf = surf.convert("rgb888", "jpeg"); + console.log(" Converted to JPEG colorspace (RGB888) for compression"); + } catch(e) { + console.log(" JPEG colorspace conversion failed:", e.message); + } + + // If YUV format, try BT.709 (HD video standard) + if (surf.format.indexOf("yuv") !== -1 || surf.format.indexOf("yuy") !== -1) { + try { + var hd_surf = surf.convert(surf.format, "bt709_limited"); + console.log(" Converted to BT.709 limited (HD video standard)"); + } catch(e) { + console.log(" BT.709 conversion failed:", e.message); + } + } + + console.log("\nTest complete!"); + $_.stop(); +} + +// Start capture test after a short delay +$_.delay(capture_test, 0.5); \ No newline at end of file diff --git a/tests/surface_colorspace.js b/tests/surface_colorspace.js new file mode 100644 index 00000000..86873351 --- /dev/null +++ b/tests/surface_colorspace.js @@ -0,0 +1,63 @@ +// Test surface colorspace conversion +var surface = use('surface'); +var json = use('json'); + +// Create a test surface +var surf = surface({ + width: 640, + height: 480, + format: "rgb888" +}); + +console.log("Created surface:"); +console.log(" Size:", surf.width + "x" + surf.height); +console.log(" Format:", surf.format); + +// Fill with a test color +surf.fill([1, 0.5, 0.25, 1]); // Orange color + +// Test 1: Convert format only (no colorspace change) +console.log("\nTest 1: Convert to RGBA8888 format only"); +var converted1 = surf.convert("rgba8888"); +console.log(" New format:", converted1.format); + +// Test 2: Convert format and colorspace +console.log("\nTest 2: Convert to YUY2 format with JPEG colorspace"); +var converted2 = surf.convert("yuy2", "jpeg"); +console.log(" New format:", converted2.format); + +// Test 3: Try different colorspaces +var colorspaces = ["srgb", "srgb_linear", "jpeg", "bt601_limited", "bt709_limited"]; +var test_format = "rgba8888"; + +console.log("\nTest 3: Converting to", test_format, "with different colorspaces:"); +for (var i = 0; i < colorspaces.length; i++) { + try { + var conv = surf.convert(test_format, colorspaces[i]); + console.log(" " + colorspaces[i] + ": Success"); + } catch(e) { + console.log(" " + colorspaces[i] + ": Failed -", e.message); + } +} + +// Test 4: YUV formats with appropriate colorspaces +console.log("\nTest 4: YUV format conversions:"); +var yuv_tests = [ + {format: "yuy2", colorspace: "jpeg"}, + {format: "nv12", colorspace: "bt601_limited"}, + {format: "nv21", colorspace: "bt709_limited"}, + {format: "yvyu", colorspace: "bt601_full"} +]; + +for (var i = 0; i < yuv_tests.length; i++) { + var test = yuv_tests[i]; + try { + var conv = surf.convert(test.format, test.colorspace); + console.log(" " + test.format + " with " + test.colorspace + ": Success"); + } catch(e) { + console.log(" " + test.format + " with " + test.colorspace + ": Failed -", e.message); + } +} + +console.log("\nColorspace conversion test complete!"); +$_.stop(); \ No newline at end of file diff --git a/tests/webcam.js b/tests/webcam.js index 49903ed4..b5487973 100644 --- a/tests/webcam.js +++ b/tests/webcam.js @@ -86,12 +86,15 @@ send(video_actor, { var formats = camera.supported_formats(cam_id); console.log("Camera supports", formats.length, "formats"); - // Look for a 640x480 format, or fall back to first format + // Look for a 640x480 format with preferred colorspace var preferred_format = null; for (var i = 0; i < formats.length; i++) { if (formats[i].width === 640 && formats[i].height === 480) { preferred_format = formats[i]; - break; + // Prefer JPEG or sRGB colorspace if available + if (formats[i].colorspace === "jpeg" || formats[i].colorspace === "srgb") { + break; + } } } @@ -100,13 +103,15 @@ send(video_actor, { preferred_format = formats[0]; } + preferred_format.framerate_numerator = 30 + if (preferred_format) { console.log("Using format:", preferred_format.width + "x" + preferred_format.height, "FPS:", preferred_format.framerate_numerator + "/" + preferred_format.framerate_denominator, - "Format:", preferred_format.format); + "Format:", preferred_format.format, + "Colorspace:", preferred_format.colorspace); cam_obj = camera.open(cam_id, preferred_format); } else { - console.log("Using default format"); cam_obj = camera.open(cam_id); } @@ -123,6 +128,7 @@ send(video_actor, { console.log("Actual camera format:"); console.log(" Resolution:", actual_format.width + "x" + actual_format.height); console.log(" Format:", actual_format.format); + console.log(" Colorspace:", actual_format.colorspace); console.log(" FPS:", actual_format.framerate_numerator + "/" + actual_format.framerate_denominator); // Start capturing after a short delay to wait for approval @@ -130,6 +136,8 @@ send(video_actor, { }); }); +var captured = false + function start_capturing() { if (!cam_approved) { console.log("Waiting for camera approval..."); @@ -163,7 +171,7 @@ function start_capturing() { draw2d.clear(); // Capture frame from camera - var surface = cam_obj.capture().convert("rgba8888"); + var surface = cam_obj.capture() if (surface) { // Create texture from surface directly @@ -195,12 +203,12 @@ function start_capturing() { op: "copyTexture", data: { texture_id: webcam_texture, - dest: {x: 50, y: 50, width: 700, height: 500} + dest: {x: 50, y: 50, width: 640, height: 480} } }); } else { // Draw placeholder text while waiting for first frame - draw2d.text("Waiting for webcam...", {x: 200, y: 250, size: 20}); +// draw2d.text("Waiting for webcam...", {x: 200, y: 250, size: 20}); } // Draw info @@ -244,5 +252,12 @@ function start_capturing() { capture_and_draw(); } +$_.delay(_ => { + // Capture frame from camera + var surface = cam_obj.capture().convert("rgba8888", "srgb") + console.log('capturing!') + graphics.save_png("test.png", surface.width, surface.height, surface.pixels(),surface.pitch) +}, 3) + // Stop after 12 seconds if not already stopped -$_.delay($_.stop, 12);1 \ No newline at end of file +$_.delay($_.stop, 12); \ No newline at end of file