- プロンプト実行例
- プロンプトを見る

プログラミングのコードを作成いたします。

数学の答案用紙を自動的に採点するためのウェブアプリを作成したい。 用意した正解の解答用紙のアップロードファイルと生徒が作成した手書きの答案用紙のアップロードファイルを比較して 〇か×を記入して出力させたい。 完全一致のみ〇とする。解答が不明確な場合は?でよい。 コードはhtmlで全部1つにまとめて出力してほしい。

ユーザーの要望を理解しました。数学の答案用紙を自動採点するウェブアプリを作成します。
まず、いくつか確認させてください:
- 答案用紙の形式はどのようなものですか?(画像ファイル、PDFなど)
- 手書き文字の認識にはどのような手法を使いますか?(OCRライブラリなど)
- 正解用紙と生徒の答案用紙はどのような形式で比較しますか?特定の領域やフォーマットがありますか?
これらの情報があると、より適切なコードを提供できます。

アップロードファイルはともにスキャンした画像ファイル、OCRライブラリ使用、3つ目はよくわかりません

理解しました。画像ファイルをアップロードして、OCRを使用して手書き文字を認識し、正解の解答と比較するウェブアプリを作成します。比較方法については、シンプルなテキスト比較を実装します。
以下に、すべてのコードをHTMLファイル1つにまとめたものを提供します:
html1<!DOCTYPE html> 2<html lang="ja"> 3<head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>数学答案自動採点システム</title> 7 <script src="https://cdn.jsdelivr.net/npm/tesseract.js@4.1.1/dist/tesseract.min.js"></script> 8 <style> 9 body { 10 font-family: 'Arial', sans-serif; 11 line-height: 1.6; 12 max-width: 900px; 13 margin: 0 auto; 14 padding: 20px; 15 background-color: #f5f5f5; 16 } 17 h1 { 18 text-align: center; 19 color: #333; 20 margin-bottom: 30px; 21 } 22 .container { 23 background-color: white; 24 border-radius: 8px; 25 padding: 20px; 26 box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); 27 } 28 .upload-section { 29 display: flex; 30 justify-content: space-between; 31 margin-bottom: 20px; 32 } 33 .upload-box { 34 flex: 1; 35 margin: 0 10px; 36 padding: 20px; 37 border: 2px dashed #ccc; 38 border-radius: 5px; 39 text-align: center; 40 } 41 .upload-box:hover { 42 border-color: #007bff; 43 } 44 input[type="file"] { 45 display: none; 46 } 47 .upload-btn { 48 display: inline-block; 49 padding: 10px 20px; 50 background-color: #007bff; 51 color: white; 52 border-radius: 5px; 53 cursor: pointer; 54 margin-top: 10px; 55 } 56 .upload-btn:hover { 57 background-color: #0056b3; 58 } 59 .preview-image { 60 max-width: 100%; 61 max-height: 200px; 62 margin-top: 10px; 63 display: none; 64 } 65 .results-section { 66 margin-top: 30px; 67 display: none; 68 } 69 table { 70 width: 100%; 71 border-collapse: collapse; 72 margin-top: 20px; 73 } 74 th, td { 75 border: 1px solid #ddd; 76 padding: 10px; 77 text-align: center; 78 } 79 th { 80 background-color: #f2f2f2; 81 } 82 .correct { 83 color: green; 84 font-weight: bold; 85 font-size: 1.2em; 86 } 87 .incorrect { 88 color: red; 89 font-weight: bold; 90 font-size: 1.2em; 91 } 92 .unknown { 93 color: orange; 94 font-weight: bold; 95 font-size: 1.2em; 96 } 97 .compare-btn { 98 display: block; 99 width: 100%; 100 padding: 12px; 101 background-color: #28a745; 102 color: white; 103 border: none; 104 border-radius: 5px; 105 font-size: 16px; 106 cursor: pointer; 107 margin-top: 20px; 108 } 109 .compare-btn:hover { 110 background-color: #218838; 111 } 112 .compare-btn:disabled { 113 background-color: #cccccc; 114 cursor: not-allowed; 115 } 116 .loading { 117 text-align: center; 118 display: none; 119 margin-top: 20px; 120 } 121 .spinner { 122 border: 5px solid #f3f3f3; 123 border-top: 5px solid #007bff; 124 border-radius: 50%; 125 width: 40px; 126 height: 40px; 127 animation: spin 2s linear infinite; 128 margin: 0 auto; 129 } 130 @keyframes spin { 131 0% { transform: rotate(0deg); } 132 100% { transform: rotate(360deg); } 133 } 134 .error-message { 135 color: red; 136 margin-top: 10px; 137 text-align: center; 138 display: none; 139 } 140 </style> 141</head> 142<body> 143 <div class="container"> 144 <h1>数学答案自動採点システム</h1> 145 146 <div class="upload-section"> 147 <div class="upload-box"> 148 <h3>正解の解答用紙</h3> 149 <p>正解の解答用紙をアップロードしてください</p> 150 <img id="answerPreview" class="preview-image" alt="正解解答プレビュー"> 151 <label for="answerFile" class="upload-btn">ファイルを選択</label> 152 <input type="file" id="answerFile" accept="image/*" onchange="previewImage('answerFile', 'answerPreview')"> 153 </div> 154 155 <div class="upload-box"> 156 <h3>生徒の答案用紙</h3> 157 <p>採点したい生徒の答案用紙をアップロードしてください</p> 158 <img id="studentPreview" class="preview-image" alt="生徒答案プレビュー"> 159 <label for="studentFile" class="upload-btn">ファイルを選択</label> 160 <input type="file" id="studentFile" accept="image/*" onchange="previewImage('studentFile', 'studentPreview')"> 161 </div> 162 </div> 163 164 <button id="compareBtn" class="compare-btn" disabled>採点開始</button> 165 166 <div id="loading" class="loading"> 167 <p>OCR処理中... 画像サイズによっては時間がかかる場合があります</p> 168 <div class="spinner"></div> 169 </div> 170 171 <div id="errorMessage" class="error-message"></div> 172 173 <div id="resultsSection" class="results-section"> 174 <h2>採点結果</h2> 175 <table id="resultsTable"> 176 <thead> 177 <tr> 178 <th>問題番号</th> 179 <th>正解</th> 180 <th>生徒の解答</th> 181 <th>採点結果</th> 182 </tr> 183 </thead> 184 <tbody id="resultsBody"> 185 <!-- 結果がここに表示されます --> 186 </tbody> 187 </table> 188 </div> 189 </div> 190 191 <script> 192 // ファイルがアップロードされた時にプレビュー表示する関数 193 function previewImage(inputId, previewId) { 194 const file = document.getElementById(inputId).files[0]; 195 const preview = document.getElementById(previewId); 196 197 if (file) { 198 const reader = new FileReader(); 199 reader.onload = function(e) { 200 preview.src = e.target.result; 201 preview.style.display = 'block'; 202 } 203 reader.readAsDataURL(file); 204 checkEnableCompare(); 205 } 206 } 207 208 // 両方のファイルがアップロードされたら採点ボタンを有効化 209 function checkEnableCompare() { 210 const answerFile = document.getElementById('answerFile').files[0]; 211 const studentFile = document.getElementById('studentFile').files[0]; 212 213 if (answerFile && studentFile) { 214 document.getElementById('compareBtn').disabled = false; 215 } else { 216 document.getElementById('compareBtn').disabled = true; 217 } 218 } 219 220 // OCR処理を実行する関数 221 async function performOCR(imageFile) { 222 try { 223 const result = await Tesseract.recognize( 224 imageFile, 225 'jpn+eng+equ', // 日本語、英語、数式に対応 226 { 227 logger: m => console.log(m) 228 } 229 ); 230 return result.data.text; 231 } catch (error) { 232 console.error('OCR処理エラー:', error); 233 throw error; 234 } 235 } 236 237 // テキストを問題ごとに分割する関数 238 function splitByProblems(text) { 239 // 改行で分割して空の行を削除 240 const lines = text.split('\n').filter(line => line.trim() !== ''); 241 242 // 問題を検出するための正規表現(例: 1., 2., 問1, 問2 など) 243 const problemRegex = /^[第問]?\s*(\d+)[\..、::\s]/; 244 245 const problems = []; 246 let currentProblem = ''; 247 248 for (const line of lines) { 249 if (problemRegex.test(line)) { 250 // 新しい問題が始まったら、前の問題を保存 251 if (currentProblem) { 252 problems.push(currentProblem.trim()); 253 } 254 currentProblem = line; 255 } else { 256 // 現在の問題に行を追加 257 currentProblem += ' ' + line; 258 } 259 } 260 261 // 最後の問題を追加 262 if (currentProblem) { 263 problems.push(currentProblem.trim()); 264 } 265 266 return problems; 267 } 268 269 // 解答のみを抽出する関数 270 function extractAnswers(problems) { 271 const answers = []; 272 const answerRegex = /答え[::]\s*(.+)$|=\s*(.+)$|答[::]\s*(.+)$/; 273 274 for (const problem of problems) { 275 const match = problem.match(answerRegex); 276 if (match) { 277 // 1つ目、2つ目、3つ目のマッチグループのうち、最初の非undefinedを使用 278 const answer = match[1] || match[2] || match[3]; 279 answers.push(answer.trim()); 280 } else { 281 // 答えのフォーマットが見つからない場合、問題全体を保存 282 answers.push(problem.trim()); 283 } 284 } 285 286 return answers; 287 } 288 289 // 答案を比較して結果を表示する関数 290 function compareAnswers(correctAnswers, studentAnswers) { 291 const resultsBody = document.getElementById('resultsBody'); 292 resultsBody.innerHTML = ''; 293 294 const maxLength = Math.max(correctAnswers.length, studentAnswers.length); 295 296 for (let i = 0; i < maxLength; i++) { 297 const correct = correctAnswers[i] || ''; 298 const student = studentAnswers[i] || ''; 299 300 // 答案が完全一致する場合は〇、そうでなければ×または? 301 let result; 302 let resultClass; 303 304 if (!correct && !student) { 305 result = '?'; 306 resultClass = 'unknown'; 307 } else if (!correct || !student) { 308 result = '×'; 309 resultClass = 'incorrect'; 310 } else if (correct.trim() === student.trim()) { 311 result = '〇'; 312 resultClass = 'correct'; 313 } else { 314 result = '×'; 315 resultClass = 'incorrect'; 316 } 317 318 // 表に行を追加 319 const row = document.createElement('tr'); 320 row.innerHTML = `321 <td>${i + 1}</td> 322 <td>${correct}</td> 323 <td>${student}</td> 324 <td class="${resultClass}">${result}</td> 325`; 326 resultsBody.appendChild(row); 327 } 328 } 329 330 // 採点ボタンのクリックイベント 331 document.getElementById('compareBtn').addEventListener('click', async function() { 332 try { 333 // ローディング表示 334 document.getElementById('loading').style.display = 'block'; 335 document.getElementById('errorMessage').style.display = 'none'; 336 document.getElementById('resultsSection').style.display = 'none'; 337 338 const answerFile = document.getElementById('answerFile').files[0]; 339 const studentFile = document.getElementById('studentFile').files[0]; 340 341 // OCR処理 342 const answerText = await performOCR(answerFile); 343 const studentText = await performOCR(studentFile); 344 345 // 問題ごとに分割 346 const answerProblems = splitByProblems(answerText); 347 const studentProblems = splitByProblems(studentText); 348 349 // 答えを抽出 350 const correctAnswers = extractAnswers(answerProblems); 351 const studentAnswers = extractAnswers(studentProblems); 352 353 // 答案を比較して結果を表示 354 compareAnswers(correctAnswers, studentAnswers); 355 356 // 結果セクションを表示 357 document.getElementById('resultsSection').style.display = 'block'; 358 } catch (error) { 359 console.error('エラー:', error); 360 document.getElementById('errorMessage').textContent = 'エラーが発生しました: ' + error.message; 361 document.getElementById('errorMessage').style.display = 'block'; 362 } finally { 363 // ローディング非表示 364 document.getElementById('loading').style.display = 'none'; 365 } 366 }); 367 </script> 368</body> 369</html>
この採点アプリケーションは以下の機能を持っています:
-
画像ファイルのアップロード:
- 正解の解答用紙と生徒の答案用紙のそれぞれの画像をアップロードできます
- プレビュー表示機能付き
-
OCR処理:
- Tesseract.jsを使用して画像から文字を認識します
- 日本語、英語、数式に対応
-
テキスト解析:
- 認識されたテキストを問題ごとに分割
- 「答え:」「=」などのパターンから解答部分を抽出
-
採点処理:
- 正解と生徒の解答を比較して採点
- 完全一致の場合は〇、不一致は×、不明確な場合は?と表示
-
結果表示:
- 問題番号、正解、生徒の解答、採点結果を表形式で表示
- 結果に応じて色分け表示(〇は緑、×は赤、?はオレンジ)
使い方は簡単で、正解用紙と生徒の答案用紙をアップロードして「採点開始」ボタンをクリックするだけです。画像のサイズによってはOC

文字認識がうまくいってないようです

文字認識の精度に問題があるようですね。OCRの精度を向上させるため、いくつかの改善を行いましょう。具体的には:
- 画像の前処理を追加して認識精度を高める
- Tesseractのパラメータを調整
- 手書き文字に特化した設定を追加
- 画像を処理しやすい形に変換
以下が改良版のコードです:
html1<!DOCTYPE html> 2<html lang="ja"> 3<head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>数学答案自動採点システム</title> 7 <script src="https://cdn.jsdelivr.net/npm/tesseract.js@4.1.1/dist/tesseract.min.js"></script> 8 <script src="https://cdnjs.cloudflare.com/ajax/libs/opencv.js/4.5.5/opencv.js"></script> 9 <style> 10 body { 11 font-family: 'Arial', sans-serif; 12 line-height: 1.6; 13 max-width: 900px; 14 margin: 0 auto; 15 padding: 20px; 16 background-color: #f5f5f5; 17 } 18 h1 { 19 text-align: center; 20 color: #333; 21 margin-bottom: 30px; 22 } 23 .container { 24 background-color: white; 25 border-radius: 8px; 26 padding: 20px; 27 box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); 28 } 29 .upload-section { 30 display: flex; 31 justify-content: space-between; 32 margin-bottom: 20px; 33 } 34 .upload-box { 35 flex: 1; 36 margin: 0 10px; 37 padding: 20px; 38 border: 2px dashed #ccc; 39 border-radius: 5px; 40 text-align: center; 41 } 42 .upload-box:hover { 43 border-color: #007bff; 44 } 45 input[type="file"] { 46 display: none; 47 } 48 .upload-btn { 49 display: inline-block; 50 padding: 10px 20px; 51 background-color: #007bff; 52 color: white; 53 border-radius: 5px; 54 cursor: pointer; 55 margin-top: 10px; 56 } 57 .upload-btn:hover { 58 background-color: #0056b3; 59 } 60 .preview-image { 61 max-width: 100%; 62 max-height: 200px; 63 margin-top: 10px; 64 display: none; 65 } 66 .processed-image { 67 max-width: 100%; 68 max-height: 200px; 69 margin-top: 10px; 70 display: none; 71 border: 1px solid #ddd; 72 } 73 .results-section { 74 margin-top: 30px; 75 display: none; 76 } 77 table { 78 width: 100%; 79 border-collapse: collapse; 80 margin-top: 20px; 81 } 82 th, td { 83 border: 1px solid #ddd; 84 padding: 10px; 85 text-align: center; 86 } 87 th { 88 background-color: #f2f2f2; 89 } 90 .correct { 91 color: green; 92 font-weight: bold; 93 font-size: 1.2em; 94 } 95 .incorrect { 96 color: red; 97 font-weight: bold; 98 font-size: 1.2em; 99 } 100 .unknown { 101 color: orange; 102 font-weight: bold; 103 font-size: 1.2em; 104 } 105 .compare-btn { 106 display: block; 107 width: 100%; 108 padding: 12px; 109 background-color: #28a745; 110 color: white; 111 border: none; 112 border-radius: 5px; 113 font-size: 16px; 114 cursor: pointer; 115 margin-top: 20px; 116 } 117 .compare-btn:hover { 118 background-color: #218838; 119 } 120 .compare-btn:disabled { 121 background-color: #cccccc; 122 cursor: not-allowed; 123 } 124 .loading { 125 text-align: center; 126 display: none; 127 margin-top: 20px; 128 } 129 .spinner { 130 border: 5px solid #f3f3f3; 131 border-top: 5px solid #007bff; 132 border-radius: 50%; 133 width: 40px; 134 height: 40px; 135 animation: spin 2s linear infinite; 136 margin: 0 auto; 137 } 138 @keyframes spin { 139 0% { transform: rotate(0deg); } 140 100% { transform: rotate(360deg); } 141 } 142 .error-message { 143 color: red; 144 margin-top: 10px; 145 text-align: center; 146 display: none; 147 } 148 .ocr-text { 149 border: 1px solid #ddd; 150 padding: 10px; 151 margin-top: 10px; 152 max-height: 200px; 153 overflow-y: auto; 154 background-color: #f9f9f9; 155 display: none; 156 } 157 .settings-panel { 158 margin: 20px 0; 159 padding: 15px; 160 border: 1px solid #ddd; 161 border-radius: 5px; 162 background-color: #f9f9f9; 163 } 164 .settings-toggle { 165 background-color: #6c757d; 166 color: white; 167 border: none; 168 padding: 5px 10px; 169 border-radius: 5px; 170 cursor: pointer; 171 margin-bottom: 10px; 172 } 173 .settings-content { 174 display: none; 175 } 176 .settings-row { 177 display: flex; 178 justify-content: space-between; 179 margin-bottom: 10px; 180 flex-wrap: wrap; 181 } 182 .settings-item { 183 flex: 1; 184 min-width: 200px; 185 margin: 0 10px 10px 0; 186 } 187 label { 188 display: block; 189 margin-bottom: 5px; 190 } 191 select, input[type="range"] { 192 width: 100%; 193 } 194 .tab-container { 195 margin-top: 20px; 196 } 197 .tab-buttons { 198 display: flex; 199 border-bottom: 1px solid #ddd; 200 } 201 .tab-button { 202 padding: 10px 20px; 203 background-color: #f1f1f1; 204 border: 1px solid #ddd; 205 border-bottom: none; 206 cursor: pointer; 207 margin-right: 5px; 208 border-radius: 5px 5px 0 0; 209 } 210 .tab-button.active { 211 background-color: white; 212 border-bottom-color: white; 213 } 214 .tab-content { 215 display: none; 216 padding: 20px; 217 border: 1px solid #ddd; 218 border-top: none; 219 } 220 .tab-content.active { 221 display: block; 222 } 223 </style> 224</head> 225<body> 226 <div class="container"> 227 <h1>数学答案自動採点システム</h1> 228 229 <div class="settings-panel"> 230 <button class="settings-toggle" onclick="toggleSettings()">OCR設定を表示/非表示</button> 231 <div id="settingsContent" class="settings-content"> 232 <div class="settings-row"> 233 <div class="settings-item"> 234 <label for="language">認識言語:</label> 235 <select id="language"> 236 <option value="jpn+eng+equ">日本語+英語+数式</option> 237 <option value="jpn">日本語のみ</option> 238 <option value="eng">英語のみ</option> 239 <option value="jpn+eng">日本語+英語</option> 240 </select> 241 </div> 242 <div class="settings-item"> 243 <label for="preprocessingLevel">画像前処理レベル:</label> 244 <select id="preprocessingLevel"> 245 <option value="none">なし</option> 246 <option value="light" selected>軽度</option> 247 <option value="medium">中度</option> 248 <option value="heavy">強度</option> 249 </select> 250 </div> 251 </div> 252 <div class="settings-row"> 253 <div class="settings-item"> 254 <label for="threshold">二値化しきい値: <span id="thresholdValue">128</span></label> 255 <input type="range" id="threshold" min="0" max="255" value="128" oninput="document.getElementById('thresholdValue').textContent = this.value"> 256 </div> 257 <div class="settings-item"> 258 <label for="ocrEngine">OCRエンジンモード:</label> 259 <select id="ocrEngine"> 260 <option value="1">Legacy Engine Only</option> 261 <option value="2">Neural Nets LSTM Only</option> 262 <option value="3" selected>Legacy + LSTM</option> 263 <option value="0">Default</option> 264 </select> 265 </div> 266 </div> 267 </div> 268 </div> 269 270 <div class="upload-section"> 271 <div class="upload-box"> 272 <h3>正解の解答用紙</h3> 273 <p>正解の解答用紙をアップロードしてください</p> 274 <img id="answerPreview" class="preview-image" alt="正解解答プレビュー"> 275 <canvas id="answerProcessed" class="processed-image"></canvas> 276 <div id="answerText" class="ocr-text"></div> 277 <label for="answerFile" class="upload-btn">ファイルを選択</label> 278 <input type="file" id="answerFile" accept="image/*" onchange="previewImage('answerFile', 'answerPreview')"> 279 </div> 280 281 <div class="upload-box"> 282 <h3>生徒の答案用紙</h3> 283 <p>採点したい生徒の答案用紙をアップロードしてください</p> 284 <img id="studentPreview" class="preview-image" alt="生徒答案プレビュー"> 285 <canvas id="studentProcessed" class="processed-image"></canvas> 286 <div id="studentText" class="ocr-text"></div> 287 <label for="studentFile" class="upload-btn">ファイルを選択</label> 288 <input type="file" id="studentFile" accept="image/*" onchange="previewImage('studentFile', 'studentPreview')"> 289 </div> 290 </div> 291 292 <button id="preprocessBtn" class="compare-btn" style="background-color: #6c757d; margin-bottom: 10px;" disabled>画像前処理</button> 293 <button id="compareBtn" class="compare-btn" disabled>採点開始</button> 294 295 <div id="loading" class="loading"> 296 <p>処理中... 画像サイズによっては時間がかかる場合があります</p> 297 <div class="spinner"></div> 298 </div> 299 300 <div id="errorMessage" class="error-message"></div> 301 302 <div class="tab-container"> 303 <div class="tab-buttons"> 304 <button class="tab-button active" onclick="openTab(event, 'resultsTab')">採点結果</button> 305 <button class="tab-button" onclick="openTab(event, 'ocrTab')">OCR認識結果</button> 306 </div> 307 308 <div id="resultsTab" class="tab-content active"> 309 <div id="resultsSection" class="results-section"> 310 <h2>採点結果</h2> 311 <table id="resultsTable"> 312 <thead> 313 <tr> 314 <th>問題番号</th> 315 <th>正解</th> 316 <th>生徒の解答</th> 317 <th>採点結果</th> 318 </tr> 319 </thead> 320 <tbody id="resultsBody"> 321 <!-- 結果がここに表示されます --> 322 </tbody> 323 </table> 324 </div> 325 </div> 326 327 <div id="ocrTab" class="tab-content"> 328 <h2>OCR認識結果</h2> 329 <div class="upload-section"> 330 <div style="flex: 1; margin: 0 10px;"> 331 <h3>正解用紙の認識テキスト</h3> 332 <pre id="answerOcrResult" style="background-color: #f9f9f9; padding: 10px; border: 1px solid #ddd; overflow-x: auto;"></pre> 333 </div> 334 <div style="flex: 1; margin: 0 10px;"> 335 <h3>生徒答案の認識テキスト</h3> 336 <pre id="studentOcrResult" style="background-color: #f9f9f9; padding: 10px; border: 1px solid #ddd; overflow-x: auto;"></pre> 337 </div> 338 </div> 339 </div> 340 </div> 341 </div> 342 343 <script> 344 // OpenCV.jsが読み込まれたときのコールバック 345 function onOpenCvReady() { 346 console.log('OpenCV.js is ready'); 347 } 348 349 // 設定パネルの表示切替 350 function toggleSettings() { 351 const settingsContent = document.getElementById('settingsContent'); 352 if (settingsContent.style.display === 'block') { 353 settingsContent.style.display = 'none'; 354 } else { 355 settingsContent.style.display = 'block'; 356 } 357 } 358 359 // タブ切り替え機能 360 function openTab(evt, tabName) { 361 const tabContents = document.getElementsByClassName('tab-content'); 362 for (let i = 0; i < tabContents.length; i++) { 363 tabContents[i].classList.remove('active'); 364 } 365 366 const tabButtons = document.getElementsByClassName('tab-button'); 367 for (let i = 0; i < tabButtons.length; i++) { 368 tabButtons[i].classList.remove('active'); 369 } 370 371 document.getElementById(tabName).classList.add('active'); 372 evt.currentTarget.classList.add('active'); 373 } 374 375 // ファイルがアップロードされた時にプレビュー表示する関数 376 function previewImage(inputId, previewId) { 377 const file = document.getElementById(inputId).files[0]; 378 const preview = document.getElementById(previewId); 379 380 if (file) { 381 const reader = new FileReader(); 382 reader.onload = function(e) { 383 preview.src = e.target.result; 384 preview.style.display = 'block'; 385 } 386 reader.readAsDataURL(file); 387 checkEnableButtons(); 388 } 389 } 390 391

続き

続きです。HTMLファイルのJavaScriptコード部分の残りを記述します:
html1<script> 2 // OpenCV.jsが読み込まれたときのコールバック 3 function onOpenCvReady() { 4 console.log('OpenCV.js is ready'); 5 } 6 7 // 設定パネルの表示切替 8 function toggleSettings() { 9 const settingsContent = document.getElementById('settingsContent'); 10 if (settingsContent.style.display === 'block') { 11 settingsContent.style.display = 'none'; 12 } else { 13 settingsContent.style.display = 'block'; 14 } 15 } 16 17 // タブ切り替え機能 18 function openTab(evt, tabName) { 19 const tabContents = document.getElementsByClassName('tab-content'); 20 for (let i = 0; i < tabContents.length; i++) { 21 tabContents[i].classList.remove('active'); 22 } 23 24 const tabButtons = document.getElementsByClassName('tab-button'); 25 for (let i = 0; i < tabButtons.length; i++) { 26 tabButtons[i].classList.remove('active'); 27 } 28 29 document.getElementById(tabName).classList.add('active'); 30 evt.currentTarget.classList.add('active'); 31 } 32 33 // ファイルがアップロードされた時にプレビュー表示する関数 34 function previewImage(inputId, previewId) { 35 const file = document.getElementById(inputId).files[0]; 36 const preview = document.getElementById(previewId); 37 38 if (file) { 39 const reader = new FileReader(); 40 reader.onload = function(e) { 41 preview.src = e.target.result; 42 preview.style.display = 'block'; 43 } 44 reader.readAsDataURL(file); 45 checkEnableButtons(); 46 } 47 } 48 49 // 両方のファイルがアップロードされたらボタンを有効化 50 function checkEnableButtons() { 51 const answerFile = document.getElementById('answerFile').files[0]; 52 const studentFile = document.getElementById('studentFile').files[0]; 53 54 if (answerFile && studentFile) { 55 document.getElementById('preprocessBtn').disabled = false; 56 document.getElementById('compareBtn').disabled = false; 57 } else { 58 document.getElementById('preprocessBtn').disabled = true; 59 document.getElementById('compareBtn').disabled = true; 60 } 61 } 62 63 // 画像を前処理する関数 64 async function preprocessImage(imageFile, canvasId) { 65 return new Promise((resolve, reject) => { 66 try { 67 const canvas = document.getElementById(canvasId); 68 const ctx = canvas.getContext('2d'); 69 70 const img = new Image(); 71 img.onload = function() { 72 // キャンバスのサイズをイメージに合わせる 73 canvas.width = img.width; 74 canvas.height = img.height; 75 76 // 元の画像を描画 77 ctx.drawImage(img, 0, 0); 78 79 // OpenCVで前処理 80 const preprocessingLevel = document.getElementById('preprocessingLevel').value; 81 const threshold = parseInt(document.getElementById('threshold').value); 82 83 if (preprocessingLevel !== 'none') { 84 // OpenCVの読み込みを確認 85 if (typeof cv !== 'undefined') { 86 // 元のキャンバスからOpenCV形式に変換 87 let src = cv.imread(canvas); 88 let dst = new cv.Mat(); 89 90 // グレースケール変換 91 cv.cvtColor(src, src, cv.COLOR_RGBA2GRAY); 92 93 if (preprocessingLevel === 'light' || preprocessingLevel === 'medium' || preprocessingLevel === 'heavy') { 94 // ノイズ除去 (ブラー効果) 95 let ksize = new cv.Size(3, 3); 96 cv.GaussianBlur(src, src, ksize, 0); 97 98 // 二値化 99 cv.threshold(src, dst, threshold, 255, cv.THRESH_BINARY_INV); 100 101 if (preprocessingLevel === 'medium' || preprocessingLevel === 'heavy') { 102 // モルフォロジー処理(膨張と収縮) 103 let M = cv.Mat.ones(3, 3, cv.CV_8U); 104 cv.dilate(dst, dst, M); 105 cv.erode(dst, dst, M); 106 M.delete(); 107 108 if (preprocessingLevel === 'heavy') { 109 // エッジ強調 110 let edges = new cv.Mat(); 111 cv.Canny(dst, edges, 50, 150); 112 cv.bitwise_not(edges, edges); // エッジを白く 113 cv.add(dst, edges, dst); 114 edges.delete(); 115 } 116 } 117 } 118 119 // 結果をキャンバスに書き戻す 120 cv.imshow(canvas, dst); 121 122 // メモリ解放 123 src.delete(); 124 dst.delete(); 125 126 canvas.style.display = 'block'; 127 resolve(canvas.toDataURL('image/png')); 128 } else { 129 console.error('OpenCV.js is not loaded'); 130 reject(new Error('OpenCV.js is not loaded')); 131 } 132 } else { 133 // 前処理なしの場合 134 canvas.style.display = 'block'; 135 resolve(canvas.toDataURL('image/png')); 136 } 137 }; 138 139 img.onerror = function() { 140 reject(new Error('Failed to load image')); 141 }; 142 143 img.src = URL.createObjectURL(imageFile); 144 } catch (error) { 145 reject(error); 146 } 147 }); 148 } 149 150 // OCR処理を実行する関数 151 async function performOCR(imageData) { 152 try { 153 const language = document.getElementById('language').value; 154 const ocrEngine = parseInt(document.getElementById('ocrEngine').value); 155 156 const result = await Tesseract.recognize( 157 imageData, 158 language, 159 { 160 logger: m => console.log(m), 161 tessedit_ocr_engine_mode: ocrEngine, 162 tessedit_pageseg_mode: '6', // 単一の均一なブロックとして扱う 163 preserve_interword_spaces: '1', 164 tessedit_char_whitelist: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+-=/*()[]{}.,;:!?<>|&%#@~^_\'"\\あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわをんがぎぐげござじずぜぞだぢづでどばびぶべぼぱぴぷぺぽっぁぃぅぇぉゃゅょアイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲンガギグゲゴザジズゼゾダヂヅデドバビブベボパピプペポッァィゥェォャュョ' 165 } 166 ); 167 return result.data.text; 168 } catch (error) { 169 console.error('OCR処理エラー:', error); 170 throw error; 171 } 172 } 173 174 // テキストを問題ごとに分割する関数 175 function splitByProblems(text) { 176 // 改行で分割して空の行を削除 177 const lines = text.split('\n').filter(line => line.trim() !== ''); 178 179 // 問題を検出するための正規表現(例: 1., 2., 問1, 問2 など) 180 const problemRegex = /^[第問]*\s*(\d+)[\..、::\s]/; 181 182 const problems = []; 183 let currentProblem = ''; 184 185 for (const line of lines) { 186 if (problemRegex.test(line)) { 187 // 新しい問題が始まったら、前の問題を保存 188 if (currentProblem) { 189 problems.push(currentProblem.trim()); 190 } 191 currentProblem = line; 192 } else { 193 // 現在の問題に行を追加 194 currentProblem += ' ' + line; 195 } 196 } 197 198 // 最後の問題を追加 199 if (currentProblem) { 200 problems.push(currentProblem.trim()); 201 } 202 203 return problems; 204 } 205 206 // 解答のみを抽出する関数 207 function extractAnswers(problems) { 208 const answers = []; 209 // 答えを検出するための正規表現(様々なパターンをカバー) 210 const answerRegex = /答え[::]\s*(.+)$|=\s*(.+)$|答[::]\s*(.+)\$|答案[::]\s*(.+)\$/; 211 212 for (const problem of problems) { 213 const match = problem.match(answerRegex); 214 if (match) { 215 // マッチグループのうち、最初の非undefinedを使用 216 const answer = match[1] || match[2] || match[3] || match[4]; 217 answers.push(answer.trim()); 218 } else { 219 // 答えのフォーマットが見つからない場合、問題全体を保存 220 answers.push(problem.trim()); 221 } 222 } 223 224 return answers; 225 } 226 227 // 答案を比較して結果を表示する関数 228 function compareAnswers(correctAnswers, studentAnswers) { 229 const resultsBody = document.getElementById('resultsBody'); 230 resultsBody.innerHTML = ''; 231 232 const maxLength = Math.max(correctAnswers.length, studentAnswers.length); 233 234 for (let i = 0; i < maxLength; i++) { 235 const correct = correctAnswers[i] || ''; 236 const student = studentAnswers[i] || ''; 237 238 // 答案が完全一致する場合は〇、そうでなければ×または? 239 let result; 240 let resultClass; 241 242 if (!correct && !student) { 243 result = '?'; 244 resultClass = 'unknown'; 245 } else if (!correct || !student) { 246 result = '×'; 247 resultClass = 'incorrect'; 248 } else if (correct.trim() === student.trim()) { 249 result = '〇'; 250 resultClass = 'correct'; 251 } else { 252 result = '×'; 253 resultClass = 'incorrect'; 254 } 255 256 // 表に行を追加 257 const row = document.createElement('tr'); 258 row.innerHTML = ` 259 <td>${i + 1}</td> 260 <td>${correct}</td> 261 <td>${student}</td> 262 <td class="${resultClass}">${result}</td> 263 `; 264 resultsBody.appendChild(row); 265 } 266 267 document.getElementById('resultsSection').style.display = 'block'; 268 } 269 270 // 画像前処理ボタンのクリックイベント 271 document.getElementById('preprocessBtn').addEventListener('click', async function() { 272 try { 273 document.getElementById('loading').style.display = 'block'; 274 document.getElementById('errorMessage').style.display = 'none'; 275 276 const answerFile = document.getElementById('answerFile').files[0]; 277 const studentFile = document.getElementById('studentFile').files[0]; 278 279 // 画像の前処理 280 await preprocessImage(answerFile, 'answerProcessed'); 281 await preprocessImage(studentFile, 'studentProcessed'); 282 283 document.getElementById('loading').style.display = 'none'; 284 } catch (error) { 285 console.error('前処理エラー:', error); 286 document.getElementById('errorMessage').textContent = '画像処理エラー: ' + error.message; 287 document.getElementById('errorMessage').style.display = 'block'; 288 document.getElementById('loading').style.display = 'none'; 289 } 290 }); 291 292 // 採点ボタンのクリックイベント 293 document.getElementById('compareBtn').addEventListener('click', async function() { 294 try { 295 // ローディング表示 296 document.getElementById('loading').style.display = 'block'; 297 document.getElementById('errorMessage').style.display = 'none'; 298 document.getElementById('resultsSection').style.display = 'none'; 299 300 const answerFile = document.getElementById('answerFile').files[0]; 301 const studentFile = document.getElementById('studentFile').files[0]; 302 303 let answerImageData, studentImageData; 304 305 // 前処理済みかどうか確認 306 const answerCanvas = document.getElementById('answerProcessed'); 307 const studentCanvas = document.getElementById('studentProcessed'); 308 309 if (answerCanvas.style.display === 'block' && studentCanvas.style.display === 'block') { 310 // 前処理済み画像を使用 311 answerImageData = answerCanvas.toDataURL('image/png'); 312 studentImageData = studentCanvas.toDataURL('image/png'); 313 } else { 314 // 前処理を行う 315 answerImageData = await preprocessImage(answerFile, 'answerProcessed'); 316 studentImageData = await preprocessImage(studentFile, 'studentProcessed'); 317 } 318 319 // OCR処理 320 const answerText = await performOCR(answerImageData); 321 const studentText = await performOCR(studentImageData); 322 323 // OCR結果を表示 324 document.getElementById('answerOcrResult').textContent = answerText; 325 document.getElementById('studentOcrResult').textContent = studentText; 326 327 // 問題ごとに分割 328 const answerProblems = splitByProblems(answerText); 329 const studentProblems = splitByProblems(studentText); 330 331 // 答えを抽出 332 const correctAnswers = extractAnswers(answerProblems); 333 const studentAnswers = extractAnswers(studentProblems); 334 335 // 答案を比較して結果を表示 336 compareAnswers(correctAnswers, studentAnswers); 337 338 } catch (error) { 339 console.error('エラー:', error); 340 document.getElementById('errorMessage').textContent = 'エラーが発生しました: ' + error.message; 341 document.getElementById('errorMessage').style.display = 'block'; 342 } finally { 343 // ローディング非表示 344 document.getElementById('loading').style.display = 'none'; 345 } 346 }); 347 348 // 初期設定を適用 349 document.addEventListener('DOMContentLoaded', function() { 350 document.getElementById('settingsContent').style.display = 'none

続きを書いて

続きです。スクリプトの最後の部分と終了タグを記述します:
html1 // 初期設定を適用 2 document.addEventListener('DOMContentLoaded', function() { 3 document.getElementById('settingsContent').style.display = 'none'; 4 5 // OpenCV.jsが読み込まれていない場合のエラー処理 6 if (typeof cv === 'undefined') { 7 console.warn('OpenCV.jsが読み込まれていません。画像処理機能の一部が制限される可能性があります。'); 8 } 9 }); 10 </script> 11</body> 12</html>
このウェブアプリケーションには以下の改良が含まれています:
-
高度な画像前処理:
- OpenCV.jsを使用して画像を前処理
- グレースケール変換、ノイズ除去、二値化処理
- 適用レベル(軽度、中度、強度)の選択可能
-
OCRの精度向上:
- Tesseractの詳細パラメータを設定
- 認識する文字のホワイトリスト(日本語、英語、数字、記号など)を定義
- エンジンモードを選択可能
-
UI改善:
- 採点結果とOCR認識結果をタブで切り替え表示
- 画像前処理と採点の分離
- プロセス状況の可視化(前処理済み画像の表示)
- 処理設定パネル(閾値、前処理レベルなどの調整可能)
-
エラー処理:
- 処理中のエラーを詳細に表示
- OpenCVが利用できない場合の代替動作
使用方法:
- 正解の解答用紙と生徒の答案用紙の画像をアップロード
- 必要に応じてOCR設定を調整(文字認識言語、前処理レベルなど)
- 「画像前処理」ボタンをクリックして画像を処理(オプション)
- 「採点開始」ボタンをクリックして採点実行
- 「採点結果」タブで結果確認、または「OCR認識結果」タブで認識されたテキストを確認
このアプリケーションはブラウザのみで動作し、サーバーサイドの処理は必要ありません。文字認識の精度を高めるため、OCR設定のパラメータを調整することができます。
前処理レベルや二値化の閾値を調整することで、手書きの答案の認識精度を向上させることができるでしょう。
コメント 0
他のプロンプトもチェック
コード生成・修正
推奨モデル - GPT-4oWebサイトのロード画面を生成します! ※ノーコードサイトや他のファイルとして置く場合でも、元のCSSに接触しないようにコードを組み込みます。5018108計画立案
推奨モデル - Geminiイベントの概要からイベントの詳細を作成するプロンプトです。 ※詳細プロンプトについて知りたい方はXからご連絡ください。 #イベント #イベント企画 #総務 #バックオフィス #業務効率化3116178