Skip to content

Commit 795a2cb

Browse files
committed
Add code for plotting bounding boxes and fault types in exported data
1 parent a77f671 commit 795a2cb

3 files changed

Lines changed: 415 additions & 0 deletions

File tree

backend/src/main/java/com/apexgrid/transformertracker/web/InspectionController.java

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111
import com.fasterxml.jackson.databind.node.ArrayNode;
1212
import com.fasterxml.jackson.databind.node.NullNode;
1313
import com.fasterxml.jackson.databind.node.ObjectNode;
14+
import org.springframework.core.io.ClassPathResource;
1415
import org.springframework.http.ContentDisposition;
1516
import org.springframework.http.HttpHeaders;
1617
import org.springframework.http.MediaType;
1718
import org.springframework.http.ResponseEntity;
19+
import org.springframework.util.StringUtils;
1820
import org.springframework.web.bind.annotation.*;
1921
import org.springframework.web.multipart.MultipartFile;
2022

@@ -24,10 +26,13 @@
2426
import java.awt.image.BufferedImage;
2527
import java.io.ByteArrayInputStream;
2628
import java.io.ByteArrayOutputStream;
29+
import java.io.IOException;
30+
import java.io.InputStream;
2731
import java.nio.charset.StandardCharsets;
2832
import java.time.Instant;
2933
import java.util.Base64;
3034
import java.util.List;
35+
import java.util.Locale;
3136
import java.util.Map;
3237
import java.util.zip.ZipEntry;
3338
import java.util.zip.ZipOutputStream;
@@ -99,6 +104,9 @@ public ResponseEntity<?> exportInspection(@PathVariable String id) {
99104
} else {
100105
inspectionNode.putNull("transformer");
101106
}
107+
BaselineSelection baselineSelection = resolveBaselineSelection(inspection);
108+
putNullable(inspectionNode, "baselineWeather",
109+
baselineSelection != null ? baselineSelection.weatherLabel() : null);
102110

103111
JsonNode currentBoxes = parseJsonNode(mapper, inspection.getBoundingBoxes());
104112
JsonNode currentFaults = parseJsonNode(mapper, inspection.getFaultTypes());
@@ -185,6 +193,26 @@ public ResponseEntity<?> exportInspection(@PathVariable String id) {
185193
zos.write(imageBytes);
186194
zos.closeEntry();
187195
}
196+
197+
if (baselineSelection != null) {
198+
byte[] baselineBytes = decodeDataUrl(baselineSelection.dataUrl());
199+
if (baselineBytes != null && baselineBytes.length > 0) {
200+
String baselineExt = guessImageExtension(baselineSelection.dataUrl());
201+
String weatherLabel = baselineSelection.weatherLabel() != null
202+
? sanitizeFilename(baselineSelection.weatherLabel())
203+
: "baseline";
204+
if (weatherLabel.isBlank()) {
205+
weatherLabel = "baseline";
206+
}
207+
ZipEntry baselineEntry = new ZipEntry("baseline-" + weatherLabel + "." + baselineExt);
208+
zos.putNextEntry(baselineEntry);
209+
zos.write(baselineBytes);
210+
zos.closeEntry();
211+
}
212+
}
213+
214+
writeResourceToZip(zos, "export/plot_bounding_boxes.py", "tools/plot_bounding_boxes.py");
215+
writeResourceToZip(zos, "export/README.md", "tools/README.md");
188216
}
189217

190218
byte[] zipBytes = baos.toByteArray();
@@ -674,6 +702,71 @@ private static String sanitizeFilename(String value) {
674702
return sanitized.isBlank() ? "inspection" : sanitized;
675703
}
676704

