Skip to content

Commit

Permalink
Add heatmap. Plots.html.
Browse files Browse the repository at this point in the history
  • Loading branch information
michal-lightly committed Nov 9, 2023
1 parent 65a3d11 commit cd87e3b
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 61 deletions.
13 changes: 13 additions & 0 deletions src/lightly_insights/analyze.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from pathlib import Path
from typing import Counter, Dict, List, Set, Tuple

import numpy as np
import tqdm
from labelformat.model.object_detection import ObjectDetectionInput
from PIL import Image
Expand All @@ -22,6 +23,8 @@
".webp",
)

HEATMAP_SIZE = 100


@dataclass(frozen=True)
class ImageAnalysis:
Expand All @@ -41,6 +44,7 @@ class ClassAnalysis:
objects_per_image: Counter[int]
object_sizes_abs: List[Tuple[float, float]]
object_sizes_rel: List[Tuple[float, float]]
heatmap: np.ndarray

sample_filenames: List[str]

Expand Down Expand Up @@ -72,6 +76,7 @@ def create_empty(cls, id: int, name: str) -> "ClassAnalysis":
objects_per_image=Counter(),
object_sizes_abs=[],
object_sizes_rel=[],
heatmap=np.zeros((HEATMAP_SIZE, HEATMAP_SIZE)),
sample_filenames=[],
)

