mobileNet/随机森林/rf-classifier.html
2025-08-11 17:44:57 +08:00

543 lines
15 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>图像分类器 - TensorFlow.js & decision-tree.js</title>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/mobilenet@latest"></script>
<!-- 引入 decision-tree.js -->
<script src="decision-tree.js"></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>🤖 图像分类器 - 随机森林</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="">
<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">训练模型</button>
<button class="btn btn-danger" id="clearDataBtn">清空数据</button>
</div>
</div>
<!--参数调整-->
<div class="card">
<h2>模型参数调整</h2>
<div class="k-selector">
<label>
树的数量
<span class="k-value-display" id="numTreesDisplay">10</span>
</label>
<input type="range" id="numTrees" min="5" max="50" value="10"
oninput="document.getElementById('numTreesDisplay').textContent = this.value">
<small style="color: #718096;">随机森林中决策树的数量</small>
</div>
<div class="k-selector">
<label>
子集大小 (比例)
<span class="k-value-display" id="subsetSizeDisplay">0.7</span>
</label>
<input type="range" id="subsetSize" min="0.1" max="1" step="0.1" value="0.7"
oninput="document.getElementById('subsetSizeDisplay').textContent = this.value">
<small style="color: #718096;">用于训练每棵树的数据子集占比 (0.1-1.0)</small>
</div>
<div id="dataStatus"></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">随机森林</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>
</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="rf-classifier.js"></script>
</body>
</html>