Skip to content

Commit 204730a

Browse files
authored
Merge pull request #200 from FolkComputer/osnr/camera-usb-set-exposure
/calibrate: Manual exposure slider
2 parents 5e5657f + 75325d1 commit 204730a

File tree

2 files changed

+103
-12
lines changed

2 files changed

+103
-12
lines changed

virtual-programs/calibrate/calibrate.folk

Lines changed: 75 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,9 @@ Wish the web server handles route "/calibrate$" with handler [list apply {{UNIT_
384384
[list /someone/ claims the default program geometry is /defaultGeom/]] 0] defaultGeom]
385385
fn defaultGeomGet {key} { return [string map {mm ""} [dict get $defaultGeom $key]] }
386386

387+
set camera [dict get [lindex [Statements::findMatches \
388+
[list /someone/ claims camera /camera/ has width /cameraWidth/ height /cameraHeight/]] 0] camera]
389+
387390
upvar ^html ^html
388391
html [csubst {
389392
<html>
@@ -464,15 +467,15 @@ Wish the web server handles route "/calibrate$" with handler [list apply {{UNIT_
464467
});
465468
</script>
466469

467-
<p>Once you start calibration, you'll see some AprilTags get automatically projected on your table. Move your board to the projected tags <strong>so that at least one projected tag sits inside the gap between printed AprilTags</strong>, wait a second for the projected tags to refit into the grid,
470+
<p>Once you start calibration, you'll see some AprilTags get automatically projected on your table. Move your board to the projected tags <em>so that at least one projected tag sits inside the gap between printed AprilTags</em>, wait a second for the projected tags to refit into the grid,
468471
then <strong>hold the board still for a few seconds until
469472
the pose is recorded.</strong></p>
470473

471474
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/l1liP4_yiVM?si=DqgfNKq05EPBT3hT" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
472475

473-
<p style="font-style: italic; width: 100%; text-align: center;">Example video of Andr&eacute;s calibration the Folk0 system (playing at 2x speed).</p>
476+
<p style="font-style: italic; width: 100%; text-align: center;">Example video of Andr&eacute;s calibrating the folk0 system (2x speed)</p>
474477

475-
<p>Are the projected tags too big to fit in the gaps between printed tags? Adjust this slider to reset & adjust the default projected tag size:
478+
<p><strong>Are the projected tags too big to fit in the gaps between printed tags?</strong> Adjust this slider to reset & adjust the default projected tag size:
476479
<input type="range" min="10" max="100" value="100" class="slider" id="projected-tag-slider">
477480
</p>
478481
<script>
@@ -482,11 +485,69 @@ Wish the web server handles route "/calibrate$" with handler [list apply {{UNIT_
482485
{*}\$::HoldDefaultModel \${scale}
483486
`);
484487
});
488+
489+
function advanceCamera() {
490+
cameraFrame.src = cameraFrame.src + '0'
491+
}
492+
</script>
493+
494+
<p>Use this camera preview to debug why printed and/or projected tags aren't being recognized (maybe overexposure, maybe your camera isn't in a good position): <button id="refreshButton" onclick="advanceCamera()">Refresh Preview</button> <input type="checkbox" value="true" id="auto-refresh-checkbox" checked>
495+
<label for="auto-refresh-checkbox">Automatically refresh preview (May not work well during calibration)</label> </p><br> <img src="/camera-frame?0" id="cameraFrame" style="max-width: 100%">
496+
497+
<script>
498+
const refreshButton = document.getElementById('refreshButton');
499+
const autoRefreshCheckbox = document.getElementById('auto-refresh-checkbox');
500+
setInterval(() => {
501+
if (autoRefreshCheckbox.checked) {
502+
refreshButton.click()
503+
}
504+
console.log("checked?", autoRefreshCheckbox.checked)
505+
}, 100)
485506
</script>
486507

487-
<p>Use this camera preview to debug why printed and/or projected tags aren't being recognized (maybe overexposure, maybe your camera isn't in a good position): <button onclick="cameraFrame.src = cameraFrame.src + '0'">Refresh Preview</button></p><br> <img src="/camera-frame?0" id="cameraFrame" style="max-width: 100%">
488508

489-
<p>Once you've recorded the first pose, <strong>slowly drag the board around your space</strong>, going slow enough for the projected AprilTags to catch up with the printed AprilTags and fit into the gaps on your board. When you've moved the board at least a full board-length away from the first pose, try to slant it 45 degrees or so off the table and hold it still again to capture another pose.</p>
509+
<p><strong>Is the projection too bright and washing out the camera?</strong>
510+
<br/>
511+
Adjust camera exposure:
512+
<input type="range" min="10" max="3200" value="100" class="slider" id="camera-exposure-slider">
513+
<input type="number" min="1000" max="320000" step="100" value="10000" id="camera-exposure-number">μs
514+
</p>
515+
516+
<script>
517+
const slider = document.getElementById('camera-exposure-slider');
518+
const numberInput = document.getElementById('camera-exposure-number');
519+
let lastRefreshTimeout = null;
520+
521+
function updateExposure(valueInMicroseconds) {
522+
const sliderValue = valueInMicroseconds / 100;
523+
524+
slider.value = sliderValue;
525+
numberInput.value = valueInMicroseconds;
526+
527+
ws.hold('exposure', tcl`
528+
Wish camera $camera uses exposure time \${valueInMicroseconds} us
529+
`, 'virtual-programs/calibrate/calibrate.folk');
530+
531+
if (lastRefreshTimeout != null) {
532+
window.clearTimeout(lastRefreshTimeout);
533+
}
534+
lastRefreshTimeout = setTimeout(() => { advanceCamera() }, 500);
535+
}
536+
537+
slider.addEventListener('input', (e) => {
538+
const exposureValue = Math.round(e.target.value * 100);
539+
updateExposure(exposureValue);
540+
});
541+
542+
numberInput.addEventListener('input', (e) => {
543+
const exposureValue = parseInt(e.target.value);
544+
updateExposure(exposureValue);
545+
});
546+
547+
updateExposure(10000);
548+
</script>
549+
550+
<p>Once you've recorded the first pose, <em>slowly drag the board around your space</em>, going slow enough for the projected AprilTags to catch up with the printed AprilTags and fit into the gaps on your board. When you've moved the board at least a full board-length away from the first pose, try to slant it 45 degrees or so off the table and hold it still again to capture another pose.</p>
490551

491552
<p>Repeat this process of dragging the board around and
492553
capturing a new pose. You'll need to record 10 different
@@ -498,7 +559,7 @@ Wish the web server handles route "/calibrate$" with handler [list apply {{UNIT_
498559
<details>
499560
<summary>Troubleshooting</summary>
500561
<p>Look at ~/folk-calibration-poses to see images of the captured poses (maybe tags are distorted or washed out?).</p>
501-
<p>You can try manually adjusting webcam settings if your poses are bad. (They should be immediately reflected in the camera preview once you refresh.) Folk tries to turn off autofocus by default, but you might also want to turn off autoexposure and set a manual exposure time. For example:</p>
562+
<p>You can try manually adjusting webcam settings if your poses are bad. (They should be immediately reflected in the camera preview once you refresh.) Folk tries to turn off autofocus by default, and you might also want to check that your camera actually has an exposure setting and focus setting. For example:</p>
502563
<pre>
503564
\$ v4l2-ctl --device=/dev/video0 --list-ctrls
504565

@@ -524,9 +585,10 @@ Camera Controls
524585
focus_absolute 0x009a090a (int) : min=0 max=250 step=5 default=0 value=30
525586
focus_automatic_continuous 0x009a090c (bool) : default=1 value=0
526587
zoom_absolute 0x009a090d (int) : min=100 max=500 step=1 default=100 value=100
527-
\$ v4l2-ctl --device=/dev/video0 --set-ctrl=auto_exposure=1
588+
\$ v4l2-ctl --device=/dev/video0 --set-ctrl=auto_exposure=1 # to set them manually from terminal
528589
\$ v4l2-ctl --device=/dev/video0 --set-ctrl=exposure_time_absolute=25
529590
</pre>
591+
<p>Camera needs to have auto_exposure and exposure_time_absolute settings listed for Folk to be able to set them.</p>
530592
</details>
531593
</li>
532594

@@ -671,10 +733,6 @@ When camera /camera/ has width /cameraWidth/ height /cameraHeight/ &\
671733
# TODO: restore old camera resolution later
672734
}
673735

674-
# HACK: hard-coded for now; assumes dark room. Won't work on USB
675-
# webcams yet, either (just Pi).
676-
Wish camera $camera uses exposure time 16000 us
677-
678736
set tagSideLength 1.0
679737
set tagOuterLength [expr {$tagSideLength * 10/6}]
680738
set pad $tagSideLength
@@ -793,7 +851,12 @@ When camera /camera/ has width /cameraWidth/ height /cameraHeight/ &\
793851
model /anything/ version /anything/ timestamp /anything/ {
794852
HoldDefaultModel 1.0
795853
}
796-
On unmatch { Hold H_modelToDisplay {} }
854+
On unmatch {
855+
Hold H_modelToDisplay {}
856+
Hold exposure {
857+
Wish camera $camera uses exposure time auto us
858+
}
859+
}
797860

798861
When main-detector detects tags /tags/ on $camera at /timestamp/ in time /something/ & \
799862
the calibration model-to-display homography is /H_modelToDisplay/ with \

virtual-programs/camera-usb.folk

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,24 @@ set makeCamera {
261261
folkHeapFree(image.data);
262262
}
263263

264+
camc proc setExposure {camera_t* camera int value} void {
265+
struct v4l2_control c;
266+
267+
c.id = V4L2_CID_EXPOSURE_AUTO;
268+
c.value = V4L2_EXPOSURE_MANUAL;
269+
FOLK_ENSURE(xioctl(camera->fd, VIDIOC_S_CTRL, &c) == 0);
270+
271+
c.id = V4L2_CID_EXPOSURE_ABSOLUTE;
272+
c.value = value;
273+
FOLK_ENSURE(xioctl(camera->fd, VIDIOC_S_CTRL, &c) == 0);
274+
}
275+
camc proc setExposureAuto {camera_t* camera} void {
276+
struct v4l2_control c;
277+
c.id = V4L2_CID_EXPOSURE_AUTO;
278+
c.value = V4L2_EXPOSURE_APERTURE_PRIORITY;
279+
FOLK_ENSURE(xioctl(camera->fd, VIDIOC_S_CTRL, &c) == 0);
280+
}
281+
264282
if {$::tcl_platform(os) eq "Darwin"} {
265283
c loadlib "/opt/homebrew/lib/libjpeg.dylib"
266284
} else {
@@ -324,6 +342,8 @@ When /someone/ wishes $::thisNode uses camera /cameraPath/ with /...options/ {
324342

325343
Wish $::thisProcess shares statements like \
326344
[list /someone/ claims camera $cameraPath /...anything/]
345+
Wish $::thisProcess receives statements like \
346+
[list /someone/ wishes camera $cameraPath uses exposure time /exposureTimeUs/ us]
327347

328348
namespace eval Camera $makeCamera
329349
set camera [Camera::new $cameraPath $width $height $bufferCount]
@@ -333,6 +353,14 @@ When /someone/ wishes $::thisNode uses camera /cameraPath/ with /...options/ {
333353

334354
puts "camera-usb: $cameraPath ($options) (tid [getTid]) booted at [clock milliseconds]"
335355

356+
When /someone/ wishes camera $cameraPath uses exposure time /exposureTimeUs/ us {
357+
if {$exposureTimeUs eq "auto"} {
358+
Camera::setExposureAuto $camera
359+
} else {
360+
Camera::setExposure $camera [expr {int($exposureTimeUs / 100)}]
361+
}
362+
}
363+
336364
set ::oldFrames [list]
337365
When $::thisProcess has step count /c/ {
338366
set frame [Camera::grayFrame $camera]

0 commit comments

Comments
 (0)