0918課堂點名

image.png

流星燈

int numLights = 8;       // 燈泡數(總共有幾顆)
int currentLight = 0;    // 目前亮到第幾顆(用索引 0..numLights-1)
int interval = 200;      // 每顆燈泡切換時間(毫秒)
int lastTime = 0;        // 上一次切換的時間(用 millis() 比較)

void setup() {
  size(200, 400);       // 視窗大小:寬 200、高 400
  noStroke();           // 畫形狀時不要描邊(只有填色)
  frameRate(60);        // 目標每秒畫面更新 60 次(可選)
}

void draw() {
  background(30);       // 每一幀用灰 30 清除畫面(相當於黑色背景)

  // 計時:如果從上次切換以來已經超過 interval,就換下一顆燈
  if (millis() - lastTime > interval) {
    currentLight++;                // 指向下一顆燈
    if (currentLight >= numLights) {
      currentLight = 0;            // 如果超過最後一顆,就回到第一顆(循環)
    }
    lastTime = millis();           // 更新上次切換時間為現在
  }

  // 畫每一顆燈(垂直排列)
  for (int i = 0; i < numLights; i++) {
    float y = 50 + i * 40;         // 第 i 顆燈的 y 座標(50 為上方邊距,40 為間距)
    if (i == currentLight) {
      fill(255, 255, 150);         // 亮燈時的顏色(淡黃色)
    } else {
      fill(80);                    // 未亮時的顏色(暗灰)
    }
    ellipse(width/2, y, 30, 30);   // 在畫面中間 x=width/2,y 為上面計算,畫直徑 30 的圓
  }
}

錄製內容 2025-09-25 095401.gif

9/23

靠近鼠標的球

float x, y;   // 圓球目前的位置

void setup() {
  size(600, 400);   // 畫布大小
  x = width/2;      // 初始位置在畫布中間
  y = height/2;
}

void draw() {
  background(0);     // 清空背景(黑色)

  // 計算位置,讓圓球逐漸靠近滑鼠
  float easing = 0.05;             // 越小越慢,越大越快
  x += (mouseX - x) * easing;      // 水平方向緩慢靠近
  y += (mouseY - y) * easing;      // 垂直方向緩慢靠近

  // 畫出圓球
  fill(255, 100, 100);  
  noStroke();
  ellipse(x, y, 50, 50);           // 在 (x,y) 畫一個直徑50的圓
}

螢幕擷取畫面 2025-10-01 131946.png

paper.pdf

