pulse-monitor/backups/backup_20251127/html_report.go

282 lines
10 KiB
Go
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.

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()">&times;</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 (&lt;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 &lt;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()
}