282 lines
10 KiB
Go
282 lines
10 KiB
Go
package main
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
)
|
||
|
||
type ReviewEntry struct {
|
||
FrameNum int
|
||
Timestamp string
|
||
SpO2Value int
|
||
SpO2LeftDigit int
|
||
SpO2LeftConf float64
|
||
SpO2RightDigit int
|
||
SpO2RightConf float64
|
||
HRValue int
|
||
HRLeftDigit int
|
||
HRLeftConf float64
|
||
HRRightDigit int
|
||
HRRightConf float64
|
||
Failed bool
|
||
FailureReason string
|
||
Unstable bool
|
||
UnstableReason string
|
||
}
|
||
|
||
// initReviewHTML creates the HTML file with header and opens the table
|
||
func initReviewHTML() error {
|
||
html := `<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>Pulse-Ox Recognition Review</title>
|
||
<style>
|
||
body { font-family: Arial, sans-serif; margin: 20px; background-color: #000; color: #fff; }
|
||
h1 { color: #fff; }
|
||
table { border-collapse: collapse; width: 100%; margin-top: 20px; }
|
||
th, td { border: 1px solid #444; padding: 12px; text-align: center; }
|
||
th { background-color: #2a5c2e; color: white; }
|
||
tr:nth-child(even) { background-color: #1a1a1a; }
|
||
tr:nth-child(odd) { background-color: #0a0a0a; }
|
||
.conf-high { color: #4CAF50; font-weight: bold; }
|
||
.conf-med { color: #ff9800; font-weight: bold; }
|
||
.conf-low { color: #f44336; font-weight: bold; }
|
||
.digit-box { display: inline-flex; align-items: center; margin: 5px; gap: 8px; }
|
||
.digit-img { max-height: 40px; cursor: pointer; transition: transform 0.2s; }
|
||
.digit-img:hover { opacity: 0.8; }
|
||
.ocr-value { font-size: 54px; font-weight: bold; }
|
||
p { color: #ccc; }
|
||
strong { color: #fff; }
|
||
code { background-color: #222; padding: 2px 6px; border-radius: 3px; color: #4CAF50; }
|
||
.modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.95); }
|
||
.modal-content { margin: auto; display: block; max-width: 90%; max-height: 90%; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }
|
||
.close { position: absolute; top: 20px; right: 35px; color: #f1f1f1; font-size: 40px; font-weight: bold; cursor: pointer; }
|
||
.close:hover { color: #bbb; }
|
||
</style>
|
||
<script>
|
||
function enlargeImage(imgSrc) {
|
||
var modal = document.getElementById('imageModal');
|
||
var modalImg = document.getElementById('enlargedImg');
|
||
modal.style.display = 'block';
|
||
modalImg.src = imgSrc;
|
||
}
|
||
function closeModal() {
|
||
document.getElementById('imageModal').style.display = 'none';
|
||
}
|
||
window.onclick = function(event) {
|
||
var modal = document.getElementById('imageModal');
|
||
if (event.target == modal) {
|
||
modal.style.display = 'none';
|
||
}
|
||
}
|
||
</script>
|
||
</head>
|
||
<body>
|
||
<div id="imageModal" class="modal" onclick="closeModal()">
|
||
<span class="close" onclick="closeModal()">×</span>
|
||
<img class="modal-content" id="enlargedImg">
|
||
</div>
|
||
|
||
<h1>Pulse-Ox Recognition Review (Live)</h1>
|
||
<p><strong>Tip:</strong> Press Ctrl+F (or Cmd+F on Mac) and search for "CHECK" to find frames with low confidence (<85%)</p>
|
||
<p><em>Refresh page to see latest frames...</em></p>
|
||
<table>
|
||
<tr>
|
||
<th>Frame</th>
|
||
<th>Time</th>
|
||
<th>Detected Boxes</th>
|
||
<th>SpO2 Recognition</th>
|
||
<th>HR Recognition</th>
|
||
</tr>
|
||
`
|
||
return os.WriteFile("review/review.html", []byte(html), 0644)
|
||
}
|
||
|
||
// appendReviewEntry appends a single entry to the HTML file
|
||
func appendReviewEntry(e ReviewEntry) error {
|
||
confClass := func(conf float64) string {
|
||
if conf >= 85 {
|
||
return "conf-high"
|
||
} else if conf >= 70 {
|
||
return "conf-med"
|
||
}
|
||
return "conf-low"
|
||
}
|
||
|
||
needsReview := e.SpO2LeftConf < 85 || e.SpO2RightConf < 85 || e.HRLeftConf < 85 || e.HRRightConf < 85
|
||
reviewMarker := ""
|
||
if needsReview {
|
||
reviewMarker = " ⚠️CHECK"
|
||
}
|
||
|
||
var rowHTML string
|
||
|
||
if e.Failed {
|
||
// Failed recognition entry
|
||
rowHTML = fmt.Sprintf(` <tr style="background-color: #330000;">
|
||
<td>%d%s</td>
|
||
<td>%s</td>
|
||
<td>
|
||
<img class="digit-img" src="f%d_boxes.jpg" onclick="enlargeImage('f%d_boxes.jpg')">
|
||
</td>
|
||
<td style="color: #ff6666;">
|
||
<strong style="color: #ff6666;">❌ FAILED: %s</strong><br>
|
||
<strong>SpO2: %d</strong><br>
|
||
<div style="margin: 10px 0;">
|
||
<img class="digit-img" src="f%d_spo2_checks.png" onclick="enlargeImage('f%d_spo2_checks.png')" style="max-height: 60px; border: 1px solid #444;">
|
||
</div>
|
||
<div class="digit-box">
|
||
<img class="digit-img" src="f%d_spo2_digit1.png" onclick="enlargeImage('f%d_spo2_digit1.png')">
|
||
<span class="ocr-value" style="font-size: 24px;">D1</span>
|
||
</div>
|
||
<div class="digit-box">
|
||
<img class="digit-img" src="f%d_spo2_digit2.png" onclick="enlargeImage('f%d_spo2_digit2.png')">
|
||
<span class="ocr-value">%d</span>
|
||
<span class="%s">(%.0f%%)</span>
|
||
</div>
|
||
<div class="digit-box">
|
||
<img class="digit-img" src="f%d_spo2_digit3.png" onclick="enlargeImage('f%d_spo2_digit3.png')">
|
||
<span class="ocr-value">%d</span>
|
||
<span class="%s">(%.0f%%)</span>
|
||
</div>
|
||
</td>
|
||
<td style="color: #ff6666;">
|
||
<strong>HR: %d</strong><br>
|
||
<div style="margin: 10px 0;">
|
||
<img class="digit-img" src="f%d_hr_checks.png" onclick="enlargeImage('f%d_hr_checks.png')" style="max-height: 60px; border: 1px solid #444;">
|
||
</div>
|
||
<div class="digit-box">
|
||
<img class="digit-img" src="f%d_hr_digit1.png" onclick="enlargeImage('f%d_hr_digit1.png')">
|
||
<span class="ocr-value" style="font-size: 24px;">D1</span>
|
||
</div>
|
||
<div class="digit-box">
|
||
<img class="digit-img" src="f%d_hr_digit2.png" onclick="enlargeImage('f%d_hr_digit2.png')">
|
||
<span class="ocr-value">%d</span>
|
||
<span class="%s">(%.0f%%)</span>
|
||
</div>
|
||
<div class="digit-box">
|
||
<img class="digit-img" src="f%d_hr_digit3.png" onclick="enlargeImage('f%d_hr_digit3.png')">
|
||
<span class="ocr-value">%d</span>
|
||
<span class="%s">(%.0f%%)</span>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`, e.FrameNum, reviewMarker, e.Timestamp, e.FrameNum, e.FrameNum, e.FailureReason,
|
||
e.SpO2Value,
|
||
e.FrameNum, e.FrameNum,
|
||
e.FrameNum, e.FrameNum,
|
||
e.FrameNum, e.FrameNum, e.SpO2LeftDigit, confClass(e.SpO2LeftConf), e.SpO2LeftConf,
|
||
e.FrameNum, e.FrameNum, e.SpO2RightDigit, confClass(e.SpO2RightConf), e.SpO2RightConf,
|
||
e.HRValue,
|
||
e.FrameNum, e.FrameNum,
|
||
e.FrameNum, e.FrameNum,
|
||
e.FrameNum, e.FrameNum, e.HRLeftDigit, confClass(e.HRLeftConf), e.HRLeftConf,
|
||
e.FrameNum, e.FrameNum, e.HRRightDigit, confClass(e.HRRightConf), e.HRRightConf)
|
||
} else {
|
||
// Successful recognition entry
|
||
unstableMarker := ""
|
||
if e.Unstable {
|
||
unstableMarker = fmt.Sprintf(" <br><span style='color: #ff9800; font-size: 12px;'>⚠️ %s</span>", e.UnstableReason)
|
||
}
|
||
rowHTML = fmt.Sprintf(` <tr>
|
||
<td>%d%s%s</td>
|
||
<td>%s</td>
|
||
<td>
|
||
<img class="digit-img" src="f%d_boxes.jpg" onclick="enlargeImage('f%d_boxes.jpg')">
|
||
</td>
|
||
<td>
|
||
<strong>%d</strong><br>
|
||
<div style="margin: 10px 0;">
|
||
<img class="digit-img" src="f%d_spo2_checks.png" onclick="enlargeImage('f%d_spo2_checks.png')" style="max-height: 60px; border: 1px solid #444;">
|
||
</div>
|
||
<div class="digit-box">
|
||
<img class="digit-img" src="f%d_spo2_digit1.png" onclick="enlargeImage('f%d_spo2_digit1.png')">
|
||
<span class="ocr-value" style="font-size: 24px;">D1</span>
|
||
</div>
|
||
<div class="digit-box">
|
||
<img class="digit-img" src="f%d_spo2_digit2.png" onclick="enlargeImage('f%d_spo2_digit2.png')">
|
||
<span class="ocr-value">%d</span>
|
||
<span class="%s">(%.0f%%)</span>
|
||
</div>
|
||
<div class="digit-box">
|
||
<img class="digit-img" src="f%d_spo2_digit3.png" onclick="enlargeImage('f%d_spo2_digit3.png')">
|
||
<span class="ocr-value">%d</span>
|
||
<span class="%s">(%.0f%%)</span>
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<strong>%d</strong><br>
|
||
<div style="margin: 10px 0;">
|
||
<img class="digit-img" src="f%d_hr_checks.png" onclick="enlargeImage('f%d_hr_checks.png')" style="max-height: 60px; border: 1px solid #444;">
|
||
</div>
|
||
<div class="digit-box">
|
||
<img class="digit-img" src="f%d_hr_digit1.png" onclick="enlargeImage('f%d_hr_digit1.png')">
|
||
<span class="ocr-value" style="font-size: 24px;">D1</span>
|
||
</div>
|
||
<div class="digit-box">
|
||
<img class="digit-img" src="f%d_hr_digit2.png" onclick="enlargeImage('f%d_hr_digit2.png')">
|
||
<span class="ocr-value">%d</span>
|
||
<span class="%s">(%.0f%%)</span>
|
||
</div>
|
||
<div class="digit-box">
|
||
<img class="digit-img" src="f%d_hr_digit3.png" onclick="enlargeImage('f%d_hr_digit3.png')">
|
||
<span class="ocr-value">%d</span>
|
||
<span class="%s">(%.0f%%)</span>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`, e.FrameNum, reviewMarker, unstableMarker, e.Timestamp,
|
||
e.FrameNum, e.FrameNum,
|
||
e.SpO2Value,
|
||
e.FrameNum, e.FrameNum,
|
||
e.FrameNum, e.FrameNum,
|
||
e.FrameNum, e.FrameNum, e.SpO2LeftDigit, confClass(e.SpO2LeftConf), e.SpO2LeftConf,
|
||
e.FrameNum, e.FrameNum, e.SpO2RightDigit, confClass(e.SpO2RightConf), e.SpO2RightConf,
|
||
e.HRValue,
|
||
e.FrameNum, e.FrameNum,
|
||
e.FrameNum, e.FrameNum,
|
||
e.FrameNum, e.FrameNum, e.HRLeftDigit, confClass(e.HRLeftConf), e.HRLeftConf,
|
||
e.FrameNum, e.FrameNum, e.HRRightDigit, confClass(e.HRRightConf), e.HRRightConf)
|
||
}
|
||
|
||
// Open file in append mode
|
||
f, err := os.OpenFile("review/review.html", os.O_APPEND|os.O_WRONLY, 0644)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer f.Close()
|
||
|
||
_, err = f.WriteString(rowHTML)
|
||
return err
|
||
}
|
||
|
||
// closeReviewHTML writes the closing tags
|
||
func closeReviewHTML() error {
|
||
footer := ` </table>
|
||
<p style="margin-top: 20px;">
|
||
<strong>Color coding:</strong>
|
||
<span class="conf-high">Green ≥85%</span>,
|
||
<span class="conf-med">Orange 70-84%</span>,
|
||
<span class="conf-low">Red <70%</span>
|
||
</p>
|
||
<p>To add digits to training set, copy files like: <code>cp review/f5_spo2_digit2.png training_digits/9_2.png</code></p>
|
||
</body>
|
||
</html>`
|
||
|
||
f, err := os.OpenFile("review/review.html", os.O_APPEND|os.O_WRONLY, 0644)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer f.Close()
|
||
|
||
_, err = f.WriteString(footer)
|
||
return err
|
||
}
|
||
|
||
// writeReviewHTML - kept for backward compatibility (final shutdown)
|
||
func writeReviewHTML(entries []ReviewEntry) error {
|
||
// Just close the HTML properly - entries already appended
|
||
return closeReviewHTML()
|
||
}
|