<!DOCTYPE html>
<html lang="zh-TW">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>BubbleView Full System</title>
    <style>
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background-color: #f4f4f4;
            display: flex;
            flex-direction: column;
            align-items: center;
            padding: 20px;
        }
        h1 { color: #333; margin-bottom: 10px; }
        .panel {
            background: white;
            padding: 15px;
            border-radius: 8px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.1);
            margin-bottom: 15px;
            width: 100%;
            max-width: 900px;
            box-sizing: border-box;
        }
        .controls {
            display: flex;
            gap: 10px;
            align-items: center;
            flex-wrap: wrap;
            justify-content: center;
        }
        #canvas-wrapper {
            position: relative;
            box-shadow: 0 4px 8px rgba(0,0,0,0.2);
            border: 2px solid #ddd;
            background-color: #fff;
            cursor: crosshair;
            margin: 0 auto;
        }
        canvas { display: block; }
        #heatmapCanvas {
            position: absolute; top: 0; left: 0; pointer-events: none; opacity: 0.7; display: none;
        }
        
        /* 任務區塊樣式 */
        #task-panel { display: none; border-left: 4px solid #007bff; }
        #description-box {
            width: 100%;
            height: 60px;
            margin-top: 10px;
            padding: 8px;
            border: 1px solid #ccc;
            border-radius: 4px;
            font-family: sans-serif;
        }
        .timer { font-weight: bold; color: #d9534f; font-size: 1.2em; margin-left: 10px; }

        /* 時間軸滑桿樣式 (論文 Figure 3 功能) */
        #timeline-panel { display: none; border-left: 4px solid #28a745; }
        input[type=range] { width: 100%; margin: 10px 0; }
        
        button {
            padding: 8px 16px;
            background-color: #007bff;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            transition: background 0.2s;
        }
        button:hover { background-color: #0056b3; }
        .btn-green { background-color: #28a745; }
        .btn-green:hover { background-color: #218838; }
        .btn-orange { background-color: #fd7e14; }
        .btn-orange:hover { background-color: #e6700c; }
    </style>
</head>
<body>

    <h1>BubbleView</h1>

    <div class="panel controls">
        <label>任務模式:
            <select id="taskMode">
                <option value="free">自由瀏覽 (限時 10秒)</option>
                <option value="desc">描述任務 (不限時)</option>
            </select>
        </label>
        <button onclick="loadDemoImage()" class="btn-orange">1. 載入範例並開始</button>
        <label style="margin-left:10px; cursor:pointer; color:#007bff;">
            (或上傳圖片) <input type="file" id="imgUpload" accept="image/*" style="display:none;">
        </label>
    </div>

    <div id="task-panel" class="panel">
        <div style="display:flex; justify-content:space-between; align-items:center;">
            <span id="task-instruction" style="font-weight:bold;"></span>
            <span id="timer-display" class="timer"></span>
        </div>
        <textarea id="description-box" placeholder="請在此描述您看到的圖片內容... (僅描述模式可用)"></textarea>
        <div style="text-align:right; margin-top:5px;">
            <button onclick="finishTask()" class="btn-green">完成任務</button>
        </div>
    </div>

    <div id="timeline-panel" class="panel">
        <div style="font-weight:bold; margin-bottom:5px;">數據回放監控 (Timeline Analysis)</div>
        <div class="controls">
            <button onclick="toggleHeatmap()">切換熱圖顯示</button>
            <button onclick="downloadHeatmapImage()">下載截圖</button>
            <button onclick="exportData()">輸出 JSON</button>
        </div>
        <div style="margin-top:10px;">
            <label>時間軸回放: <span id="time-val">0</span> ms / <span id="time-total">0</span> ms</label>
            <input type="range" id="timeSlider" value="0" min="0" step="100" oninput="updateTimeline(this.value)">
        </div>
    </div>

    <div id="canvas-wrapper">
        <canvas id="mainCanvas"></canvas>
        <canvas id="heatmapCanvas"></canvas>
    </div>

    <script>
        // --- 參數 ---
        const CONFIG = {
            BLUR_SIGMA: 30,
            BUBBLE_RADIUS: 40,
            HEATMAP_RADIUS: 50,
            IMG_MAX_WIDTH: 800,
            FREE_VIEW_TIME: 10 // 自由瀏覽秒數 (論文 Exp 2.1 設定)
        };

        const mainCanvas = document.getElementById('mainCanvas');
        const heatCanvas = document.getElementById('heatmapCanvas');
        const mainCtx = mainCanvas.getContext('2d');
        const heatCtx = heatCanvas.getContext('2d');
        
        let originalImage = null;
        let clickData = [];
        let startTime = 0;
        let isHeatmapVisible = false;
        let gradientPalette = null;
        let taskActive = false;
        let timerInterval = null;

        // 初始化
        createGradientPalette();

        // 監聽上傳
        document.getElementById('imgUpload').addEventListener('change', function(e) {
            const file = e.target.files[0];
            if (!file) return;
            const reader = new FileReader();
            reader.onload = (event) => {
                const img = new Image();
                img.onload = () => startExperiment(img);
                img.src = event.target.result;
            };
            reader.readAsDataURL(file);
        });

        function loadDemoImage() {
            const tempCanvas = document.createElement('canvas');
            tempCanvas.width = 800; tempCanvas.height = 600;
            const tCtx = tempCanvas.getContext('2d');
            tCtx.fillStyle = '#f0f0f0'; tCtx.fillRect(0,0,800,600);
            tCtx.fillStyle = '#333'; tCtx.font = '30px Arial';
            tCtx.fillText('BubbleView 任務測試', 50, 50);
            tCtx.fillText('請尋找並點擊畫面中的幾何圖形', 50, 100);
            tCtx.fillStyle = '#e74c3c'; tCtx.beginPath(); tCtx.arc(200, 300, 50, 0, Math.PI*2); tCtx.fill();
            tCtx.fillStyle = '#3498db'; tCtx.fillRect(400, 250, 100, 100);
            const img = new Image();
            img.onload = () => startExperiment(img);
            img.src = tempCanvas.toDataURL();
        }

        function startExperiment(img) {
            originalImage = img;
            clickData = [];
            
            // UI 重置
            document.getElementById('task-panel').style.display = 'block';
            document.getElementById('timeline-panel').style.display = 'none';
            document.getElementById('description-box').value = '';
            heatCanvas.style.display = 'none';
            isHeatmapVisible = false;

            // 調整 Canvas
            let width = img.width;
            let height = img.height;
            if (width > CONFIG.IMG_MAX_WIDTH) {
                const scale = CONFIG.IMG_MAX_WIDTH / width;
                width = CONFIG.IMG_MAX_WIDTH;
                height = height * scale;
            }
            mainCanvas.width = width; mainCanvas.height = height;
            heatCanvas.width = width; heatCanvas.height = height;

            // 設定任務模式
            const mode = document.getElementById('taskMode').value;
            const descBox = document.getElementById('description-box');
            const instruction = document.getElementById('task-instruction');
            const timerDisplay = document.getElementById('timer-display');

            if (mode === 'free') {
                descBox.style.display = 'none';
                instruction.textContent = "任務:請自由瀏覽圖片 (限時 10 秒)";
                startTimer(CONFIG.FREE_VIEW_TIME);
            } else {
                descBox.style.display = 'block';
                instruction.textContent = "任務:請點擊並在下方描述圖片內容";
                timerDisplay.textContent = "";
                if (timerInterval) clearInterval(timerInterval);
            }

            drawBlurredImage();
            taskActive = true;
            startTime = Date.now();
        }

        function startTimer(seconds) {
            let remaining = seconds;
            const display = document.getElementById('timer-display');
            display.textContent = `剩餘時間: ${remaining}s`;
            
            if (timerInterval) clearInterval(timerInterval);
            timerInterval = setInterval(() => {
                remaining--;
                display.textContent = `剩餘時間: ${remaining}s`;
                if (remaining <= 0) {
                    clearInterval(timerInterval);
                    finishTask();
                    alert("時間到!");
                }
            }, 1000);
        }

        // 完成任務
        function finishTask() {
            if (!taskActive) return;
            taskActive = false;
            if (timerInterval) clearInterval(timerInterval);

            // 紀錄描述文字
            const desc = document.getElementById('description-box').value;
            
            // 顯示分析面板
            document.getElementById('task-panel').style.display = 'none';
            document.getElementById('timeline-panel').style.display = 'block';
            
            // 設定滑桿
            const totalTime = clickData.length > 0 ? clickData[clickData.length-1].time : 0;
            const slider = document.getElementById('timeSlider');
            slider.max = totalTime;
            slider.value = totalTime;
            document.getElementById('time-total').textContent = totalTime;
            document.getElementById('time-val').textContent = totalTime;

            // 自動開啟熱圖
            isHeatmapVisible = true;
            heatCanvas.style.display = 'block';
            drawHeatmap(clickData); // 繪製完整熱圖
            alert(`任務完成!\\n共收集 ${clickData.length} 個點擊。\\n描述長度: ${desc.length} 字。`);
        }

        // 互動邏輯
        mainCanvas.addEventListener('mousedown', function(e) {
            if (!taskActive) return; // 任務結束後不能再點
            const rect = mainCanvas.getBoundingClientRect();
            const x = e.clientX - rect.left;
            const y = e.clientY - rect.top;

            clickData.push({ x: Math.round(x), y: Math.round(y), time: Date.now() - startTime });
            revealBubble(x, y);
        });

        // 論文 Figure 3 的回放功能
        function updateTimeline(val) {
            const timeLimit = parseInt(val);
            document.getElementById('time-val').textContent = timeLimit;
            
            // 過濾數據:只顯示時間點之前的點擊
            const filteredData = clickData.filter(d => d.time <= timeLimit);
            
            // 重繪熱圖
            drawHeatmap(filteredData);
        }

        function drawBlurredImage() {
            mainCtx.filter = `blur(${CONFIG.BLUR_SIGMA}px)`;
            mainCtx.drawImage(originalImage, 0, 0, mainCanvas.width, mainCanvas.height);
            mainCtx.filter = 'none';
        }

        function revealBubble(x, y) {
            mainCtx.save();
            mainCtx.beginPath();
            mainCtx.arc(x, y, CONFIG.BUBBLE_RADIUS, 0, Math.PI * 2, true);
            mainCtx.closePath();
            mainCtx.clip();
            mainCtx.drawImage(originalImage, 0, 0, mainCanvas.width, mainCanvas.height);
            mainCtx.lineWidth = 2; mainCtx.strokeStyle = 'rgba(255, 50, 50, 0.6)'; mainCtx.stroke();
            mainCtx.restore();
        }

        function toggleHeatmap() {
            if (!originalImage) return;
            isHeatmapVisible = !isHeatmapVisible;
            heatCanvas.style.display = isHeatmapVisible ? 'block' : 'none';
            if (isHeatmapVisible) {
                // 根據目前滑桿位置繪製
                updateTimeline(document.getElementById('timeSlider').value);
            }
        }

        function createGradientPalette() {
            const pCanvas = document.createElement('canvas');
            pCanvas.width = 256; pCanvas.height = 1;
            const pCtx = pCanvas.getContext('2d');
            const g = pCtx.createLinearGradient(0,0,256,0);
            g.addColorStop(0, 'rgba(0,0,255,0)');
            g.addColorStop(0.2, 'rgba(0,0,255,0.5)');
            g.addColorStop(0.4, 'rgba(0,255,0,0.8)');
            g.addColorStop(0.6, 'rgba(255,255,0,0.9)');
            g.addColorStop(1, 'rgba(255,0,0,1)');
            pCtx.fillStyle = g; pCtx.fillRect(0,0,256,1);
            gradientPalette = pCtx.getImageData(0,0,256,1).data;
        }

        function drawHeatmap(dataPoints) {
            if (!dataPoints || dataPoints.length === 0) {
                heatCtx.clearRect(0,0,heatCanvas.width,heatCanvas.height);
                return;
            }
            heatCtx.clearRect(0, 0, heatCanvas.width, heatCanvas.height);
            heatCtx.globalCompositeOperation = 'screen'; 
            dataPoints.forEach(p => {
                const g = heatCtx.createRadialGradient(p.x, p.y, 0, p.x, p.y, CONFIG.HEATMAP_RADIUS);
                g.addColorStop(0, 'rgba(255, 255, 255, 0.3)'); 
                g.addColorStop(1, 'rgba(255, 255, 255, 0)');
                heatCtx.fillStyle = g;
                heatCtx.beginPath(); heatCtx.arc(p.x, p.y, CONFIG.HEATMAP_RADIUS, 0, Math.PI * 2); heatCtx.fill();
            });
            const imgData = heatCtx.getImageData(0, 0, heatCanvas.width, heatCanvas.height);
            const d = imgData.data;
            for (let i = 0; i < d.length; i += 4) {
                const a = d[i + 3];
                if (a > 0) {
                    const idx = a * 4;
                    d[i] = gradientPalette[idx]; d[i+1] = gradientPalette[idx+1]; d[i+2] = gradientPalette[idx+2];
                    d[i+3] = Math.min(a + 50, 200);
                }
            }
            heatCtx.putImageData(imgData, 0, 0);
            heatCtx.globalCompositeOperation = 'source-over';
        }

        function exportData() {
            const desc = document.getElementById('description-box').value;
            const exportObj = {
                description: desc,
                clicks: clickData,
                totalTime: clickData.length > 0 ? clickData[clickData.length-1].time : 0
            };
            const json = JSON.stringify(exportObj, null, 2);
            const blob = new Blob([json], {type: "application/json"});
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url; a.download = `bubbleview-data-${Date.now()}.json`;
            a.click();
        }

        function downloadHeatmapImage() {
            // (同上個版本的實作,略作簡化以節省空間)
            const tC = document.createElement('canvas'); const tX = tC.getContext('2d');
            tC.width = mainCanvas.width; tC.height = mainCanvas.height;
            tX.drawImage(originalImage,0,0,tC.width,tC.height);
            tX.globalAlpha = 0.7; 
            // 確保繪製目前滑桿狀態的熱圖
            const currentHeatmapData = heatCtx.getImageData(0,0,heatCanvas.width,heatCanvas.height);
            const tempHeatCanvas = document.createElement('canvas');
            tempHeatCanvas.width = heatCanvas.width; tempHeatCanvas.height = heatCanvas.height;
            tempHeatCanvas.getContext('2d').putImageData(currentHeatmapData,0,0);
            tX.drawImage(tempHeatCanvas,0,0);
            const a = document.createElement('a');
            a.download = `heatmap-${Date.now()}.png`; a.href = tC.toDataURL(); a.click();
        }
    </script>
</body>
</html>

螢幕擷取畫面 2025-12-17 152801.png

try:
    import json
    import numpy as np
    import matplotlib
    # 強制使用 TkAgg 介面,解決視窗不跳出的問題
    matplotlib.use('TkAgg') 
    import matplotlib.pyplot as plt
    from scipy.ndimage import gaussian_filter
    from PIL import Image
    print("工具載入成功!")
except ImportError as e:
    print("嚴重錯誤:您的電腦缺少必要套件!")
    print(f"請關閉這個視窗,執行指令:py -m pip install matplotlib scipy numpy pillow")
    input("按 Enter 結束...")
    sys.exit()

# ==========================================
# 設定區
JSON_FILE = 'data.json'      # 您的 json 檔名
IMAGE_FILE = 'image.png'     # 您的圖片檔名 (請確認副檔名)
SIGMA = 25                   # 模糊程度
# ==========================================

def main():
    try:
        # 2. 讀取 JSON
        print(f"正在讀取 {JSON_FILE}...")
        with open(JSON_FILE, 'r', encoding='utf-8') as f:
            data = json.load(f)
            if isinstance(data, list):
                clicks = data
            elif 'clicks' in data:
                clicks = data['clicks']
            else:
                print("錯誤:JSON 檔案裡面找不到 'clicks' 數據!")
                return

        print(f"讀取成功!共有 {len(clicks)} 個點擊。")

        # 3. 讀取圖片
        print(f"正在讀取圖片 {IMAGE_FILE}...")
        img = Image.open(IMAGE_FILE)
        width, height = img.size

        # 4. 製作熱圖矩陣
        heatmap = np.zeros((height, width))
        for click in clicks:
            x, y = int(click['x']), int(click['y'])
            if 0 <= x < width and 0 <= y < height:
                heatmap[y, x] += 1

        # 5. 高斯模糊
        print("正在運算熱圖 (Gaussian Blur)...")
        heatmap_blurred = gaussian_filter(heatmap, sigma=SIGMA)

        # 6. 畫圖
        plt.figure(figsize=(10, 10))
        plt.imshow(img)
        plt.imshow(heatmap_blurred, cmap='jet', alpha=0.6)
        plt.axis('off')
        plt.title(f'BubbleView Heatmap (N={len(clicks)})')
        plt.colorbar(label='Attention Density')

        # === 關鍵修改:不管視窗有沒有跳出來,都先存一張圖 ===
        output_filename = "result_heatmap.png"
        plt.savefig(output_filename, bbox_inches='tight')
        print(f"\\n★ 成功!熱點圖已經存檔為:{output_filename}")
        print("請去您的資料夾打開這張圖片看看!")

        # 嘗試開啟視窗
        print("正在嘗試開啟預覽視窗...")
        plt.show()

    except FileNotFoundError:
        print("\\n錯誤:找不到檔案!")
        print(f"請確認資料夾內是否有 {JSON_FILE} 和 {IMAGE_FILE}")
    except Exception as e:
        print(f"\\n發生未預期的錯誤:{e}")

if __name__ == "__main__":
    main()
    input("\\n程式執行完畢,按 Enter 鍵離開...")

result_heatmap.png

import json
import numpy as np
import matplotlib
matplotlib.use('TkAgg') # 強制開啟視窗
import matplotlib.pyplot as plt
import matplotlib.cm as cm
from PIL import Image

# ====================
# 設定區
JSON_FILE = 'data.json'
IMAGE_FILE = 'image.png'
# ====================

def main():
    try:
        # 1. 讀取資料
        with open(JSON_FILE, 'r', encoding='utf-8') as f:
            data = json.load(f)
            clicks = data if isinstance(data, list) else data.get('clicks', [])

        if not clicks:
            print("錯誤:找不到點擊數據!")
            return

        # 讀取圖片
        img = Image.open(IMAGE_FILE)
        width, height = img.size

        # 提取 x, y, time
        x = [c['x'] for c in clicks]
        y = [c['y'] for c in clicks]
        t = [c['time'] for c in clicks]
        
        # 將時間正規化 (變成 0~1 之間的小數,方便上色)
        t_normalized = np.array(t)
        t_normalized = (t_normalized - min(t_normalized)) / (max(t_normalized) - min(t_normalized) + 1e-5)

        # 設定畫布 (建立兩張圖:左邊是路徑,右邊是時間顏色)
        fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 7))

        # === 圖表 1: 掃描路徑圖 (Scanpath) ===
        ax1.imshow(img, alpha=0.6) # 淡化背景圖
        ax1.plot(x, y, 'k-', alpha=0.5, linewidth=1) # 畫出黑色連線
        
        # 畫出帶有數字的圓點
        for i in range(len(x)):
            # 前 3 個點用紅色特別標示 (起始點)
            color = 'red' if i < 3 else 'blue'
            ax1.plot(x[i], y[i], 'o', color=color, markersize=10, alpha=0.8)
            ax1.text(x[i], y[i], str(i+1), color='white', fontsize=8, ha='center', va='center', fontweight='bold')
        
        ax1.set_title(f"Scanpath (Order 1-{len(x)})")
        ax1.axis('off')

        # === 圖表 2: 時間分佈圖 (Temporal Scatter) ===
        ax2.imshow(img, cmap='gray', alpha=0.4) # 背景轉灰階,讓顏色更明顯
        
        # 根據時間畫點 (顏色對應時間:紫=早 -> 黃=晚)
        sc = ax2.scatter(x, y, c=t, cmap='viridis', s=150, edgecolors='black', alpha=0.8)
        
        ax2.set_title("Temporal Distribution (Color = Time)")
        ax2.axis('off')
        
        # 加入色條
        cbar = plt.colorbar(sc, ax=ax2, orientation='horizontal', fraction=0.05, pad=0.05)
        cbar.set_label('Time (ms)')

        print("分析完成!正在顯示圖表...")
        plt.tight_layout()
        plt.show()

    except FileNotFoundError:
        print("錯誤:找不到 data.json 或 image.png,請確認檔案位置。")
    except Exception as e:
        print(f"發生錯誤:{e}")

if __name__ == "__main__":
    main()

Figure_1.png