script.js

10.55 KB
14/07/2025 14:40
JS
script.js
const video = document.getElementById('video');
const imageUpload = document.getElementById('imageUpload');
const refImage = document.getElementById('refImage');
const statusDiv = document.getElementById('status');
const resultDiv = document.getElementById('result');

let referenceDescriptor; // ตัวแปรสำหรับเก็บ "ลายเซ็น" ของใบหน้าต้นฉบับ

// ฟังก์ชันสำหรับโหลดโมเดลทั้งหมด
async function loadModels() {
  const MODEL_URL = 'https://raw.githubusercontent.com/justadudewhohacks/face-api.js/master/weights';
  statusDiv.innerText = 'กำลังโหลดโมเดล AI จาก GitHub...';
  try {
    await faceapi.nets.tinyFaceDetector.loadFromUri(MODEL_URL);
    await faceapi.nets.faceLandmark68Net.loadFromUri(MODEL_URL);
    await faceapi.nets.faceRecognitionNet.loadFromUri(MODEL_URL);
    statusDiv.innerText = '✅ โมเดล AI พร้อมใช้งาน! กรุณาอัปโหลดรูปภาพ';
  } catch (error) {
    console.error("เกิดข้อผิดพลาดในการโหลดโมเดล:", error);
    statusDiv.innerText = '❌ ไม่สามารถโหลดโมเดล AI ได้ - ตรวจสอบการเชื่อมต่ออินเทอร์เน็ต';
  }
}

// ฟังก์ชันสำหรับเริ่มวิดีโอจาก Webcam
function startVideo() {
  navigator.mediaDevices.getUserMedia({video: {}})
    .then(stream => {
      video.srcObject = stream;
    })
    .catch(err => {
      console.error("เกิดข้อผิดพลาดในการเข้าถึงกล้อง:", err);
      statusDiv.innerText = 'ไม่สามารถเข้าถึงกล้องได้';
    });
}

// Minimal style overlay for video and image
function applyMinimalOverlayStyle(target) {
  target.style.borderRadius = '12px';
  target.style.boxShadow = '0 2px 16px rgba(0,0,0,0.08)';
  target.style.background = '#fff';
  target.style.border = '1.5px solid #e0e0e0';
  target.style.display = 'block';
  target.style.margin = '0 auto 16px auto';
  target.style.maxWidth = '100%';
}

// ปรับ canvas overlay ให้ minimal
function styleOverlayCanvas(canvas) {
  canvas.style.borderRadius = '12px';
  canvas.style.boxShadow = '0 2px 16px rgba(0,0,0,0.08)';
  canvas.style.border = '1.5px solid #e0e0e0';
  canvas.style.pointerEvents = 'none';
}

// เพิ่มฟังก์ชันวาดกรอบใบหน้าบน canvas
function drawBoxOnImage(image, detection) {
  // ลบ canvas เดิมถ้ามี
  const oldCanvas = document.getElementById('refCanvas');
  if (oldCanvas) oldCanvas.remove();
  // สร้าง canvas ใหม่
  const canvas = faceapi.createCanvasFromMedia(image);
  canvas.id = 'refCanvas';
  canvas.style.position = 'absolute';
  canvas.style.left = image.offsetLeft + 'px';
  canvas.style.top = image.offsetTop + 'px';
  canvas.style.zIndex = 10;
  image.parentNode.appendChild(canvas);
  faceapi.matchDimensions(canvas, {width: image.width, height: image.height});
  faceapi.draw.drawDetections(canvas, [detection]);
  styleOverlayCanvas(canvas);
}

// ฟังก์ชันสำหรับ crop ใบหน้าจากรูปภาพ
function cropFaceFromImage(img, detection) {
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');

  // ขยายขอบเขตของใบหน้าออกมาเล็กน้อยเพื่อให้ดูสมบูรณ์
  const padding = 30;
  const box = detection.box;

  const cropX = Math.max(0, box.x - padding);
  const cropY = Math.max(0, box.y - padding);
  const cropWidth = Math.min(img.width - cropX, box.width + padding * 2);
  const cropHeight = Math.min(img.height - cropY, box.height + padding * 2);

  canvas.width = cropWidth;
  canvas.height = cropHeight;

  // วาดส่วนที่ crop มาลงใน canvas
  ctx.drawImage(
    img,
    cropX, cropY, cropWidth, cropHeight,
    0, 0, cropWidth, cropHeight
  );

  return canvas.toDataURL();
}

