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.

before

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
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.