705+
private static BaselineSelection resolveBaselineSelection(Inspection inspection) {
706+
if (inspection == null) {
707+
return null;
708+
}
709+
Transformer transformer = inspection.getTransformer();
710+
if (transformer == null) {
711+
return null;
712+
}
713+
String preferredWeather = determinePreferredWeather(inspection);
714+
if (StringUtils.hasText(preferredWeather)) {
715+
String candidate = lookupBaselineForWeather(transformer, preferredWeather);
716+
if (StringUtils.hasText(candidate)) {
717+
return new BaselineSelection(candidate, preferredWeather.toLowerCase(Locale.ROOT));
718+
}
719+
}
720+
if (StringUtils.hasText(transformer.getSunnyImage())) {
721+
return new BaselineSelection(transformer.getSunnyImage(), "sunny");
722+
}
723+
if (StringUtils.hasText(transformer.getCloudyImage())) {
724+
return new BaselineSelection(transformer.getCloudyImage(), "cloudy");
725+
}
726+
if (StringUtils.hasText(transformer.getWindyImage())) {
727+
return new BaselineSelection(transformer.getWindyImage(), "windy");
728+
}
729+
return null;
730+
}
731+
732+
private static String determinePreferredWeather(Inspection inspection) {
733+
String weather = inspection.getLastAnalysisWeather();
734+
if (!StringUtils.hasText(weather)) {
735+
weather = inspection.getWeather();
736+
}
737+
if (!StringUtils.hasText(weather)) {
738+
return null;
739+
}
740+
return weather.trim().toLowerCase(Locale.ROOT);
741+
}
742+
743+
private static String lookupBaselineForWeather(Transformer transformer, String weather) {
744+
if (!StringUtils.hasText(weather) || transformer == null) {
745+
return null;
746+
}
747+
return switch (weather.toLowerCase(Locale.ROOT)) {
748+
case "sunny" -> transformer.getSunnyImage();
749+
case "cloudy" -> transformer.getCloudyImage();
750+
case "rainy", "windy" -> transformer.getWindyImage();
751+
default -> null;
752+
};
753+
}
754+
755+
private static void writeResourceToZip(ZipOutputStream zos, String resourcePath, String entryName) throws IOException {
756+
ClassPathResource resource = new ClassPathResource(resourcePath);
757+
if (!resource.exists()) {
758+
return;
759+
}
760+
ZipEntry entry = new ZipEntry(entryName);
761+
zos.putNextEntry(entry);
762+
try (InputStream is = resource.getInputStream()) {
763+
is.transferTo(zos);
764+
}
765+
zos.closeEntry();
766+
}
767+
768+
private record BaselineSelection(String dataUrl, String weatherLabel) {}
769+
677770
@PostMapping("/{id}/clear-analysis")
678771
public ResponseEntity<?> clearAnalysis(@PathVariable String id) {
679772
return repo.findById(id).map(i -> {
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Transformer Tracker Export Toolkit
2+
3+
## Contents
4+
5+
- `metadata.json`: JSON snapshot of the inspection, including current and
6+
historical bounding boxes, annotators, and severities.
7+
- `history.csv`: Tabular view of the same snapshot data for quick analysis.
8+
- `image-original.*`: The candidate image used for the current inspection.
9+
- `baseline-<weather>.*` (optional): Baseline transformer image that matches the
10+
weather used during the last analysis.
11+
- `tools/plot_bounding_boxes.py`: Helper script for visualising snapshots.
12+
13+
## Quick Start
14+
15+
1. Ensure Python 3.9+ is installed. The script depends on Pillow, which can be
16+
installed with:
17+
18+
```bash
19+
pip install Pillow
20+
```
21+
22+
2. Extract the ZIP archive and change into the directory that contains
23+
`metadata.json`.
24+
25+
3. Render overlays for every snapshot using the provided script (replace the
26+
image extension with the one present in your export):
27+
28+
```bash
29+
python tools/plot_bounding_boxes.py metadata.json image-original.png --output-dir plots
30+
```
31+
32+
The script writes one image per snapshot to the output directory. Each
33+
bounding box is labelled with its fault classification and severity (when
34+
available). If a baseline image is present, you can render overlays on it as
35+
well:
36+
37+
```bash
38+
python tools/plot_bounding_boxes.py metadata.json image-original.png \
39+
--baseline baseline-sunny.png --output-dir plots
40+
```
41+
42+
4. Review the generated PNG files inside the output directory to inspect how
43+
annotations changed over time.
44+
45+
## Tips
46+
47+
- `history.csv` lines align with the order shown in `metadata.json` and the visual
48+
outputs produced by the script.
49+
- You can re-run the script at any time with a different `--output-dir` to compare
50+
alternative overlay parameters (e.g., `--alpha` for transparency adjustments).

0 commit comments

Comments
 (0)