Please bookmark this page to avoid losing your image tool!

Image Path Finder Tool

(Free & Supports Bulk Upload)

Drag & drop your images here or

The result will appear here...
You can edit the below JavaScript code to customize the image tool.
async function processImage(originalImg, startRegion = "top", endRegion = "bottom", pathPreference = "bright", pathColor = "#ff0000", pathThickness = "3") {
  // To avoid freezing the browser on very large images, calculate the path on a downscaled version.
  const MAX_DIM = 400;
  let scale = 1;
  if (originalImg.width > MAX_DIM || originalImg.height > MAX_DIM) {
    scale = MAX_DIM / Math.max(originalImg.width, originalImg.height);
  }
  
  const W = Math.round(originalImg.width * scale);
  const H = Math.round(originalImg.height * scale);
  
  const offscreen = document.createElement('canvas');
  offscreen.width = W;
  offscreen.height = H;
  const ctxOff = offscreen.getContext('2d');
  ctxOff.drawImage(originalImg, 0, 0, W, H);
  const imgData = ctxOff.getImageData(0, 0, W, H).data;
  
  // Create a cost map based on image brightness. 
  // Using a cubic function ensures the algorithm strictly avoids walls (which represent massive costs).
  const cost = new Float32Array(W * H);
  const prefersDark = String(pathPreference).toLowerCase() === "dark";
  for (let i = 0; i < W * H; i++) {
    const r = imgData[i * 4];
    const g = imgData[i * 4 + 1];
    const b = imgData[i * 4 + 2];
    const lum = 0.299 * r + 0.587 * g + 0.114 * b;
    if (prefersDark) {
      cost[i] = 1 + Math.pow(lum, 3);
    } else {
      cost[i] = 1 + Math.pow(255 - lum, 3);
    }
  }

  // Fast Binary MinHeap for Dijkstra's Algorithm
  class MinHeap {
    constructor() {
      this.heap = [];
    }
    push(val, priority) {
      this.heap.push({val, priority});
      this.bubbleUp(this.heap.length - 1);
    }
    pop() {
      const h = this.heap;
      if (h.length === 0) return null;
      if (h.length === 1) return h.pop().val;
      const top = h[0].val;
      h[0] = h.pop();
      this.sinkDown(0);
      return top;
    }
    isEmpty() {
      return this.heap.length === 0;
    }
    bubbleUp(idx) {
      const h = this.heap;
      const el = h[idx];
      while (idx > 0) {
        let pIdx = (idx - 1) >>> 1;
        let parent = h[pIdx];
        if (el.priority >= parent.priority) break;
        h[idx] = parent;
        idx = pIdx;
      }
      h[idx] = el;
    }
    sinkDown(idx) {
      const h = this.heap;
      const len = h.length;
      const el = h[idx];
      while (true) {
        let leftIdx = (idx << 1) + 1;
        let rightIdx = leftIdx + 1;
        let swap = -1;
        
        if (leftIdx < len && h[leftIdx].priority < el.priority) {
          swap = leftIdx;
        }
        if (rightIdx < len) {
          if ((swap === -1 && h[rightIdx].priority < el.priority) ||
              (swap !== -1 && h[rightIdx].priority < h[leftIdx].priority)) {
            swap = rightIdx;
          }
        }
        if (swap === -1) break;
        h[idx] = h[swap];
        idx = swap;
      }
      h[idx] = el;
    }
  }

  const dist = new Float32Array(W * H).fill(Infinity);
  const prev = new Int32Array(W * H).fill(-1);
  const visited = new Uint8Array(W * H);
  const isEnd = new Uint8Array(W * H);
  const pq = new MinHeap();

  // Helper mapping regions to flat array indices
  function getRegionIndices(region) {
    const indices = [];
    region = String(region).toLowerCase();
    if (region === "top") {
      for (let x = 0; x < W; x++) indices.push(x);
    } else if (region === "bottom") {
      for (let x = 0; x < W; x++) indices.push((H - 1) * W + x);
    } else if (region === "left") {
      for (let y = 0; y < H; y++) indices.push(y * W);
    } else if (region === "right") {
      for (let y = 0; y < H; y++) indices.push(y * W + (W - 1));
    } else if (region === "top-left") {
      indices.push(0);
    } else if (region === "bottom-right") {
      indices.push(W * H - 1);
    } else if (region === "top-right") {
      indices.push(W - 1);
    } else if (region === "bottom-left") {
      indices.push((H - 1) * W);
    } else if (region === "center") {
      indices.push(Math.floor(H / 2) * W + Math.floor(W / 2));
    } else {
      for (let x = 0; x < W; x++) indices.push(x); // default to top
    }
    return indices;
  }

  const starts = getRegionIndices(startRegion);
  const ends = getRegionIndices(endRegion);
  
  for (let idx of ends) {
    isEnd[idx] = 1;
  }

  // Initialize start priorities inside the queue
  for (let idx of starts) {
    dist[idx] = cost[idx];
    pq.push(idx, dist[idx]);
  }

  // 8-Connected grid offsets (ensuring paths aren't strongly locked to taxi-cab geometry)
  const neighbors = [
    [-1, -1], [0, -1], [1, -1],
    [-1, 0],           [1, 0],
    [-1, 1],  [0, 1],  [1, 1]
  ];

  let endNode = -1;

  // Dijkstra's Shortest Path evaluation
  while (!pq.isEmpty()) {
    const current = pq.pop();
    if (current === null) break;
    
    // Prevent duplicate evaluation
    if (visited[current]) continue;
    visited[current] = 1;

    // Check if reached destination region
    if (isEnd[current]) {
      endNode = current;
      break;
    }

    const cx = current % W;
    const cy = Math.floor(current / W);

    for (const [dx, dy] of neighbors) {
      const nx = cx + dx;
      const ny = cy + dy;
      if (nx >= 0 && nx < W && ny >= 0 && ny < H) {
        const nIdx = ny * W + nx;
        if (visited[nIdx]) continue;
        
        // Diagonals cost slightly more mathematically
        const stepDist = (dx === 0 || dy === 0) ? 1 : 1.414;
        const newDist = dist[current] + cost[nIdx] * stepDist;
        
        if (newDist < dist[nIdx]) {
          dist[nIdx] = newDist;
          prev[nIdx] = current;
          pq.push(nIdx, newDist);
        }
      }
    }
  }

  // Output onto full-resolution canvas
  const outCanvas = document.createElement('canvas');
  outCanvas.width = originalImg.width;
  outCanvas.height = originalImg.height;
  const ctx = outCanvas.getContext('2d');
  ctx.drawImage(originalImg, 0, 0);

  // If a path was found, backtrack and trace it out
  if (endNode !== -1) {
    const path = [];
    let curr = endNode;
    while (curr !== -1) {
      path.push(curr);
      curr = prev[curr];
    }
    
    ctx.beginPath();
    for (let i = 0; i < path.length; i++) {
      const idx = path[i];
      const currX = (idx % W);
      const currY = Math.floor(idx / W);
      // Re-map back to the true resolution with +0.5 to anchor at the scaled pixel's exact center
      const x = (currX + 0.5) / scale;
      const y = (currY + 0.5) / scale;
      if (i === 0) ctx.moveTo(x, y);
      else ctx.lineTo(x, y);
    }
    ctx.strokeStyle = String(pathColor);
    ctx.lineWidth = parseInt(pathThickness) || 3;
    ctx.lineJoin = "round";
    ctx.lineCap = "round";
    ctx.stroke();
  }

  return outCanvas;
}

Free Image Tool Creator

Can't find the image tool you're looking for?
Create one based on your own needs now!

Description

The Image Path Finder Tool uses advanced algorithms to find and trace the most efficient route through an image based on pixel brightness. Users can define specific starting and ending regions (such as top, bottom, left, right, or corners) and choose whether the path should prefer darker or brighter areas. This tool is useful for analyzing visual flow, finding paths through complex textures, or creating stylized path overlays for graphic design and map-based visualizations.

Leave a Reply

Your email address will not be published. Required fields are marked *