DEV Community

Cover image for From Lab to Code #1: Exploring Classical Computer Vision with Python and OpenCV
Ertugrul
Ertugrul

Posted on

From Lab to Code #1: Exploring Classical Computer Vision with Python and OpenCV

Most conversations about computer vision today revolve around deep learning, neural networks, and increasingly sophisticated AI models.

But not every image processing problem requires a trained model.

Sometimes, a Gaussian filter is enough to reduce noise. A threshold can separate objects from their background. A Fourier transform can reveal interference that is difficult to understand in the spatial domain.

These techniques are relatively simple, mathematically interpretable, and still useful in practical applications.

Recently, while exploring image and signal processing concepts in my computer engineering studies, I decided to start a small programming series called From Lab to Code.

The idea is simple: take engineering concepts I encounter, build independent implementations, visualize the results, and share what I learn.

For the first project, I built a small interactive image processing toolkit using Python, OpenCV, NumPy, and Streamlit.

GitHub repository: https://github.com/Ertugrulmutlu/from-lab-to-code

Project 01 — Image Processing: https://github.com/Ertugrulmutlu/from-lab-to-code/tree/main/01-image-processing

What Are We Building?

The application explores four classical computer vision techniques:

  1. Image denoising using Gaussian and median filters.
  2. Object counting using illumination correction, thresholding, and connected components.
  3. Periodic noise removal using the Fast Fourier Transform (FFT).
  4. Color clustering using K-means.

The application provides interactive controls, generated example images, and visual comparisons.

No pretrained models, API calls, or external datasets are required.

Image processing examples

All bundled demonstration images are generated programmatically.


1. Image Denoising: Gaussian vs. Median Filtering

Digital images are often affected by noise.

Noise can originate from camera sensors, poor lighting conditions, signal transmission, or image acquisition hardware.

Two common types are:

  • Gaussian noise: Random variations in pixel intensity, often modeled using a normal distribution.
  • Salt-and-pepper noise: Individual pixels unexpectedly taking very dark or very bright values.

Different noise distributions require different filtering strategies.

Gaussian Filtering

A Gaussian filter performs a weighted average of neighboring pixels.

Pixels closer to the center receive higher weights, following a Gaussian distribution.

Mathematically, the two-dimensional Gaussian kernel can be written as:

G(x, y) = (1 / 2πσ²) × exp(-(x² + y²) / 2σ²)

The parameter σ controls the amount of smoothing.

A larger σ produces stronger smoothing but can also remove fine image details.

In OpenCV, Gaussian filtering is straightforward:

import cv2

gaussian = cv2.GaussianBlur(
    image, (5, 5), 0
)
Enter fullscreen mode Exit fullscreen mode

Median Filtering

Unlike Gaussian filtering, a median filter replaces the center pixel with the median value of its neighborhood.

Consider these pixel values:

[10, 11, 12, 13, 255]
Enter fullscreen mode Exit fullscreen mode

Their average is 60.2, but their median is 12.

If the value 255 represents an isolated noise spike, the median is much closer to the surrounding pixel values.

This makes median filtering particularly useful for salt-and-pepper noise.

median = cv2.medianBlur(image, 5)
Enter fullscreen mode Exit fullscreen mode

Comparing the Results

The application applies both filters to the same noisy image.

It also calculates the Mean Squared Error (MSE) against the original image when a reference is available.

MSE = (1/N) × Σ(original - processed)²

A lower MSE indicates a smaller average squared difference from the reference image.

However, MSE alone does not fully describe perceptual image quality.

Key takeaway: Gaussian filtering is useful for smoothing distributed noise, while median filtering is often more effective against isolated intensity spikes.

Neither method is universally superior.


2. Object Counting: From Pixels to Objects

Suppose we want to count small objects placed on a table.

At first, this sounds like a simple thresholding problem.

If the objects are darker than the background, we could classify pixels based on their intensity.

But what happens when the background illumination is uneven?

One side of the image might be noticeably darker than the other, making a single global threshold unreliable.

Step 1: Estimate the Background

We can approximate slowly changing illumination using a large Gaussian blur.

background = cv2.GaussianBlur(
    gray, (0, 0), sigmaX=30
)
Enter fullscreen mode Exit fullscreen mode

Subtracting the original image from the estimated background helps emphasize darker foreground objects.

