AI 감독 시스템: 컴퓨터 비전을 통한 개인정보 보호 우선
2020년에는 대학 시험이 공학 문제로 바뀌었습니다. 전염병으로, 수백만 명의 학생들이 집과 교육기관에서 능력 시험을 치르고 있습니다. 학교는 디지털 감시 시스템으로 신속하게 대응해야 했습니다. 결과는 시장 폭발이었다. AI 감독, 그러나 또한 이러한 시스템의 윤리적, 법적 의미에 대한 인식이 커지고 있습니다.
2025년에 이 분야는 중요한 성숙도에 도달했습니다. 그만큼'EU AI법, 입력 완전히 시행되면서 교육의 생체인식 시스템을 광고 시스템으로 분류합니다. 위험, 엄격한 투명성, 문서화 및 인간의 감독. 그만큼 GDPR 데이터 처리에 추가적인 제약을 추가합니다. 생체 인식. 이러한 요구 사항을 무시하는 플랫폼에는 최대 4%의 벌금이 부과될 수 있습니다. 연간 글로벌 매출액.
본 논문에서는 개인정보 보호에서 시작되는 AI 감독 시스템을 구축합니다. 나중에 생각한 것이 아닌 창립 원칙. 얼굴 인식, 시선 구현 방법을 살펴보겠습니다. 로컬에서 처리된 생체 인식 데이터를 유지하면서 추적 및 이상 탐지를 수행합니다. 수집 최소화 및 완전한 GDPR 준수.
무엇을 배울 것인가
- 개인 정보 보호 우선 아키텍처: 온디바이스 처리와 클라우드 처리 비교
- MediaPipe FaceMesh를 사용한 얼굴 감지: 실시간 랜드마크 468개
- 시선 추적: 표준 웹캠에서 시선 방향 추정
- 의심스러운 행동에 대한 이상 탐지: 오디오, 탭 전환, 다중 얼굴
- GDPR 준수: 데이터 최소화, 동의, 보존 정책
- 인적 감독: 플래그 시스템 및 필수 인적 검토
1. 개인정보 보호 우선 설계 원칙
개인 정보 보호를 최우선으로 하는 감독 시스템은 기존의 "모든 것을 수집하고, 나중에 필터링하세요." 기본 원칙은 다음과 같습니다.
- 데이터 최소화: 연속 영상을 수집하지 않습니다. 녹음만 제한된 시간 버퍼(예: 이상 현상으로부터 ±10초)가 있는 비정상적인 이벤트입니다.
- 온디바이스 처리: 브라우저에서 생체정보를 처리합니다. 웹어셈블리/자바스크립트. 서버는 원시 비디오 피드를 볼 수 없으며 메타데이터만 볼 수 있습니다. 사건의 익명화.
- 유사 익명화: 학생증에 되돌릴 수 없는 해시를 사용합니다. 감독 시스템에서는 등록된 신원과 분리됩니다.
- 목적 제한: 수집된 데이터는 다음 목적으로만 사용될 수 있습니다. 특정 시험 세션은 30일 후 자동 취소됩니다.
- 의무적인 인간 감독: 구속력 있는 자동 결정이 없습니다. AI가 이상 징후를 표시하면 인간이 결과를 결정합니다.
// Architettura Privacy-First: on-device processing con MediaPipe
// Nessun dato biometrico trasmesso al server - solo eventi anonimizzati
import * as faceMesh from '@mediapipe/face_mesh';
import * as camera from '@mediapipe/camera_utils';
interface ProctoringEvent {
type: 'gaze_away' | 'face_missing' | 'multiple_faces' | 'tab_switch' | 'audio_anomaly';
timestamp: number;
duration?: number; // ms di persistenza dell'anomalia
confidence: number; // 0-1
// NESSUN dato biometrico raw - solo metadata
}
interface ProctoringSession {
sessionId: string; // Pseudoanonimo - hash dell'exam_id + student_hash
startTime: number;
events: ProctoringEvent[];
snapshotCount: number; // Solo contatore, no immagini
}
class PrivacyFirstProctor {
private faceMeshInstance: faceMesh.FaceMesh | null = null;
private session: ProctoringSession;
private gazeTracker: GazeTracker;
private audioAnalyzer: AudioAnomalyDetector;
private eventBuffer: ProctoringEvent[] = [];
// Soglie configurabili per bilanciare falsi positivi/negativi
private readonly CONFIG = {
GAZE_AWAY_THRESHOLD_DEG: 30, // Gradi di deviazione per "gaze away"
GAZE_AWAY_DURATION_MS: 3000, // 3s continuativi per flaggare
FACE_MISSING_DURATION_MS: 2000, // 2s senza volto per flaggare
MAX_EVENTS_PER_SESSION: 100, // Limite per GDPR data minimization
SNAPSHOT_BUFFER_SEC: 10, // Buffer ±10s per snapshot anomalia
};
constructor(private readonly sessionHash: string) {
this.session = {
sessionId: sessionHash,
startTime: Date.now(),
events: [],
snapshotCount: 0
};
this.gazeTracker = new GazeTracker(this.CONFIG.GAZE_AWAY_THRESHOLD_DEG);
this.audioAnalyzer = new AudioAnomalyDetector();
}
async initialize(videoElement: HTMLVideoElement): Promise<void> {
// Inizializza MediaPipe FaceMesh - processing LOCALE nel browser
this.faceMeshInstance = new faceMesh.FaceMesh({
locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/${file}`
});
this.faceMeshInstance.setOptions({
maxNumFaces: 3, // Rileva fino a 3 volti per multi-face detection
refineLandmarks: true, // Landmark oculari precisi per gaze tracking
minDetectionConfidence: 0.7,
minTrackingConfidence: 0.7
});
this.faceMeshInstance.onResults((results) => {
this.processResults(results);
});
// Setup camera
const cameraInstance = new camera.Camera(videoElement, {
onFrame: async () => {
await this.faceMeshInstance!.send({ image: videoElement });
},
width: 640,
height: 480
});
await cameraInstance.start();
// Setup visibility API per tab switching
this.setupTabSwitchDetection();
// Setup audio monitoring (opzionale, richiede consenso separato)
await this.audioAnalyzer.initialize();
}
private processResults(results: faceMesh.Results): void {
const landmarks = results.multiFaceLandmarks;
const faceCount = landmarks?.length || 0;
// Anomalia: nessun volto rilevato
if (faceCount === 0) {
this.recordEvent({
type: 'face_missing',
timestamp: Date.now(),
confidence: 0.9
});
return;
}
// Anomalia: più di un volto (possibile persona che aiuta)
if (faceCount > 1) {
this.recordEvent({
type: 'multiple_faces',
timestamp: Date.now(),
confidence: Math.min(0.6 + faceCount * 0.1, 0.95)
});
}
// Analisi gaze per il volto principale
const primaryFace = landmarks[0];
const gazeResult = this.gazeTracker.analyze(primaryFace);
if (gazeResult.isLookingAway) {
this.recordEvent({
type: 'gaze_away',
timestamp: Date.now(),
confidence: gazeResult.confidence,
// NON salvare la direzione precisa dello sguardo - solo il flag
});
}
}
private recordEvent(event: ProctoringEvent): void {
// GDPR: limit massimo eventi per minimizzazione dati
if (this.session.events.length >= this.CONFIG.MAX_EVENTS_PER_SESSION) {
console.warn('Max events per session reached - data minimization applied');
return;
}
this.session.events.push(event);
}
private setupTabSwitchDetection(): void {
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
this.recordEvent({
type: 'tab_switch',
timestamp: Date.now(),
confidence: 1.0 // Deterministica
});
}
});
// Previeni apertura DevTools (limitata efficacia, ma segnale utile)
window.addEventListener('resize', () => {
const threshold = 160;
if (
window.outerWidth - window.innerWidth > threshold ||
window.outerHeight - window.innerHeight > threshold
) {
this.recordEvent({
type: 'tab_switch', // Approssimazione per DevTools
timestamp: Date.now(),
confidence: 0.5
});
}
});
}
getReport(): ProctoringReport {
return ProctoringReportGenerator.generate(this.session);
}
}
2. MediaPipe FaceMesh를 사용한 시선 추적
표준 웹캠 시선 추적(특수 하드웨어 없음)은 방향을 추정합니다. 눈 가장자리를 기준으로 동공의 위치를 분석하여 시선을 측정합니다. MediaPipe FaceMesh는 각 눈에 대한 71개의 랜드마크를 포함하여 468개의 얼굴 랜드마크를 제공합니다. 상당한 편차를 감지하기에 충분한 정밀도를 가지고 있습니다.
// Indici landmark MediaPipe FaceMesh per gli occhi
// Documentazione: https://google.github.io/mediapipe/solutions/face_mesh.html
const FACE_MESH_INDICES = {
// Occhio sinistro
LEFT_EYE: {
OUTER: 33, // Angolo esterno
INNER: 133, // Angolo interno
TOP: 159, // Palpebra superiore
BOTTOM: 145, // Palpebra inferiore
IRIS_CENTER: 468, // Centro iride (solo con refineLandmarks: true)
},
// Occhio destro
RIGHT_EYE: {
OUTER: 362,
INNER: 263,
TOP: 386,
BOTTOM: 374,
IRIS_CENTER: 473,
},
// Naso (per orientamento testa)
NOSE_TIP: 4,
LEFT_CHEEK: 234,
RIGHT_CHEEK: 454
};
interface GazeResult {
isLookingAway: boolean;
horizontalAngle: number; // Gradi: negativo=sinistra, positivo=destra
verticalAngle: number; // Gradi: negativo=basso, positivo=alto
confidence: number;
}
class GazeTracker {
private gazeAwayStartTime: number | null = null;
private readonly AWAY_THRESHOLD_DEG: number;
private readonly AWAY_DURATION_MS = 3000;
constructor(awayThresholdDeg: number = 30) {
this.AWAY_THRESHOLD_DEG = awayThresholdDeg;
}
analyze(landmarks: faceMesh.NormalizedLandmarkList): GazeResult {
// Stima orientamento della testa tramite landmark
const headPose = this.estimateHeadPose(landmarks);
// Stima posizione iride relativa all'occhio
const leftGaze = this.estimateEyeGaze(
landmarks,
FACE_MESH_INDICES.LEFT_EYE,
FACE_MESH_INDICES.LEFT_EYE.IRIS_CENTER
);
const rightGaze = this.estimateEyeGaze(
landmarks,
FACE_MESH_INDICES.RIGHT_EYE,
FACE_MESH_INDICES.RIGHT_EYE.IRIS_CENTER
);
// Media pesata occhio sinistro e destro
const combinedHorizontal = (leftGaze.horizontal + rightGaze.horizontal) / 2;
const combinedVertical = (leftGaze.vertical + rightGaze.vertical) / 2;
// Combina con head pose
const totalHorizontal = combinedHorizontal + headPose.yaw;
const totalVertical = combinedVertical + headPose.pitch;
const isAway = (
Math.abs(totalHorizontal) > this.AWAY_THRESHOLD_DEG ||
Math.abs(totalVertical) > this.AWAY_THRESHOLD_DEG
);
// Timing per durata
if (isAway && this.gazeAwayStartTime === null) {
this.gazeAwayStartTime = Date.now();
} else if (!isAway) {
this.gazeAwayStartTime = null;
}
const isPersistentlyAway = this.gazeAwayStartTime !== null &&
(Date.now() - this.gazeAwayStartTime) > this.AWAY_DURATION_MS;
return {
isLookingAway: isPersistentlyAway,
horizontalAngle: totalHorizontal,
verticalAngle: totalVertical,
confidence: 0.7 // Stima conservativa per webcam standard
};
}
private estimateEyeGaze(
landmarks: faceMesh.NormalizedLandmarkList,
eyeIndices: typeof FACE_MESH_INDICES.LEFT_EYE,
irisIndex: number
): { horizontal: number; vertical: number } {
const outer = landmarks[eyeIndices.OUTER];
const inner = landmarks[eyeIndices.INNER];
const top = landmarks[eyeIndices.TOP];
const bottom = landmarks[eyeIndices.BOTTOM];
const iris = landmarks[irisIndex];
if (!iris) {
// refineLandmarks disabilitato o iris non rilevata
return { horizontal: 0, vertical: 0 };
}
// Centro geometrico dell'occhio
const eyeCenterX = (outer.x + inner.x) / 2;
const eyeCenterY = (top.y + bottom.y) / 2;
// Dimensioni dell'occhio
const eyeWidth = Math.abs(outer.x - inner.x);
const eyeHeight = Math.abs(top.y - bottom.y);
// Offset normalizzato dell'iride rispetto al centro
const normalizedX = (iris.x - eyeCenterX) / (eyeWidth / 2 + 1e-6);
const normalizedY = (iris.y - eyeCenterY) / (eyeHeight / 2 + 1e-6);
// Converti in gradi approssimativi
return {
horizontal: normalizedX * 45, // Max ~45 gradi
vertical: normalizedY * 30 // Max ~30 gradi verticali
};
}
private estimateHeadPose(
landmarks: faceMesh.NormalizedLandmarkList
): { yaw: number; pitch: number } {
const noseTip = landmarks[FACE_MESH_INDICES.NOSE_TIP];
const leftCheek = landmarks[FACE_MESH_INDICES.LEFT_CHEEK];
const rightCheek = landmarks[FACE_MESH_INDICES.RIGHT_CHEEK];
// Yaw: asimmetria sinistra/destra delle guance
const cheekMidX = (leftCheek.x + rightCheek.x) / 2;
const cheekWidth = Math.abs(rightCheek.x - leftCheek.x);
const yawRatio = (noseTip.x - cheekMidX) / (cheekWidth / 2 + 1e-6);
const yaw = yawRatio * 45;
// Pitch: posizione verticale del naso
const cheekMidY = (leftCheek.y + rightCheek.y) / 2;
const pitchRatio = (noseTip.y - cheekMidY) / 0.1;
const pitch = Math.max(-30, Math.min(30, pitchRatio * 20));
return { yaw, pitch };
}
}
3. 다중 모드 이상 탐지
강력한 감독 시스템은 여러 소스의 신호를 결합하여 거짓 긍정. 단일 신호(예: 3초 동안 시선을 돌리는 것)에는 높은 신호가 있습니다. 무해한 행동일 가능성이 높습니다. 신호의 조합 효율성이 크게 향상됩니다.
interface AnomalyScore {
overall: number; // 0-1
breakdown: {
gaze: number;
face: number;
audio: number;
behavior: number;
};
riskLevel: 'low' | 'medium' | 'high';
requiresHumanReview: boolean;
}
class MultiModalAnomalyDetector {
/**
* Calcola uno score di anomalia composito.
* Nessuna decisione automatica - solo scoring per review umana.
*/
computeRiskScore(session: ProctoringSession): AnomalyScore {
const events = session.events;
const sessionDuration = (Date.now() - session.startTime) / 60000; // minuti
// Normalizza per durata sessione (eventi per 10 minuti)
const normalize = (count: number) => Math.min(count / (sessionDuration / 10 + 1), 1.0);
// Score gaze: ponderato per confidenza degli eventi
const gazeEvents = events.filter(e => e.type === 'gaze_away');
const gazeScore = normalize(
gazeEvents.reduce((sum, e) => sum + e.confidence, 0)
);
// Score face: assenza o volti multipli
const faceEvents = events.filter(e =>
e.type === 'face_missing' || e.type === 'multiple_faces'
);
const faceScore = Math.min(
normalize(faceEvents.length) +
(faceEvents.some(e => e.type === 'multiple_faces') ? 0.3 : 0),
1.0
);
// Score audio: anomalie audio
const audioEvents = events.filter(e => e.type === 'audio_anomaly');
const audioScore = normalize(audioEvents.length);
// Score comportamentale: tab switching
const tabEvents = events.filter(e => e.type === 'tab_switch');
const behaviorScore = Math.min(tabEvents.length * 0.25, 1.0);
// Score overall: media pesata
const overall = (
gazeScore * 0.25 +
faceScore * 0.35 +
audioScore * 0.20 +
behaviorScore * 0.20
);
const riskLevel = overall < 0.3 ? 'low' : overall < 0.6 ? 'medium' : 'high';
return {
overall: Math.round(overall * 100) / 100,
breakdown: {
gaze: Math.round(gazeScore * 100) / 100,
face: Math.round(faceScore * 100) / 100,
audio: Math.round(audioScore * 100) / 100,
behavior: Math.round(behaviorScore * 100) / 100
},
riskLevel,
// GDPR EU AI Act: review umana OBBLIGATORIA per qualunque livello di rischio
requiresHumanReview: riskLevel === 'high' || behaviorScore > 0.5
};
}
}
// Report finale - struttura GDPR-compliant
interface ProctoringReport {
sessionId: string; // Pseudoanonimo
examId: string;
eventCount: number;
anomalyScore: AnomalyScore;
recommendation: 'pass' | 'review_required';
reviewDeadline: Date; // 30 giorni per revisione, poi cancellazione
dataRetentionDate: Date; // Data cancellazione automatica
// NESSUN video, NESSUNA immagine biometrica
}
4. GDPR 및 EU AI법 준수
생체정보를 수집하는 AI 감독 시스템은 법률 제9조의 적용을 받습니다. GDPR(특수 범주의 데이터) 및 EU AI Act(상황에 따른 고위험 시스템) 교육). 주요 요구사항은 다음과 같습니다.
| 요구 사항 | GDPR | EU AI법 | 구현 |
|---|---|---|---|
| 명시적 동의 | 제9(2)(a)조 | 제13조 | 시험 전 동의서 |
| 데이터 최소화 | 제5(1)(c)조 | 제10조 | 이벤트 메타데이터만, 연속 동영상 없음 |
| 제한된 보존 | 제5(1)(e)조 | - | 30일 후 자동 삭제 |
| 인간의 감독 | 제22조 | 제14조 | 구속력 있는 자동 결정 없음 |
| 투명도 | 제13/14조 | 제13조 | 감지된 내용에 대한 명확한 정보 |
| DPIA 필수 | 제35조 | - | 배포 전 영향 평가 |
// Implementazione consenso GDPR-compliant
// Il sistema non si avvia senza consenso esplicito e registrato
interface ConsentRecord {
studentHash: string; // Hash irreversibile dell'ID studente
examId: string;
timestamp: number;
consentVersion: string; // Versione dell'informativa
dataProcessed: string[]; // Lista esplicita di dati raccolti
retentionDays: number;
signature: string; // Hash consenso per audit
}
class GDPRConsentManager {
private static readonly CONSENT_VERSION = '2025-v2';
private static readonly DEFAULT_RETENTION_DAYS = 30;
async requestConsent(
studentId: string,
examId: string
): Promise<ConsentRecord | null> {
const consentData = {
dataProcessed: [
'Rilevamento presenza volto (si/no)',
'Direzione sguardo (deviazione significativa si/no)',
'Numero di volti rilevati',
'Cambio scheda/finestra (si/no)',
'Anomalie audio (si/no)'
],
notProcessed: [
'Video continuo della sessione',
'Riconoscimento facciale o identificazione biometrica',
'Screenshot dello schermo',
'Contenuto audio'
],
retentionDays: this.constructor.DEFAULT_RETENTION_DAYS,
canWithdraw: true,
consequenceOfWithdrawal: 'Impossibilita di sostenere l\'esame in modalità remota'
};
// Mostra UI consenso e attende risposta (implementazione UI omessa)
const accepted = await this.showConsentDialog(consentData);
if (!accepted) return null;
const studentHash = await this.hashStudentId(studentId);
const consentRecord: ConsentRecord = {
studentHash,
examId,
timestamp: Date.now(),
consentVersion: GDPRConsentManager.CONSENT_VERSION,
dataProcessed: consentData.dataProcessed,
retentionDays: consentData.retentionDays,
signature: await this.computeConsentSignature(studentHash, examId)
};
// Persistenza del consenso per audit trail
await this.persistConsent(consentRecord);
return consentRecord;
}
private async hashStudentId(studentId: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(studentId + process.env.PROCTORING_SALT);
const hash = await crypto.subtle.digest('SHA-256', data);
return Array.from(new Uint8Array(hash))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
async scheduleDataDeletion(consentRecord: ConsentRecord): Promise<void> {
const deleteAt = new Date(
consentRecord.timestamp + consentRecord.retentionDays * 86400 * 1000
);
// Schedule deletion job (implementazione dipende dall'infrastruttura)
await scheduleJob({
type: 'delete_proctoring_data',
sessionId: consentRecord.studentHash + '_' + consentRecord.examId,
executeAt: deleteAt.toISOString()
});
}
}
경고: AI 감독 시스템의 편견
MIT(2019) 이후의 연구에 따르면 인식 시스템은 얼굴은 어두운 피부를 가진 사람들의 경우 훨씬 더 높은 오류율을 보여줍니다. 안경을 쓴 여성과 개인. 감독 시스템은 데이터세트에서 테스트되어야 합니다. 인구통계학적으로 대표적이며 모니터링에 형평성 지표를 포함합니다. 계속. 특정 그룹에 대한 시스템적으로 더 높은 오탐률은 EU AI법에 따른 간접적 차별.
5. 인적 검토 인터페이스
고위험 시스템에 대한 EU AI법의 기본 원칙은 다음과 같은 결정을 내리는 것입니다. 개인에게 큰 영향을 미치려면 상당한 인적 감독이 필요합니다. 검토 인터페이스는 시스템의 중요한 구성 요소입니다.
// API endpoint per il pannello di review dei docenti
// Solo chi ha role 'exam_reviewer' accede ai report
import express from 'express';
import { authenticate, requireRole } from './auth';
const router = express.Router();
// GET /api/proctoring/review/:examId
router.get('/review/:examId',
authenticate,
requireRole('exam_reviewer'),
async (req, res) => {
const { examId } = req.params;
// Solo sessioni con flag HIGH o che richiedono review
const flaggedSessions = await db.query(`
SELECT
session_id,
overall_score,
risk_level,
event_count,
gaze_score,
face_score,
behavior_score,
created_at,
review_deadline
FROM proctoring_sessions
WHERE exam_id = $1
AND (risk_level = 'high' OR requires_human_review = true)
AND reviewed_by IS NULL
ORDER BY overall_score DESC
`, [examId]);
res.json({
examId,
pendingReviews: flaggedSessions.rows.length,
sessions: flaggedSessions.rows,
reviewGuidelines: {
highRisk: 'Score > 0.6: valuta attentamente tutti gli eventi',
mediumRisk: 'Score 0.3-0.6: verifica pattern di tab switching e multi-face',
note: 'Lo score AI e indicativo. La decisione finale spetta al revisore umano.'
}
});
}
);
// POST /api/proctoring/review/:sessionId/decision
router.post('/review/:sessionId/decision',
authenticate,
requireRole('exam_reviewer'),
async (req, res) => {
const { sessionId } = req.params;
const { decision, notes, reviewerId } = req.body;
// Validation
if (!['pass', 'fail', 'inconclusive'].includes(decision)) {
return res.status(400).json({ error: 'Invalid decision value' });
}
await db.query(`
UPDATE proctoring_sessions
SET
final_decision = $1,
reviewer_notes = $2,
reviewed_by = $3,
reviewed_at = NOW()
WHERE session_id = $4
`, [decision, notes, reviewerId, sessionId]);
// Audit log obbligatorio per EU AI Act
await auditLog.record({
action: 'proctoring_decision',
sessionId,
decision,
reviewerId,
timestamp: new Date().toISOString()
});
res.json({ success: true, decision });
}
);
결론
책임 있는 AI 감독 시스템은 모순되지 않습니다. 가능합니다. 학생의 존엄성을 존중하는 학문적 진실성을 위한 효과적인 도구 구축 현행법을 준수해야 합니다. 핵심은 개인정보 보호 설계: 로컬에서 데이터를 처리하고, 비정상적인 이벤트에 대한 메타데이터만 수집하고, 인간이 최종 결정권을 갖고 있는 한.
EU AI법이 본격적으로 시행됨에 따라 규정 준수는 선택 사항이 아닙니다. 해당 기관은 감독 시스템을 구현하려면 DPIA를 완료하고 시스템을 문서화해야 합니다. 감사자를 교육하고 결정에 이의를 제기할 수 있는 능력을 보장합니다.
EdTech 시리즈 관련 기사
- 기사 00: 확장 가능한 LMS 아키텍처: 다중 테넌트 패턴
- 기사 01: 적응형 학습 알고리즘
- 기사 06: xAPI 및 Kafka를 사용한 분석 학습







