0918課堂點名

流星燈
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 的圓
}
}

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的圓
}

<!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>

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 鍵離開...")

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()
