Adjust values for ExportQuality

This commit is contained in:
Pierre-Yves Nicolas
2026-01-15 09:55:42 +01:00
parent b979362fa8
commit 5464935291
2 changed files with 222 additions and 2 deletions

View File

@@ -0,0 +1,220 @@
/*
* Copyright 2025 Pierre-Yves Nicolas
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option)
* any later version.
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.fairscan.evaluation
import org.fairscan.imageprocessing.detectDocumentQuad
import org.fairscan.imageprocessing.extractDocument
import org.fairscan.imageprocessing.isColoredDocument
import org.fairscan.imageprocessing.scaledTo
import org.opencv.core.MatOfInt
import org.opencv.imgcodecs.Imgcodecs
import java.io.File
fun main() {
nu.pattern.OpenCV.loadLocally()
ExportQualityEvaluator.runEvaluation()
}
object ExportQualityEvaluator {
fun runEvaluation() {
val root = File("evaluation")
val datasetDir = File(root, "dataset")
val imageDir = File(datasetDir, "images")
val outputDir = File("evaluation/reports/export_quality").apply { mkdirs() }
val imgFiles = imageDir.listFiles { f -> f.extension.lowercase() == "jpg" }
?.toList() ?: listOf<File>()
val qualities = listOf(60, 75, 80)
val maxPixelsList = listOf(1_500_000, 2_000_000, 4_000_000)
for (imgFile in imgFiles) {
val imgName = imgFile.nameWithoutExtension
val maskFile = File(datasetDir, "masks/$imgName.png")
if (!maskFile.exists()) continue
val sourceMat = Imgcodecs.imread(imgFile.absolutePath)
if (sourceMat.empty()) continue
val maskMat = Imgcodecs.imread(maskFile.absolutePath, Imgcodecs.IMREAD_UNCHANGED)
if (maskMat.empty()) continue
println("Processing ${imgName}...")
val mask = MatMask(maskMat)
val quad = detectDocumentQuad(mask, isLiveAnalysis = false)
?.scaledTo(mask.width, mask.height, sourceMat.width(), sourceMat.height())
if (quad == null) {
println("Failed to detect quad for $imgName")
continue
}
val isColored = isColoredDocument(sourceMat, mask, quad)
for (quality in qualities) {
for (maxPixels in maxPixelsList) {
val outputMat =
extractDocument(sourceMat, quad, 0, isColored, maxPixels.toLong())
val outputFile = File(outputDir, "$imgName-$quality-$maxPixels.jpg")
val params = MatOfInt(Imgcodecs.IMWRITE_JPEG_QUALITY, quality)
if (!Imgcodecs.imwrite(outputFile.absolutePath, outputMat, params)) {
throw RuntimeException("Could not write image to ${outputFile.absolutePath}")
}
params.release()
outputMat.release()
}
}
sourceMat.release()
generateHtmlReport(outputDir, imgName, qualities, maxPixelsList, crop = CropParams())
}
}
data class CropParams(
val centerX: Double = 0.5,
val centerY: Double = 0.5,
val size: Double = 0.5
)
fun generateHtmlReport(
outputDir: File,
imgName: String,
qualities: List<Int>,
maxPixelsList: List<Int>,
crop: CropParams
) {
val htmlFile = File(outputDir, "$imgName.html")
htmlFile.writeText(
"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>$imgName Export quality comparison</title>
<style>
body {
font-family: sans-serif;
}
.controls {
margin-bottom: 1em;
}
.grid {
display: grid;
grid-template-columns: repeat(${maxPixelsList.size}, 1fr);
gap: 8px;
}
.cell {
border: 1px solid #ccc;
padding: 4px;
font-size: 12px;
}
.crop {
width: 600px;
height: 300px;
overflow: hidden;
border: 1px solid #000;
}
.crop img {
transform-origin: top left;
}
</style>
</head>
<body>
<h1>$imgName</h1>
<div class="controls">
Center X <input type="range" id="cx" min="0" max="1" step="0.01" value="${crop.centerX}">
Center Y <input type="range" id="cy" min="0" max="1" step="0.01" value="${crop.centerY}">
Size <input type="range" id="size" min="0.05" max="0.5" step="0.01" value="${crop.size}">
</div>
${qualities.joinToString("") { q ->
"""
<div class="grid">
${
maxPixelsList.joinToString("") { mp ->
val fileName = "$imgName-$q-$mp.jpg"
"""
<div class="cell">
<div>q$q - $mp px - ${File(outputDir, fileName).length() / 1024}kB</div>
<div class="crop">
<img src="$fileName" data-img>
</div>
</div>
"""
}
}
</div>
"""
}}
<script>
const imgs = document.querySelectorAll('[data-img]');
const cxInput = document.getElementById('cx');
const cyInput = document.getElementById('cy');
const sizeInput = document.getElementById('size');
function update() {
const cx = parseFloat(cxInput.value);
const cy = parseFloat(cyInput.value);
const size = parseFloat(sizeInput.value);
imgs.forEach(img => {
const crop = img.parentElement;
const cropSize = crop.clientWidth;
const iw = img.naturalWidth;
const ih = img.naturalHeight;
const minDim = Math.min(iw, ih);
const cropPixels = size * minDim;
const scale = cropSize / cropPixels;
const centerX = cx * iw;
const centerY = cy * ih;
const x0 = centerX - cropPixels / 2;
const y0 = centerY - cropPixels / 2;
img.style.transform =
`translate(${'$'}{-x0 * scale}px, ${'$'}{-y0 * scale}px) scale(${'$'}{scale})`;
});
}
imgs.forEach(img => {
if (img.complete) update();
else img.onload = update;
});
cxInput.oninput = update;
cyInput.oninput = update;
sizeInput.oninput = update;
</script>
</body>
</html>
""".trimIndent()
)
}
}