corrected = cv2.subtract(
    background, gray
)
Enter fullscreen mode Exit fullscreen mode

This reduces the influence of gradual illumination changes.

Step 2: Apply Otsu Thresholding

Otsu's method automatically selects a threshold based on the image's intensity distribution.

Instead of manually choosing a number such as 100 or 150, it searches for a threshold that separates the intensity classes according to their statistical variance.

_, mask = cv2.threshold(
    corrected,
    0,
    255,
    cv2.THRESH_BINARY + cv2.THRESH_OTSU
)
Enter fullscreen mode Exit fullscreen mode

The output is a binary image containing foreground and background pixels.

Step 3: Identify Connected Components

After thresholding, neighboring foreground pixels can be grouped into connected regions.

OpenCV provides a useful function for this:

count, labels, stats, centroids = (
    cv2.connectedComponentsWithStats(
        mask, connectivity=8
    )
)
Enter fullscreen mode Exit fullscreen mode

The function returns component labels and information such as bounding boxes and pixel areas.

By excluding the background and removing regions below a minimum area, we can estimate the number of objects.

The project also applies a small morphological opening operation before counting to remove isolated foreground noise.

Where Can This Be Useful?

This approach can be applied to controlled visual inspection tasks involving separate objects, such as small manufactured components.

However, it comes with important assumptions.

Objects should be sufficiently separated, and the contrast between objects and background should be reasonably clear.

Touching objects may merge into a single connected component.

Key takeaway: Reliable object counting often requires preprocessing, not just thresholding.


3. Fourier Transform: Removing Periodic Noise

This was one of the most interesting parts of the project.

Images are normally represented in the spatial domain, where each pixel corresponds to a position.

But some image artifacts are easier to identify in the frequency domain.

For example, imagine an image affected by regularly repeating stripes.

Those stripes represent periodic variations in pixel intensity.

Moving to the Frequency Domain

The two-dimensional Discrete Fourier Transform decomposes an image into spatial frequency components.

Each component represents a particular frequency and orientation.

Using NumPy:

import numpy as np

spectrum = np.fft.fft2(gray)

shifted = np.fft.fftshift(spectrum)

magnitude = np.log1p(
    np.abs(shifted)
)
Enter fullscreen mode Exit fullscreen mode

Here:

  • fft2() computes the two-dimensional Fourier transform.
  • fftshift() moves the zero-frequency component to the center.
  • abs() obtains the magnitude.
  • log1p() makes the magnitude spectrum easier to visualize.

Periodic interference may appear as distinct peaks in the Fourier spectrum.

Removing Specific Frequencies

If we know which frequencies correspond to interference, we can suppress them using a notch filter.

A notch filter removes selected frequency regions while preserving much of the remaining spectrum.

For real-valued images, the Fourier spectrum has conjugate symmetry.

This means a sinusoidal interference component generally produces a symmetric pair of frequency peaks.

Both should be handled to preserve the symmetry of the filtered spectrum.

The basic operation is:

filtered_spectrum = shifted * mask
Enter fullscreen mode Exit fullscreen mode

Here, mask contains zeros at the frequencies we want to suppress and ones elsewhere.

We then reconstruct the image using the inverse Fourier transform.

restored = np.fft.ifft2(
    np.fft.ifftshift(filtered_spectrum)
).real
Enter fullscreen mode Exit fullscreen mode

An Important Limitation

In this project, the periodic noise is generated with known frequencies.

Therefore, the notch filter already knows where the unwanted frequency components are located.

This is intentional: it lets us isolate and study the effect of frequency-domain filtering.

Automatically identifying unknown interference frequencies in arbitrary photographs is a separate problem and is not implemented here.

A notch filter can also remove useful image information if the rejected frequencies overlap with actual image content.

Key takeaway: The Fourier transform provides another perspective on image processing, allowing us to manipulate structures according to their spatial frequencies rather than their pixel positions.


4. K-Means: Unsupervised Color Clustering

The final technique explores unsupervised learning.

Unlike classification models, K-means does not require predefined class labels.

Instead, it groups observations based on similarity.

For an RGB image, each pixel can be represented as a three-dimensional vector:

Pixel = [Red, Green, Blue]
Enter fullscreen mode Exit fullscreen mode

