Skip to content

Commit 1fef8e8

Browse files
committed
feat: implement single-point calibration mode (#99)
- Add calibration mode selection (multi-point vs single-point) - Update store with calibrationMode state and mutations - Add single-point pattern generation (center screen) - Update DoubleCalibrationRecord.vue to support single-point mode - Add calibration mode selector in CameraConfiguration.vue - Dynamic stepper content based on calibration mode - Maintain backward compatibility with existing multi-point mode Features: - Quick single-point calibration for faster setup - Lower accuracy trade-off for speed - Center screen point positioning - Dynamic UI based on selected mode - Proper state management integration Addresses #99
1 parent de6aecc commit 1fef8e8

3 files changed

Lines changed: 270 additions & 8 deletions

File tree

src/store/calibration.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ export default {
2323
threshold: 200,
2424
calibrations: [],
2525
fromDashboard: false,
26+
// New single-point calibration features
27+
calibrationMode: 'multi-point', // 'multi-point' | 'single-point'
28+
singlePointPosition: null, // Will be calculated as screen center
2629
runtime: {
2730
circleIrisPoints: [],
2831
calibPredictionPoints: [],
@@ -119,6 +122,22 @@ export default {
119122
setFromDashboard(state, newFromDashboard) {
120123
state.fromDashboard = newFromDashboard;
121124
},
125+
126+
// New single-point calibration mutations
127+
setCalibrationMode(state, mode) {
128+
state.calibrationMode = mode;
129+
// If switching to single-point, set pointNumber to 1
130+
if (mode === 'single-point') {
131+
state.pointNumber = 1;
132+
} else {
133+
state.pointNumber = 9; // Default multi-point
134+
}
135+
},
136+
137+
setSinglePointPosition(state, position) {
138+
state.singlePointPosition = position;
139+
},
140+
122141
setRuntimeData(state, payload) {
123142
state.runtime.circleIrisPoints = payload.circleIrisPoints;
124143
state.runtime.calibPredictionPoints = payload.calibPredictionPoints;
@@ -155,6 +174,9 @@ export default {
155174
state.threshold = 200;
156175
state.calibrations = [];
157176
state.fromDashboard = false;
177+
// Reset single-point calibration properties
178+
state.calibrationMode = 'multi-point';
179+
state.singlePointPosition = null;
158180
},
159181
},
160182
actions: {
@@ -180,10 +202,28 @@ export default {
180202

181203
return positions;
182204
},
205+
<<<<<<< HEAD
183206
async saveCalib({ state, dispatch }) {
184207
try {
185208
const data = { ...state };
186209
delete data.calibrations;
210+
=======
211+
212+
// Generate single-point calibration pattern (center of screen)
213+
generateSinglePointPattern({ commit }, { width, height }) {
214+
const centerPoint = {
215+
x: width / 2,
216+
y: height / 2,
217+
};
218+
219+
commit('setSinglePointPosition', centerPoint);
220+
return [centerPoint];
221+
},
222+
async saveCalib({ state, dispatch }) {
223+
try {
224+
const data = { ...state };
225+
delete data.calibrations;
226+
>>>>>>> 5886ed7 (feat: implement single-point calibration mode (#99))
187227

188228
await firebase.firestore().collection("calibrations").add(data);
189229
dispatch("getAllCalibs");

src/views/CameraConfiguration.vue

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@
198198
<v-icon left>mdi-arrow-left</v-icon>
199199
Back
200200
</v-btn>
201+
<<<<<<< HEAD
201202
<v-btn
202203
color="#FF425A"
203204
dark
@@ -216,6 +217,152 @@
216217
</v-row>
217218
</v-container>
218219
</div>
220+
=======
221+
</v-card-actions>
222+
</v-card>
223+
</v-dialog>
224+
225+
<v-container fluid>
226+
<v-row justify="center" align="center">
227+
<v-col cols="12" md="10" lg="8" xl="6">
228+
<v-stepper v-model="setupStep" elevation="0" class="mx-auto compact-stepper">
229+
<v-stepper-header>
230+
<v-stepper-step :complete="setupStep > 1" step="1" color="#FF425A">
231+
Camera Setup
232+
</v-stepper-step>
233+
<v-divider></v-divider>
234+
<v-stepper-step :complete="setupStep > 2" step="2" color="#FF425A">
235+
Preview & Calibration
236+
</v-stepper-step>
237+
</v-stepper-header>
238+
239+
<v-stepper-items>
240+
<!-- Step 1: Instructions -->
241+
<v-stepper-content step="1" class="compact-content">
242+
<v-card flat>
243+
<v-card-text class="text-center py-2">
244+
<div class="mb-2">
245+
<v-icon size="64" color="#FF425A">mdi-camera-iris</v-icon>
246+
</div>
247+
<v-alert color="#fff" dense class="mb-3">
248+
<h4 class="mb-2 text-center">What will happen:</h4>
249+
<div class="text-left mx-auto" style="max-width: 400px; font-size: 16px;">
250+
<ul class="compact-list">
251+
<li>The system will request camera permission</li>
252+
<li>Your webcam image will appear with a face guide</li>
253+
<li>Position your face inside the mask overlay</li>
254+
<li>Make sure both eyes are clearly visible</li>
255+
</ul>
256+
</div>
257+
</v-alert>
258+
<v-alert outlined color="#FF425A" dense class="mx-auto" style="max-width: 400px; font-size: 14px;">
259+
<strong>Important:</strong> Please allow camera access when prompted.
260+
</v-alert>
261+
</v-card-text>
262+
</v-card>
263+
<div class="text-center pb-2">
264+
<v-btn color="#FF425A" dark @click="startCameraSetup">
265+
<v-icon left>mdi-arrow-right</v-icon>
266+
Continue
267+
</v-btn>
268+
</div>
269+
</v-stepper-content>
270+
271+
<!-- Step 2: Camera Preview -->
272+
<v-stepper-content step="2" class="compact-content">
273+
<v-card flat>
274+
<!-- Calibration Mode Selection -->
275+
<v-card-text v-if="!fromRuxailab" class="pb-2 pt-2">
276+
<v-card outlined class="pa-3 mb-3">
277+
<v-card-title class="text-h6 pa-2">
278+
<v-icon left color="#FF425A">mdi-target</v-icon>
279+
Calibration Mode
280+
</v-card-title>
281+
<v-card-text class="pa-2">
282+
<v-radio-group v-model="calibrationMode" @change="updateCalibrationMode">
283+
<v-radio
284+
label="Multi-point (High Accuracy)"
285+
value="multi-point"
286+
color="#FF425A"
287+
>
288+
<template v-slot:label>
289+
<div>
290+
<strong>Multi-point (High Accuracy)</strong>
291+
<div class="text-caption text--secondary">
292+
9 points across screen - Best accuracy for precise tracking
293+
</div>
294+
</div>
295+
</template>
296+
</v-radio>
297+
<v-radio
298+
label="Single-point (Quick Setup)"
299+
value="single-point"
300+
color="#FF425A"
301+
>
302+
<template v-slot:label>
303+
<div>
304+
<strong>Single-point (Quick Setup)</strong>
305+
<div class="text-caption text--secondary">
306+
Center point only - Faster setup, lower accuracy
307+
</div>
308+
</div>
309+
</template>
310+
</v-radio>
311+
</v-radio-group>
312+
</v-card-text>
313+
</v-card>
314+
</v-card-text>
315+
316+
<!-- Blink Threshold Configuration -->
317+
<v-card-text v-if="!fromRuxailab" class="pb-1 pt-2 text-center">
318+
<BlinkTresholdCard />
319+
</v-card-text>
320+
321+
<!-- Camera Preview -->
322+
<v-card-text class="pa-2 text-center">
323+
<div class="d-flex justify-center mb-2">
324+
<v-btn x-small outlined color="#002D51" @click="showCameraModal = true">
325+
<v-icon left x-small>mdi-help-circle</v-icon>
326+
Camera Help
327+
</v-btn>
328+
</div>
329+
<div v-if="isModelLoaded" class="camera-wrapper mx-auto">
330+
<!-- Simple video test -->
331+
<video
332+
id="video-tag"
333+
autoplay
334+
playsinline
335+
style="width: 100%; height: auto; transform: scaleX(-1); display: block;"
336+
/>
337+
<canvas id="canvas" />
338+
<v-img v-if="isCameraOn" class="mask" src="@/assets/mask_desktop.svg" />
339+
</div>
340+
<div v-else class="loading-container" style="min-height: 300px;">
341+
<v-progress-circular :size="50" :width="6" color="#FF425A"
342+
indeterminate></v-progress-circular>
343+
<h4 class="mt-3">Loading face detection model...</h4>
344+
</div>
345+
</v-card-text>
346+
347+
<v-card-actions class="justify-center py-2">
348+
<v-btn text @click="setupStep = 1" class="mr-2">
349+
<v-icon left>mdi-arrow-left</v-icon>
350+
Back
351+
</v-btn>
352+
<v-btn color="#FF425A" dark :disabled="!isCameraOn" @click="goToCalibRecord()">
353+
<v-icon left>mdi-play</v-icon>
354+
Start Calibration
355+
</v-btn>
356+
</v-card-actions>
357+
</v-card>
358+
</v-stepper-content>
359+
</v-stepper-items>
360+
</v-stepper>
361+
</v-col>
362+
</v-row>
363+
</v-container>
364+
</div>
365+
>>>>>>> 5886ed7 (feat: implement single-point calibration mode (#99))
219366
</template>
220367

221368
<script>
@@ -249,8 +396,23 @@ export default {
249396
model() {
250397
return this.$store.state.detect.model;
251398
},
399+
<<<<<<< HEAD
252400
isModelLoaded() {
253401
return this.$store.state.detect.loaded;
402+
=======
403+
data() {
404+
return {
405+
isCameraOn: false,
406+
webcamStream: null,
407+
video: null,
408+
fromRuxailab: false,
409+
mediaDevices: [],
410+
selectedMediaDevice: null,
411+
setupStep: 1,
412+
showCameraModal: false,
413+
calibrationMode: 'multi-point', // Default to multi-point
414+
};
415+
>>>>>>> 5886ed7 (feat: implement single-point calibration mode (#99))
254416
},
255417
predictions() {
256418
return this.$store.state.detect.predictions;
@@ -359,10 +521,34 @@ export default {
359521
width: 600,
360522
height: 500,
361523
},
524+
<<<<<<< HEAD
362525
})
363526
.then((stream) => {
364527
// stream is a MediaStream object
365528
this.video.srcObject = stream;
529+
=======
530+
deep: true,
531+
},
532+
},
533+
mounted() {
534+
this.verifyFromRuxailab()
535+
},
536+
methods: {
537+
updateCalibrationMode(mode) {
538+
this.$store.commit('setCalibrationMode', mode);
539+
},
540+
541+
startCameraSetup() {
542+
this.setupStep = 2;
543+
this.setupCamera();
544+
},
545+
async setupCamera() {
546+
// Load the faceLandmarksDetection model assets.
547+
const model = await faceLandmarksDetection.load(
548+
faceLandmarksDetection.SupportedPackages.mediapipeFacemesh,
549+
{ maxFaces: 1 }
550+
);
551+
>>>>>>> 5886ed7 (feat: implement single-point calibration mode (#99))
366552
367553
this.webcamStream = stream;
368554

src/views/DoubleCalibrationRecord.vue

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -91,16 +91,26 @@
9191
<v-card flat>
9292
<v-card-text class="text-center px-6 py-6">
9393
<v-icon size="50" color="#FF425A" class="mb-4">mdi-eye-settings</v-icon>
94-
<h2 class="text-h4 font-weight-bold mb-4">Eye Tracking Calibration</h2>
94+
<h2 class="text-h4 font-weight-bold mb-4">{{ calibrationTitle }}</h2>
9595

9696
<v-alert color="#002D51" dark class="mb-4 text-left">
9797
<div class="text-body-1">
98-
<p class="font-weight-bold mb-2">This calibration consists of two phases:</p>
99-
<ol class="pl-4">
100-
<li class="mb-1"><strong>Training Phase:</strong> The system learns your eye movement patterns
101-
</li>
102-
<li><strong>Validation Phase:</strong> The system verifies the calibration accuracy</li>
103-
</ol>
98+
<p class="font-weight-bold mb-2">{{ calibrationDescription }}</p>
99+
<div v-if="isSinglePointMode">
100+
<p class="mb-1"><strong>Single-Point Mode:</strong></p>
101+
<ul class="pl-4">
102+
<li>Quick setup with center screen point only</li>
103+
<li>Lower accuracy but faster calibration</li>
104+
<li>Good for quick testing or demos</li>
105+
</ul>
106+
</div>
107+
<div v-else>
108+
<p class="font-weight-bold mb-2">This calibration consists of two phases:</p>
109+
<ol class="pl-4">
110+
<li class="mb-1"><strong>Training Phase:</strong> The system learns your eye movement patterns</li>
111+
<li><strong>Validation Phase:</strong> The system verifies the calibration accuracy</li>
112+
</ol>
113+
</div>
104114
</div>
105115
</v-alert>
106116

@@ -155,6 +165,7 @@
155165

156166
<v-alert color="#FF425A" dark dense class="text-center">
157167
<strong>Total points to calibrate:</strong> {{ usedPattern.length }}
168+
<span v-if="isSinglePointMode"> (Center point only)</span>
158169
</v-alert>
159170
</v-card-text>
160171
<v-card-actions class="justify-space-between px-6 pb-6">
@@ -379,6 +390,22 @@ export default {
379390
isControlled() {
380391
return this.$store.state.calibration.isControlled
381392
},
393+
394+
// New single-point calibration computed properties
395+
calibrationMode() {
396+
return this.$store.state.calibration.calibrationMode;
397+
},
398+
isSinglePointMode() {
399+
return this.calibrationMode === 'single-point';
400+
},
401+
calibrationTitle() {
402+
return this.isSinglePointMode ? 'Single-Point Calibration' : 'Multi-Point Calibration';
403+
},
404+
calibrationDescription() {
405+
return this.isSinglePointMode
406+
? 'Quick calibration using center point only. Lower accuracy but faster setup.'
407+
: 'High accuracy calibration using multiple points across the screen.';
408+
},
382409
},
383410
async created() {
384411
<<<<<<< HEAD
@@ -1249,14 +1276,23 @@ export default {
12491276
generateRuntimePattern() {
12501277
const width = window.innerWidth
12511278
const height = window.innerHeight
1279+
1280+
// Check if single-point mode is enabled
1281+
if (this.isSinglePointMode) {
1282+
return [{
1283+
x: width / 2,
1284+
y: height / 2
1285+
}];
1286+
}
1287+
1288+
// Multi-point calibration (existing logic)
12521289
const offset = this.offset || 100
12531290
const points = this.$store.state.calibration.pointNumber || 9
12541291
12551292
const minCols = 3
12561293
const cols = Math.max(minCols, Math.round(Math.sqrt(points)))
12571294
const rows = Math.ceil(points / cols)
12581295
1259-
12601296
const usableWidth = width - 2 * offset
12611297
const usableHeight = height - 2 * offset
12621298

0 commit comments

Comments
 (0)