比較検索なら 天秤AI byGMO
クロード3.7coder

クロード3.7coder

ウェブアプリ開発支援

推奨モデル - Claude
0
0
38
0

投稿日時:

  • プロンプト実行例
  • プロンプトを見る
クロード3.7coder

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

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

クロード3.7coder

ユーザーの要望を理解しました。数学の答案用紙を自動採点するウェブアプリを作成します。

まず、いくつか確認させてください:

  1. 答案用紙の形式はどのようなものですか?(画像ファイル、PDFなど)
  2. 手書き文字の認識にはどのような手法を使いますか?(OCRライブラリなど)
  3. 正解用紙と生徒の答案用紙はどのような形式で比較しますか?特定の領域やフォーマットがありますか?

これらの情報があると、より適切なコードを提供できます。

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

クロード3.7coder

理解しました。画像ファイルをアップロードして、OCRを使用して手書き文字を認識し、正解の解答と比較するウェブアプリを作成します。比較方法については、シンプルなテキスト比較を実装します。

以下に、すべてのコードをHTMLファイル1つにまとめたものを提供します:

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

この採点アプリケーションは以下の機能を持っています:

  1. 画像ファイルのアップロード

    • 正解の解答用紙と生徒の答案用紙のそれぞれの画像をアップロードできます
    • プレビュー表示機能付き
  2. OCR処理

    • Tesseract.jsを使用して画像から文字を認識します
    • 日本語、英語、数式に対応
  3. テキスト解析

    • 認識されたテキストを問題ごとに分割
    • 「答え:」「=」などのパターンから解答部分を抽出
  4. 採点処理

    • 正解と生徒の解答を比較して採点
    • 完全一致の場合は〇、不一致は×、不明確な場合は?と表示
  5. 結果表示

    • 問題番号、正解、生徒の解答、採点結果を表形式で表示
    • 結果に応じて色分け表示(〇は緑、×は赤、?はオレンジ)

使い方は簡単で、正解用紙と生徒の答案用紙をアップロードして「採点開始」ボタンをクリックするだけです。画像のサイズによってはOC

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

クロード3.7coder

文字認識の精度に問題があるようですね。OCRの精度を向上させるため、いくつかの改善を行いましょう。具体的には:

  1. 画像の前処理を追加して認識精度を高める
  2. Tesseractのパラメータを調整
  3. 手書き文字に特化した設定を追加
  4. 画像を処理しやすい形に変換

以下が改良版のコードです:

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

続き

クロード3.7coder

続きです。HTMLファイルのJavaScriptコード部分の残りを記述します:

html
1<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

続きを書いて

クロード3.7coder

続きです。スクリプトの最後の部分と終了タグを記述します:

html
1        // 初期設定を適用
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>

このウェブアプリケーションには以下の改良が含まれています:

  1. 高度な画像前処理

    • OpenCV.jsを使用して画像を前処理
    • グレースケール変換、ノイズ除去、二値化処理
    • 適用レベル(軽度、中度、強度)の選択可能
  2. OCRの精度向上

    • Tesseractの詳細パラメータを設定
    • 認識する文字のホワイトリスト(日本語、英語、数字、記号など)を定義
    • エンジンモードを選択可能
  3. UI改善

    • 採点結果とOCR認識結果をタブで切り替え表示
    • 画像前処理と採点の分離
    • プロセス状況の可視化(前処理済み画像の表示)
    • 処理設定パネル(閾値、前処理レベルなどの調整可能)
  4. エラー処理

    • 処理中のエラーを詳細に表示
    • OpenCVが利用できない場合の代替動作

使用方法:

  1. 正解の解答用紙と生徒の答案用紙の画像をアップロード
  2. 必要に応じてOCR設定を調整(文字認識言語、前処理レベルなど)
  3. 「画像前処理」ボタンをクリックして画像を処理(オプション)
  4. 「採点開始」ボタンをクリックして採点実行
  5. 「採点結果」タブで結果確認、または「OCR認識結果」タブで認識されたテキストを確認

このアプリケーションはブラウザのみで動作し、サーバーサイドの処理は必要ありません。文字認識の精度を高めるため、OCR設定のパラメータを調整することができます。

前処理レベルや二値化の閾値を調整することで、手書きの答案の認識精度を向上させることができるでしょう。

コメント 0

他のプロンプトもチェック