// เมื่อมีการอัปโหลดรูปภาพ
imageUpload.addEventListener('change', async () => {
  if (imageUpload.files && imageUpload.files[0]) {
    statusDiv.innerText = 'กำลังวิเคราะห์ภาพต้นฉบับ...';

    try {
      // โหลดรูปภาพ
      const image = await faceapi.bufferToImage(imageUpload.files[0]);
      console.log('Image loaded:', image.width, 'x', image.height);

      // แสดงรูปภาพต้นฉบับก่อน
      refImage.src = image.src;
      refImage.style.display = 'block';
      applyMinimalOverlayStyle(refImage);

      // รอให้รูปโหลดแล้วตรวจจับใบหน้า
      await new Promise(resolve => {
        if (refImage.complete) {
          resolve();
        } else {
          refImage.onload = resolve;
        }
      });

      // ตรวจจับใบหน้าจากรูปต้นฉบับ
      const detections = await faceapi.detectSingleFace(image, new faceapi.TinyFaceDetectorOptions())
        .withFaceLandmarks()
        .withFaceDescriptor();

      if (detections) {
        referenceDescriptor = detections.descriptor;

        // สร้างรูปภาพที่ crop ใบหน้า
        const croppedFaceDataUrl = cropFaceFromImage(image, detections.detection);
        console.log('Cropped face created, data URL length:', croppedFaceDataUrl.length);

        // แสดงรูปที่ crop แล้ว
        refImage.src = croppedFaceDataUrl;
        refImage.classList.add('loaded');
        statusDiv.innerText = '✅ วิเคราะห์ภาพต้นฉบับสำเร็จ! กรุณามองกล้องเพื่อเปรียบเทียบ';

      } else {
        statusDiv.innerText = '❌ ไม่พบใบหน้าในรูปภาพต้นฉบับ! กรุณาเลือกรูปที่มีใบหน้าชัดเจน';
        referenceDescriptor = null;
      }

    } catch (error) {
      console.error('Error processing image:', error);
      statusDiv.innerText = '❌ เกิดข้อผิดพลาดในการประมวลผลรูปภาพ';
    }
  }
});

// สำหรับวาดกรอบบนวิดีโอ
function drawBoxOnVideo(detection) {
  let canvas = document.getElementById('videoCanvas');
  if (!canvas) {
    canvas = faceapi.createCanvasFromMedia(video);
    canvas.id = 'videoCanvas';
    canvas.style.position = 'absolute';
    canvas.style.left = video.offsetLeft + 'px';
    canvas.style.top = video.offsetTop + 'px';
    canvas.style.zIndex = 10;
    video.parentNode.appendChild(canvas);
  }
  faceapi.matchDimensions(canvas, {width: video.width, height: video.height});
  const resizedDetections = faceapi.resizeResults(detection, {width: video.width, height: video.height});
  canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
  faceapi.draw.drawDetections(canvas, [resizedDetections.detection]);
  styleOverlayCanvas(canvas);
}

// เมื่อวิดีโอเริ่มเล่น จะเริ่มทำการตรวจสอบใบหน้า
video.addEventListener('play', () => {
  setInterval(async () => {
    // ถ้ายังไม่มีรูปต้นฉบับ ก็ไม่ต้องทำอะไร
    if (!referenceDescriptor) {
      return;
    }
    // ตรวจจับใบหน้าจากวิดีโอ
    const detections = await faceapi.detectSingleFace(video, new faceapi.TinyFaceDetectorOptions())
      .withFaceLandmarks()
      .withFaceDescriptor();

    if (detections) {
      // สร้าง FaceMatcher เพื่อเปรียบเทียบ
      const faceMatcher = new faceapi.FaceMatcher(referenceDescriptor);

      // หาใบหน้าที่คล้ายที่สุดในวิดีโอ
      const bestMatch = faceMatcher.findBestMatch(detections.descriptor);

      const similarity = (1 - bestMatch.distance).toFixed(2);

      // ค่า distance ยิ่งน้อยยิ่งเหมือน (0 = เหมือนสุดๆ, 1 = ไม่เหมือนเลย)
      // เราจะใช้ 1 - distance เพื่อให้ค่าความเหมือนยิ่งเยอะยิ่งดี
      const threshold = 0.5; // ค่าเกณฑ์สำหรับตัดสินว่าใช่คนเดียวกันหรือไม่ (ปรับค่าได้ 0-1)

      if (bestMatch.distance < threshold) {
        resultDiv.innerHTML = `ตรงกัน! <br> ความคล้ายคลึง: ${similarity}`;
        resultDiv.style.color = 'green';
      } else {
        resultDiv.innerHTML = `ไม่ตรงกัน <br> ความคล้ายคลึง: ${similarity}`;
        resultDiv.style.color = 'red';
      }
      drawBoxOnVideo(detections); // วาดกรอบบนวิดีโอ
    } else {
      resultDiv.innerText = 'ไม่พบใบหน้าในกล้อง...';
      resultDiv.style.color = '#333';
      // ลบกรอบถ้าไม่เจอใบหน้า
      const canvas = document.getElementById('videoCanvas');
      if (canvas) canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
    }
  }, 200); // ตรวจสอบทุกๆ 200 มิลลิวินาที
});

// เริ่มต้นการทำงานทั้งหมด
loadModels().then(startVideo);

// ปรับแต่ง status/result ให้ minimal
statusDiv.style.fontFamily = 'Inter, Segoe UI, sans-serif';
statusDiv.style.fontSize = '1.1em';
statusDiv.style.color = '#222';
statusDiv.style.background = '#f7f7f9';
statusDiv.style.borderRadius = '8px';
statusDiv.style.padding = '10px 18px';
statusDiv.style.margin = '12px auto';
statusDiv.style.maxWidth = '420px';
statusDiv.style.boxShadow = '0 1px 6px rgba(0,0,0,0.04)';
resultDiv.style.fontFamily = 'Inter, Segoe UI, sans-serif';
resultDiv.style.fontSize = '1.2em';
resultDiv.style.background = '#fff';
resultDiv.style.borderRadius = '8px';
resultDiv.style.padding = '12px 18px';
resultDiv.style.margin = '12px auto';
resultDiv.style.maxWidth = '420px';
resultDiv.style.boxShadow = '0 1px 6px rgba(0,0,0,0.04)';