661. Image Smoother
Problem Description
You are given a 2D matrix img
representing a grayscale image where each cell contains an integer value. Your task is to apply an image smoother filter to this image.
An image smoother is a 3×3 filter that works by:
- For each cell in the image, consider the cell itself and its 8 surrounding neighbors (forming a 3×3 grid with the current cell at the center)
- Calculate the average of all valid cells in this 3×3 grid
- Round down this average (using integer division) to get the smoothed value
- Replace the original cell value with this smoothed value
Important edge cases:
- If a cell is at the border or corner of the image, some of its surrounding cells won't exist
- In such cases, only consider the cells that actually exist when calculating the average
- For example, a corner cell will only have 4 cells total (itself + 3 neighbors), while an edge cell will have 6 cells total (itself + 5 neighbors)
The function should return a new 2D matrix where each cell contains the smoothed value calculated using the above rules.
Example:
- For a cell in the middle of the image with value 5, surrounded by values
[1, 2, 3, 4, 6, 7, 8, 9]
, the smoothed value would be(5 + 1 + 2 + 3 + 4 + 6 + 7 + 8 + 9) // 9 = 45 // 9 = 5
- For a corner cell with value 10, with only 3 neighbors
[20, 30, 40]
, the smoothed value would be(10 + 20 + 30 + 40) // 4 = 100 // 4 = 25
Intuition
The problem asks us to smooth each pixel by averaging it with its neighbors. The key insight is that this is essentially a local operation - each cell in the output depends only on a small neighborhood around the corresponding cell in the input.
Since we need to process every cell independently and the smoothing of one cell doesn't affect the smoothing of another cell, we can use a straightforward approach: iterate through each cell and compute its smoothed value directly.
For each cell (i, j)
, we need to look at all cells in a 3×3 grid centered at (i, j)
. This means checking cells from (i-1, j-1)
to (i+1, j+1)
. The challenge is handling boundary conditions - when we're at the edges or corners of the image, some of these neighboring positions will be out of bounds.
Rather than writing separate logic for corners, edges, and interior cells, we can use a unified approach: for each of the 9 potential positions in the 3×3 grid, check if the position is valid (within the image bounds). If it is valid, include that cell's value in our sum and increment our count. This way, the code automatically adapts to any position - interior cells will have 9 valid neighbors, edge cells will have 6, and corner cells will have 4.
The final smoothed value is simply the integer division of the sum by the count: sum // count
. We need to create a new matrix to store these smoothed values because we can't modify the original image while we're still reading from it (otherwise, we'd be using already-smoothed values when calculating subsequent cells, which would give incorrect results).
Solution Approach
We implement a direct traversal approach to solve this problem. Here's the step-by-step implementation:
-
Initialize the result matrix: First, we get the dimensions of the input image with
m, n = len(img), len(img[0])
. Then we create a new 2D matrixans
with the same dimensions, initialized with zeros:ans = [[0] * n for _ in range(m)]
. This will store our smoothed values. -
Iterate through each cell: We use two nested loops to traverse every cell in the original image:
for i in range(m): for j in range(n):
-
Process the 3×3 neighborhood: For each cell
(i, j)
, we need to examine all cells in its 3×3 neighborhood. We initialize two variables:s = 0
: to accumulate the sum of valid cell valuescnt = 0
: to count how many valid cells we find
-
Check surrounding cells: We use another pair of nested loops to check all 9 positions in the 3×3 grid:
for x in range(i - 1, i + 2): for y in range(j - 1, j + 2):
This gives us positions from
(i-1, j-1)
to(i+1, j+1)
, covering the entire 3×3 area. -
Validate and accumulate: For each position
(x, y)
, we check if it's within bounds:if 0 <= x < m and 0 <= y < n: cnt += 1 s += img[x][y]
Only valid positions contribute to our sum and count.
-
Calculate the smoothed value: After checking all 9 potential positions, we compute the average using integer division:
ans[i][j] = s // cnt
. This automatically rounds down as required. -
Return the result: After processing all cells, we return the
ans
matrix containing all smoothed values.
The time complexity is O(m × n)
since we visit each cell once, and for each cell, we check at most 9 neighbors (constant work). The space complexity is also O(m × n)
for storing the result matrix.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with a 3×3 matrix to illustrate the solution approach:
Input:
img = [[1, 1, 1], [1, 0, 1], [1, 1, 1]]
We'll calculate the smoothed value for a few representative cells:
Cell (0, 0) - Corner Cell:
- Position (0, 0) has value 1
- Check 3×3 neighborhood centered at (0, 0):
- (-1, -1): Out of bounds, skip
- (-1, 0): Out of bounds, skip
- (-1, 1): Out of bounds, skip
- (0, -1): Out of bounds, skip
- (0, 0): Valid, value = 1
- (0, 1): Valid, value = 1
- (1, -1): Out of bounds, skip
- (1, 0): Valid, value = 1
- (1, 1): Valid, value = 0
- Sum = 1 + 1 + 1 + 0 = 3
- Count = 4 valid cells
- Smoothed value = 3 // 4 = 0
Cell (1, 1) - Center Cell:
- Position (1, 1) has value 0
- Check 3×3 neighborhood centered at (1, 1):
- All 9 positions are valid: (0,0), (0,1), (0,2), (1,0), (1,1), (1,2), (2,0), (2,1), (2,2)
- Values: 1, 1, 1, 1, 0, 1, 1, 1, 1
- Sum = 1 + 1 + 1 + 1 + 0 + 1 + 1 + 1 + 1 = 8
- Count = 9 valid cells
- Smoothed value = 8 // 9 = 0
Cell (0, 1) - Edge Cell:
- Position (0, 1) has value 1
- Check 3×3 neighborhood centered at (0, 1):
- (-1, 0), (-1, 1), (-1, 2): Out of bounds
- (0, 0), (0, 1), (0, 2): Valid, values = 1, 1, 1
- (1, 0), (1, 1), (1, 2): Valid, values = 1, 0, 1
- Sum = 1 + 1 + 1 + 1 + 0 + 1 = 5
- Count = 6 valid cells
- Smoothed value = 5 // 6 = 0
Final Output:
ans = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
The algorithm processes each cell independently, counting only valid neighbors within bounds, and computes the integer average to produce the smoothed image.
Solution Implementation
1class Solution:
2 def imageSmoother(self, img: List[List[int]]) -> List[List[int]]:
3 # Get dimensions of the image
4 rows, cols = len(img), len(img[0])
5
6 # Initialize result matrix with same dimensions
7 result = [[0] * cols for _ in range(rows)]
8
9 # Process each cell in the image
10 for row in range(rows):
11 for col in range(cols):
12 # Initialize sum and count for averaging
13 total_sum = 0
14 cell_count = 0
15
16 # Check all 9 cells in 3x3 grid centered at (row, col)
17 for neighbor_row in range(row - 1, row + 2):
18 for neighbor_col in range(col - 1, col + 2):
19 # Only include cells that are within image boundaries
20 if 0 <= neighbor_row < rows and 0 <= neighbor_col < cols:
21 cell_count += 1
22 total_sum += img[neighbor_row][neighbor_col]
23
24 # Calculate average using integer division (floor)
25 result[row][col] = total_sum // cell_count
26
27 return result
28
1class Solution {
2 /**
3 * Applies a smoothing filter to an image by replacing each pixel value
4 * with the average of its surrounding cells (including itself).
5 *
6 * @param img The input 2D array representing the image
7 * @return A new 2D array with smoothed values
8 */
9 public int[][] imageSmoother(int[][] img) {
10 // Get dimensions of the input image
11 int rows = img.length;
12 int cols = img[0].length;
13
14 // Initialize result array with same dimensions
15 int[][] result = new int[rows][cols];
16
17 // Iterate through each pixel in the image
18 for (int row = 0; row < rows; row++) {
19 for (int col = 0; col < cols; col++) {
20 // Calculate smoothed value for current pixel
21 int sum = 0;
22 int validCellCount = 0;
23
24 // Check all 9 cells in the 3x3 grid centered at (row, col)
25 for (int neighborRow = row - 1; neighborRow <= row + 1; neighborRow++) {
26 for (int neighborCol = col - 1; neighborCol <= col + 1; neighborCol++) {
27 // Only include cells that are within image boundaries
28 if (neighborRow >= 0 && neighborRow < rows &&
29 neighborCol >= 0 && neighborCol < cols) {
30 validCellCount++;
31 sum += img[neighborRow][neighborCol];
32 }
33 }
34 }
35
36 // Calculate average and apply floor division
37 result[row][col] = sum / validCellCount;
38 }
39 }
40
41 return result;
42 }
43}
44
1class Solution {
2public:
3 vector<vector<int>> imageSmoother(vector<vector<int>>& img) {
4 // Get the dimensions of the image
5 int rows = img.size();
6 int cols = img[0].size();
7
8 // Initialize the result matrix with the same dimensions
9 vector<vector<int>> result(rows, vector<int>(cols));
10
11 // Iterate through each cell in the image
12 for (int row = 0; row < rows; ++row) {
13 for (int col = 0; col < cols; ++col) {
14 int sum = 0; // Sum of valid neighboring cells
15 int count = 0; // Count of valid neighboring cells
16
17 // Check all 9 cells in the 3x3 grid centered at (row, col)
18 for (int neighborRow = row - 1; neighborRow <= row + 1; ++neighborRow) {
19 for (int neighborCol = col - 1; neighborCol <= col + 1; ++neighborCol) {
20 // Skip if the neighbor is out of bounds
21 if (neighborRow < 0 || neighborRow >= rows ||
22 neighborCol < 0 || neighborCol >= cols) {
23 continue;
24 }
25
26 // Add valid neighbor to sum and increment count
27 count++;
28 sum += img[neighborRow][neighborCol];
29 }
30 }
31
32 // Calculate the average (floor division) and store in result
33 result[row][col] = sum / count;
34 }
35 }
36
37 return result;
38 }
39};
40
1/**
2 * Applies a smoothing filter to an image by replacing each pixel value
3 * with the average of its surrounding 3x3 grid (including itself)
4 * @param img - 2D array representing the image where each element is a pixel value
5 * @returns A new 2D array with smoothed pixel values
6 */
7function imageSmoother(img: number[][]): number[][] {
8 const rowCount: number = img.length;
9 const columnCount: number = img[0].length;
10
11 // Initialize result matrix with the same dimensions as input
12 const result: number[][] = Array.from(
13 { length: rowCount },
14 () => Array(columnCount).fill(0)
15 );
16
17 // Process each pixel in the image
18 for (let row = 0; row < rowCount; row++) {
19 for (let col = 0; col < columnCount; col++) {
20 let sum: number = 0;
21 let validCellCount: number = 0;
22
23 // Check all cells in the 3x3 grid centered at (row, col)
24 for (let neighborRow = row - 1; neighborRow <= row + 1; neighborRow++) {
25 for (let neighborCol = col - 1; neighborCol <= col + 1; neighborCol++) {
26 // Only include cells that are within image boundaries
27 if (neighborRow >= 0 && neighborRow < rowCount &&
28 neighborCol >= 0 && neighborCol < columnCount) {
29 validCellCount++;
30 sum += img[neighborRow][neighborCol];
31 }
32 }
33 }
34
35 // Calculate and store the floor of the average value
36 result[row][col] = Math.floor(sum / validCellCount);
37 }
38 }
39
40 return result;
41}
42
Time and Space Complexity
The time complexity is O(m × n)
, where m
and n
are the number of rows and columns of the input matrix img
, respectively. This is because the algorithm iterates through each cell in the matrix exactly once with the outer two loops. For each cell, it examines at most 9 neighboring cells (including itself) in a 3×3 window, which is a constant operation O(1)
. Therefore, the overall time complexity is O(m × n) × O(1) = O(m × n)
.
The space complexity is O(m × n)
for the output array ans
which stores the smoothed image. However, if we exclude the space required for the output array (as it's necessary for returning the result), the additional space complexity is O(1)
since the algorithm only uses a constant amount of extra variables (s
, cnt
, x
, y
) regardless of the input size.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Modifying the Original Image In-Place
One of the most common mistakes is trying to update the original image directly instead of creating a new result matrix. This leads to incorrect results because as you smooth cells, you're changing values that neighboring cells will later need for their own calculations.
Incorrect approach:
def imageSmoother(self, img: List[List[int]]) -> List[List[int]]:
rows, cols = len(img), len(img[0])
for row in range(rows):
for col in range(cols):
total_sum = 0
cell_count = 0
for neighbor_row in range(row - 1, row + 2):
for neighbor_col in range(col - 1, col + 2):
if 0 <= neighbor_row < rows and 0 <= neighbor_col < cols:
cell_count += 1
total_sum += img[neighbor_row][neighbor_col]
# WRONG: This modifies the original values that neighbors need
img[row][col] = total_sum // cell_count
return img
Why this fails: When processing cell (0,1), it uses the already-modified value of cell (0,0) instead of the original value, leading to cascading errors throughout the image.
Solution: Always create a separate result matrix to store the smoothed values while keeping the original image unchanged during the entire computation.
2. Incorrect Boundary Checking Logic
Another frequent error is using or
instead of and
in boundary conditions, or getting the comparison operators wrong.
Incorrect boundary check:
# Wrong: Using OR instead of AND if 0 <= neighbor_row or neighbor_row < rows or 0 <= neighbor_col or neighbor_col < cols: # This will include out-of-bounds cells # Wrong: Incorrect comparison if neighbor_row >= 0 < rows and neighbor_col >= 0 < cols: # This doesn't properly check the upper bounds
Solution: Use proper logical AND operations with clear boundary checks:
if 0 <= neighbor_row < rows and 0 <= neighbor_col < cols: # Correctly checks all boundaries
3. Using Float Division Instead of Integer Division
The problem specifically asks to round down the average, which means using integer division.
Incorrect:
result[row][col] = total_sum / cell_count # Returns float
Solution: Use integer division operator //
:
result[row][col] = total_sum // cell_count # Correctly rounds down
Which of the following uses divide and conquer strategy?
Recommended Readings
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!