Expand Down Expand Up @@ -174,6 +179,14 @@ def analyze_object_detections(
class_datum.object_sizes_abs.append(obj_size_abs)
class_datum.object_sizes_rel.append(obj_size_rel)

# Heatmap.
x1 = obj.box.xmin / label.image.width * HEATMAP_SIZE
x2 = obj.box.xmax / label.image.width * HEATMAP_SIZE
y1 = obj.box.ymin / label.image.height * HEATMAP_SIZE
y2 = obj.box.ymax / label.image.height * HEATMAP_SIZE
total_data.heatmap[int(y1) : int(y2), int(x1) : int(x2)] += 1
class_datum.heatmap[int(y1) : int(y2), int(x1) : int(x2)] += 1

if len(class_datum.sample_filenames) < 4:
class_datum.sample_filenames.append(label.image.filename)

Expand Down
57 changes: 48 additions & 9 deletions src/lightly_insights/plots.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from pathlib import Path
from typing import Counter, Tuple, Union

import numpy as np
from matplotlib import pyplot as plt
from matplotlib.ticker import MaxNLocator

Expand All @@ -15,6 +16,7 @@ class PlotPaths:
side_length_avg: str
rel_area: str
objects_per_image: str
heatmap: str


def create_object_plots(
Expand All @@ -32,14 +34,17 @@ def create_object_plots(
side_length_avg_path = plot_folder / "side_length_avg_plot.png"
rel_area_path = plot_folder / "rel_area.png"
objects_per_image_path = plot_folder / "objects_per_image.png"
heatmap_path = plot_folder / "heatmap.png"

# TODO: Remove.
if plot_folder.name != "plots":
return PlotPaths(
object_sizes_abs=str(object_sizes_abs_path.relative_to(output_folder)),
object_sizes_rel=str(object_sizes_rel_path.relative_to(output_folder)),
side_length_avg=str(side_length_avg_path.relative_to(output_folder)),
rel_area=str(rel_area_path.relative_to(output_folder)),
objects_per_image=str(objects_per_image_path.relative_to(output_folder)),
heatmap=str(heatmap_path.relative_to(output_folder)),
)

# Bucket by multiples of 20px.
Expand All @@ -58,7 +63,7 @@ def create_object_plots(
# Bucket by multiples of 5%.
size_histogram_rel = Counter(
[
(0.05 * round(w / 0.05), 0.05 * round(h / 0.05))
(100 * 0.05 * round(w / 0.05), 100 * 0.05 * round(h / 0.05))
for w, h in class_analysis.object_sizes_rel
]
)
Expand All @@ -75,7 +80,7 @@ def create_object_plots(
_histogram(
output_file=side_length_avg_path,
hist=side_length_avg_histogram,
title="Object Side Length Average in Pixels (buckets by 50px)",
title="Object Side Length Average (buckets by 50px)",
xlabel="Width/2 + Height/2 (px)",
ylabel="Number of Objects",
bar_width=50,
Expand All @@ -84,15 +89,15 @@ def create_object_plots(

# Side length histogram. Bucket by multiples of 5%.
rel_area_histogram = Counter(
0.05 * round(w * h / 0.05) for w, h in class_analysis.object_sizes_rel
100 * 0.05 * round(w * h / 0.05) for w, h in class_analysis.object_sizes_rel
)
_histogram(
output_file=rel_area_path,
hist=rel_area_histogram,
title="Object Relative Area in Pixels (buckets by 5%)",
title="Object Relative Area (buckets by 5%)",
xlabel="Object Area (% of Image Area)",
ylabel="Number of Objects",
bar_width=0.05,
bar_width=100 * 0.05,
x_average_line=True,
)

Expand All @@ -107,12 +112,19 @@ def create_object_plots(
y_average_line=True,
)

# Heatmap.
_heatmap(
output_file=heatmap_path,
heatmap=class_analysis.heatmap,
)

return PlotPaths(
object_sizes_abs=str(object_sizes_abs_path.relative_to(output_folder)),
object_sizes_rel=str(object_sizes_rel_path.relative_to(output_folder)),
side_length_avg=str(side_length_avg_path.relative_to(output_folder)),
rel_area=str(rel_area_path.relative_to(output_folder)),
objects_per_image=str(objects_per_image_path.relative_to(output_folder)),
heatmap=str(heatmap_path.relative_to(output_folder)),
)


Expand Down Expand Up @@ -179,8 +191,8 @@ def _width_heigth_percent_plot(
ax.set_ylabel("Height (%)")
ax.set_title(title)
ax.set_aspect("equal", "box")
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
ax.set_xlim(0, 100)
ax.set_ylim(0, 100)

# Save the plot.
plt.savefig(output_file)
Expand Down Expand Up @@ -227,7 +239,7 @@ def _histogram(
ax.text(
0.95,
0.95,
f"avg={avg:.2f}",
f"avg={avg:.1f}",
horizontalalignment="right",
verticalalignment="top",
transform=ax.transAxes,
Expand All @@ -245,7 +257,7 @@ def _histogram(
ax.text(
0.95,
0.95,
f"avg={avg:.2f}",
f"avg={avg:.1f}",
horizontalalignment="right",
verticalalignment="top",
transform=ax.transAxes,
Expand All @@ -262,3 +274,30 @@ def _histogram(
# Save the plot.
plt.savefig(output_file)
plt.close(fig)


def _heatmap(
output_file: Path,
heatmap: np.ndarray,
) -> None:
# Image size plot.
fig = plt.figure(figsize=(6, 6))
ax = fig.add_subplot(111)

ax.imshow(
heatmap,
cmap="cividis",
# cmap="viridis",
# cmap="BuGn",
# cmap="hot",
# cmap="hot",
interpolation="nearest",
)

ax.set_xlabel("X (%)")
ax.set_ylabel("Y (%)")
ax.set_title("Object Location Heatmap")

# Save the plot.
plt.savefig(output_file)
plt.close(fig)
20 changes: 20 additions & 0 deletions src/lightly_insights/templates/plots.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<div class="row">
<div class="col-lg-6">
<img src="{{ plots.objects_per_image }}" class="img-fluid" alt="Objects per Image">
</div>
<div class="col-lg-6">
<img src="{{ plots.heatmap }}" class="img-fluid" alt="Object Location Heatmap">
</div>
<div class="col-lg-6">
<img src="{{ plots.object_sizes_abs }}" class="img-fluid" alt="Object Sizes in Pixels">
</div>
<div class="col-lg-6">
<img src="{{ plots.object_sizes_rel }}" class="img-fluid" alt="Object Sizes in Percent">
</div>
<div class="col-lg-6">
<img src="{{ plots.side_length_avg }}" class="img-fluid" alt="Object Side Length Average">
</div>
<div class="col-lg-6">
<img src="{{ plots.rel_area }}" class="img-fluid" alt="Object Relative Area">
</div>
</div>
102 changes: 50 additions & 52 deletions src/lightly_insights/templates/report.html
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ <h3 class="card-titl mb-3">{{ image_analysis.num_images }} Images</h4>
</tr>
<tr class="table-primary">
<th scope="row">Avg objects per image</th>
<td>{{ "{0:.1f}".format(object_detection_analysis.total.num_objects / image_analysis.num_images) }}</td>
<td>{{ "{0:.1f}".format(object_detection_analysis.total.num_objects / image_analysis.num_images)
}}</td>
</tr>
</tbody>
</table>
Expand All @@ -130,15 +131,17 @@ <h3 class="card-title mb-3">{{ object_detection_analysis.total.num_objects }} Ob
</tr>
<tr class="table-secondary">
<th scope="row">Avg objects per class</th>
<td>{{ "{0:.1f}".format(object_detection_analysis.total.num_objects / object_detection_insights.num_classes) }}</td>
<td>{{ "{0:.1f}".format(object_detection_analysis.total.num_objects /
object_detection_insights.num_classes) }}</td>
</tr>
<tr class="table-secondary">
<th scope="row">Avg images per class</th>
<td>{{ "{0:.1f}".format(image_analysis.num_images / object_detection_insights.num_classes) }}</td>
</tr>
<tr class="table-secondary">
<th scope="row">Avg size</th>
<td>{{ "{0:.1f}".format(object_detection_analysis.total.avg_size[0]) }} × {{ "{0:.1f}".format(object_detection_analysis.total.avg_size[1]) }}</td>
<td>{{ "{0:.1f}".format(object_detection_analysis.total.avg_size[0]) }} × {{
"{0:.1f}".format(object_detection_analysis.total.avg_size[1]) }}</td>
</tr>
<tr class="table-secondary">
<th scope="row">Avg area</th>
Expand Down Expand Up @@ -224,32 +227,28 @@ <h4 class="card-header">Object Detection Insights</h4>
<div class="card-body">
<h4 class="card-title">{{ object_detection_analysis.total.num_objects }} Objects</h4>

<div class="row">
<div class="col-lg-6">
<img src="{{ object_detection_insights.plots.object_sizes_abs }}" class="img-fluid" alt="Object Size in Pixels Plot">
</div>
<div class="col-lg-6">
<img src="{{ object_detection_insights.plots.object_sizes_rel }}" class="img-fluid"
alt="Object Size in Percent Plot">
</div>
</div>
<div class="col-lg-6">

<div class="row">
<div class="col-lg-6">
<img src="{{ object_detection_insights.plots.side_length_avg }}" class="img-fluid" alt="TODO">
</div>
<div class="col-lg-6">
<img src="{{ object_detection_insights.plots.rel_area }}" class="img-fluid"
alt="TODO">
</div>
</div>
<table class="table table-hover mt-4">
<tbody>
<tr>
<th scope="row">Avg size</th>
<td>{{ "{0:.1f}".format(object_detection_analysis.total.avg_size[0]) }} × {{
"{0:.1f}".format(object_detection_analysis.total.avg_size[1]) }}</td>
</tr>
<tr>
<th scope="row">Avg area</th>
<td>{{ "{0:.0f}".format(100 * object_detection_analysis.total.avg_rel_area) }} %</td>
</tr>
</tbody>
</table>

<div class="col-lg-6">
<img src="{{ object_detection_insights.plots.objects_per_image }}" class="img-fluid" alt="Objects per Image">
</div>

<p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's
content.</p>

{% with plots=object_detection_insights.plots %}
{% include 'plots.html' %}
{% endwith %}

<table class="table table-sm table-hover">
<thead>
Expand All @@ -268,8 +267,8 @@ <h4 class="card-title">{{ object_detection_analysis.total.num_objects }} Objects
<td class="col-8">
<div class="progress mt-1">
<div class="progress-bar" role="progressbar"
style="width: {{ class.num_objects / object_detection_analysis.total.num_objects * 100 }}%;" aria-valuenow="25"
aria-valuemin="0" aria-valuemax="100"></div>
style="width: {{ class.num_objects / object_detection_analysis.total.num_objects * 100 }}%;"
aria-valuenow="25" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</td>
</tr>
Expand Down Expand Up @@ -297,7 +296,8 @@ <h4 class="card-title">{{ object_detection_insights.num_classes }} Classes</h4>
<li class="nav-item" role="presentation">
<a class="btn btn-outline-primary btn-sm m-1" data-bs-toggle="tab" data-bs-target="#c{{ class.class_id }}"
href="#c{{ class.class_id }}">
{{ class.class_name }}&nbsp;&nbsp;<span class="badge bg-light rounded-pill">{{ class.num_objects }}</span>
{{ class.class_name }}&nbsp;&nbsp;<span class="badge bg-light rounded-pill">{{ class.num_objects
}}</span>
</a>
</li>
{% endfor %}
Expand All @@ -309,35 +309,33 @@ <h4 class="card-title">{{ object_detection_insights.num_classes }} Classes</h4>

{% for class_id in object_detection_insights.class_ids_most_common %}
{% set class = object_detection_analysis.classes[class_id] %}
{% set class_plots = object_detection_insights.class_plots[class_id] %}
<div class="tab-pane fade" id="c{{ class_id }}" role="tabpanel">
<h4>{{ class.num_objects }} Objects of Class "{{ class.class_name }}"</h4>

{% if class.num_objects > 0 %}
<div class="row">
<div class="col-lg-6">
<img src="{{ class_plots.object_sizes_abs }}" class="img-fluid" alt="Object Size in Pixels Plot">
</div>
<div class="col-lg-6">
<img src="{{ class_plots.object_sizes_rel }}" class="img-fluid"
alt="Object Size in Percent Plot">
</div>
</div>

<div class="row">
<div class="col-lg-6">
<img src="{{ class_plots.side_length_avg }}" class="img-fluid" alt="TODO">
</div>
<div class="col-lg-6">
<img src="{{ class_plots.rel_area }}" class="img-fluid"
alt="TODO">
</div>
</div>


<div class="col-lg-6">
<img src="{{ class_plots.objects_per_image }}" class="img-fluid" alt="Objects per Image">

<table class="table table-hover mt-4">
<tbody>
<tr>
<th scope="row">Avg size</th>
<td>{{ "{0:.1f}".format(class.avg_size[0]) }} × {{ "{0:.1f}".format(class.avg_size[1]) }}</td>
</tr>
<tr>
<th scope="row">Avg area</th>
<td>{{ "{0:.0f}".format(100 * class.avg_rel_area) }} %</td>
</tr>
</tbody>
</table>

</div>


{% with plots=object_detection_insights.class_plots[class_id] %}
{% include 'plots.html' %}
{% endwith %}

<h4>Sample Images</h4>

<div class="row">
Expand All @@ -352,8 +350,8 @@ <h4>Sample Images</h4>
</div>

{% endif %}


</div>
{% endfor %}

Expand Down

0 comments on commit cd87e3b

Please sign in to comment.