Optimizing Header Images with OpenCV Saliency Detection
Mar 16, 2025
I use Jekyll to make this site.
In the current layout of my site I have a header image stretched long and narrow across the top of the page.
The problem?
The focal point isn’t centered! It doesn’t look good.
How to fix this issue?
Well obviously we need to fix this systematically.
I used OpenCV’s saliency module to identify a focal point in every image in percent:
class SmartCropper:
"""Smart cropper that uses a saliency map to determine the most interesting point in an image."""
def __init__(self):
# Initialize the saliency detector (using the spectral residual method)
try:
self.saliency = cv2.saliency.StaticSaliencySpectralResidual_create()
except AttributeError:
print("run `pip install opencv_contrib_python`")
raise
def find_interesting_centroid(self, image_path):
"""
Analyze an image using a saliency map and return the centroid of the most salient region.
Args:
image_path: Path to the image file.
Returns:
tuple: (x_percent, y_percent) coordinates of the most interesting point as percentages.
"""
img = cv2.imread(str(image_path))
if img is None:
print(f"Error: Cannot read image at {image_path}")
return None
# Compute the saliency map
success, saliencyMap = self.saliency.computeSaliency(img)
if not success:
print("Error: Saliency computation failed")
return None
saliencyMap = (saliencyMap * 255).astype("uint8")
# Calculate image moments of the saliency map to determine the centroid
moments = cv2.moments(saliencyMap)
if moments["m00"] != 0:
cX = int(moments["m10"] / moments["m00"])
cY = int(moments["m01"] / moments["m00"])
else:
# Fall back to the image center if moments are zero
cX, cY = img.shape[1] // 2, img.shape[0] // 2
centroid_x_percent = (cX / img.shape[1]) * 100.0
centroid_y_percent = (cY / img.shape[0]) * 100.0
return centroid_x_percent, centroid_y_percent
I created a data file _data/image_metadata.yml
with the output from this script for every image
assets/installation_images/breathy/mask_on_table.jpg:
centroid_x: 46
centroid_y: 43
assets/installation_images/breathy/mask_top.jpg:
centroid_x: 54
centroid_y: 55
assets/installation_images/goggles/bottle_closeup.jpg:
centroid_x: 53
centroid_y: 40
...
My CSS for the header image is:
.project-header-image {
width: 100%;
height: 300px;
object-fit: cover;
}
Setting object-position
to the centroid in percent will center any image according to the algorithmic centroid calculation in CSS.
I created an _include
file:
{%- assign metadata = site.data.image_metadata[include.src] -%}
{%- if metadata and metadata.centroid_x and metadata.centroid_y %}
style="object-position: {{ metadata.centroid_x }}% {{ metadata.centroid_y }}%;"
{%- endif -%}
Which I can use like this:
{% if page.image %}
<img src="{{ page.image | relative_url }}" alt="{{ page.title | escape_once }}" class="project-header-image"
{%- include centroid.html src=page.image -%}>
{% endif %}
The result looks a lot better:
Before | After |
---|---|
![]() |
![]() |
How it works
Saliency Map Computation
success, saliencyMap = self.saliency.computeSaliency(img)
This calculates a saliency map—essentially a grayscale image where brighter areas indicate regions that are more likely to attract visual attention. The map is then normalized to a standard 8-bit image format (values 0-255).
Finding the Center of Interest
moments = cv2.moments(saliencyMap)
if moments["m00"] != 0:
cX = int(moments["m10"] / moments["m00"])
cY = int(moments["m01"] / moments["m00"])
else:
cX, cY = img.shape[1] // 2, img.shape[0] // 2
This uses image moments, a mathematical technique that calculates statistical properties of pixel distributions. The code finds the centroid (center of mass) of the saliency map, which represents the average position of salient features. If this calculation fails, it defaults to the center of the image.