mobileNet/完善KNN/knn-classifier.html

685 lines
24 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KNN 图像分类器 - TensorFlow.js</title>
<!-- !!!!!! 核心劫持代码:确保在任何 TF.js 库之前加载 !!!!!! -->
<script>
(function() {
// 定义你的镜像服务器的公共前缀,用于存放 MobileNet 模型文件
// 根据你提供的最新路径进行更新。
// 确保这些文件就命名为 model.json, group1-shardXof4.bin并直接在此目录下
const MOBILENET_MIRROR_BASE_URL = 'https://goood-space-assets.oss-cn-beijing.aliyuncs.com/public/fetch/mobilenet/';
// 定义需要被劫持的原始 URL 的域名模式
// 我们观察到最终请求来自 storage.googleapis.com
const INTERCEPT_DOMAINS = [
'https://storage.googleapis.com/tfjs-models/', // tfjs 官方模型常用的 CDN
'https://storage.googleapis.com/', // 更宽泛的匹配 Google Storage
'https://tfhub.dev/', // 如果 MobileNet 也会通过 tfhub.dev 间接加载
];
// 备份原始的 fetch 函数
const originalFetch = window.fetch;
window.fetch = function(input, init) {
let url = input;
if (input instanceof Request) {
url = input.url;
}
let newUrl = url;
let isIntercepted = false;
// 检查 URL 是否以我们关注的域名开头
for (const domain of INTERCEPT_DOMAINS) {
if (url.startsWith(domain)) {
// 尝试从 URL 中提取文件名 (不包含查询参数)
// 匹配 model.json 或 group1-shardXofY.bin
const fileNameMatch = url.match(/(model\.json|group1-shard\dof\d\.bin)/);
if (fileNameMatch) {
const fileName = fileNameMatch[0]; // 获取匹配到的文件名
newUrl = MOBILENET_MIRROR_BASE_URL + fileName; // 拼接新的镜像 URL
isIntercepted = true;
break; // 找到匹配的域名和文件,停止循环
}
}
}
if (isIntercepted) {
console.warn(`[TFJS Fetch Intercepted] Original: ${url}`);
console.warn(`[TFJS Fetch Intercepted] Redirecting to: ${newUrl}`);
// 如果 input 是 Request 对象,需要创建新的 Request 对象来修改 URL
if (input instanceof Request) {
try {
input = new Request(newUrl, {
method: input.method,
headers: input.headers,
body: input.body,
referrer: input.referrer,
referrerPolicy: input.referrerPolicy,
mode: 'cors', // 总是使用 CORS 模式
credentials: input.credentials,
cache: 'default',
redirect: 'follow',
integrity: undefined, // 移除 integrity 属性以避免校验失败
signal: input.signal,
});
} catch (e) {
console.error(`[TFJS Fetch Intercepted Error] Failed to create new Request object: ${e.message}. Falling back to URL string.`, input);
// 如果创建 Request 对象失败,回退到直接使用 URL 字符串
input = newUrl;
}
} else {
// 如果 input 是 URL 字符串,直接替换
input = newUrl;
}
}
return originalFetch(input, init).catch(error => {
console.error(`[TFJS Fetch Intercepted Error] Failed to load ${url} (redirected to ${newUrl || url || input}):`, error);
throw error;
});
};
// -------------------- 劫持 XMLHttpRequest API (备用安全网) --------------------
const originalXHR = window.XMLHttpRequest;
window.XMLHttpRequest = function() {
const xhr = new originalXHR();
const originalOpen = xhr.open;
xhr.open = function(method, url, async = true, user = null, password = null) {
let newUrl = url;
let isIntercepted = false;
for (const domain of INTERCEPT_DOMAINS) {
if (url.startsWith(domain)) {
const fileNameMatch = url.match(/(model\.json|group1-shard\dof\d\.bin)/);
if (fileNameMatch) {
const fileName = fileNameMatch[0];
newUrl = MOBILENET_MIRROR_BASE_URL + fileName;
isIntercepted = true;
break;
}
}
}
if (isIntercepted) {
console.warn(`[TFJS XHR Intercepted] Original: ${url}`);
console.warn(`[TFJS XHR Intercepted] Redirecting to: ${newUrl}`);
url = newUrl; // 修改传入 open 的 URL
}
// 调用原始的 open 方法
return originalOpen.apply(this, arguments);
};
// 将原始 XMLHttpRequest 的所有静态属性和方法复制到劫持后的 XMLHttpRequest 构造函数
// 这样像 XHR.UNSENT 等常量仍然可用
for (const key in originalXHR) {
if (typeof originalXHR[key] !== 'function' && originalXHR.hasOwnProperty(key)) {
window.XMLHttpRequest[key] = originalXHR[key];
}
}
return xhr;
};
})();
</script>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/mobilenet@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/knn-classifier@latest"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.main-container {
max-width: 1400px;
margin: 0 auto;
}
h1 {
color: white;
text-align: center;
margin-bottom: 30px;
font-size: 2.5em;
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
}
.grid-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
@media (max-width: 768px) {
.grid-container {
grid-template-columns: 1fr;
}
}
.card {
background: white;
border-radius: 15px;
padding: 25px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
}
.card h2 {
color: #333;
margin-bottom: 20px;
border-bottom: 2px solid #667eea;
padding-bottom: 10px;
}
.class-input {
margin-bottom: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 10px;
}
.class-input h3 {
color: #555;
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 10px;
}
.class-number {
background: #667eea;
color: white;
width: 25px;
height: 25px;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 14px;
}
input[type="text"] {
width: 100%;
padding: 10px;
border: 2px solid #e0e0e0;
border-radius: 5px;
margin-bottom: 10px;
font-size: 16px;
transition: border-color 0.3s;
}
input[type="text"]:focus {
outline: none;
border-color: #667eea;
}
input[type="file"] {
display: none;
}
.file-label {
display: inline-block;
padding: 10px 20px;
background: #667eea;
color: white;
border-radius: 5px;
cursor: pointer;
transition: background 0.3s;
margin-right: 10px;
}
.file-label:hover {
background: #5a67d8;
}
.btn {
padding: 12px 30px;
border: none;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
transition: all 0.3s;
margin: 5px;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #5a67d8;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.btn-success {
background: #48bb78;
color: white;
}
.btn-success:hover {
background: #38a169;
}
.btn-danger {
background: #f56565;
color: white;
}
.btn-danger:hover {
background: #e53e3e;
}
.btn:disabled {
background: #cbd5e0;
cursor: not-allowed;
transform: none;
}
#webcam-container {
position: relative;
width: 100%;
max-width: 640px;
margin: 20px auto;
}
#webcam {
width: 100%;
border-radius: 10px;
background: #000;
}
.samples-count {
display: inline-block;
background: #edf2f7;
padding: 2px 8px;
border-radius: 10px;
font-size: 12px;
color: #4a5568;
margin-left: 5px;
}
.image-preview {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 10px;
max-height: 150px;
overflow-y: auto;
}
.preview-img {
width: 60px;
height: 60px;
object-fit: cover;
border-radius: 5px;
border: 2px solid #e0e0e0;
}
.status-message {
padding: 15px;
border-radius: 5px;
margin: 10px 0;
text-align: center;
font-weight: 500;
}
.status-success {
background: #c6f6d5;
color: #22543d;
border: 1px solid #9ae6b4;
}
.status-error {
background: #fed7d7;
color: #742a2a;
border: 1px solid #fc8181;
}
.status-info {
background: #bee3f8;
color: #2c5282;
border: 1px solid #90cdf4;
}
.button-group {
display: flex;
gap: 10px;
margin: 20px 0;
flex-wrap: wrap;
}
.full-width {
grid-column: 1 / -1;
}
.prediction-results {
margin-top: 20px;
padding: 20px;
background: #f7fafc;
border-radius: 10px;
}
.prediction-item {
padding: 15px;
margin: 10px 0;
background: white;
border-radius: 8px;
border-left: 4px solid #667eea;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.prediction-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.prediction-label {
font-weight: 600;
color: #2d3748;
font-size: 16px;
}
.prediction-confidence {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
padding: 4px 12px;
border-radius: 20px;
font-size: 14px;
font-weight: 500;
min-width: 60px;
text-align: center;
}
.confidence-bar-container {
width: 100%;
height: 24px;
background: #e2e8f0;
border-radius: 12px;
overflow: hidden;
position: relative;
}
.confidence-bar {
height: 100%;
background: linear-gradient(90deg, #667eea, #764ba2);
border-radius: 12px;
transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
min-width: 0;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
}
.confidence-bar::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
animation: shimmer 2s infinite;
}
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
.confidence-bar.high {
background: linear-gradient(90deg, #48bb78, #38a169);
}
.confidence-bar.medium {
background: linear-gradient(90deg, #ed8936, #dd6b20);
}
.confidence-bar.low {
background: linear-gradient(90deg, #f56565, #e53e3e);
}
.confidence-percentage {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
color: white;
font-weight: 600;
font-size: 12px;
text-shadow: 0 1px 2px rgba(0,0,0,0.2);
z-index: 1;
}
.top-tags {
margin: 20px 0;
padding: 15px;
background: #edf2fe;
border-radius: 10px;
}
.tag-item {
display: inline-block;
background: white;
padding: 5px 12px;
margin: 5px;
border-radius: 15px;
font-size: 14px;
border: 1px solid #cbd5e0;
}
.tag-weight {
color: #667eea;
font-weight: bold;
margin-left: 5px;
}
.k-selector {
margin: 15px 0;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
}
.k-selector label {
display: block;
margin-bottom: 10px;
color: #555;
font-weight: 500;
}
.k-value-display {
display: inline-block;
background: #667eea;
color: white;
padding: 2px 8px;
border-radius: 5px;
margin-left: 10px;
}
input[type="range"] {
width: 100%;
margin: 10px 0;
}
.model-info {
margin-top: 20px;
padding: 15px;
background: #f0f4f8;
border-radius: 8px;
font-size: 14px;
}
.info-item {
display: flex;
justify-content: space-between;
margin: 5px 0;
}
.info-label {
color: #718096;
}
.info-value {
color: #2d3748;
font-weight: 500;
}
</style>
</head>
<body>
<div class="main-container">
<h1>🤖 KNN 图像分类器(基于特征标签)</h1>
<div class="grid-container">
<!-- 数据采集卡片 -->
<div class="card">
<h2>📸 数据采集</h2>
<div class="class-input">
<h3><span class="class-number">1</span> 第一类</h3>
<input type="text" id="class1Name" placeholder="输入类别名称(如:猫)" value="类别1">
<label class="file-label" for="class1Images">
选择图片
</label>
<input type="file" id="class1Images" multiple accept="image/*">
<span class="samples-count" id="class1Count">0 张图片</span>
<button class="btn btn-primary" onclick="captureFromWebcam(0)">从摄像头采集</button>
<div class="image-preview" id="class1Preview"></div>
</div>
<div class="class-input">
<h3><span class="class-number">2</span> 第二类</h3>
<input type="text" id="class2Name" placeholder="输入类别名称(如:狗)" value="类别2">
<label class="file-label" for="class2Images">
选择图片
</label>
<input type="file" id="class2Images" multiple accept="image/*">
<span class="samples-count" id="class2Count">0 张图片</span>
<button class="btn btn-primary" onclick="captureFromWebcam(1)">从摄像头采集</button>
<div class="image-preview" id="class2Preview"></div>
</div>
<div class="class-input">
<h3><span class="class-number">3</span> 第三类(可选)</h3>
<input type="text" id="class3Name" placeholder="输入类别名称(可选)" value="类别3">
<label class="file-label" for="class3Images">
选择图片
</label>
<input type="file" id="class3Images" multiple accept="image/*">
<span class="samples-count" id="class3Count">0 张图片</span>
<button class="btn btn-primary" onclick="captureFromWebcam(2)">从摄像头采集</button>
<div class="image-preview" id="class3Preview"></div>
</div>
<div class="button-group">
<button class="btn btn-success" id="addDataBtn">训练KNN模型</button>
<button class="btn btn-danger" id="clearDataBtn">清空数据</button>
</div>
<div id="dataStatus"></div>
</div>
<!-- KNN模型信息卡片 -->
<div class="card">
<h2>🎯 KNN 模型设置</h2>
<div class="k-selector">
<label>
K值最近邻数量
<span class="k-value-display" id="kValueDisplay">3</span>
</label>
<input type="range" id="kValue" min="1" max="20" value="3"
oninput="document.getElementById('kValueDisplay').textContent = this.value">
<small style="color: #718096;">K值越大预测越保守K值越小对局部特征越敏感</small>
</div>
<div class="k-selector">
<label>
滤波器系数 (α)
<span class="k-value-display" id="filterAlphaDisplay">0.3</span>
</label>
<input type="range" id="filterAlpha" min="0.05" max="1.0" step="0.05" value="0.3"
oninput="document.getElementById('filterAlphaDisplay').textContent = this.value">
<small style="color: #718096;">低通滤波器系数:值越小输出越平滑(0.1-0.3推荐),值越大响应越快</small>
</div>
<div class="k-selector">
<label>
距离阈值 (Distance Threshold)
<span class="k-value-display" id="distanceThresholdDisplay">0.5</span>
</label>
<input type="range" id="distanceThreshold" min="0.1" max="2.0" step="0.05" value="0.5"
oninput="document.getElementById('distanceThresholdDisplay').textContent = this.value">
<small style="color: #718096;">距离阈值:样本与训练数据的最大距离,超过此值判定为"未知/背景"(单品类检测关键参数)</small>
</div>
<div class="top-tags" id="topTags">
<h3 style="margin-bottom: 10px;">📊 特征标签提取预览</h3>
<div id="tagsList">等待数据...</div>
</div>
<div class="model-info">
<h3 style="margin-bottom: 10px;"> 模型信息</h3>
<div class="info-item">
<span class="info-label">预训练模型:</span>
<span class="info-value">MobileNet v2</span>
</div>
<div class="info-item">
<span class="info-label">特征维度:</span>
<span class="info-value">1280维嵌入向量</span>
</div>
<div class="info-item">
<span class="info-label">分类器类型:</span>
<span class="info-value">K-最近邻 (KNN)</span>
</div>
<div class="info-item">
<span class="info-label">总样本数:</span>
<span class="info-value" id="totalSamples">0</span>
</div>
</div>
</div>
</div>
<!-- 预测卡片 -->
<div class="card full-width">
<h2>📹 实时预测</h2>
<div class="button-group">
<button class="btn btn-primary" id="startWebcamBtn">启动摄像头</button>
<button class="btn btn-danger" id="stopWebcamBtn" disabled>停止摄像头</button>
<button class="btn btn-success" id="saveModelBtn">保存模型</button>
<button class="btn btn-primary" id="loadModelBtn">加载模型</button>
</div>
<div id="webcam-container">
<video id="webcam" autoplay playsinline muted></video>
</div>
<div class="prediction-results" id="predictionResults">
<h3>预测结果</h3>
<div id="predictions">等待预测...</div>
</div>
<div id="predictionStatus"></div>
</div>
</div>
<script src="knn-classifier.js"></script>
</body>
</html>