K-means attempts to divide these vectors into K clusters by minimizing the squared distances between pixels and their assigned cluster centers.

The objective function is:

J = Σ ||xᵢ - μ(clusterᵢ)||²

where xᵢ is a pixel vector and μ is the corresponding cluster center.

Implementing K-Means with OpenCV

First, reshape the image into a collection of RGB vectors:

pixels = image.reshape(
    -1, 3
).astype(np.float32)
Enter fullscreen mode Exit fullscreen mode

Then run K-means:

criteria = (
    cv2.TERM_CRITERIA_EPS +
    cv2.TERM_CRITERIA_MAX_ITER,
    25,
    0.2
)

_, labels, centers = cv2.kmeans(
    pixels,
    4,
    None,
    criteria,
    5,
    cv2.KMEANS_PP_CENTERS
)
Enter fullscreen mode Exit fullscreen mode

We can reconstruct an image by replacing each pixel with the color of its assigned cluster center.

This produces a simplified image containing a small number of representative colors.

Color Clustering Is Not Semantic Segmentation

This distinction is important.

K-means only sees RGB values in this implementation.

It does not understand objects, scenes, or semantic categories.

Two unrelated objects with similar colors may belong to the same cluster.

Likewise, one object with multiple colors may be split across several clusters.

Still, color clustering is useful for exploring image structure, reducing color complexity, and understanding unsupervised learning.


Running the Project Locally

The project is available on GitHub:

https://github.com/Ertugrulmutlu/from-lab-to-code/tree/main/01-image-processing

You will need Python 3.10 or newer.

Clone the repository:

git clone https://github.com/Ertugrulmutlu/from-lab-to-code.git
cd from-lab-to-code/01-image-processing
Enter fullscreen mode Exit fullscreen mode

Create a virtual environment:

python -m venv .venv
Enter fullscreen mode Exit fullscreen mode

Activate it on Windows PowerShell:

.\.venv\Scripts\Activate.ps1
Enter fullscreen mode Exit fullscreen mode

Install the dependencies:

python -m pip install -r requirements.txt
Enter fullscreen mode Exit fullscreen mode

Start the application:

python -m streamlit run app.py
Enter fullscreen mode Exit fullscreen mode

Streamlit will open an interactive application in your browser.

You can switch between the four techniques, adjust processing parameters, and compare the results.

Each mode includes a programmatically generated example image. You can also upload an image of your own.

To regenerate the demonstration gallery:

python demo.py
Enter fullscreen mode Exit fullscreen mode

To run the included tests:

python -m unittest discover -s tests -v
Enter fullscreen mode Exit fullscreen mode

The processing algorithms are implemented in processing.py, while synthetic.py generates the example images and app.py contains the Streamlit interface.


What I Learned

Building this project reinforced several important ideas.

First, understanding the characteristics of a problem matters as much as selecting an algorithm.

Gaussian filtering and median filtering may look similar at first, but they behave differently depending on the noise distribution.

Second, preprocessing can determine whether a simple computer vision algorithm succeeds or fails.

In the object counting example, background illumination correction can be just as important as the thresholding operation itself.

Third, the frequency domain provides a different way to reason about images.

Some patterns that are difficult to isolate spatially become much easier to analyze after a Fourier transform.

Finally, unsupervised clustering demonstrates that useful structure can be extracted from images without labeled training data, even though grouping colors is not the same as understanding a scene.

Why I Started From Lab to Code

I wanted a lightweight way to turn engineering concepts into practical software projects.

Not every project needs to become a startup, a large framework, or a long-term product.

Sometimes, a small, well-explained implementation is enough to explore an idea and demonstrate what you have learned.

That is the philosophy behind From Lab to Code.

Each entry will focus on an engineering topic, build a small independent implementation, and document the underlying concepts and results.

The projects are independently implemented, with original demonstration materials rather than redistributed university coursework.

This first project focuses on classical image processing. Future entries may explore other engineering topics as I encounter them.

If you are learning computer vision or signal processing, I hope the code and explanations help you experiment with these concepts yourself.

Explore the series: https://github.com/Ertugrulmutlu/from-lab-to-code

Explore the image processing project: https://github.com/Ertugrulmutlu/from-lab-to-code/tree/main/01-image-processing

Thanks for reading!

Top comments (0)