If you had read my previous articles on matrix operations, by now you would have already know what a matrix is. Yes, a matrix is a `2D`

representation of an array with `M`

rows and `N`

columns. The shape of the matrix is generally referred to as dimension. Thus the shape of any typical matrix is represented or assumed to have (`M`

x `N`

) dimensions.

**Credits of Cover Image** - Photo by Carl Nenzen Loven on Unsplash

**Row Matrix**- Collection of identical elements or objects stored in`1`

row and`N`

columns.**Column Matrix**- Collection of identical elements or objects stored in`N`

rows and`1`

column.

**Note** - Matrices of shapes (`1`

x `N`

) and (`N`

x `1`

) are generally called row vector and column vector respectively.

For instance, let's assume we have two matrices `A`

and `B`

. The general rule before multiplying is that the number of **COLUMNS** of `A`

should be exactly equal to the number of **ROWS** of `B`

. If this rule is satisfied then -

We shall compute the **dot product** for each row of `A`

with respect to each column of `B`

. This process continues until there are no elements left to compute.

- Each row of
`A`

is considered to be a row vector. - Each column of
`B`

is considered to be a column vector. **Dot Product**- It is an algebraic operation that is computed on two equal-sized vectors which result in a single number. It is also called a scalar product. Mathematically, we represent it in the form of -

$$r.c = \sum_{i=1}^{n} r_i c_i \rightarrow \text{for} \ i = \text{1 to n}$$

The resultant matrix's (after operation) size would be equal to the number of **ROWS** of `A`

and the number of **COLUMNS** of `B`

.

**Note** - The computation can be risky or slow when we are dealing with large matrices. This can be easily handled by `NumPy`

.

**GIF by Author**

If we try to break down the whole algorithm, the very first thing that we have to do is to transpose one of the matrices and compute the dot or scalar product for each row of the matrix to each column of the other matrix.

**Matrix Transpose**

```
def transpose(m):
trans_mat = [[row[i] for row in m] for i in range(len(m[0]))]
return trans_mat
```

**Dot Product**

```
def scalar_product(r, c):
ps = [i * j for (i, j) in zip(r, c)]
return sum(ps)
```

**Matrix Multiplication**

```
def mats_product(m1, m2):
m2_t = transpose(m=m2)
mats_p = [[scalar_product(r=r, c=c) for c in m2_t] for r in m1]
return mats_p
```

To wrap all the functions together, we can do the following -

**Wrap Up**

```
def easy_product(m1, m2):
def transpose(m):
trans_mat = [[row[i] for row in m] for i in range(len(m[0]))]
return trans_mat
def scalar_product(r, c):
ps = [i * j for (i, j) in zip(r, c)]
return sum(ps)
def mats_product(m1, m2):
m2_t = transpose(m=m2)
mats_p = [[scalar_product(r=r, c=c) for c in m2_t] for r in m1]
return mats_p
return mats_product(m1, m2)
```

The above `easy_product()`

can still be optimized by using the built-in methods of Python. Better improvement is needed on the method `transpose()`

.

**Matrix Transpose**

```
transpose = lambda m : list(map(list, zip(*m)))
```

**Dot Product**

The above `scalar_product()`

can still be reduced and maintained like -

```
scalar_product = lambda r, c: sum([i * j for (i, j) in zip(r, c)])
```

**Matrix Multiplication**

```
def mats_product(m1, m2):
m2_t = transpose(m=m2)
mats_p = [[scalar_product(r=r, c=c) for c in m2_t] for r in m1]
return mats_p
```

To wrap all the functions together, we can do the following -

**Wrap Up**

```
def optimized_product(m1, m2):
transpose = lambda m : list(map(list, zip(*m)))
scalar_product = lambda r, c: sum([i * j for (i, j) in zip(r, c)])
def mats_product(m1, m2):
m2_t = transpose(m=m2)
mats_p = [[scalar_product(r=r, c=c) for c in m2_t] for r in m1]
return mats_p
return mats_product(m1, m2)
```

Awesome! Both the functions are ready to be tested. In order to test so, we need to have matrices defined. We will create random matrices (function) which can further be helpful to check the speed compatibility of both functions.

**Random matrices creation**

```
import random
def create_matrix(rcount, ccount):
random.seed(10)
m = [[random.randint(10, 80) for i in range(ccount)] for j in range(rcount)]
return m
```

```
>>> nr = 2
>>> nc = 3
>>>
>>> m1 = create_matrix(nr, nc)
>>> m2 = create_matrix(nc, nr)
>>> m1
[[14, 64, 71], [11, 36, 69]]
>>> m2
[[14, 64], [71, 11], [36, 69]]
```

**Normal Function**

```
>>> mm = easy_product(m1, m2)
>>> print(mm)
[[7296, 6499], [5194, 5861]]
```

**Improved Code**

```
>>> mm = optimized_product(m1, m2)
>>> print(mm)
[[7296, 6499], [5194, 5861]]
```

Now, both the functions seem working well enough. But it is also important to check the algorithm performance in terms of speedy computation and time.

For this, we will run both the functions in a loop for a defined set of matrix shapes and store the required amount of time that each takes. We shall also plot the same to represent it visually.

**Performace Check**

```
import time
from matplotlib import pyplot as plt
def check_speed():
shapes = [(3, 2), (5, 4), (10, 9), (20, 5), (30, 8), (40, 2), (50, 10), (2, 10), (5, 80), (100, 34), (200, 100), (300, 200)]
x = [str(i) for i in shapes]
y1 = []; y2 = [];
for sp in shapes:
m1 = create_matrix(sp[0], sp[1])
m2 = create_matrix(sp[1], sp[0])
start_e = time.time()
res_easy = easy_product(m1, m2)
end_e = time.time()
easy_elapse = end_e - start_e
start_o = time.time()
res_opt = optimized_product(m1, m2)
end_o = time.time()
opt_elapse = end_o - start_o
y1.append(easy_elapse)
y2.append(opt_elapse)
plt.figure(figsize=(15, 6))
plt.plot(x, y1, 'o-', label='Normal Function')
plt.plot(x, y2, 'o-', label='Optimized Code')
plt.legend()
plt.show()
return None
```

**Performace Graph**

Both the algorithms seem to work similarly except for few cases. But there is a catch, we wrote these functions from a personal point. If we introduce `NumPy`

for doing the same, the performance would be much better. The below graph includes the performance of `NumPy`

as well.

Yes! NumPy is much faster. NumPy consumes very little time to compute the same operation no matter what the sizes are.

Breaking down a given problem and approaching each one to ultimately solve the original problem.

Comparing algorithms and tracking the computation.

If you have liked my article you can buy some coffee support me here. That would motivate me to write and learn more about what I know. It is completely fine even if you don't.

]]>In order to understand what an outlier is, we can think of a great example that explains the concept in a nutshell. Imagine, there is summer season going on. You are with your friends chilling. Everyone in your group is wearing normal clothes (light clothes that people usually wear in the summer season) except for the one who is like a weirdo. That single person is wearing a sweater. Now clearly, he/she is totally a different person who is not like the rest and certainly, he/she doesn't fit in your group.

The explanation can be clearly understood by seeing the cover image of this article.

**Credits of Cover Image** - Photo by Will Myers on Unsplash

Wait a second. Let's say I have numerical data, how can I be a detective to catch hold of an outlier in my data? Obviously, the data will be not like the cover image. (dang it)

In that case what should I do? (double dang it)

Yes, what you are saying is absolutely logical. In order to detect the outlier in numerical data (or any data for reason), we can use various statistical methods that help find out the outlier.

**Definition of Outlier** - Statistical term

In statistics, an outlier is a data point that differs significantly from other observations. An outlier may be due to variability in the measurement or it may indicate the experimental error; the latter are sometimes excluded from the data set. An outlier can cause serious problems in statistical analyses.

There are 3 easy ways to detect the outliers within the data.

- Visual Exploration
- Scatter Plot
- Box Plot

- Z-score Method
- IQR - (Inter Quartile Range)

**Note** - The readers of this article are assumed to know basic statistical measures and how to calculate them. If yes, you can proceed further. Otherwise, you may want to refer to this article.

Outliers can occur due to the wrong entry of the data or typo into the original dataset. Now that we know different ways of detecting, we shall understand each with an example.

Let's have data values say -

$$x = [10, 14, 15, 12, 18, 20, 19, 17, 22, 25, 80, 21, 25, 24, 13]$$

and

$$y = [13, 24, 17, 22, 26, 21, 14, 11, 10, 19, 23, 27, 11, 15, 85]$$

**Necessary Imports**

By representing the data in the form of a graph, one can easily identify the variability or the difference in data patterns that may lead to having an outlier. The visual representation is not more effective than the other two methods.

**Scatter Plot**

Scatter Plot basically plots or visualizes the data along the coordinate axes based on the scale range mentioned.

Code

Output

**Box Plot**

Box plot basically plots or visualizes the statistical measures of the data. The outliers are automatically shown either side `min`

and `max`

values. This is more robust than the scatter plot in detecting the outliers. The explanation of the box plot is below.

**Credits** - The above image is taken from the Internet.

Code - `x`

Output - `x`

The point (50) outside the `max`

line is an outlier.

Code - `y`

Output - `y`

The point (60) outside the `max`

line is an outlier.

Dang you (back)

The Z-score method is used to normalize (standard normalize) the data. The normalized data is also called Standardized Normal Distribution. One important thing about this is that the mean is always `0`

and the standard deviation is always `1`

.

**Formula**

Let

$$X = [x_1, x_2, x_3, \dots, x_n]$$

and

$$\mu_x \rightarrow \text{Mean}$$

$$\sigma_x \rightarrow \text{Standard Deviation}$$

then

$$z_x = \frac{(x_i - \mu_x)}{\sigma_x}$$

When we compute the above, it generates a sequence of numbers that are mostly normalized (standardized) values.

- If any value is greater than
`+3`

is an outlier. - Similarly, if any value less than
`-3`

is an outlier.

**Code**

The above code returns a sequence of normalized values.

The above code returns the outliers from the original data by getting the indices.

**Testing**

The above result is clearly stating that `50`

and `60`

are outliers of `x`

and `y`

.

Double dang you (back)

IQR is self-explained by just looking at the box plot chart. The formula for IQR is

$$IQR = Q_3 - Q_1$$

where

$$Q_1 \rightarrow \text{First Quartile}$$

$$Q_3 \rightarrow \text{Third Quartile}$$

and

$$Q_2 \rightarrow \text{Median}$$

**Code**

Once we compute IQR, we need to find the bounds, i.e., upper bound and lower bound. We use a constant (`1.5`

) to get the bounds.

- The data point which is less than the lower bound is an outlier.
- The data point which is greater than the upper bound is an outlier.

**Code**

**Testing**

The above result is clearly stating that `50`

and `60`

are outliers of `x`

and `y`

.

Triple dang you (back)

Hence the Questions are answered. You have now successfully become the detective who can detect the outliers in the data.

Well, that's all for now. This article is included in the series **Exploratory Data Analysis**, where I share tips and tutorials helpful to get started. We will learn step-by-step how to explore and analyze the data. As a pre-condition, knowing the fundamentals of programming would be helpful. The link to this series can be found here.

Statistics is a branch of mathematics (applied mathematics) concerned mainly with the analysis of the data. It includes - collection, analysis, interpretation, and presentation of huge numerical data. There are a variety of numerical measures such as `mean`

, `median`

, `mode`

, `percentiles`

, `variance`

, and `standard deviation`

to summarize the data.

We shall understand the meaning of each measure briefly and programmatically implement the same using Python.

**Credits of Cover Image** - Photo by Crissy Jarvis on Unsplash

Mean is generally referred to as the average value for the given set of values. It is the central value or central tendency of a finite set of numbers (data). In a lot of ways, mean is misinterpreted as median (which is the middle value) but it more convenient to take mean as the measure of central tendency.

**Formula**

Let

$$X = [x_1, x_2, x_3, x_4, \dots, x_n]$$

be the finite set of numbers (data) and the mean of this data is given as

$$\mu = \frac{1}{n} \sum_{i=1}^n x_i$$

**Code**

**Limitation**

- Means do get affected when introduced outliers in the data.

Median is generally referred to as the mid data point from the data provided. In other words, it is the value that separates the data into two - lower half and higher half. The only constraint to compute the median is that the data needs to be in sorted order.

**Formula**

Let

$$X = [x_1, x_2, x_3, x_4, \dots, x_n]$$

be the finite set of numbers (data) which is sorted and the median of the data is given as (considering the index starting from `0`

)-

- If
`n`

is odd, then

$$Med(X) = X\bigg[\frac{n}{2}\bigg]$$

- If
`n`

is even, then

$$Med(X) = \frac{(X[\frac{n}{2}-1] + X[\frac{n}{2}])}{2}$$

**Code**

**Pro**

- Medians do not get affected when introduced outliers in the data.

Mode is generally referred to as a number that appears most frequently in the given dataset. There is a formula to get the modal value which is more likely applicable to the data represented in class intervals. Other than that, we can easily compute the mode by counting the occurrence of each data point.

**Code**

Standard Deviation is generally referred to as the overall dispersion of each data point with respect to the data mean. Here, dispersion is simply the distance that is measured. Always, the distances are positive, we never find the distance that is measured in negative. But, the dispersion for some points can be negative. In order to avoid that, we apply a mathematical hack that can be observed in the formula.

**Formula**

Let

$$X = [x_1, x_2, x_3, x_4, \dots, x_n]$$

be the finite set of numbers (data) and the standard deviation of the data is given as

$$\sigma = \sqrt{\frac{\sum_{i=1}^n (x_i - \mu)^2}{n}}$$

**Code**

**Limitation**

- Standard Deviation does get affected when introduced outliers in the data.

Variance is generally referred to as the square of standard deviation. In case if the data is normally distributed, then the standard deviation is equal to variance which is again equal to `1`

.

**Formula**

Let

$$X = [x_1, x_2, x_3, x_4, \dots, x_n]$$

be the finite set of numbers (data) and variance of the data is given as

$$Var(X) = \sigma^2$$

**Code**

**Limitation**

- Variance does get affected when introduced outliers in the data.

Percentile is generally referred to as the single score value which falls below the given percentage of score in its frequency distribution. The median value is also equal to the `50`

th percentile of the given data distribution.

**Formula**

Let

$$X = [x_1, x_2, x_3, x_4, \dots, x_n]$$

be the finite set of numbers (data) and location percentile can be computed by

$$l_p = \bigg[(n - 1) \frac{p}{100}\bigg] + 1$$

separate the integer part and floating part from the location percentile value and get the previous data value (integer part - 1) and current data value (integer part) with the help of indexing and compute the percentile value by

$$p_v = X[prev] + [\text{floating part of }l_p * (X[curr] - X[prev])]$$

**Code**

**Limitation**

- The data needs to in sorted order (ascending) to be able to compute percentile efficiently.

Median Absolute Deviation is generally referred to as the median value of absolute dispersion from each data point to the median of the data itself. Standard deviation is computed with respect to the mean value whereas median absolute deviation (MAD) is computed with respect to the median value.

**Formula**

Let

$$X = [x_1, x_2, x_3, x_4, \dots, x_n]$$

be the finite set of numbers (data) and MAD of the data is given as

$$MAD(X) = Med(|x_i - Med(X)|)$$

**Code**

Covariance is generally referred to as a measure of the relationship between two random variables `X`

and `Y`

. This measure evaluates how much – to what extent – the variables change together. In other words, it is essentially a measure of the variance between two variables.

**Formula**

Let

$$X = [x_1, x_2, x_3, x_4, \dots, x_n]$$

and

$$Y = [y_1, y_2, y_3, y_4, \dots, y_n]$$

be two finite sets of numbers (data) and the Covariance of `X`

and `Y`

is given as

$$\text{Cov(X, Y)} = \frac{1}{n} \sum_{i=1}^n (x_i - \mu_x)(y_i - \mu_y)$$

**Code**

**Types**

`Positive Covariance`

→ Indicates that two variables tend to move in the same direction.`Negative Covariance`

→ Indicates that two variables tend to move in the inverse direction.

**Properties**

- Cov(X, Y) = Cov(Y, X)
- Cov(X, X) = Var(X)

Correlation is generally referred to as a measure of the strength of the relationship between two finite sets (data). Correlation is the scaled measure of covariance. The correlation value is always in the range of `-1`

to `+1`

.

**Formula**

Let

$$X = [x_1, x_2, x_3, x_4, \dots, x_n]$$

and

$$Y = [y_1, y_2, y_3, y_4, \dots, y_n]$$

be two finite sets of numbers (data) and the Correlation of `X`

and `Y`

is given as

$$\text{Corr(X, Y)} = \frac{\text{Cov(X, Y)}}{\sigma_X \sigma_Y}$$

**Code**

**Types**

`Positive Correlation`

→ Indicates that two variables have a strong relationship and tend to move in a positive direction. The value is generally`+1`

.`Zero Correlation`

→ Indicates that there is no relationship between two variables. The value is generally`0`

.`Negative Correlation`

→ Indicates that two variables have a strong relationship and tend to move in the inverse direction. The value is generally`-1`

.

```
import math
from collections import Counter
class StatsBasics():
def compute_mean(self, data):
mean_val = sum(data)/len(data)
return mean_val
def compute_median(self, data):
data = sorted(data)
n = len(data)
mid_idx = n // 2
if (n % 2 != 0):
return data[mid_idx]
return (data[mid_idx - 1] + data[mid_idx]) / 2
def compute_mode(self, data):
datac = Counter(data)
max_freq = max(list(datac.values()))
if (max_freq == 1):
return "Mode doesn't exist"
modals = [i for (i, j) in datac.items() if (j == max_freq)]
return min(modals)
def compute_stddev(self, data):
mean_val = self.compute_mean(data=data)
dispersions = [(i - mean_val)**2 for i in data]
dispersion_mean = self.compute_mean(data=dispersions)
return math.sqrt(dispersion_mean)
def compute_variance(self, data):
stddev = self.compute_stddev(data=data)
return stddev**2
def compute_percentile(self, p, data):
data = sorted(data)
if (p == 100):
return data[-1]
l_p = (len(data) - 1) * (p / 100) + 1
int_l_p = int(l_p)
fl_l_p = l_p - int_l_p
val1 = data[int_l_p - 1]
val2 = data[int_l_p]
pval = val1 + (fl_l_p * (val2 - val1))
return round(pval, 2)
def compute_mad(self, data, c=0.6745):
median_val = self.compute_median(data=data)
abs_std = [abs(i - median_val) for i in data]
mad = self.compute_median(data=abs_std) / c
return round(mad, 2)
def compute_covariance(self, X, Y):
if (len(X) != len(Y)):
return None
mean_x = self.compute_mean(data=X)
mean_y = self.compute_mean(data=Y)
covals = [(x - mean_x)*(y - mean_y) for (x, y) in zip(X, Y)]
covar_val = self.compute_mean(data=covals)
return covar_val
def compute_correlation(self, X, Y):
covar_val = self.compute_covariance(X=X, Y=Y)
std_X = self.compute_stddev(data=X)
std_Y = self.compute_stddev(data=Y)
corr_val = covar_val / (std_X * std_Y)
return corr_val
```

Well, that's all for now. This article is included in the series **Exploratory Data Analysis**, where I share tips and tutorials helpful to get started. We will learn step-by-step how to explore and analyze the data. As a pre-condition, knowing the fundamentals of programming would be helpful. The link to this series can be found here.

Identifying Patterns helps us understand more about the data, which is gaining insights by observing trends and patterns. This also helps in finding the relationship between the two sets.

From the business point, observing trends really helps in tracking the overall sales and returns. Providing the data stored yearly-wise, we can easily plot and identify a trend pattern - an upward trend or constant trend or a lower trend. And thus, decisions are made based on facts.

**Google Trends** is one of the best applications available on the web to see the trend graph on the `search term`

.

**Credits of Cover Image** - Photo by Clark Van Der Beken on Unsplash

Depending on the data we can gain insights either by just looking at the tabular data (assuming data is in tabular format) or plotting it.

From a personal point, it is always good to visualize the data in order to properly identify the patterns. This really makes sense to me.

Let's try to identify patterns in one dataset.

**Note** - Throughout this series, we will be dealing with one dataset originally taken from Kaggle.

For the hands-on practice, we will use a dataset in the domain of health care on heart attack possibility.

*Let's get started ...*

We are using Python and specifically Pandas library - developed with a clear intention to approach the key aspects of data analytics and data science problems.

```
pip install pandas --user
pip install numpy --user
pip install seaborn --user
pip install matplotlib --user
```

The above installs the current version of packages in the system. Let's import the same.

```
import pandas as pd
import numpy as np
import seaborn as sns
from matplotlib import pyplot as plt
```

In order to load or read the data, we use the `read_csv()`

method where we explicitly tell Pandas that we want to read a `CSV`

file.

```
df = pd.read_csv('heart.csv)
```

Since `df`

is a Pandas object, we can instantiate the other methods that are made available to understand the data.

From the last tutorial, we know that `cp`

(chest pain), `thalach`

(heartbeat rate), `chol`

(cholesterol) (not so important), and `slope`

are happened to be the most important features.

`target`

Display the list of columns in the dataset.

```
>>> df.columns
Index(['age', 'sex', 'cp', 'trestbps', 'chol', 'fbs', 'restecg', 'thalach', 'exang', 'oldpeak', 'slope', 'ca', 'thal', 'target'], dtype='object')
```

Filter the dataset considering the `target`

variable.

```
df_1 = df[df['target'] == 1] # data of people who are risky
df_0 = df[df['target'] == 0] # data of people who are safe
```

The cholesterol is measured in milligrams (mg) and the heartbeat rate is measured over a minute. Let's plot the [`chol`

, `thalach`

] features with respect to `age`

for both `df_1`

and `df_0`

.

**Age group - Risky**

- First group the data (
`df_1`

) by`age`

considering the columns`chol`

and`thalach`

.

```
ag_df_1 = df_1.groupby(by=['age'])[['chol', 'thalach']].sum()
```

- Plot a
`bar`

chart for the grouped data (above).

```
ag_df_1.plot(kind='bar', figsize=(15, 6), title='Risky')
plt.show()
```

The plot looks like this -

**Age group - Safe**

- First group the data (
`df_0`

) by`age`

considering the columns`chol`

and`thalach`

.

```
ag_df_0 = df_0.groupby(by=['age'])[['chol', 'thalach']].sum()
```

- Plot a
`bar`

chart for the grouped data (above).

```
ag_df_0.plot(kind='bar', figsize=(15, 6), title='Safe')
plt.show()
```

The plot looks like this -

**Conclusion**

People who are in the risky group have high heartbeat rates compared with those who are in the safe group.

Also, the cholesterol measurements are higher in the risky group

One can easily assume that optimal heartbeat rate is a goal for heart patients in order to be healthy - clearly understood from the graphs.

Let's plot the [`chol`

, `thalach`

] features with respect to `cp`

(chest pain) for both `df_1`

and `df_0`

.

**CP group - Risky**

- First group the data (
`df_1`

) by`cp`

considering the columns`chol`

and`thalach`

.

```
cp_df_1 = df_1.groupby(by=['cp'])[['chol', 'thalach']].sum()
```

- Plot a
`pie`

chart for the grouped data (above).

```
cp_df_1.plot(kind='pie', figsize=(15, 6), subplots=True, title='Risky')
plt.show()
```

The plot looks like this -

**CP group - Safe**

- First group the data (
`df_0`

) by`cp`

considering the columns`chol`

and`thalach`

.

```
cp_df_0 = df_0.groupby(by=['cp'])[['chol', 'thalach']].sum()
```

- Plot a
`pie`

chart for the grouped data (above).

```
cp_df_0.plot(kind='pie', figsize=(15, 6), subplots=True, title='Safe')
plt.show()
```

The plot looks like this -

**Conclusion**

From the above two graphs, we can observe that safe category people have lesser degrees of chest pain than the risky.

It also very significant that

`cp`

and`thalach`

both are equally important with respect to the`age`

of the patient.

`age`

and `cp`

Scatter Plot helps us identify an upward trend or downward trend for two variables that are taken. It is just another form of correlation represented graphically. Based on two datasets `df_1`

and `df_0`

, let's compare `cp`

with respect to `age`

.

```
plt.figure(figsize=(10, 6))
plt.scatter(df_1['age'], df_1['cp'], label='Risky')
plt.scatter(df_0['age'], df_0['cp'], label='Safe')
plt.xlabel('age')
plt.ylabel('chest pain')
plt.legend()
plt.show()
```

**Conclusion**

From the plot, we can see that most of the safe category people have a lesser degree of chest pain than those of risky people.

In the safe category, though the age is more, the chest pain degree is less and of course there are others who have a high degree.

`age`

and `thalach`

Now, let's compare `thalach`

with respect to `age`

for both `df_1`

and `df_0`

.

```
plt.figure(figsize=(10, 6))
plt.scatter(df_1['age'], df_1['thalach'], label='Risky')
plt.scatter(df_0['age'], df_0['thalach'], label='Safe')
plt.xlabel('age')
plt.ylabel('thalach')
plt.legend()
plt.show()
```

**Conclusion**

If we just observe the safe (orange) category, the heartbeat level is constant with respect to age.

The same is not with the risky (blue) category.

We can also assume that people in the group between 29 to 60, have higher rates that lead to an attack with adding higher degrees of chest pain.

Well, that's all for now. This article is included in the series **Exploratory Data Analysis**, where I share tips and tutorials helpful to get started. We will learn step-by-step how to explore and analyze the data. As a pre-condition, knowing the fundamentals of programming would be helpful. The link to this series can be found here.

Identifying or selecting the important features is one of the crucial processes in data science and data analytics. After all, the features that are selected decide how qualitative the data is. It is exactly like the phrase ** Garbage in, Garbage out** - meaning, whatever features that we consider, the result of the whole analytical process depends on those features. It is also said that data analysts spend at least

`60`

to `70`

percent of their time preparing the data.**Credits of Cover Image** - Photo by Brian Lundquist on Unsplash

Data Preprocessing is an important phase of the whole analytical process, where relevant fields are extracted or queried from the gathered data. It is in this step, data analysts try their best to retain or increase the quality of the data, with which further steps become handy. During the process of data gathering, it is quite sure to end up collecting ** messy data** and therefore it has to be preprocessed right after it is collected. It includes steps like -

**Data Cleaning**- In this process, unnecessary data values which do not add importance are removed. Mostly, it includes missing values that should be cleaned or filled.**Data Editing**- In this process, the original data is edited or changed in order to maintain uniqueness. For example, in the dataset, if there is an`age`

column then it is important to have all the age values in numeric, and there can be chances to get the data in non-numeric. In that case, it is always encouraged to edit the data.**Data Wrangling**- In this process, the raw data is transformed or manipulated in such a format that it is easy to use and readily available for data analytics. Oftentimes, it is also called data manipulation.

**Note** - The above steps are explained in a nutshell. Moreover, the overall complexity (to maintain data quality) depends on the dataset that is collected.

Correlation is a statistical measurement that tells how one feature is affecting the target variable. It returns a percentage value describing the relationship. The correlation value (percentage) lies between `-1`

and `+1`

.

`-1`

→ describes that the feature and the target variables are negatively correlated, meaning - one increases when the other decreases and vice-versa.`0`

→ describes that there is no correlation.`+1`

→ describes that the feature and the target variables are positively correlated, meaning - one increases when the other increases and vice-versa.

Correlation is really helpful in knowing those features that actually affect the target variable.

Let's try to identify which features are important in one dataset.

**Note** - Throughout this series, we will be dealing with one dataset originally taken from Kaggle.

For the hands-on practice, we will use a dataset in the domain of health care on heart attack possibility.

*Let's get started ...*

We are using Python and specifically Pandas library - developed with a clear intention to approach the key aspects of data analytics and data science problems.

```
pip install pandas --user
pip install numpy --user
pip install seaborn --user
pip install matplotlib --user
```

The above installs the current version of packages in the system. Let's import the same.

```
import pandas as pd
import numpy as np
import seaborn as sns
from matplotlib import pyplot as plt
```

In order to load or read the data, we use the `read_csv()`

method where we explicitly tell Pandas that we want to read a `CSV`

file.

```
df = pd.read_csv('heart.csv)
```

Since `df`

is a Pandas object, we can instantiate the other methods that are made available to understand the data.

```
df.head()
```

`head()`

is used to display the first five rows. By default, we don't explicitly mention how many we want to display. However, if you want to display other than five rows you can mention a number like `head(<any_number>)`

.

```
age sex cp trestbps chol fbs restecg thalach exang oldpeak slope ca thal target
0 63 1 3 145 233 1 0 150 0 2.3 0 0 1 1
1 37 1 2 130 250 0 1 187 0 3.5 0 0 2 1
2 41 0 1 130 204 0 0 172 0 1.4 2 0 2 1
3 56 1 1 120 236 0 1 178 0 0.8 2 0 2 1
4 57 0 0 120 354 0 1 163 1 0.6 2 0 2 1
```

We have a `column`

called `target`

that has two unique values.

`0`

→ indicates fewer chances of getting a heart attack`1`

→ indicates more chances of getting a heart attack

`target`

columnTo count the frequency of the data we will use the `value_counts()`

method.

```
ha_df = df['target'].value_counts().to_frame()
```

The above methods convert the count (data) to a data frame.

```
target
----------
1 165
0 138
```

Visually, we can represent the above count (data) as a `pie`

chart.

```
ha_df.plot(kind='pie', figsize=(10, 6), subplots=True)
plt.show()
```

`sex`

ratioFrom the original data frame `df`

, we have a column called `sex`

which is again numerical where -

`1`

→ indicates male`0`

→ indicates female

Let's visualize the `pie`

chart of the same.

```
sdf = df['sex'].value_counts().to_frame()
sdf.plot(kind='pie', figsize=(10, 6), subplots=True)
plt.show()
```

`Correlation`

PlotWith the help of Pandas, we can easily find correlations considering the entire data frame.

```
cor_df = df.corr()
```

This will return the correlations to each column with all the other columns. We can directly visualize the correlation matrix using `seaborn`

plots.

```
plt.figure(figsize=(10, 6))
sns.heatmap(
data=cor_df,
vmin=-1,
vmax=1,
center=0,
cmap='seismic',
annot=True
)
plt.show()
```

If we carefully observe the correlation matrix, there are totally 3 features that are affecting the `target`

variable. They are -

`cp`

- chest pain`thalach`

- heartbeat rate`slope`

- may be

The variable `chol`

(cholesterol) has no relationship. In fact, from the correlation plot, we see that the relationship ratio is `-0.085`

. Whereas, `cp`

and `thalach`

have around `0.43`

and `0.42`

respectively.

**Exploratory Data Analysis**, where I share tips and tutorials helpful to get started. We will learn step-by-step how to explore and analyze the data. As a pre-condition, knowing the fundamentals of programming would be helpful. The link to this series can be found here.

In any business problem, data analysts are often asked to find out the patterns and trends from the data in order to identify the strong root cause of problems (why something is happening). Throughout the time, they try to maintain the quality of the data to be precise in finding the true objectives. These objectives (targets) are not formulated from personal opinions or prejudices.

**Credits of Cover Image** - Photo by Silvan Arnet on Unsplash

To first understand the data, one has to be a great observant. By just observing the data one can know what kind of facts are given and what are those factors - can be considered to be the most important features. This stage is called **Data Inspection**.

It is in this stage where one acts as **Sherlock Holmes** to understand the data better.

As soon as the data is loaded, to make sure one is following the right track one has to be curious to ask the following questions -

- What is the problem statement?
- What is the structure of the data?
- What are the variables present?
- What is the type of each variable?

- What is the total size of the data?
- Are there any missing or
`NaN`

values within the data?

Let's try to understand one dataset from the above questions along with the objectives.

**Note** - Throughout this series, we will be dealing with one dataset originally taken from Kaggle.

For the hands-on practice, we will use one dataset in the domain of health care on heart attack possibility.

*Let's get started ...*

We are using Python and specifically Pandas library that was developed with a clear intention to approach the key aspects of data analytics and data science problems.

```
pip install pandas --user
pip install numpy --user
```

The above installs the current version of packages in the system. Let's import the same.

```
import pandas as pd
import numpy as np
```

The title of the dataset itself gives a brief idea of the problem statement that we need to work on, i.e., predicting the possibility of a heart attack. But, for this article, our goal is to understand the data by answering the above questions.

In order to load or read the data, we use the `read_csv()`

method where we explicitly tell Pandas that we want to read a `CSV`

file.

```
df = pd.read_csv('heart.csv)
```

The dataset that we have just loaded is a `CSV`

file data where all the values are separated with a character `,`

. By default, the data is read as a `DataFrame`

which is basically tabular data.

```
df.info()
```

Since we have made an object called `df`

, we can instantiate different methods available in the scope. One such method is `info()`

which gives the information related to the data frame.

```
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 303 entries, 0 to 302
Data columns (total 14 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 age 303 non-null int64
1 sex 303 non-null int64
2 cp 303 non-null int64
3 trestbps 303 non-null int64
4 chol 303 non-null int64
5 fbs 303 non-null int64
6 restecg 303 non-null int64
7 thalach 303 non-null int64
8 exang 303 non-null int64
9 oldpeak 303 non-null float64
10 slope 303 non-null int64
11 ca 303 non-null int64
12 thal 303 non-null int64
13 target 303 non-null int64
dtypes: float64(1), int64(13)
memory usage: 33.2 KB
```

No doubt that we have got the entire information, structure, columns by just one command. We also got the types of each column which indirectly answers the 3rd question.

Variables are also called features which are the columns of the data. Each column or variable has a specific type of data that can be `int`

, `float`

, `object`

etc.

```
df.columns
```

The above gives a list of columns or variables and the type of column name. All the columns are read as `object`

.

```
Index(['age', 'sex', 'cp', 'trestbps', 'chol', 'fbs', 'restecg', 'thalach',
'exang', 'oldpeak', 'slope', 'ca', 'thal', 'target'],
dtype='object')
```

```
df.dtypes
```

The above lists out the type of data that each variable or column holds.

```
age int64
sex int64
cp int64
trestbps int64
chol int64
fbs int64
restecg int64
thalach int64
exang int64
oldpeak float64
slope int64
ca int64
thal int64
target int64
dtype: object
```

The size of the data can be determined by -

```
df.shape
```

this gives the total number of rows and columns in the form of a `tuple`

.

```
(303, 14)
```

The dataset has in total `303`

rows and `14`

columns. Now comes the last and final question.

`NaN`

values within the data?Missing values in the data indicate that the value for that column or variable at a particular index is `Null`

or `None`

. In order to find the same, we have a method called `isnull()`

which can be understood in this way -

is the data frame null?

```
df.isnull()
```

This, in return, creates a new data frame of `boolean`

values representing

`True`

, wherever the value is`Null`

`False`

, wherever the value is`NOT-Null`

```
age sex cp trestbps chol fbs restecg thalach exang oldpeak slope ca thal target
0 False False False False False False False False False False False False False False
1 False False False False False False False False False False False False False False
2 False False False False False False False False False False False False False False
... ... ... ... ... ... ... ... ... ... ... ... ... ...
301 False False False False False False False False False False False False False False
302 False False False False False False False False False False False False False False
```

However, looking at the above result, it can be tedious to exactly pinpoint the `True`

value signifying the `Null`

in that location. We can achieve it with the help of NumPy's `np.where()`

method.

```
def get_nan_location(dframe):
nan_locs = {}
for col in dframe.columns:
nl = list(np.where(dframe[col].isnull())[0])
nan_locs[col] = nl
return nan_locs
```

The above takes one parameter i.e., `dframe`

(the actual data frame), and returns a dictionary where each key is the column and each value is the list of indices where there is `NaN`

pertaining to that respective column.

```
get_nan_location(dframe=df)
#--------------------------
{'age': [],
'sex': [],
'cp': [],
'trestbps': [],
'chol': [],
'fbs': [],
'restecg': [],
'thalach': [],
'exang': [],
'oldpeak': [],
'slope': [],
'ca': [],
'thal': [],
'target': []}
```

We can see all the columns have an empty list which simply indicates that there are no `NaN`

values.

Once this step or stage is over, we should be formulating other questions that mainly depend on the data we are working with.

**Exploratory Data Analysis**, where I share tips and tutorials helpful to get started. We will learn step-by-step how to explore and analyze the data. As a pre-condition, knowing the fundamentals of programming would be helpful. The link to this series can be found here.

Data is the knowledge gained from a factual basis. It can be related to an object or a person. An explanation obtained from processing the data is called information. Thus, data and information are two different things.

**Data**→ Facts and Figures**Information**→ Processed data which is understood better

**Credits of Cover Image** - Photo by Hunter Harritt on Unsplash

From the above two, we can get clarity of the term **" Data Analysis "**. Data Analysis is a field of Statistics, Mathematics, and Computer Science combined together in processing the raw data to produce insightful or valuable information. You might have this question - Statistics and Mathematics are fine but why Computer Science? The knowledge of programming helps in different ways in analyzing data. Some of which are -

- Process Automation
- Handling Large Datasets
- Querying Databases
- Creating Models
- Data Visualization
- Dashboard Development

Of course, we cannot just analyze the given data with a piece of paper and pencil. We need to find one such platform to do all three - Stats, Math, and Programming.

Tools involved in Data Analysis -

- Python
- R
- Julia
- Matlab

**Note** - There are so many languages or tools available. But here, I talk about Python. If you want to know the list then do refer to this article.

To get started in the field of data, learning Python would benefit in many ways. Python has a wide variety of packages that have been developed over the years. From data collection to data modeling, Python has everything set for you.

List of Packages or libraries:

- NumPy
- Pandas
- Statsmodels
- Matplotlib
- OpenCV
- Scikit-Learn
- Pytorch
- Tensorflow
- Plotly
- Py-Spark, etc

These packages have been extensively used for data-related problems. There is no requirement to learn all the packages as long as one is curious enough to understand the problem and implement the method. But the deeper one goes the deeper knowledge of using these are a must.

**Exploratory Data Analysis**, where I share tips and tutorials helpful to get started. We will learn step-by-step how to explore and analyze the data. As a pre-condition, knowing the fundamentals of programming would be helpful. The link to this series can be found here.

**Credits of Cover Image** - Photo by Kelly Sikkema on Unsplash

For a while till now, I have been working on my basic image processing app developed in Python using the frameworks and libraries like -

- Dash
- Plotly
- Plotly express
- NumPy &
- OpenCV
- and some other dependencies

Let me explain how the journey of developing this app began.

First, I didn't have any idea or plan to develop an app (that too for image processing). It is when one of my colleagues asked in our common group -

How do I re-mirror the image in the zoom app? By default, it takes the virtual background image that is already mirrored.

At that point, I really wanted to understand how this mirroring feature works in any image application. I already know the basics of image processing, like - how to read the image, how to convert the image into grayscale - stuff like that. I thought, why don't I try this in Python, in fact mirroring the image is simply reversing the rows of the image matrix (images are considered as matrices with rows and columns).

The moment I knew what to do, I searched online. I got the resources that we can easily implement the `mirroring`

operation using the `PIL`

(Pillow) library. I remember I was once told that the NumPy library is mainly used for scientific and complex mathematical calculations. I fixed in my mind that I would only go to use the NumPy library for both mathematical understanding and as well as implementation.

Firstly, I implemented using a regular `for`

loop. I observed that it was taking too much time to compute. Although it was working, I chose the `np.fliplr()`

(`flip-left-right`

) method. I plotted the result after operating and the image is completely mirrored. The same technique I used for flipping the image up to down. The `np.flipud()`

(`flip-up-down`

) method is used.

Likewise, step by step I learned other operations and executed them. Image Flipping and Mirroring with NumPy and OpenCV was the first blog I wrote in the category of image processing.

Image Operations

Image Transformation (Morphological)

- Image Erosion
- Image Dilation
- Other transformations which use the concept of erosion and dilation.

**Note** - I have re-ordered the blogs in my series, in a way that a beginner can also understand from the initial step.

After having developed some image operations, I wanted to compile everything together as an app with a proper UI. I have extensively used `dash`

& `plotly`

for designing the UI part along with the user-upload component.

**Note** - I won't be sharing the whole code but for sure the code snippets.

`import`

s```
import dash
import dash_core_components as dcc
import dash_html_components as html
import dash_daq as daq
import plotly.express as px
import cv2
import numpy as np
import json
import base64
from matplotlib import pyplot as plt
from dash.dependencies import (Input, Output, State)
from image_ops_scratch import (ImageOperations, read_image_string)
from image_morphs_scratch import MorphologicalTransformations
```

`image_ops_scratch`

→ the file where all the image operations are defined.`image_morphs_scratch`

→ the file where all the morphological image transformations are defined.

```
external_stylesheets = [
'https://codepen.io/chriddyp/pen/bWLwgP.css'
]
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
app.config['suppress_callback_exceptions'] = True
app.title = 'Image Processing App'
server = app.server
```

For developing the UI, we shall use the sub-packages of `dash`

namely -

- dash_daq
- dash_core_components
- dash_html_components
- dash.dependencies

Please refer to the official documentation to understand the basics of dash callbacks and other important UI components.

This component is used to upload a local file from the system to the application. When the user clicks on this, the file directory pops up to select an appropriate file. Firstly, the image file that we select is read in `base64 code`

which is to be converted into an image array for the operation to work effectively (the operations work only for `nd`

arrays).

```
html.Div([
dcc.Upload(
id='upload-image',
children=html.Div([
'Drag and Drop or ',
html.A('Select Files')
]),
style={
'width': '100%',
'height': '70px',
'lineHeight': '60px',
'borderWidth': '1px',
'borderStyle': 'dashed',
'borderRadius': '5px',
'textAlign': 'center',
'margin': '10px',
'backgroundColor': '#F0F1F1'
},
multiple=True
),
], style={'paddingTop' : 50})
```

```
def read_image_string(contents):
encoded_data = contents[0].split(',')[1]
nparr = np.frombuffer(base64.b64decode(encoded_data), np.uint8)
img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
return img
```

When the toggle switch is turned on, the image shall be converted directly to grayscale. By default, the image is read in RGB mode.

```
html.Div([
daq.ToggleSwitch(
id='image-mode',
size=60,
label='Gray Scale',
labelPosition='top',
color='#717171',
value=False,
)
], style={'paddingTop' : 30, 'paddingBottom' : 10})
```

To represent each operation we will use radio buttons. When the user selects any particular operation, the output of the image changes and the same is displayed in the app.

```
image_ops = ['None', 'Equalize', 'Flip', 'Mirror', 'Binarize', 'Invert', 'Solarize']
html.Div([
dcc.RadioItems(
id='in-operation',
options=[{'label' : op, 'value' : op.lower()} for op in image_ops],
value='none'
),
], className='select-operation')
########################
image_morphs = ['None', 'Erode', 'Dilate', 'Open', 'Close', 'Gradient', 'Boundary Extraction']
html.Div([
html.P('Morph level - '),
dcc.Input(id='morph-level', type='number', placeholder='Enter Morph Level - ', value=3),
html.Div([
dcc.RadioItems(
id='in-transformation',
options=[{'label' : tr, 'value' : tr.lower()} for tr in image_morphs],
value='none'
),
], style={'paddingTop' : 20})
], className='select-operation')
```

The app should have two tabs so as to separate image operations and image transformations. Also, in the transformations tab, the image needs to be converted into binary as they only work on binary images. Similarly, in the operations tab, we shall place the toggle switch.

```
tab_style = {
'borderBottom': '1px solid #d6d6d6',
'padding': '10px',
'fontWeight': 'bold'
}
tab_selected_style = {
'borderTop': '5px solid #d6d6d6',
'borderBottom': '3px solid #d6d6d6',
'backgroundColor': '#7E8483',
'color': 'white',
'padding': '6px'
}
html.Div([
dcc.Tabs(
id='image-processors-tabs',
value='operators',
children=[
dcc.Tab(
label='Operations',
value='operators',
style=tab_style,
selected_style=tab_selected_style,
children=[
# toggle switch
# operations - radio buttons
]
),
dcc.Tab(
label='Transformations',
value='transformers',
style=tab_style,
selected_style=tab_selected_style,
children=[
# transformations - radio buttons
]
)
]
)
], className='tab-div')
```

The output `div`

should be the same for both operations and transformations. It has to be already preserved with a unique `id`

with which it can be updated based on the `tab`

that is selected.

```
html.Div(
id='result-in-out-image',
className='flex-item-right'
)
```

In dash, we mainly have `callbacks`

based on which the output is displayed or updated. Each output callback identified with a unique `id`

can take the `n`

number of `Inputs`

or `States`

. But each `callback`

takes only one output with a unique `id`

that can be repeated exactly once.

The callback chart for this app is as follows -

```
def parse_contents(contents, filename, date):
image_mat = read_image_string(contents=contents)
return image_mat
```

Here -

- contents → are the data in the file that is selected
- filename → points to the name of the file that is selected
- date → points to the status to when the file is last updated

We have one output `div`

that is reserved. Based on the `tab`

selected, the app should change its nature. Hence, the following code.

```
@app.callback(
Output('result-in-out-image', 'children'),
[Input('image-processors-tabs', 'value')]
)
def set_output_layout(which_tab):
if which_tab == 'operators':
in_out_image_div = html.Div([
html.Div(
children= [
html.H5('Image Used - Output'),
html.Div(id='output-image-op'),
],
style={'textAlign' : 'center', 'paddingTop' : 50}
)
])
elif which_tab == 'transformers':
in_out_image_div = html.Div([
html.Div(
children= [
html.H5('Image Used - Output'),
html.Div(id='output-image-morph'),
],
style={'textAlign' : 'center', 'paddingTop' : 50}
)
])
return in_out_image_div
```

In this, we have -

- 1 Output
`output-image-op`

→ pointing to the change in children (HTML collection)

- 3 Inputs
`upload-image`

→ pointing to the contents of the file`image-mode`

→ pointing to the mode (RGB or grayscale by toggle switch)`in-operation`

→ pointing to the input operation that is selected

- 2 States
`upload-image`

→ pointing to the state of the file name`upload-image`

→ pointing to the state of the date (when the file is last modified)

```
@app.callback(
Output('output-image-op', 'children'),
[
Input('upload-image', 'contents'),
Input('image-mode', 'value'),
Input('in-operation', 'value'),
# -------
State('upload-image', 'filename'),
State('upload-image', 'last_modified'),
]
)
def get_operated_image(contents, image_mode, operation, filenames, dates):
if contents is not None:
imsrc = parse_contents(contents, filenames, dates)
imo = ImageOperations(image_file_src=imsrc)
if (operation == 'equalize'):
out_img = imo.equalize_this(gray_scale=True) if image_mode else imo.equalize_this()
elif (operation == 'flip'):
out_img = imo.flip_this(gray_scale=True) if image_mode else imo.flip_this()
elif (operation == 'mirror'):
out_img = imo.mirror_this(gray_scale=True) if image_mode else imo.mirror_this()
elif (operation == 'binarize'):
out_img = imo.binarize_this(gray_scale=True) if image_mode else imo.binarize_this()
elif (operation == 'invert'):
out_img = imo.invert_this(gray_scale=True) if image_mode else imo.invert_this()
elif (operation == 'solarize'):
out_img = imo.solarize_this(gray_scale=True) if image_mode else imo.solarize_this()
else:
out_img = imo.read_this(gray_scale=True) if image_mode else imo.read_this()
out_image_fig = px.imshow(out_img, color_continuous_scale='gray') if image_mode else px.imshow(out_img)
out_image_fig.update_layout(
coloraxis_showscale=False,
width=600, height=400,
margin=dict(l=0, r=0, b=0, t=0)
)
out_image_fig.update_xaxes(showticklabels=False)
out_image_fig.update_yaxes(showticklabels=False)
output_result = html.Div([
dcc.Graph(id='out-op-img', figure=out_image_fig)
], style={'paddingTop' : 50})
return output_result
```

In this, we have -

- 1 Output
`output-image-morph`

→ pointing to the change in children (HTML collection)

- 3 Inputs
`upload-image`

→ pointing to the contents of the file`morph-level`

→ pointing to the input value with which the level of the structuring element is decided`in-transformation`

→ pointing to the input transformation that is selected

- 2 States
`upload-image`

→ pointing to the state of the file name`upload-image`

→ pointing to the state of the date (when the file is last modified)

```
@app.callback(
Output('output-image-morph', 'children'),
[
Input('upload-image', 'contents'),
Input('morph-level', 'value'),
Input('in-transformation', 'value'),
# -------
State('upload-image', 'filename'),
State('upload-image', 'last_modified'),
]
)
def get_transformed_image(contents, level, transformation, filenames, dates):
if contents is not None:
imsrc = parse_contents(contents, filenames, dates)
morph = MorphologicalTransformations(image_file_src=imsrc, level=level)
level = 3 if level == None else level
image_src = morph.read_this()
if (transformation == 'erode'):
out_img = morph.erode_image(image_src=image_src)
elif (transformation == 'dilate'):
out_img = morph.dilate_image(image_src=image_src)
elif (transformation == 'open'):
out_img = morph.open_image(image_src=image_src)
elif (transformation == 'close'):
out_img = morph.close_image(image_src=image_src)
elif (transformation == 'gradient'):
out_img = morph.morph_gradient(image_src=image_src)
elif (transformation == 'boundary extraction'):
out_img = morph.extract_boundary(image_src=image_src)
else:
out_img = image_src
out_image_fig = px.imshow(out_img, color_continuous_scale='gray')
out_image_fig.update_layout(
coloraxis_showscale=False,
width=600, height=400,
margin=dict(l=0, r=0, b=0, t=0)
)
out_image_fig.update_xaxes(showticklabels=False)
out_image_fig.update_yaxes(showticklabels=False)
output_result = html.Div([
dcc.Graph(id='out-morph-img', figure=out_image_fig)
], style={'paddingTop' : 50})
return output_result
```

**Note** - The above code snippets belong to one file i.e., `app.py`

. The actual image processing code is not disclosed. One can look it up in my GitHub repository.

The fun part was during the deployment. Although I have experience in deploying Python web apps on Heroku, I faced slight issues (which helped me learn) while deploying this app. I organized all the code and checked if there is any change that needed to be done.

```
- image-app/
- assets/
- custom_style.css
- images/
- lena_original.png
- pinktree.jpg
- scenary.jpg
- .gitignore
- app.py
- image_morphs_scratch.py
- image_ops_scratch.py
- Procfile
- requirements.txt
```

I specified all the requirements in a separate `requirements.txt`

file. The errors occurred during the installation of `OpenCV`

inside Heroku. Actually, I was supposed to specify `opencv-contrib-python-headless`

instead of `opencv-contrib-python`

to get the package installed properly.

```
dash
plotly
dash-core-components
dash-html-components
dash-daq
numpy
matplotlib
pandas
opencv-contrib-python-headless
gunicorn
```

```
web: gunicorn app:server
```

Everything is set now, We can easily follow these steps and deploy the app that can be accessible to everyone.

By developing this app, I got to learn some image processing concepts along with the mathematics behind it. I feel happy to finally be able to deploy this app on Heroku. I know the app is sort of basic and will be adding other interesting features to it in the future. I would like to see your feedback or suggestions in the comments.

- GitHub link → https://github.com/msameeruddin/image-app
- App link → https://process-image-app.herokuapp.com/

If you have liked this project, do drop a star, and buy coffee for me from here. We will meet in the next blog.

**Credits of Cover Image** - Photo by Vlado Paunovic on Unsplash

A matrix is a 2D representation of an array with M rows and N columns. An array is a collection of identical elements or objects stored in 1 row and N columns. There are so many mathematical operations and properties that can be implemented in a matrix. One such operation is the transpose operation. Transposing a matrix is easy, just converting rows into columns and vice-versa.

Let’s try to code this in 3 different ways and compare the performance.

In every matrix, the length of rows should be equal and the same follows for the columns.

We require two `for`

loops:

In the first loop, we consider the size of any row. This will decide the column size for our transposed matrix.

In the second loop, we consider each row in the original matrix and extract every item order-wise.

```
def transpose_matrix_1(matrix):
# take the first row length
row_len = len(matrix[0])
trans_mat = []
for i in range(row_len):
trans_row = []
for row in matrix:
# extract every item from each row order-wise
trans_row.append(row[i])
trans_mat.append(trans_row)
return trans_mat
```

```
>>> matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]]
>>> t_matrix = transpose_matrix_1(matrix=matrix)
>>> print(t_matrix)
[[ 1 4 7 10]
[ 2 5 8 11]
[ 3 6 9 12]]
```

We can improvise the above function by implementing list comprehension. List comprehension is easy to implement and performance-wise, it is way faster than normal `for`

loops.

```
def transpose_matrix_2(matrix):
row_len = len(matrix[0])
trans_mat = [[row[i] for row in matrix] for i in range(row_len)]
return trans_mat
```

```
>>> matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]]
>>> t_matrix = transpose_matrix_2(matrix=matrix)
>>> print(t_matrix)
[[ 1 4 7 10]
[ 2 5 8 11]
[ 3 6 9 12]]
```

Lambda function is a small anonymous function that takes n arguments and returns only one output.

`map()`

is used to map a particular function with a sequence of objects that we can achieve using for loop. But here, we don’t require any.`zip()`

is used to zip every item from every row sequentially. It basically used to club things together.

```
# zip all the items sequentially and convert it as a list
transpose_matrix_3 = lambda matrix: list(map(list, zip(*matrix)))
```

```
>>> matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]]
>>> t_matrix = transpose_matrix_3(matrix=matrix)
>>> print(t_matrix)
[[ 1 4 7 10]
[ 2 5 8 11]
[ 3 6 9 12]]
```

We shall compare each function’s performance based on how much time it takes to complete the task. For this, we will rely on the `timeit`

module.

**Note**: We will mostly use the square matrix of different sizes. However, we can tweak the dimension values.

```
import random
from timeit import timeit
import matplotlib.pyplot as plt
from matplotlib import style
style.use('seaborn')
def time_convertor(val, time_type):
if time_type == 'µs':
val = val / 1000000
elif time_type == 'ms':
val = val / 1000
else:
val = val
return val
def compare_performace():
r = [3, 5, 10, 50, 100, 500, 1000]
X = []; Y1 = []; Y2 = []; Y3 = []
for i in range(len(r)):
matrix = [[random.randint(1, 20) for j in range(r[i])] for k in range(r[i])]
x_val = "{} x {}".format(r[i], r[i])
X.append(x_val)
y_val_1 = %timeit -o transpose_matrix_1(matrix=matrix)
y_val_2 = %timeit -o transpose_matrix_2(matrix=matrix)
y_val_3 = %timeit -o transpose_matrix_3(matrix=matrix)
print('---------')
y_val_1 = str(y_val_1).split(' ', 2)
y_val_2 = str(y_val_2).split(' ', 2)
y_val_3 = str(y_val_3).split(' ', 2)
y_val_1 = time_convertor(val=float(y_val_1[0]), time_type=y_val_1[1])
y_val_2 = time_convertor(val=float(y_val_2[0]), time_type=y_val_2[1])
y_val_3 = time_convertor(val=float(y_val_3[0]), time_type=y_val_3[1])
Y1.append(y_val_1)
Y2.append(y_val_2)
Y3.append(y_val_3)
plt.figure(figsize=(15, 8))
plt.plot(X, Y1, '-o', label='Normal function')
plt.plot(X, Y2, '-o', label='List comprehension')
plt.plot(X, Y3, '-o', label='Lambda function')
plt.xlabel('Matrix dimension')
plt.ylabel('Function performance')
plt.legend()
plt.show()
return True
```

```
2.97 µs ± 81.9 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
2.6 µs ± 85.1 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
1.49 µs ± 223 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
---------
4.65 µs ± 158 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
4.35 µs ± 84.8 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
1.85 µs ± 90.4 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
---------
13.9 µs ± 582 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
9.82 µs ± 110 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
3.13 µs ± 124 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
---------
267 µs ± 10.9 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
144 µs ± 2.41 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
29.7 µs ± 863 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
---------
1.02 ms ± 27.2 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
527 µs ± 9.94 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
113 µs ± 1.89 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
---------
33.1 ms ± 815 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
20.9 ms ± 887 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
3.58 ms ± 57.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
---------
120 ms ± 1.71 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
73.1 ms ± 3.08 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
20 ms ± 1.73 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)
```

The time complexity is similar for our first and second functions — `O(M*N)`

. But when we compare, the list comprehension is always faster than a regular `for`

loop.

In the other case, where we used the lambda function, it is quite faster than the list comprehension. All I had to do is simply use the built-in functions which are always faster than user-defined functions.

]]>The word intersection in mathematics is termed as the similar (smaller) objects between two different objects. Intuitively, we can say the intersection of objects is that it belongs to all of them.

**Credits of Cover Image** - Photo by Benjamin Elliott on Unsplash

Geometrically speaking, if we have two distinct lines (assuming these lines are two objects), the intersection of these two lines would be the point where both the lines meet. Well, in the case of parallel lines, the intersection doesn’t exist. Geographically, the common junction between two or more roads can be taken as the area or region of intersection.

In Set theory, the intersection of two objects such as `A`

and `B`

is defined as the set of elements contained in both `A`

and `B`

. Symbolically, we represent the intersection as -

We also the above symbol as `AND`

. Programatically this becomes very easy to code or to find the intersection between two sets.

Using NumPy, we can find the intersection of two matrices in two different ways. In fact, there are in-built methods with which we can easily find.

Let’s first define the two matrices of the same size.

```
>>> import numpy as np
>>> import matplotlib.pyplot as plt
>>>
>>> a = np.random.randint(low=0, high=2, size=(5, 5))
>>> b = np.random.randint(low=0, high=2, size=(5, 5))
>>>
>>> print(a)
[[1 0 1 1 0]
[1 0 0 1 0]
[1 0 1 1 0]
[1 0 1 0 1]
[0 1 0 0 0]]
>>>
>>> print(b)
[[0 0 1 1 0]
[0 0 0 0 0]
[1 0 0 1 1]
[0 0 1 1 0]
[0 1 0 0 0]]
```

`&`

In mathematics, intersection (&) is often called `and`

. When we operate the same on `a`

and `b`

, using a broadcasting technique — automatically do the elementwise calculation.

```
>>> c = a & b
>>> print(c)
[[0 0 1 1 0]
[0 0 0 0 0]
[1 0 0 1 0]
[0 0 1 0 0]
[0 1 0 0 0]]
```

The result `c`

is the intersection of the matrices `a`

and `b`

.

`np.where()`

To get the gist of `np.where()`

— is a special method that acts like a ternary operator on NumPy arrays. By default, it takes 3 parameters -

`condition`

→ The condition which is either true or false at certain positions (boolean value) of an array.`x`

→ The value that gets replaced at the positions of the array where the condition is true.`y`

→ The value that gets replaced at the positions of the array where the condition is false.

Let’s have a function to get the intersection of the matrices.

```
def intersect_matrices(mat1, mat2):
if not (mat1.shape == mat2.shape):
return False
mat_intersect = np.where((mat1 == mat2), mat1, 0)
return mat_intersect
```

If we call the above function with our matrices `a`

and `b`

, we get -

```
>>> c = intersect_matrices(mat1=a, mat2=b)
>>> print(c)
[[0 0 1 1 0]
[0 0 0 0 0]
[1 0 0 1 0]
[0 0 1 0 0]
[0 1 0 0 0]]
```

If we visualize the above results, we can get -

In the plot, white patches represent the true values and black patches represent the false values. Particularly, in subplot `c`

, white patches represent the intersection value of sets `a`

, and `b`

.

**Note**: The above implementation is done for the matrices which are of binary format. When we have values other than `0`

and `1`

. It is better to go with the second procedure where we replace the values with `0`

at the positions where the condition is false.

For different values other than `0`

and `1`

, we have the plot something like below.

**End**

In the previous articles on morphological transformations, we learned the two important transformations namely erosion and dilation. In this article, we will implement the other transformations which are built on top of these two. They are -

- Opening
- Closing
- Morphological Gradient
- Top hat
- Black hat
- Boundary Extraction
- Hit — Miss Transformation

We have seen a step-by-step implementation of erosion and dilation explaining the convolution method with simple matrix operations. In all of these transformations, we rely on the binary input image, structuring element, or kernel. The structuring element needs to be a square matrix which is again a binary matrix.

**Note**: If you are not familiar with erosion and dilation, please read my previous articles.

For now, we will consider

**A**→ input image matrix**B**→ kernel matrix

We know erosion and dilation are quite opposite to each other. But Opening is just another name of erosion followed by dilation. Mathematically, we can represent it as -

$$(A \circ B) \rightarrow (1)$$

If we further break it down, we can represent it as -

$$(A \circ B) = (A \ominus B) \oplus B \rightarrow (2)$$

**Eq (1)** is represented in terms of erosion and dilation, the same can be seen in **Eq (2)**.

This transformation is helpful in removing the noise from the image.

Closing transformation is quite opposite of Opening transformation. Closing is just another name of a dilation followed by erosion. Mathematically, we can represent it as -

$$(A \bullet B) \rightarrow (3)$$

If we further break it down, we can represent it as -

$$(A \bullet B) = (A \oplus B) \ominus B \rightarrow (4)$$

**Eq (3)** is represented in terms of dilation and erosion, the same can be seen in **Eq (4)**.

This transformation is helpful in closing the holes in the foreground object of the image.

The morphological gradient can be easily obtained once we have the eroded image and dilated image. It is the difference between dilated image and an eroded image. Mathematically, we can represent it as -

$$(A \oplus B) - (A \ominus B)$$

The resultant of this transformation appears to be an outline of the foreground object.

Top Hat transformation is the difference between the input image and the opening of the image. Mathematically, we can represent it as -

$$A - (A \circ B) \rightarrow (5)$$

If we further break it down, we can represent it as -

$$A - [\ (A \ominus B) \oplus B \ ] \rightarrow (6)$$

**Eq (5)** is represented in terms of erosion and dilation, the same can be seen in **Eq (6)**.

Back Hat transformation is the difference between the closing of the input image and the input image. Mathematically, we can represent it as -

$$(A \bullet B) - A \rightarrow (7)$$

If we further break it down, we can represent it as -

$$[\ (A \oplus B) \ominus B \ ] - A \rightarrow (8)$$

**Eq (7)** is represented in terms of dilation and erosion, the same can be seen in **Eq (8)**.

Boundary extraction is one of the applications of morphological transformations. In simple words, it is the difference between the input image and the eroded image. Mathematically, we can represent it as -

$$A - (A \ominus B)$$

The boundary of the foreground object is represented in white color and the rest be in black color. If we do the reverse process, i.e., the difference between the input image and dilated image — the boundary will get in black color, and the rest will be in white color.

The packages that we mainly use are:

- NumPy
- Matplotlib
- OpenCV — only used for reading the image (in this article).

`import`

the Packages```
import numpy as np
import cv2
import json
from matplotlib import pyplot as plt
```

For the entire implementation, I will be using the finger-print image, you can use the same for the practice.

```
class MorphologicalTransformations(object):
def __init__(self, image_file, level):
self.level = 3 if level < 3 else level
self.image_file = image_file
self.MAX_PIXEL = 255
self.MIN_PIXEL = 0
self.MID_PIXEL = self.MAX_PIXEL // 2
self.kernel = np.full(shape=(level, level), fill_value=255)
def read_this(self):
image_src = cv2.imread(self.image_file, 0)
return image_src
def convert_binary(self, image_src, thresh_val):
color_1 = self.MAX_PIXEL
color_2 = self.MIN_PIXEL
initial_conv = np.where((image_src <= thresh_val), image_src, color_1)
final_conv = np.where((initial_conv > thresh_val), initial_conv, color_2)
return final_conv
def binarize_this(self):
image_src = self.read_this()
image_b = self.convert_binary(image_src=image_src, thresh_val=self.MID_PIXEL)
return image_b
def get_flat_submatrices(self, image_src, h_reduce, w_reduce):
image_shape = image_src.shape
flat_submats = np.array([
image_src[i:(i + self.level), j:(j + self.level)]
for i in range(image_shape[0] - h_reduce) for j in range(image_shape[1] - w_reduce)
])
return flat_submats
def erode_image(self, image_src, with_plot=False):
orig_shape = image_src.shape
pad_width = self.level - 2
image_pad = np.pad(array=image_src, pad_width=pad_width, mode='constant')
pimg_shape = image_pad.shape
h_reduce, w_reduce = (pimg_shape[0] - orig_shape[0]), (pimg_shape[1] - orig_shape[1])
flat_submats = self.get_flat_submatrices(
image_src=image_pad, h_reduce=h_reduce, w_reduce=w_reduce
)
image_eroded = np.array([255 if (i == self.kernel).all() else 0 for i in flat_submats])
image_eroded = image_eroded.reshape(orig_shape)
if with_plot:
self.plot_it(orig_matrix=image_src, trans_matrix=image_eroded, head_text='Eroded - {}'.format(self.level))
return None
return image_eroded
def dilate_image(self, image_src, with_plot=False):
orig_shape = image_src.shape
pad_width = self.level - 2
image_pad = np.pad(array=image_src, pad_width=pad_width, mode='constant')
pimg_shape = image_pad.shape
h_reduce, w_reduce = (pimg_shape[0] - orig_shape[0]), (pimg_shape[1] - orig_shape[1])
flat_submats = self.get_flat_submatrices(
image_src=image_pad, h_reduce=h_reduce, w_reduce=w_reduce
)
image_dilated = np.array([255 if (i == self.kernel).any() else 0 for i in flat_submats])
image_dilated = image_dilated.reshape(orig_shape)
if with_plot:
self.plot_it(orig_matrix=image_src, trans_matrix=image_dilated, head_text='Dilated - {}'.format(self.level))
return None
return image_dilated
def open_image(self, image_src, with_plot=False):
image_eroded = self.erode_image(image_src=image_src)
image_opening = self.dilate_image(image_src=image_eroded)
if with_plot:
self.plot_it(orig_matrix=image_src, trans_matrix=image_opening, head_text='Opening - {}'.format(self.level))
return None
return image_opening
def close_image(self, image_src, with_plot=False):
image_dilated = self.dilate_image(image_src=image_src)
image_closing = self.erode_image(image_src=image_dilated)
if with_plot:
self.plot_it(orig_matrix=image_src, trans_matrix=image_closing, head_text='Closing - {}'.format(self.level))
return None
return image_closing
def morph_gradient(self, image_src, with_plot=False):
image_dilated = self.dilate_image(image_src=image_src)
image_eroded = self.erode_image(image_src=image_src)
image_grad = image_dilated - image_eroded
if with_plot:
self.plot_it(orig_matrix=image_src, trans_matrix=image_grad, head_text='Gradient Morph - {}'.format(self.level))
return None
return image_grad
def extract_boundary(self, image_src, with_plot=False):
image_eroded = self.erode_image(image_src=image_src)
ext_bound = image_src - image_eroded
if with_plot:
self.plot_it(orig_matrix=image_src, trans_matrix=ext_bound, head_text='Boundary - {}'.format(self.level))
return None
return ext_bound
def get_tophat(self, image_src, with_plot=False):
image_opening = self.open_image(image_src=image_src)
image_tophat = image_src - image_opening
if with_plot:
self.plot_it(orig_matrix=image_src, trans_matrix=image_tophat, head_text='Top Hat - {}'.format(self.level))
return None
return image_tophat
def get_blackhat(self, image_src, with_plot=False):
image_closing = self.close_image(image_src=image_src)
image_blackhat = image_closing - image_src
if with_plot:
self.plot_it(orig_matrix=image_src, trans_matrix=image_blackhat, head_text='Black Hat - {}'.format(self.level))
return None
return image_blackhat
def plot_it(self, orig_matrix, trans_matrix, head_text):
fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(10, 20))
cmap_val = 'gray'
ax1.axis("off")
ax1.title.set_text('Original')
ax2.axis("off")
ax2.title.set_text(head_text)
ax1.imshow(orig_matrix, cmap=cmap_val)
ax2.imshow(trans_matrix, cmap=cmap_val)
plt.show()
return True
```

```
morph = MorphologicalTransformations(
image_file='Finger-arch.jpg',
level=3
)
image_src = morph.binarize_this()
```

In the above code, we have made an object called morph, and using this object we are converting the input image into a binary image.

```
morph.erode_image(image_src=image_src, with_plot=True)
```

```
morph.dilate_image(image_src=image_src, with_plot=True)
```

```
morph.open_image(image_src=image_src, with_plot=True)
```

```
morph.close_image(image_src=image_src, with_plot=True)
```

```
morph.morph_gradient(image_src=image_src, with_plot=True)
```

```
morph.get_tophat(image_src=image_src, with_plot=True)
```

```
morph.get_blackhat(image_src=image_src, with_plot=True)
```

```
morph.extract_boundary(image_src=image_src, with_plot=True)
```

These are the results for all the transformations. We obtained these with the help of erosion and dilation.

**Note**: All of the code is written from scratch using the NumPy library.

In this article, we have almost covered the important morphological transformations. Although, there are some advanced transformations like **Hit — Miss Transformation**, which I didn’t cover in this but definitely next time.

When we have speed and compatibility constraints, I would recommend using the official library methods. This was just practiced to understand the inner working and mathematics behind each algorithm.

Here I take leave. If you have liked it consider visiting this page to read more on Image Processing. And make sure to buy coffee for me from here or just hit the button below.

**End**

**Credits of Cover Image** - Photo by Jason Leung on Unsplash

**Note**: We are not expanding or increasing the image size. We are increasing the pixel strength and the size remains the same.

Mathematically, we can represent this operation in the following way -

$$A \bigoplus B$$

where -

`A`

→ Input image that is binarized`B`

→ structuring element or kernel

The resultant of the above formula is the dilated image. The technique that we apply here is the 2D convolution for the input image with respect to the kernel. The kernel is basically a **square matrix**.

A typical binary image consists of only **1**’s (255) and **0**’s. The kernel can either be a subset of the input image or not which is again in the binary form. To think of this mathematically in terms of matrices — we can have:

**A** — the matrix of input image and **B** — the matrix of structuring element. The following conditions are to be applied for convolution -

- Know the size of
**B**to pad**A**by 0's. The padding width is (`kernel_size`

- 2). - Position the center element of
**B**to every element of the original input image (matrix) iteratively. - Extract the submatrix that is exactly equal to the size of the
**B**. - Check if any (at least one) element from the submatrix is equal to the element in
**B**considering the index location.- If yes, replace the element of
**A**to 1 or 255. - 0, otherwise.

- If yes, replace the element of
- Continue this process for all the elements of
**A**.

**Note**: We shall start from the first element of **A** till the last element of **A**. The GIF can be seen below to understand the convolution technique more clearly.

Imagine the matrix **A** is a binary image -

similarly, the matrix **B** (kernel) -

Since the kernel size is `(3 x 3)`

, we need to pad **A** by width (3-2) which is 1.

To extract the submatrices we can position the center element of **B** with every element of **A** and thus breaking it down, we can obtain a giant matrix where every element is a `(3 x 3)`

submatrix of **A**.

From these submatrices, we can easily map the kernel **B** and obtain a new value for every element of **A**.

The resultant image considering the input image **A** -

Notice the difference, we have expanded the pixels of the original image with the level of 5. The black pixels did get reduced.

Let’s code this totally from scratch using NumPy.

The packages that we mainly use are:

- NumPy
- Matplotlib
- OpenCV — only used for reading the image (in this article).

```
import numpy as np
import cv2
import json
from matplotlib import pyplot as plt
```

Since we do the morphological transformations on binary images, we shall make sure whatever image we read is binarized. Therefore, we have the following functions.

```
def read_this(image_file):
image_src = cv2.imread(image_file, 0)
return image_src
def convert_binary(image_src, thresh_val):
color_1 = 255
color_2 = 0
initial_conv = np.where((image_src <= thresh_val), image_src, color_1)
final_conv = np.where((initial_conv > thresh_val), initial_conv, color_2)
return final_conv
def binarize_this(image_file, thresh_val=127):
image_src = read_this(image_file=image_file)
image_b = convert_binary(image_src=image_src, thresh_val=thresh_val)
return image_b
```

In the dilation function, the main parameters that are passed are:

**image_file**→ The input image for which the dilation operation has to be performed.**dilation_level**→ In how many levels do we have to dilate the image. By default, the value of this will be 3.**with_plot**→ Simply to visualize the result showing the comparison between the original image and the dilated image.

In the function —

- We obtain the kernel matrix based on the
`dilation_level`

. - We pad the input image matrix by
`(kernel_size - dilation_level)`

. - We obtain the submatrices and replace the new values accordingly as shown in the GIF.
- Finally, we reshape the newly obtained array into the original size of the input image and plot the same.

```
def dilate_this(image_file, dilation_level=3, with_plot=False):
# setting the dilation_level
dilation_level = 3 if dilation_level < 3 else dilation_level
# obtain the kernel by the shape of (dilation_level, dilation_level)
structuring_kernel = np.full(shape=(dilation_level, dilation_level), fill_value=255)
image_src = binarize_this(image_file=image_file)
orig_shape = image_src.shape
pad_width = dilation_level - 2
# pad the image with pad_width
image_pad = np.pad(array=image_src, pad_width=pad_width, mode='constant')
pimg_shape = image_pad.shape
h_reduce, w_reduce = (pimg_shape[0] - orig_shape[0]), (pimg_shape[1] - orig_shape[1])
# obtain the submatrices according to the size of the kernel
flat_submatrices = np.array([
image_pad[i:(i + dilation_level), j:(j + dilation_level)]
for i in range(pimg_shape[0] - h_reduce) for j in range(pimg_shape[1] - w_reduce)
])
# replace the values either 255 or 0 by dilation condition
image_dilate = np.array([255 if (i == structuring_kernel).any() else 0 for i in flat_submatrices])
# obtain new matrix whose shape is equal to the original image size
image_dilate = image_dilate.reshape(orig_shape)
# plotting
if with_plot:
cmap_val = 'gray'
fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(10, 20))
ax1.axis("off")
ax1.title.set_text('Original')
ax2.axis("off")
ax2.title.set_text("Dilated - {}".format(dilation_level))
ax1.imshow(image_src, cmap=cmap_val)
ax2.imshow(image_dilate, cmap=cmap_val)
plt.show()
return True
return image_dilate
```

Now that the dilation function is ready, all that is left is testing. We will use a different image for testing.

For dilation level 3 -

```
dilate_this(image_file='wish.jpg', dilation_level=3, with_plot=True)
```

For dilation level 5 -

```
dilate_this(image_file='wish.jpg', dilation_level=3, with_plot=True)
```

In all the above results, we can notice the increase in the pixels. And this the example code developed totally from scratch. We can also rely on the `cv2.dilate()`

method which is very faster.

- Morphological Transformations official documentation.

Here I take leave. If you have liked it consider visiting this page to read more on Image Processing. And make sure to buy coffee for me from here or just hit the button below.

]]>`mathematical morphology`

for the analysis and processing of geometrical structures.
To get a general idea of what erosion has to do with images, we can think of this as an operation in which it tries to reduce the shape that is contained in the input image. It is just like the erosion of soil but just that this operation erodes the boundaries of the foreground object.

**Credits of Cover Image** - Photo by Laura Colquitt on Unsplash

To represent this operation mathematically -

$$A \circleddash B$$

where -

`A`

→ Input Image`B`

→ Structuring element or kernel

The resultant of the above formula gives the eroded image. The structuring element is basically a kernel where the image matrix is operated as a `2D`

convolution.

**Note** - This blog post covers the erosion process done on binary images. Also, it is often preferred to use binary images for morphological transformation.

As discussed, we only use the binary images that consist of pixels either `0`

or `1`

(`0`

or `255`

to be more precise). The structuring element or kernel is either a subset of the image matrix or not which also, is a binary representation that is mostly a `square matrix`

.

Let us consider `A`

as the image matrix and `B`

as the kernel. We have conditions as follows:

We have to position the center element of

`B`

to the element iteratively taken in the image`A`

.We consider the submatrix of

`A`

to be the size`B`

and check if the submatrix is exactly equivalent to`B`

.If yes, replace the pixel value to be

`1`

or`255`

otherwise`0`

.

We need to do this for all the elements of `A`

with `B`

.

Imagine the image matrix `A`

as -

and structuring element or kernel `B`

as -

The binary image of the matrix `A`

would be something like below.

For easy calculation, we shall pad the image by a `pad_width`

equal to `(kernel size - 2)`

with which the submatrix can be selected easily. The `GIF`

can be seen below to visually know the inner working of the convolution.

Now that we know what to do, let's code the same using the library as well as from scratch.

The packages that we mainly use are:

- NumPy
- Matplotlib
- OpenCV → It is only used for reading the image (in this article).

`import`

the Packages```
import numpy as np
import cv2
import json
from matplotlib import pyplot as plt
```

Since we do the morphological transformations on binary images, we shall make sure whatever image we read is binarized. Therefore, we have the following function.

```
def read_this(image_file):
image_src = cv2.imread(image_file, 0)
return image_src
def convert_binary(image_src, thresh_val):
color_1 = 255
color_2 = 0
initial_conv = np.where((image_src <= thresh_val), image_src, color_1)
final_conv = np.where((initial_conv > thresh_val), initial_conv, color_2)
return final_conv
def binarize_this(image_file, thresh_val=127):
image_src = read_this(image_file=image_file)
image_b = convert_binary(image_src=image_src, thresh_val=thresh_val)
return image_b
```

**Note** - By default we are reading the image in grayscale mode.

For this, we will be using a different image, and for the implementation, we will use the method `erode()`

available in the module `cv2`

. The parameters are as follows:

**image_file**→ The image that we want to apply the transformation.**level**→ Basically the`erosion`

level with which the`structuring element`

or`kernel`

's size is decided.**with_plot**→ To obtain the results of both the original image and the transformed image.

```
def erode_lib(image_file, level=3, with_plot=True):
level = 3 if level < 3 else level
image_src = binarize_this(image_file=image_file)
# library method
image_eroded = cv2.erode(src=image_src, kernel=np.ones((level, level)), iterations=1)
if with_plot:
cmap_val = 'gray'
fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(10, 20))
ax1.axis("off")
ax1.title.set_text('Original')
ax2.axis("off")
ax2.title.set_text("Eroded - {}".format(level))
ax1.imshow(image_src, cmap=cmap_val)
ax2.imshow(image_eroded, cmap=cmap_val)
plt.show()
return True
return image_eroded
```

Let's test the above function -

```
erode_lib(image_file='wish.jpg', level=3, with_plot=True)
```

Clearly, we can see the some of the pixels got reduced showing the pixel erosion.

As explained earlier, we need to carefully choose the `pad_width`

depending upon the `erosion_level`

. We normally take `(kernel size - 2)`

or `(erosion_level - 2)`

and here, the `kernel`

is always `square matrix`

.

After this, we shall also take the submatrices to position the center element of the `kernel`

with each element of the image matrix iteratively. We make sure that the submatrix size is equal to kernel size. Hence we first pad the matrix with `pad_width`

.

Let's code the erosion function from scratch.

```
def erode_this(image_file, erosion_level=3, with_plot=False):
erosion_level = 3 if erosion_level < 3 else erosion_level
structuring_kernel = np.full(shape=(erosion_level, erosion_level), fill_value=255)
image_src = binarize_this(image_file=image_file)
orig_shape = image_src.shape
pad_width = erosion_level - 2
# pad the matrix with `pad_width`
image_pad = np.pad(array=image_src, pad_width=pad_width, mode='constant')
pimg_shape = image_pad.shape
h_reduce, w_reduce = (pimg_shape[0] - orig_shape[0]), (pimg_shape[1] - orig_shape[1])
# sub matrices of kernel size
flat_submatrices = np.array([
image_pad[i:(i + erosion_level), j:(j + erosion_level)]
for i in range(pimg_shape[0] - h_reduce) for j in range(pimg_shape[1] - w_reduce)
])
# condition to replace the values - if the kernel equal to submatrix then 255 else 0
image_erode = np.array([255 if (i == structuring_kernel).all() else 0 for i in flat_submatrices])
image_erode = image_erode.reshape(orig_shape)
if with_plot:
cmap_val = 'gray'
fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(10, 20))
ax1.axis("off")
ax1.title.set_text('Original')
ax2.axis("off")
ax2.title.set_text("Eroded - {}".format(erosion_level))
ax1.imshow(image_src, cmap=cmap_val)
ax2.imshow(image_erode, cmap=cmap_val)
plt.show()
return True
return image_erode
```

Let's test the above function -

```
erode_this(image_file='wish.jpg', erosion_level=3, with_plot=True)
```

Clearly, we can the reduction in the pixel values showcasing the pixel erosion. This, we implemented by convolution `2D`

technique totally from scratch. When compared to the speed of the algorithm, it is a bit slow to that of the library method.

- Morphological Transformations official documentation.

Here I take leave. If you have liked it consider visiting this page to read more on Image Processing. And make sure to buy coffee for me from here or just hit the button below.

]]>**Credits of Cover Image** - Photo by James Lewis on Unsplash

In this blog article, we will try to shift the image as we shift the point in the coordinate axis completely using NumPy operations. The image is always considered as a `2D`

plane, hence we shall also consider a `2D`

coordinate axis having `X`

as the horizontal axis and `Y`

as the vertical axis. The coordinate axis is divided into `4`

quadrants namely -

**Q1**→ Quadrant where both`X`

and`Y`

are positive.**Q2**→ Quadrant where`X`

is negative and`Y`

is positive.**Q3**→ Quadrant where both`X`

and`Y`

are negative.**Q4**→ Quadrant where`X`

is positive and`Y`

is negative.

We assume that our original image to be at origin i.e., (0, 0). To visualize this, we can imagine something like the below -

Now, let's say we want to shift the image at coordinates (3, 4). Basically, the origin of the image has to be shifted from (0, 0) to (3, 4) something like the below -

Likewise, based on the coordinate points, we need to shift the image. Let's try to understand and implement from scratch using the module NumPy starting from a `2D`

matrix because images are just large matrices.

The packages that we mainly use are:

- NumPy
- Matplotlib
- OpenCV → It is only used for reading the image (in this article).

`import`

the Packages```
import numpy as np
import cv2
import json
from matplotlib import pyplot as plt
```

`2D`

MatrixWe will be creating a `5 X 5`

matrix having random numbers.

```
>>> import random
>>>
>>> mat = [[random.randint(5, 100) for i in range(5)] for j in range(5)]
>>> mat = np.matrix(mat)
>>> print(mat)
[[ 46 13 68 54 12]
[ 7 68 32 46 26]
[ 46 43 58 27 100]
[ 64 59 76 100 41]
[ 35 62 56 44 7]]
>>>
```

For instance, let us assume that we are shifting the image in `Q1`

, for sure the image has to move left side towards the `X`

axis and topside towards the `Y`

axis. In that case, the size of the image increases. Basically, we are padding the image left side as per the `x`

coordinate depth and the bottom side as per the `y`

coordinate depth. The same has to be replicated when we are shifting the image in the rest of the quadrants `Q2`

, `Q3`

, and `Q4`

.

In order to do so, we need to create a padding function using NumPy methods.

```
def pad_vector(vector, how, depth, constant_value=0):
vect_shape = vector.shape[:2]
if (how == 'upper') or (how == 'top'):
pp = np.full(shape=(depth, vect_shape[1]), fill_value=constant_value)
pv = np.vstack(tup=(pp, vector))
elif (how == 'lower') or (how == 'bottom'):
pp = np.full(shape=(depth, vect_shape[1]), fill_value=constant_value)
pv = np.vstack(tup=(vector, pp))
elif (how == 'left'):
pp = np.full(shape=(vect_shape[0], depth), fill_value=constant_value)
pv = np.hstack(tup=(pp, vector))
elif (how == 'right'):
pp = np.full(shape=(vect_shape[0], depth), fill_value=constant_value)
pv = np.hstack(tup=(vector, pp))
else:
return vector
return pv
```

The above function is used to pad the image. The arguments used are as follows:

**vector**→ a matrix in which the padding is done.**how**→ this takes four values that decide the quadrants where the image needs to be shifted.- lower or bottom
- upper or top
- right
- left

**depth**→ the depth of the padding.**constant_value**→ signifies`black`

color and`0`

is the default value.

**Note** - For the padding-right and padding-left, we use the method `hstack()`

. In the same way, for the padding-top and padding-bottom, we use the method `vstack()`

. These two are the NumPy methods.

**hstack()**→ horizontal stack**vstack()**→ vertical stack

First, we create a padding matrix whose values are zero. And based on the direction of the shift we make use of these methods.

Let's test the above function.

```
>>> pmat = pad_vector(vector=mat, how='left', depth=3)
>>> print(pmat)
[[ 0 0 0 46 13 68 54 12]
[ 0 0 0 7 68 32 46 26]
[ 0 0 0 46 43 58 27 100]
[ 0 0 0 64 59 76 100 41]
[ 0 0 0 35 62 56 44 7]]
>>>
```

We can clearly see that the function padded the matrix left side with the depth level `3`

. If we were to plot the same (convert the padded matrix into an image), we get -

```
>>> plt.axis("off")
>>> plt.imshow(pmat, cmap='gray')
>>> plt.show()
```

Whereas the original image is -

With this, we can conclude the image is shifted to the left side towards the `X`

axis with the `x`

coordinate 3. The same technique is applied to the real image. Let's try to replicate the same for the image.

We shall have a function to read the image both in grayscale and RGB format.

```
def read_this(image_file, gray_scale=False):
image_src = cv2.imread(image_file)
if gray_scale:
image_src = cv2.cvtColor(image_src, cv2.COLOR_BGR2GRAY)
else:
image_src = cv2.cvtColor(image_src, cv2.COLOR_BGR2RGB)
return image_src
```

Let's make another function called `shifter()`

which actually shifts the image along the `Y`

axis irrespective of the quadrant.

```
def shifter(vect, y, y_):
if (y > 0):
image_trans = pad_vector(vector=vect, how='lower', depth=y_)
elif (y < 0):
image_trans = pad_vector(vector=vect, how='upper', depth=y_)
else:
image_trans = vect
return image_trans
```

Now that we have the `shifter()`

function, we will need to use this in another function that can shift anywhere in the coordinate axis. Here, we consider `X`

and `Y`

axes.

```
def shift_image(image_src, at):
x, y = at
x_, y_ = abs(x), abs(y)
if (x > 0):
left_pad = pad_vector(vector=image_src, how='left', depth=x_)
image_trans = shifter(vect=left_pad, y=y, y_=y_)
elif (x < 0):
right_pad = pad_vector(vector=image_src, how='right', depth=x_)
image_trans = shifter(vect=right_pad, y=y, y_=y_)
else:
image_trans = shifter(vect=image_src, y=y, y_=y_)
return image_trans
```

When

`x`

and`y`

coordinates are greater than 0, pad the image left side and bottom side.When

`x`

is greater than 0 and`y`

is less than 0, pad the image left side and topside.When

`x`

is less than 0 and`y`

is greater than 0, pad the image right side and bottom side.When

`x`

and`y`

coordinates are less than 0, pad the image right side and topside.When

`x`

and`y`

coordinates exactly equal to 0, do not disturb the image.

There is one problem yet to translate or shift the image. An image can be of two types - grayscale and colored. For grayscale, there won't be any problem. But for the colored image, we need to separate RGB pixels, apply the shift function, and then finally combine the pixels. Hence the below function.

```
def translate_this(image_file, at, with_plot=False, gray_scale=False):
if len(at) != 2: return False
image_src = read_this(image_file=image_file, gray_scale=gray_scale)
if not gray_scale:
r_image, g_image, b_image = image_src[:, :, 0], image_src[:, :, 1], image_src[:, :, 2]
r_trans = shift_image(image_src=r_image, at=at)
g_trans = shift_image(image_src=g_image, at=at)
b_trans = shift_image(image_src=b_image, at=at)
image_trans = np.dstack(tup=(r_trans, g_trans, b_trans))
else:
image_trans = shift_image(image_src=image_src, at=at)
if with_plot:
cmap_val = None if not gray_scale else 'gray'
fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(10, 20))
ax1.axis("off")
ax1.title.set_text('Original')
ax2.axis("off")
ax2.title.set_text("Translated")
ax1.imshow(image_src, cmap=cmap_val)
ax2.imshow(image_trans, cmap=cmap_val)
return True
return image_trans
```

Now that it all set, let's test the above function:

**For color image**

```
translate_this(
image_file='lena_original.png',
at=(60, 60),
with_plot=True
)
```

Clearly, the image is shifted to the origin (60, 60) i.e., in the first quadrant (Q1).

**For grayscale image**

```
translate_this(
image_file='lena_original.png',
at=(-60, -60),
with_plot=True,
gray_scale=True
)
```

Clearly, the image is shifted to the origin (-60, -60) i.e., in the third quadrant (Q3).

Well, that's it for this article. From this, we tried to understand how the image shifting process is done.

**Credits of Cover Image** - Photo by engin akyurt on Unsplash

When we are solarizing an image, we mainly consider a threshold pixel value in order to reverse (invert) the image partially or completely. To know more about inverting an image, you can refer to my article here.

We will rely on the `PIL`

library to solarize the image. We will also code the same without using the `PIL`

module and by just using NumPy for the matrix operations.

The packages that we mainly use are:

- NumPy
- Matplotlib
- OpenCV
- PIL

`import`

the Packages```
import numpy as np
import cv2
import json
from PIL import Image
from PIL import ImageOps as ipo
from matplotlib import pyplot as plt
```

We use the method `solarize()`

available in the module `ImageOps`

- part of the main module `PIL`

. The method takes the following arguments -

`image`

→ image file that needs to solarized.`threshold`

→ threshold value to either partially or completely reverse (invert) the image.

Now that we have all the requirements, let's write the function that solarizes the image.

```
def solarize_lib(image_file, thresh_val, gray_scale=False):
image_src = Image.open(image_file)
image_src = image_src.convert('L') if gray_scale else image_src
image_sol = ipo.solarize(image_src, threshold=thresh_val)
cmap_val = None if not gray_scale else 'gray'
fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(10, 20))
ax1.axis("off")
ax1.title.set_text('Original')
ax2.axis("off")
ax2.title.set_text("Solarized")
ax1.imshow(image_src, cmap=cmap_val)
ax2.imshow(image_sol, cmap=cmap_val)
return True
```

The above function solarizes the images that are read both in colored mode or grayscale mode. Let's test the same.

```
solarize_lib(image_file='scenary.jpg', thresh_val=130)
```

**For RGB image**

**For grayscale image**

Above are the results of the images that are solarized.

For coding from scratch, we are relying on the NumPy module since matrix operations can be achieved with so much ease. We will need to validate for both colored images as well as grayscale images.

```
def read_this(image_file, gray_scale=False):
image_src = cv2.imread(image_file)
if gray_scale:
image_src = cv2.cvtColor(image_src, cv2.COLOR_BGR2GRAY)
else:
image_src = cv2.cvtColor(image_src, cv2.COLOR_BGR2RGB)
return image_src
```

The above function reads the image either in grayscale or RGB and returns the image matrix.

As above we will have a `thresh_val`

argument based on which the partial or complete reversing (inverting) be done.

```
def solarize_this(image_file, thresh_val, with_plot=False, gray_scale=False):
image_src = read_this(image_file=image_file, gray_scale=gray_scale)
if not gray_scale:
r_image, g_image, b_image = image_src[:, :, 0], image_src[:, :, 1], image_src[:, :, 2]
## inverting the colored image (partially)
r_sol = np.where((r_image < thresh_val), r_image, ~r_image)
g_sol = np.where((g_image < thresh_val), g_image, ~g_image)
b_sol = np.where((b_image < thresh_val), b_image, ~b_image)
image_sol = np.dstack(tup=(r_sol, g_sol, b_sol))
else:
## inverting the grayscale image (partially)
image_sol = np.where((image_src < thresh_val), image_src, ~image_src)
if with_plot:
cmap_val = None if not gray_scale else 'gray'
fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(10, 20))
ax1.axis("off")
ax1.title.set_text('Original')
ax2.axis("off")
ax2.title.set_text("Solarized")
ax1.imshow(image_src, cmap=cmap_val)
ax2.imshow(image_sol, cmap=cmap_val)
return True
return image_sol
```

We are using the `where()`

method of NumPy to find out those pixels which are less than `thresh_val`

and inverting the same which are greater. Let's test the function.

**For RGB image**

```
solarize_this(
image_file='scenary.jpg',
thresh_val=130,
with_plot=True
)
```

**For grayscale image**

```
solarize_this(
image_file='lena_original.png',
thresh_val=120,
with_plot=True,
gray_scale=True
)
```

`PIL`

is a great library and so is NumPy. `PIL`

is easy to use and the results can be obtained with less code. It is great for researchers and beginners. Implementing the same from scratch is what I have gained.

Here I take leave. If you have liked it consider visiting this page to read more on Image Processing. And make sure to buy coffee for me from here.

]]>`snow`

and `ball`

resulting in `snowball`

. It need not be from the end.
**Credits of Cover Image** - Photo by Bia Andrade on Unsplash

Concatenation of images is simply concatenating multiple matrices. But in Python, we need to understand that concatenation of lists is different from the concatenation of NumPy arrays.

For concatenating two or multiple lists, we can use the `+`

operation that will combine two lists or multiple lists into one big list. Below is an example that can be understood easily.

```
>>> l1 = [1, 2, 3, 4]
>>> l2 = [4, 5, 6, 7]
>>>
>>> # concatenation
>>> l3 = l1 + l2
>>> l3
[1, 2, 3, 4, 4, 5, 6, 7]
>>>
>>> # or
>>> l3 = l1.extend(l2)
>>> l3
[1, 2, 3, 4, 4, 5, 6, 7]
```

The scenario would be completely different for NumPy arrays if we were to perform the same `+`

operation as we did for lists (above). NumPy automatically performs linear addition (broadcasting technique) considering the shape of each array is similar. Below is an example that can be understood easily.

**For 1D arrays**

```
>>> import numpy as np
>>>
>>> l1 = np.array([1, 2, 3, 4])
>>> l2 = np.array([4, 5, 6, 7])
>>>
>>> # using `+` operation
>>> l3 = l1 + l2
>>> print(l3)
[ 5 7 9 11]
>>>
```

**For 2D arrays**

```
>>> import numpy as np
>>>
>>> l1 = np.array([[1, 2], [3, 4]])
>>> l2 = np.array([[4, 5], [6, 7]])
>>>
>>> # using `+` operation
>>> l3 = l1 + l2
>>> print(l3)
[[ 5 7]
[ 9 11]]
>>>
```

For both cases, the `+`

operation does not work. Rather, we can use the `concatenate()`

method of NumPy's module to combine the arrays. The only criteria here is that we should have all the arrays in the same dimension.

With the help of this method, we can either concatenate `horizontally`

or `vertically`

. Below is an example that can be understood easily.

```
>>> import numpy as np
>>>
>>> l1 = np.array([[1, 2], [3, 4]])
>>> l2 = np.array([[4, 5], [6, 7]])
>>>
>>> # concatenation
>>> # axis = 0 → concatenates vertically
>>> l3 = np.concatenate((l1, l2), axis=0)
>>> print(l3)
[[1 2]
[3 4]
[4 5]
[6 7]]
>>>
>>> # axis = 1 → concatenates horizontally
>>> l4 = np.concatenate((l1, l2), axis=1)
>>> print(l4)
[[1 2 4 5]
[3 4 6 7]]
>>>
```

From the above example, we are clear that concatenation can be easily done and the same thing is performed on the images to combine two or multiple (different) images.

We are basically replicating the methods `hconcat()`

(horizontal concatenation) and `vconcat()`

(vertical concatenation) of the module `cv2`

in NumPy.

The packages that we mainly use are:

- NumPy
- Matplotlib
- OpenCV → It is only used for reading the image (in this article).

`import`

the Packages```
import numpy as np
import cv2
import json
from matplotlib import pyplot as plt
```

```
def read_this(image_file, gray_scale=False):
image_src = cv2.imread(image_file)
if gray_scale:
image_src = cv2.cvtColor(image_src, cv2.COLOR_BGR2GRAY)
else:
image_src = cv2.cvtColor(image_src, cv2.COLOR_BGR2RGB)
return image_src
```

The above function reads the image either in grayscale or RGB and returns the image matrix.

While concatenating two different images, there can be a difference in the dimensions of the image matrices. We need to make sure that the dimensions are the same. There is a need to resize the image matrix (refer to this article for understanding image resizing). And, hence the below function.

```
def resize_image(image_matrix, nh, nw):
image_size = image_matrix.shape
oh = image_size[0]
ow = image_size[1]
re_image_matrix = np.array([
np.array([image_matrix[(oh*h // nh)][(ow*w // nw)] for w in range(nw)])
for h in range(nh)
])
return re_image_matrix
```

For this, we will need to take two different images.

**Image 1**

**Image 2**

**Note** - The dimensions of the above images are different.

```
# grayscale mode is false by default
image1 = read_this(image_file='lena_original.png')
image2 = read_this(image_file='pinktree.jpg')
```

The function takes totally three arguments -

`image_set`

→ a list of image matrices.`how`

→ concatenating on what basis,`vertically`

or`horizontally`

.- default arg
`with_plot`

→ to plot the results or not.

```
def concat_lib(image_set, how, with_plot=False):
# dimension of each matrix in image_set
shape_vals = [imat.shape for imat in image_set]
# length of dimension of each matrix in image_set
shape_lens = [len(ishp) for ishp in shape_vals]
# if all the images in image_set are read in same mode
channel_flag = True if len(set(shape_lens)) == 1 else False
if channel_flag:
ideal_shape = max(shape_vals)
images_resized = [
# function call to resize the image
resize_image(image_matrix=imat, nh=ideal_shape[0], nw=ideal_shape[1])
if imat.shape != ideal_shape else imat for imat in image_set
]
else:
return False
# cv2 library code to concatenate the image matrices
# we use methods like
# - vconcat() → vertical concat
# - hconcat() → horizontal concat
if (how == 0) or (how == 'vertical'):
concats = cv2.vconcat(images_resized)
elif (how == 1) or (how == 'horizontal'):
concats = cv2.hconcat(images_resized)
else:
concats = cv2.hconcat(images_resized)
if with_plot:
cmap_val = None if len(concats.shape) == 3 else 'gray'
plt.figure(figsize=(10, 6))
plt.axis("off")
plt.imshow(concats, cmap=cmap_val)
return True
return concats
```

In the case of image matrices with different dimensions, they are handled carefully by taking the ideal dimension whose matrix dimension is maximum. We are using the methods `hconcat()`

and `vconcat()`

based on the value of `how`

that is passed.

Let's test the above function -

```
concat_lib(
image_set=[image1, image2, image1, image1, image2],
how='horizontal',
with_plot=True
)
```

`image2`

is automatically resized to the size of `image1`

and the same is converted into a giant matrix.

The function takes totally three arguments -

`image_set`

→ a list of image matrices.`how`

→ concatenating on what basis,`vertically`

or`horizontally`

.- default arg
`with_plot`

→ to plot the results or not.

```
def concat_images(image_set, how, with_plot=False):
# dimension of each matrix in image_set
shape_vals = [imat.shape for imat in image_set]
# length of dimension of each matrix in image_set
shape_lens = [len(ishp) for ishp in shape_vals]
# if all the images in image_set are read in same mode
channel_flag = True if len(set(shape_lens)) == 1 else False
if channel_flag:
ideal_shape = max(shape_vals)
images_resized = [
# function call to resize the image
resize_image(image_matrix=imat, nh=ideal_shape[0], nw=ideal_shape[1])
if imat.shape != ideal_shape else imat for imat in image_set
]
else:
return False
images_resized = tuple(images_resized)
if (how == 'vertical') or (how == 0):
axis_val = 0
elif (how == 'horizontal') or (how == 1):
axis_val = 1
else:
axis_val = 1
# numpy code to concatenate the image matrices
# concatenation is done based on axis value
concats = np.concatenate(images_resized, axis=axis_val)
if with_plot:
cmap_val = None if len(concats.shape) == 3 else 'gray'
plt.figure(figsize=(10, 6))
plt.axis("off")
plt.imshow(concats, cmap=cmap_val)
return True
return concats
```

In the case of image matrices with different dimensions, they are handled carefully by taking the ideal dimension whose matrix dimension is maximum. We are using the method `concatenate()`

based on the value of `how`

that is passed.

Let's test the above function -

```
concat_images(
image_set=[image1, image2, image1, image1, image2],
how='vertical',
with_plot=True
)
```

`image2`

is automatically resized to the size of `image1`

and the same is converted into a giant matrix.

We can also play with the above functions to explore more.

```
gimg1 = concat_images(image_set=[image1, image2], how='vertical')
gimg2 = concat_images(image_set=[image1, image2], how='horizontal')
concat_images(
image_set=[gimg1, gimg2],
how=1,
with_plot=True
)
```

Here I take leave. If you have liked it consider visiting this page to read more on Image Processing. And make sure to buy coffee for me from here.

]]>`2D`

matrix. The size of the image is nothing but the dimension of the matrix.
**Credits of Cover Image** - Photo by Kai Krog Halse on Unplash

In Python when we read the image, the size can be easily found by the `.shape`

method. In order to find the shape, we should first read the image and obtain the matrix.

Let's first implement for a `2D`

matrix or array and then replicate the same for images.

First, we are creating a matrix using the module `NumPy`

. The code for the same can be seen below.

```
>>> import random
>>> import numpy as np
>>>
>>> mat = np.array([
... [random.randint(10, 100) for i in range(5)] for j in range(5)
... ])
>>>
>>> print(mat)
[[ 30 91 12 44 52]
[ 37 72 19 100 77]
[ 94 77 60 48 64]
[ 65 26 59 52 40]
[ 37 58 13 74 36]]
>>>
```

We will use the method `.shape`

to find out the dimension of the matrix. Clearly, the matrix has `5`

rows and `5`

columns.

```
>>> dimension = mat.shape
>>>
>>> print(dimension)
(5, 5)
>>>
```

If we plot the same, we will get the matrix image as below.

```
>>> import matplotlib.pyplot as plt
>>>
>>> plt.figure(figsize=(12, 6))
<Figure size 1200x600 with 0 Axes>
>>> mat_plot = plt.imshow(mat)
>>> plt.colorbar(mat_plot)
<matplotlib.colorbar.Colorbar object at 0x0AF04418>
>>> plt.show()
>>>
```

We can increase the size by using the interpolation technique. In mathematics, interpolation is a type of estimation to construct new data points within the range of a discrete set of known data points.

Let's create a function that will resize the given matrix and returns a new matrix with the new shape.

```
def resize_image(image_matrix, nh, nw):
if len(image_matrix.shape) == 3:
oh, ow, _ = image_matrix.shape
else:
oh, ow = image_matrix.shape
re_image_matrix = np.array([
np.array([image_matrix[(oh*h // nh)][(ow*w // nw)] for w in range(nw)])
for h in range(nh)
])
return re_image_matrix
```

**Credits of the above code** - NumPy scaling the Image.

We are passing three arguments -

`image_matrix`

→ Basically any matrix that we want to change the dimension.`nh`

→ New height (goes row-wise).`nw`

→ New width (goes column-wise).

In the function, we are taking old height (`oh`

) and old width (`ow`

) according to the length of the image matrix's shape. The shape of the image varies for the colored image and grayscale image.

Also, we have 2 `for`

loops for 2 levels (row-wise and column-wise) and performing an integer division for each iterative in the range of new height and new width. This will decide the index for which the element is extracted from the matrix.

Let's increase the matrix size where the new matrix should have `8`

rows and `8`

columns.

```
>>> re_mat = resize_image(image_matrix=mat, nh=8, nw=8)
>>> print(re_mat)
[[ 30 30 91 91 12 44 44 52]
[ 30 30 91 91 12 44 44 52]
[ 37 37 72 72 19 100 100 77]
[ 37 37 72 72 19 100 100 77]
[ 94 94 77 77 60 48 48 64]
[ 65 65 26 26 59 52 52 40]
[ 65 65 26 26 59 52 52 40]
[ 37 37 58 58 13 74 74 36]]
>>>
```

We can clearly see, the new matrix `re_mat`

is bigger than the original matrix `mat`

. The dimension of the `re_mat`

is `8`

x`8`

. If we were to visualize the same, there won't any particular difference between the original matrix plot and the new matrix plot.

```
>>> fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(10, 20))
>>>
>>> ax1.title.set_text('Original')
>>> ax2.title.set_text("Re-sized")
>>>
>>> ax1.imshow(mat)
>>> ax2.imshow(re_mat)
>>> plt.show()
>>>
```

We can notice the difference in the axis scaling. The second image is more scaled than the first image. The resizing is handled with care to retain the pixel values. The same thing is applied to the image for increasing and decreasing the size respectively.

**Note** - We can use the `OpenCV`

's resizing techniques. I wanted to learn how we can do the same from scratch.

**Credits of Cover Image** - Photo by Erwan Hesry on Unsplash

We will use the NumPy module extensively for this project and also for the following projects related to Image Processing.

```
>>> import numpy as np
>>>
>>> mat = np.matrix(
... [[1, 2, 3, 4, 5],
... [3, 4, 5, 6, 1]]
... )
>>> mat
matrix([[1, 2, 3, 4, 5],
[3, 4, 5, 6, 1]])
>>>
>>> mat.shape
(2, 5)
>>>
```

```
>>> mat.T
matrix([[1, 3],
[2, 4],
[3, 5],
[4, 6],
[5, 1]])
>>>
```

```
>>> 3 * mat
matrix([[ 3, 6, 9, 12, 15],
[ 9, 12, 15, 18, 3]])
>>>
```

For converting the matrix into an image, we can use the Matplotlib module. The method `imshow()`

is helpful to do the conversion.

```
>>> from matplotlib import pyplot as plt
>>>
>>> plt.figure(figsize=(10, 3))
>>> image_mat = plt.imshow(mat, cmap='Reds')
>>> plt.colorbar(image_mat)
>>> plt.show()
>>>
```

In the above example, we did the conversion for a small matrix. Now we shall try to do the conversion for a large matrix. We will use the Random module to generate random numbers for the matrix.

- There should be 30 rows and 50 columns.
- Each row of the matrix should have 50 numbers in the range of 1 and 200.

We shall use the method `randint(a, b)`

to create a random large matrix.

```
>>> import random
>>>
>>> big_mat = [
... [random.randint(1, 200) for i in range(50)]
... for j in range(30)
... ]
>>>
>>> big_mat = np.matrix(big_mat)
>>>
>>> big_mat
matrix([[ 16, 184, 130, ..., 35, 126, 104],
[198, 153, 172, ..., 109, 60, 174],
[ 90, 138, 108, ..., 192, 132, 103],
...,
[172, 83, 158, ..., 154, 4, 109],
[ 23, 3, 198, ..., 147, 12, 88],
[ 62, 106, 96, ..., 191, 83, 193]])
>>>
>>> big_mat.shape
(30, 50)
>>>
```

Let's convert the above large matrix into an image.

```
>>> plt.figure(figsize=(10, 5))
>>> image_mat = plt.imshow(big_mat, cmap=None)
>>> plt.colorbar(image_mat)
>>> plt.show()
```

To convert an image into a matrix, we need to use the package called `opencv-python`

. With this package, we can read an image into a matrix and perform the matrix operations using the NumPy module.

**Installation**

```
pip install opencv-python --user
```

A typical colored image is comprised of pixels that are represented as `RGB`

pixels. A pixel is simply a number in the range of `0`

to`255`

for all `R`

, `G`

, and `B`

.

- R → Red → 0 to 255
- G → Green → 0 to 255
- B → Blue → 0 to 255

We will `cv2`

(OpenCV) module to read the image in the form of a matrix. The method `imread()`

is used to read the image either in `grayscale`

mode or `RGB`

mode.

`import`

the packages```
import numpy as np
import cv2
import json
from matplotlib import pyplot as plt
```

The image that we will be reading is the original Lenna image. Function to read the image as a matrix.

```
def read_this(image_file, gray_scale=False):
image_src = cv2.imread(image_file)
if gray_scale:
image_src = cv2.cvtColor(image_src, cv2.COLOR_BGR2GRAY)
else:
image_src = cv2.cvtColor(image_src, cv2.COLOR_BGR2RGB)
return image_src
```

By default, `imread()`

reads the image in `BGR`

(Blue, Green, and Red) format. We shall convert into `RGB`

and hence the above function. Now that we have read the image, we can separate the pixels into 3 distinct matrices separating `R`

, `G`

, and `B`

.

Let's plot the same as we have done for the above examples.

```
def separate_rgb(image_file, gray_scale=False):
image_src = read_this(image_file=image_file, gray_scale=gray_scale)
if not gray_scale:
r_, g_, b_ = image_src[:, :, 0], image_src[:, :, 1], image_src[:, :, 2]
return [r_, g_, b_]
return [image_src]
```

The above function separates the pixels into 3 distinct matrices.

```
def plot_rgb_seperated(image_file, with_plot=True, gray_scale=False):
with_plot = True
image_src = read_this(image_file=image_file, gray_scale=False)
separates = separate_rgb(image_file=image_file, gray_scale=gray_scale)
pixel_matrices = [image_src]
pixel_matrices.extend(separates)
cmap_vals = [None, 'Reds', 'Greens', 'Blues'] if not gray_scale else [None, 'gray']
titles = ['Original', 'Red', 'Green', 'Blues'] if not gray_scale else ['Original', 'Grayscale']
n_cols = 4 if not gray_scale else 2
fig_size = (15, 10) if not gray_scale else (10, 20)
if with_plot:
fig, axes = plt.subplots(nrows=1, ncols=n_cols, figsize=fig_size)
for i, ax in zip(range(n_cols), axes):
ax.axis("off")
ax.set_title(titles[i])
ax.imshow(pixel_matrices[i], cmap=cmap_vals[i])
return True
return False
```

The above function plots the matrices along with the original matrix. Let's test the function.

**For color image:**

```
plot_rgb_seperated(image_file='lena_original.png')
```

**For grayscale image:**

```
plot_rgb_seperated(image_file='lena_original.png', gray_scale=True)
```

That is it for this article. We have learned how to convert the matrix into an image and vice-versa.

]]>**Credits of Cover Image** - Photo by Tim Mossholder on Unsplash

The `cv2.rectangle()`

is OpenCV’s method — used to draw a rectangle on the image. We have options to decide the thickness and the color of the rectangle. But we need to make sure that the color is passed as the `RGB`

code (R, G, B). With this, blog article we will try to focus on understanding the inner working of this method and implement the same from scratch using the NumPy module.

A rectangle is simply a shape that we would like to draw on an image in the specified position. We can have the position to draw by passing two points `pt1`

and `pt2`

. The image matrix is considered as a 2D plane for us to decide the position of the rectangle. Let’s understand the library method first.

The arguments of the above-mentioned method are:

`img`

→ Image on which the rectangle is drawn.`pt1`

→ Point 1 to decide the position of the rectangle.`pt2`

→ Point 2 also to decide the position of the rectangle.`color`

→ Color of the rectangle which is passed as RGB code.`thickness`

→ The line thickness of the rectangle.

Other arguments and their importance can be learned by checking `help(cv2.rectangle)`

. First, we will implement the library method and then we will implement the code from scratch.

The packages that we mainly use are:

- NumPy
- Matplotlib
- OpenCV → It is only used for reading the image (in this article).

```
import numpy as np
import cv2
import json
from matplotlib import pyplot as plt
```

```
def read_this(image_file, gray_scale=False):
image_src = cv2.imread(image_file)
if gray_scale:
image_src = cv2.cvtColor(image_src, cv2.COLOR_BGR2GRAY)
else:
image_src = cv2.cvtColor(image_src, cv2.COLOR_BGR2RGB)
return image_src
```

The above function reads the image either in grayscale or RGB and returns the image matrix.

We will use `cv2.rectangle()`

method. But for better visualization, we can the following function taking arguments:

`start_pos`

→ To knowing the starting position of the rectangle to draw.`length`

→ Length of the rectangle (absolute value is considered).`width`

→ Width of the rectangle (absolute value is considered).

From the above three arguments, we can obtain pt1 and pt2 points.

`thickness`

→ Thickness of the rectangle.`color_name`

→ Instead of passing the`RGB`

code, we can extract the (R, G, B) values from the`color_names_data.json`

file where I have stored all the possible color names with their (R, G, B) values respectively. The sample of the color data can be viewed below.

```
{
"air force blue": {
"r": 93,
"g": 138,
"b": 168,
"hex": "#5d8aa8"
},
"alizarin crimson": {
"r": 227,
"g": 38,
"b": 54,
"hex": "#e32636"
},
"almond": {
"r": 239,
"g": 222,
"b": 205,
"hex": "#efdecd"
},
...
}
```

The function works for both colored images and grayscale images. But for grayscale images, a default color `Black`

is considered.

```
def rectangle_lib(image_file, start_pos, length, width, thickness=3, with_plot=False, gray_scale=False, color_name=0):
image_src = read_this(image_file=image_file, gray_scale=gray_scale)
image_main = read_this(image_file=image_file, gray_scale=gray_scale)
image_shape = image_src.shape
length = abs(length)
width = abs(width)
start_row = start_pos if start_pos >= 0 else 0
start_column = start_row
end_row = length + start_row
end_row = end_row if end_row <= image_shape[0] else image_shape[0]
end_column = width + start_column
end_column = end_column if end_column <= image_shape[1] else image_shape[1]
if gray_scale:
color_name = (0, 0, 0)
else:
with open(file='color_names_data.json', mode='r') as col_json:
color_db = json.load(fp=col_json)
color_name = str(color_name).strip().lower()
colors_list = list(color_db.keys())
if color_name not in colors_list:
color_name = (0, 0, 0)
else:
color_name = tuple([color_db[color_name][i] for i in 'rgb'])
# library method
image_rect = cv2.rectangle(image_src, (start_column, start_row), (end_column, end_row), color_name, thickness)
if with_plot:
cmap_val = None if not gray_scale else 'gray'
fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(10, 20))
ax1.axis("off")
ax1.title.set_text('Original')
ax2.axis("off")
ax2.title.set_text("Rectangle")
ax1.imshow(image_main, cmap=cmap_val)
ax2.imshow(image_rect, cmap=cmap_val)
return True
return image_rect
```

The above function is way better to implement rather than specifying the point on our own. We can simply mention the three important arguments and based on that the points are determined. Let’s test the function.

**For color image:**

```
rectangle_lib(
image_file='lena_original.png',
start_pos=199,
length=200,
width=200,
thickness=3,
with_plot=True,
color_name="yellow"
)
```

**For grayscale image:**

```
rectangle_lib(
image_file='lena_original.png',
start_pos=199,
length=200,
width=200,
thickness=3,
with_plot=True,
gray_scale=True
)
```

For thickness (-1), there is a special case in which the whole rectangle area is colored based on the color specified.

The above cases are implemented using the library method. It is obvious that the library method works for sure. It is time to do something from scratch on our own.

We will use the same arguments that we have used in the above function `rectangle_lib()`

. But the technique which we follow is different. Here, the argument `thickness`

plays an important role alongside `start_pos`

, `length`

, and `width`

.

The logic behind the implementation:

We have to grab the sub-image based on

`start_pos`

,`length`

, and`width`

. Basically, we have to crop the image. This cropped image will be the inner portion of the rectangle (Please read this article to know more info).We need to

`pad`

the image matrix with the color value (obtained from the color name). If the image is grayscaled then we will pad with the black color value i.e,`0`

. Otherwise, we will`pad`

3 times after separating the`R`

pixels,`G`

pixels, and`B`

pixels and finally merging them to form a single image. Basically, we are bordering the cropped image (Please read this article to know more info).Finally, we need to replace this bordered, cropped image in the original image and display the image.

We will use the same color data from the file `color_names_data.json`

for obtaining the color values. Let’s code the logic.

```
def draw_rectangle(image_file, start_pos, length, width, thickness=3, with_plot=False, gray_scale=False, color_name=0):
image_src = read_this(image_file=image_file, gray_scale=gray_scale)
image_main = read_this(image_file=image_file, gray_scale=gray_scale)
image_shape = image_src.shape
length = abs(length)
width = abs(width)
thickness = -1 if thickness < 0 else thickness
start_row = start_pos if start_pos >= 0 else 0
start_column = start_row
end_row = length + start_row
end_row = end_row if end_row <= image_shape[0] else image_shape[0]
end_column = width + start_column
end_column = end_column if end_column <= image_shape[1] else image_shape[1]
start_row_grab = start_row - thickness
end_row_grab = end_row + thickness
start_column_grab = start_row_grab
end_column_grab = end_column + thickness
gso_image = image_src[start_row_grab:end_row_grab, start_column_grab:end_column_grab]
gsi_image = image_src[start_row:end_row, start_column:end_column]
if gray_scale:
color_name = 0
if thickness != -1:
inner_image_rect = np.pad(array=gsi_image, pad_width=thickness, mode='constant', constant_values=color_name)
else:
max_height, max_width = gso_image.shape
inner_image_rect = np.zeros(shape=(max_height, max_width))
else:
with open(file='color_names_data.json', mode='r') as col_json:
color_db = json.load(fp=col_json)
color_name = str(color_name).strip().lower()
colors_list = list(color_db.keys())
if color_name not in colors_list:
r_cons, g_cons, b_cons = (0, 0, 0)
else:
r_cons, g_cons, b_cons = [color_db[color_name][i] for i in 'rgb']
r_inner_image, g_inner_image, b_inner_image = gsi_image[:, :, 0], gsi_image[:, :, 1], gsi_image[:, :, 2]
if thickness != -1:
r_inner_rect = np.pad(array=r_inner_image, pad_width=thickness, mode='constant', constant_values=r_cons)
g_inner_rect = np.pad(array=g_inner_image, pad_width=thickness, mode='constant', constant_values=g_cons)
b_inner_rect = np.pad(array=b_inner_image, pad_width=thickness, mode='constant', constant_values=b_cons)
inner_image_rect = np.dstack(tup=(r_inner_rect, g_inner_rect, b_inner_rect))
else:
max_height, max_width, _ = gso_image.shape
r_out_rect = np.full(shape=(max_height, max_width), fill_value=r_cons)
g_out_rect = np.full(shape=(max_height, max_width), fill_value=g_cons)
b_out_rect = np.full(shape=(max_height, max_width), fill_value=b_cons)
inner_image_rect = np.dstack(tup=(r_out_rect, g_out_rect, b_out_rect))
image_src[start_row_grab:end_row_grab, start_column_grab:end_column_grab] = inner_image_rect
image_rect = image_src
if with_plot:
cmap_val = None if not gray_scale else 'gray'
fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(10, 20))
ax1.axis("off")
ax1.title.set_text('Original')
ax2.axis("off")
ax2.title.set_text("Rectangle")
ax1.imshow(image_main, cmap=cmap_val)
ax2.imshow(image_rect, cmap=cmap_val)
plt.show()
return True
return image_rect
```

Here, we are not dealing with the points to decide the position of the rectangle. We are using the techniques of `cropping`

, `bordering`

, and `replacing`

to achieve the main motive. Let’s test the above function.

**For color image:**

```
draw_rectangle(
image_file='lena_original.png',
start_pos=199,
length=200,
width=200,
thickness=3,
with_plot=True,
color_name='red'
)
```

**For grayscale image:**

```
draw_rectangle(
image_file='lena_original.png',
start_pos=199,
length=200,
width=200,
thickness=3,
with_plot=True,
gray_scale=True
)
```

For thickness (-1), there is a special case in which the whole rectangle area is colored based on the color specified.

We finally got what we wanted. We tried to implement the code from scratch with the help of NumPy matrix operations. We used NumPy methods like:

`pad()`

→ pads the matrix with constant values.`zeros()`

→ We used this for the special case when the`thickness`

is -1. This is only used when the image is read in grayscale mode.`full()`

→ We used to obtain a matrix of identical values. This is also used when the`thickness`

is -1 in the case where the image is read in colored mode.

Personally, I learned so much from implementing the method. I hope you find this insightful. You should definitely check out my other articles on the same subject in my profile.

If you liked it, you can buy coffee for me from here.

]]>`RGB`

image then the size of the image would be `(width, height, 3)`

otherwise — grayscale would just be `(width, height)`

. But ultimately, images are just large matrices where each value is a pixel positioned `row-wise`

and `column-wise`

accordingly.
**Credits of Cover Image** - Photo by Ulrike Langner on Unsplash

Cropping the image is just obtaining the sub-matrix of the image matrix. The size of the sub-matrix (cropped image) can be of our choice and mainly it is the height and width. There needs to be one important thing for the image to be cropped, i.e., `starting position`

. The `starting position`

is helpful for obtaining the sub-matrix from that position and depending upon height and width we can easily crop cut the image.

The three important things are:

- starting_position
- length (height)
- width

Based on these three things, we can construct our cropping function completely ready.

The packages that we mainly use are:

- NumPy
- Matplotlib
- OpenCV → It is only used for reading the image.

```
import numpy as np
import cv2
import json
from matplotlib import pyplot as plt
```

```
def read_this(image_file, gray_scale=False):
image_src = cv2.imread(image_file)
if gray_scale:
image_src = cv2.cvtColor(image_src, cv2.COLOR_BGR2GRAY)
else:
image_src = cv2.cvtColor(image_src, cv2.COLOR_BGR2RGB)
return image_src
```

The above function reads the image either in grayscale or RGB and returns the image matrix.

We need to pass the above mentioned 3 things as arguments in our function. But before doing let’s try to crop (slice) the matrix with NumPy.

```
>>> import numpy as np
>>> m = np.array([
... [1, 2, 3, 4, 5, 6, 7],
... [5, 3, 4, 2, 1, 7, 6],
... [6, 4, 3, 5, 1, 2, 7],
... [5, 6, 3, 1, 4, 2, 7],
... [1, 2, 3, 4, 5, 6, 7]
... ])
>>>
>>> print(m)
[[1 2 3 4 5 6 7]
[5 3 4 2 1 7 6]
[6 4 3 5 1 2 7]
[5 6 3 1 4 2 7]
[1 2 3 4 5 6 7]]
>>>
>>> crop_m = m[1:4, 2:7]
>>> print(crop_m)
[[4 2 1 7 6]
[3 5 1 2 7]
[3 1 4 2 7]]
>>>
```

The above code is an example of how we can crop an image matrix. Notice `crop_m`

is the cropped matrix (sub-matrix) that is sliced from the original matrix `m`

. The sub-matrix `crop_m`

is taking values from `[1:4, 2:7]`

, i.e., values from `1st row`

till `4th row`

and from `2nd column`

till `7th column`

. We should something similar for the image to obtain the cropped image. Let’s write the cropping image function.

```
def crop_this(image_file, start_pos, length, width, with_plot=False, gray_scale=False):
image_src = read_this(image_file=image_file, gray_scale=gray_scale)
image_shape = image_src.shape
length = abs(length)
width = abs(width)
start_row = start_pos if start_pos >= 0 else 0
start_column = start_row
end_row = length + start_row
end_row = end_row if end_row <= image_shape[0] else image_shape[0]
end_column = width + start_column
end_column = end_column if end_column <= image_shape[1] else image_shape[1]
print("start row \t- ", start_row)
print("end row \t- ", end_row)
print("start column \t- ", start_column)
print("end column \t- ", end_column)
image_cropped = image_src[start_row:end_row, start_column:end_column]
cmap_val = None if not gray_scale else 'gray'
if with_plot:
fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(10, 20))
ax1.axis("off")
ax1.title.set_text('Original')
ax2.axis("off")
ax2.title.set_text("Cropped")
ax1.imshow(image_src, cmap=cmap_val)
ax2.imshow(image_cropped, cmap=cmap_val)
return True
return image_cropped
```

Let’s understand what this function will actually result in.

- At the first step, we read the image either in grayscale or RGB and obtain the image matrix.
- We obtain the height and width of the image which is further used in the validation of the code.
- We make sure that the length and width are positive integers. Hence absolute values are considered.
- We calculate the four important values which are useful for slicing the matrix
`start_row`

,`end_row`

,`start_column`

,`end_column`

. We obtain that using the three arguments that are passed —`start_pos`

,`length`

,`width`

. - We obtain the cropped image by slicing the matrix.
- We plot both the original and cropped images for the visualization.

Let’s test the above function —

```
crop_this(
image_file='lena_original.png',
start_pos=199,
length=100,
width=200,
with_plot=True
)
```

```
start row - 199
end row - 299
start column - 199
end column - 399
```

```
crop_this(
image_file='lena_original.png',
start_pos=199,
length=100,
width=200,
with_plot=True,
gray_scale=True
)
```

```
start row - 199
end row - 299
start column - 199
end column - 399
```

This is it!!! We finally are able to crop the image by just knowing the starting position and length & width of the cropped image. Isn’t it great? We can also add a lot of customization options like adding a border around the image and other things. To know how to add a border to the image, you can refer to my article.

Other similar articles can be found in my profile. Have a great time reading and implementing the same.

If you liked it, you can buy coffee for me from here.

]]>`0`

and `1`

. Here `0`

represents Black and `1`

represents White. When we apply inversion to these values, we get:
- 0 → inverted → 1
- 1 → inverted → 0

The above only works when we two values. `0`

for low and `1`

for high. If we were to relate the same with the Binary Image whose pixel values are just `1`

’s and `0`

's. The inversion would be reversed. To put it in words we can say from **White** and **Black** to **Black** and **White**.

**Credits of Cover Image** - Photo by Jr Korpa on Unsplash

Unlike lists, if we want to add a number to the values of the list. We iterate through each element and add the number. Whereas, in NumPy, we need not iterate through each element and add. Instead, we can treat the array list as a single element and add the number. NumPy automatically adds that number to all the elements of the array list. This technique is called broadcasting.

The broadcasting technique is applicable to both matrices and arrays. It is very fast when compared to normal loops.

```
>>> import numpy as np
>>> M = np.array([1, 2, 3, 4, 5, 6])
>>> M = 3 + M
>>> M
array([4, 5, 6, 7, 8, 9])
>>>
```

Let’s see the demonstration for a random matrix.

A simple demonstration of the **White — Black** matrix and the image can be seen below.

- 1 is visualized as
**White** - 0 is visualized as
**Black**

```
>>> import numpy as np
>>> image_b = np.array([
... [1,0,1],
... [1,1,0],
... [0,1,1]])
>>> image_b
array([[1, 0, 1],
[1, 1, 0],
[0, 1, 1]])
>>>
```

If we visualize the above matrix, we can see something like the below.

A simple demonstration of the **Black — White** matrix and the image can be seen below.

- 1 is changed to 0 →
**Black** - 0 is changed to 1 →
**White**

```
>>> # Broadcasting
>>> image_i = 1 - image_b
>>> # image_i = ~ image_b
>>> image_i
array([[0, 1, 0],
[0, 0, 1],
[1, 0, 0]])
>>>
```

If we visualize the above matrix, we can see something like the below.

To convert a matrix into an inverted matrix we can also use the operation ‘**~**'. It works in the same way.

If you want to know specifically how to convert an image into binary, you can refer to my article where I explain the procedure for both colored and grayscale images.

The above implementation worked since the images are already binarized. What if we wanted to apply this for a colored and non-binarized image? The scenarios will be different. Let’s find out what we can do for those.

The packages that we mainly use are:

- NumPy
- Matplotlib
- OpenCV

```
import numpy as np
import cv2
import json
from matplotlib import pyplot as plt
```

```
def read_this(image_file, gray_scale=False):
image_src = cv2.imread(image_file)
if gray_scale:
image_src = cv2.cvtColor(image_src, cv2.COLOR_BGR2GRAY)
else:
image_src = cv2.cvtColor(image_src, cv2.COLOR_BGR2RGB)
return image_src
```

The above function reads the image either in grayscale or RGB and returns the image matrix.

For inverting an image using the `cv2`

library, we can use the method `bitwise_not()`

which is available in the library. We can just pass the image matrix as an argument in the method.

```
def invert_lib(image_file, with_plot=False, gray_scale=False):
image_src = read_this(image_file=image_file, gray_scale=gray_scale)
cmap_val = None if not gray_scale else 'gray'
image_i = cv2.bitwise_not(image_src)
if with_plot:
fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(10, 20))
ax1.axis("off")
ax1.title.set_text('Original')
ax2.axis("off")
ax2.title.set_text("Inverted")
ax1.imshow(image_src, cmap=cmap_val)
ax2.imshow(image_i, cmap=cmap_val)
return True
return image_i
```

The above function returns the inverted image of the original image that is passed. The same can be seen when plotted.

Let’s test the above function —

```
invert_lib(image_file='lena_original.png', with_plot=True)
```

```
invert_lib(image_file='lena_original.png', with_plot=True, gray_scale=True)
```

We have obtained the inverted images for both colored and grayscale images.

In order to invert the image, we have to apply the broadcasting technique using NumPy. We have to subtract each pixel value from `255`

. It is because the highest pixel value or color value is `255`

in an image. Either way, we can apply the ‘**~**’ (negation) operation to the image.

```
def invert_this(image_file, with_plot=False, gray_scale=False):
image_src = read_this(image_file=image_file, gray_scale=gray_scale)
# image_i = ~ image_src
image_i = 255 - image_src
if with_plot:
cmap_val = None if not gray_scale else 'gray'
fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(10, 20))
ax1.axis("off")
ax1.title.set_text('Original')
ax2.axis("off")
ax2.title.set_text("Inverted")
ax1.imshow(image_src, cmap=cmap_val)
ax2.imshow(image_i, cmap=cmap_val)
return True
return image_i
```

Let’s test the above function —

```
invert_this(image_file='lena_original.png', with_plot=True)
```

```
invert_this(image_file='lena_original.png', with_plot=True, gray_scale=True)
```

This is it! We have successfully written the code for converting the image into an inverted image. The results are similar to that of the library code.

Let’s see what will it result in when applied to the binarized image. I have put the result without the code.

The inverted image is exactly opposite to the binarized image. Hence this concludes the agenda of this article.

You should definitely check out my other articles on the same subject in my profile.

If you liked it, you can buy coffee for me from here.

]]>There are two aspects in which we can start to think:

- If the image is read in grayscale, we can simply keep the default color as black. This is because the length of the shape of the image matrix would be 2. Therefore we cannot add a color border whose color value would be of size 3 and thus it cannot be mapped easily.
- If the image is read in RGB, we can have a choice to pick the color for the border. This is because the length of the shape of the image matrix would be 3. Hence we can add a color border whose color value would be of size 3 which can be mapped easily.

**Credits of Cover Image** - Photo by Kanan Khasmammadov on Unsplash

Before proceeding further, we need to make sure we have enough colors (based on the user’s choice). I have extracted the possible color values from this website.

The code of the same can be seen below. The result is stored in a JSON file named `color_names_data.json`

.

```
import requests
import json
from bs4 import BeautifulSoup
def extract_table(url):
res = requests.get(url=url)
con = res.text
soup = BeautifulSoup(con, features='lxml')
con_table = soup.find('table', attrs={'class' : 'color-list'})
headings = [th.get_text().lower() for th in con_table.find("tr").find_all("th")]
table_rows = [headings]
for row in con_table.find_all("tr")[1:]:
each_row = [td.get_text().lower() for td in row.find_all("td")]
table_rows.append(each_row)
return table_rows
col_url = "https://www.colorhexa.com/color-names"
color_rows_ = extract_table(url=col_url)
color_dict = {}
for co in color_rows_[1:]:
color_dict[co[0]] = {
'r' : int(co[2]),
'g' : int(co[3]),
'b' : int(co[4]),
'hex' : co[1]
}
with open(file='color_names_data.json', mode='w') as col_json:
json.dump(obj=color_dict, fp=col_json, indent=2)
```

It is necessary to grab R, G, and B values in order for the mapping after the separation of pixels. We follow `split`

and `merge`

methods using NumPy.

The structure of the color data can be seen below.

```
{
"air force blue": {
"r": 93,
"g": 138,
"b": 168,
"hex": "#5d8aa8"
},
"alizarin crimson": {
"r": 227,
"g": 38,
"b": 54,
"hex": "#e32636"
},
"almond": {
"r": 239,
"g": 222,
"b": 205,
"hex": "#efdecd"
},
...
}
```

The packages that we mainly use are:

- NumPy
- Matplotlib
- OpenCV

```
import numpy as np
import cv2
import json
from matplotlib import pyplot as plt
```

```
def read_this(image_file, gray_scale=False):
image_src = cv2.imread(image_file)
if gray_scale:
image_src = cv2.cvtColor(image_src, cv2.COLOR_BGR2GRAY)
else:
image_src = cv2.cvtColor(image_src, cv2.COLOR_BGR2RGB)
return image_src
```

The above function reads the image either in grayscale or RGB and returns the image matrix.

For adding/drawing borders around the image, the important arguments can be named as below:

`image_file`

→ Image file location or image name if the image is stored in the same directory.`bt`

→ Border thickness`color_name`

→ By default it takes 0 (black color). Otherwise, any color name can be taken.

We use the method `copyMakeBorder()`

available in the library OpenCV that is used to create a new bordered image with a specified thickness. In the code, we make sure to convert the color name into values from the color data we collected earlier.

The below function works for both RGB image and grayscale image as expected.

```
def add_border(image_file, bt=5, with_plot=False, gray_scale=False, color_name=0):
image_src = read_this(image_file=image_file, gray_scale=gray_scale)
if gray_scale:
color_name = 0
value = [color_name for i in range(3)]
else:
color_name = str(color_name).strip().lower()
with open(file='color_names_data.json', mode='r') as col_json:
color_db = json.load(fp=col_json)
colors_list = list(color_db.keys())
if color_name not in colors_list:
value = [0, 0, 0]
else:
value = [color_db[color_name][i] for i in 'rgb']
image_b = cv2.copyMakeBorder(image_src, bt, bt, bt, bt, cv2.BORDER_CONSTANT, value=value)
if with_plot:
cmap_val = None if not gray_scale else 'gray'
fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(10, 20))
ax1.axis("off")
ax1.title.set_text('Original')
ax2.axis("off")
ax2.title.set_text("Bordered")
ax1.imshow(image_src, cmap=cmap_val)
ax2.imshow(image_b, cmap=cmap_val)
return True
return image_b
```

Let’s test the above function —

```
add_border(image_file='lena_original.png', with_plot=True, color_name='green')
```

```
add_border(image_file='lena_original.png', with_plot=True, gray_scale=True, color_name='pink')
```

We can clearly see the borders are been added/drawn. For the grayscale image, though we have mentioned pink, a black border is drawn.

When we talk about the border, it is basically a constant pixel value of one color around the entire image. It is important to take note of the thickness of the border to be able to see. Considering the thickness we should append a constant value around the image that matches the thickness level.

In order to do so, we can use the `pad()`

method available in the library NumPy. This method appends a constant value that matches the level of the `pad_width`

argument which is mentioned.

**Example**

```
>>> import numpy as np
>>> m = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> m = np.array(m)
>>> m
array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
>>>
>>> pm = np.pad(array=m, pad_width=1, mode='constant', constant_values=12)
>>> pm
array([[12, 12, 12, 12, 12],
[12, 1, 2, 3, 12],
[12, 4, 5, 6, 12],
[12, 7, 8, 9, 12],
[12, 12, 12, 12, 12]])
>>>
>>> pmm = np.pad(array=m, pad_width=2, mode='constant', constant_values=24)
>>> pmm
array([[24, 24, 24, 24, 24, 24, 24],
[24, 24, 24, 24, 24, 24, 24],
[24, 24, 1, 2, 3, 24, 24],
[24, 24, 4, 5, 6, 24, 24],
[24, 24, 7, 8, 9, 24, 24],
[24, 24, 24, 24, 24, 24, 24],
[24, 24, 24, 24, 24, 24, 24]])
>>>
```

More examples can be found in the documentation.

We just need to change the constant_values argument by taking the actual color value.

- For a grayscale image, we simply pad the image matrix with black color i.e., 0.
- For the RGB image, we are to grab the color value from the data collected, split the image into 3 matrices, and pad each matrix with each color value. The below function would explain the flow more clearly.

The below function would explain the flow more clearly.

```
def draw_border(image_file, bt=5, with_plot=False, gray_scale=False, color_name=0):
image_src = read_this(image_file=image_file, gray_scale=gray_scale)
if gray_scale:
color_name = 0
image_b = np.pad(array=image_src, pad_width=bt, mode='constant', constant_values=color_name)
cmap_val = 'gray'
else:
color_name = str(color_name).strip().lower()
with open(file='color_names_data.json', mode='r') as col_json:
color_db = json.load(fp=col_json)
colors_list = list(color_db.keys())
if color_name not in colors_list:
r_cons, g_cons, b_cons = [0, 0, 0]
else:
r_cons, g_cons, b_cons = [color_db[color_name][i] for i in 'rgb']
r_, g_, b_ = image_src[:, :, 0], image_src[:, :, 1], image_src[:, :, 2]
rb = np.pad(array=r_, pad_width=bt, mode='constant', constant_values=r_cons)
gb = np.pad(array=g_, pad_width=bt, mode='constant', constant_values=g_cons)
bb = np.pad(array=b_, pad_width=bt, mode='constant', constant_values=b_cons)
image_b = np.dstack(tup=(rb, gb, bb))
cmap_val = None
if with_plot:
fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(10, 20))
ax1.axis("off")
ax1.title.set_text('Original')
ax2.axis("off")
ax2.title.set_text("Bordered")
ax1.imshow(image_src, cmap=cmap_val)
ax2.imshow(image_b, cmap=cmap_val)
return True
return image_b
```

Let’s test the above function —

```
draw_border(image_file='lena_original.png', with_plot=True, color_name='cyan')
```

```
draw_border(image_file='lena_original.png', bt=10, with_plot=True, gray_scale=True)
```

Yay! We did it. We coded the entire thing including color choice completely from scratch except for the part where we read the image file. We relied mostly on NumPy as it is very fast in computing matrix operations (We could have done it with `for`

loops if we wanted our code to execute very slow).

Personally, this was a great learning for me. I am starting to think about how difficult and fun that would be for the people who actually work on open source libraries.

You should definitely check out my other articles on the same subject in my profile.

If you liked it, you can buy coffee for me from here.

]]>To understand what binary is — binary is something that is made of two things. In computer terminology, binary is just ** 0** and

signifies*0***Black**signifies*1***White**.

At the initial stages of learning image processing, we often think of a grayscale image as a **binary image**. Although it is not. But slowly when we pick up the subject, we realize how wrong we were. So, moving ahead, we will learn how to binarize the image with both using the library and without using the library (NumPy is used for matrix operations just to avoid the slowness of the program when used the regular **for loops**). Besides this, we will also use Matplotlib to plot the results.

**Credits of Cover Image** - Photo by Angel Santos on Unsplash

The binary operation works really well for the grayscale images. The problem with the color (RGB) images is that each pixel is a vector representing 3 unique values one for Red, one for Green, and one for Blue.

A typical grayscale image’s matrix would look like -

```
array([[162, 162, 162, ..., 170, 155, 128],
[162, 162, 162, ..., 170, 155, 128],
[162, 162, 162, ..., 170, 155, 128],
...,
[ 43, 43, 50, ..., 104, 100, 98],
[ 44, 44, 55, ..., 104, 105, 108],
[ 44, 44, 55, ..., 104, 105, 108]], dtype=uint8)
```

A typical RGB image’s matrix would seem like -

```
array([[[226, 137, 125], ..., [200, 99, 90]],
[[226, 137, 125], ..., [200, 99, 90]],
[[226, 137, 125], ..., [200, 99, 90]],
...,
[[ 84, 18, 60], ..., [177, 62, 79]],
[[ 82, 22, 57], ..., [185, 74, 81]],
[[ 82, 22, 57], ..., [185, 74, 81]]], dtype=uint8)
```

If we were to separate R, G, and B pixels from the above matrix. We get —

```
array([[226, 226, 223, ..., 230, 221, 200],
[226, 226, 223, ..., 230, 221, 200],
[226, 226, 223, ..., 230, 221, 200],
...,
[ 84, 84, 92, ..., 173, 172, 177],
[ 82, 82, 96, ..., 179, 181, 185],
[ 82, 82, 96, ..., 179, 181, 185]], dtype=uint8)
```

```
array([[137, 137, 137, ..., 148, 130, 99],
[137, 137, 137, ..., 148, 130, 99],
[137, 137, 137, ..., 148, 130, 99],
...,
[ 18, 18, 27, ..., 73, 68, 62],
[ 22, 22, 32, ..., 70, 71, 74],
[ 22, 22, 32, ..., 70, 71, 74]], dtype=uint8)
```

```
array([[125, 125, 133, ..., 122, 110, 90],
[125, 125, 133, ..., 122, 110, 90],
[125, 125, 133, ..., 122, 110, 90],
...,
[ 60, 60, 58, ..., 84, 76, 79],
[ 57, 57, 62, ..., 79, 81, 81],
[ 57, 57, 62, ..., 79, 81, 81]], dtype=uint8)
```

Whatever operation we compute on the grayscale image, we will need to compute the same on the RGB image but for 3 times separating R, G, and B pixels and finally merging them as a proper RGB image.

The packages that we mainly use are -

- NumPy
- Matplotlib
- OpenCV

```
import cv2
import numpy as np
from matplotlib import pyplot as plt
```

```
def read_this(image_file, gray_scale=False):
image_src = cv2.imread(image_file)
if gray_scale:
image_src = cv2.cvtColor(image_src, cv2.COLOR_BGR2GRAY)
else:
image_src = cv2.cvtColor(image_src, cv2.COLOR_BGR2RGB)
return image_src
```

The above function reads the image either in grayscale or RGB and returns the image matrix.

For converting the image into a binary image, we can simply make use of the ** threshold()** method available in the

**src**→ It is basically the image matrix.**thresh**→ It is the threshold value based on which pixels are given a new value. If the pixels are less than this value, we will revalue those pixels to. Otherwise, the pixels will be revalued to*255*.*0***maxval**→ It is the maximum pixel value that a typical image could contain (255).**type →**It is basically a thresholding type that is given and based on that type the operation is computed. There are several types with which the operation is taken care of.

After this, we will plot the results to see the variation and hence the below function.

```
def binarize_lib(image_file, thresh_val=127, with_plot=False, gray_scale=False):
image_src = read_this(image_file=image_file, gray_scale=gray_scale)
th, image_b = cv2.threshold(src=image_src, thresh=thresh_val, maxval=255, type=cv2.THRESH_BINARY)
if with_plot:
cmap_val = None if not gray_scale else 'gray'
fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(10, 20))
ax1.axis("off")
ax1.title.set_text('Original')
ax2.axis("off")
ax2.title.set_text("Binarized")
ax1.imshow(image_src, cmap=cmap_val)
ax2.imshow(image_b, cmap=cmap_val)
return True
return image_b
```

Let’s test the above function -

```
binarize_lib(image_file='lena_original.png', with_plot=True)
```

```
binarize_lib(image_file='lena_original.png', with_plot=True, gray_scale=True)
```

Now that we have seen the results of both original and binary images, it is obvious that the library code works for both. It’s time to make our hands dirty to code the same from the scratch.

First, we will write a function that will revalue the pixel values which are less than the specified threshold to ** 255**.

By doing it, we will see something like below -

```
def convert_binary(image_matrix, thresh_val):
white = 255
black = 0
initial_conv = np.where((image_matrix <= thresh_val), image_matrix, white)
final_conv = np.where((initial_conv > thresh_val), initial_conv, black)
return final_conv
```

We will call the above function three times by separating R, G, and B values and finally merge the same to obtain the binarized image. Once doing it, we can plot the results just like how we did it before.

```
def binarize_this(image_file, thresh_val=127, with_plot=False, gray_scale=False):
image_src = read_this(image_file=image_file, gray_scale=gray_scale)
if not gray_scale:
cmap_val = None
r_img, g_img, b_img = image_src[:, :, 0], image_src[:, :, 1], image_src[:, :, 2]
r_b = convert_binary(image_matrix=r_img, thresh_val=thresh_val)
g_b = convert_binary(image_matrix=g_img, thresh_val=thresh_val)
b_b = convert_binary(image_matrix=b_img, thresh_val=thresh_val)
image_b = np.dstack(tup=(r_b, g_b, b_b))
else:
cmap_val = 'gray'
image_b = convert_binary(image_matrix=image_src, thresh_val=thresh_val)
if with_plot:
fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(10, 20))
ax1.axis("off")
ax1.title.set_text('Original')
ax2.axis("off")
ax2.title.set_text("Binarized")
ax1.imshow(image_src, cmap=cmap_val)
ax2.imshow(image_b, cmap=cmap_val)
return True
return image_b
```

We have made our binarizing code by just using NumPy. Let’s test the same -

```
binarize_this(image_file='lena_original.png', with_plot=True)
```

```
binarize_this(image_file='lena_original.png', with_plot=True, gray_scale=True)
```

This is it. Whatever we wanted to accomplish, we have accomplished it. The results are quite similar to the one we got by using the library code.

Hence this concludes the aim of this article.

Do give a read …

]]>**Credits of Cover Image** - Photo by Andreas Gücklhorn on Unsplash

Often times what happens is when the image is captured, it will not be the same as the natural view. In order to meet the level of natural view, post-processing should be done. Hence Histogram Equalization (Normalization) is one of those techniques to enhance the contrast by tweaking the pixel values of the image.

An example can be seen below - original image and equalized image.

If we were to plot the image histograms, it would look something like below -

**Credits** - The above images have been taken from the Internet for showing the examples.

This method works better for both bright and dark images, especially in the field of medical science there is higher importance in analyzing the X-ray images.

It is also very useful in viewing scientific images like thermal images and satellite images.

In this article, I will implement this method both by using the `openCV`

library and from scratch with just `NumPy`

and `Matplotlib`

. Although I would like to do without using `NumPy`

, it would take much time to compute.

**Note** - For coding from scratch, I will use `openCV`

to read the image and nothing else.

I have taken Lena Image for testing the functions. I have saved the same in my working directory.

The packages that we mainly use are:

- NumPy
- Matplotlib
- OpenCV → It is only used for reading the image (in this article).

```
import cv2
import numpy as np
from matplotlib import pyplot as plt
```

```
def read_this(image_file, gray_scale=False):
image_src = cv2.imread(image_file)
if gray_scale:
image_src = cv2.cvtColor(image_src, cv2.COLOR_BGR2GRAY)
else:
image_src = cv2.cvtColor(image_src, cv2.COLOR_BGR2RGB)
return image_src
```

The above function reads the image either in `gray_scale`

or `RGB`

and returns the image matrix.

For equalizing, we can simply use the `equalizeHist()`

method available in the library `cv2`

. We have two aspects here -

When the image is read in

`RGB`

.- Separate the pixels based on the color combination. We can use the
`split()`

method available in the library`cv2`

. - Apply the equalization method for each matrix.
- Merge the equalized image matrices altogether with the method
`merge()`

available in the library`cv2`

.

- Separate the pixels based on the color combination. We can use the
When the image is read in

`gray_scale`

.- Just apply the equalization method for the image matrix.

Plot the original image and equalized image.

```
def equalize_this(image_file, with_plot=False, gray_scale=False):
image_src = read_this(image_file=image_file, gray_scale=gray_scale)
if not gray_scale:
r_image, g_image, b_image = cv2.split(image_src)
r_image_eq = cv2.equalizeHist(r_image)
g_image_eq = cv2.equalizeHist(g_image)
b_image_eq = cv2.equalizeHist(b_image)
image_eq = cv2.merge((r_image_eq, g_image_eq, b_image_eq))
cmap_val = None
else:
image_eq = cv2.equalizeHist(image_src)
cmap_val = 'gray'
if with_plot:
fig = plt.figure(figsize=(10, 20))
ax1 = fig.add_subplot(2, 2, 1)
ax1.axis("off")
ax1.title.set_text('Original')
ax2 = fig.add_subplot(2, 2, 2)
ax2.axis("off")
ax2.title.set_text("Equalized")
ax1.imshow(image_src, cmap=cmap_val)
ax2.imshow(image_eq, cmap=cmap_val)
return True
return image_eq
```

Let's test the above function -

```
equalize_this(image_file='lena_original.png', with_plot=True)
```

```
equalize_this(image_file='lena_original.png', with_plot=True, gray_scale=True)
```

The above plots are clear and we can say that the equalized images look better than the original images. This was implemented using the `cv2`

library.

For this, I am using `NumPy`

for all the matrix operations. Again we can do it with `for`

loops, but it will take more time to compute. Even here we have two aspects as before -

When the image is read in

`RGB`

.- Separate the pixels based on the color combination. We can slice it down using
`NumPy`

operations. - Apply the equalization method for each matrix.
- Merge the equalized image matrices altogether with the method
`dstack(tup=())`

available in the library`NumPy`

.

- Separate the pixels based on the color combination. We can slice it down using
When the image is read in

`gray_scale`

.- Just apply the equalization method for the image matrix.

Plot the original image and equalized image.

Let's write our own function to compute the image equalization. Image pixel values are normally in the range of 0 to 255. So in total, we will have 256 pixels.

```
def enhance_contrast(image_matrix, bins=256):
image_flattened = image_matrix.flatten()
image_hist = np.zeros(bins)
# frequency count of each pixel
for pix in image_matrix:
image_hist[pix] += 1
# cummulative sum
cum_sum = np.cumsum(image_hist)
norm = (cum_sum - cum_sum.min()) * 255
# normalization of the pixel values
n_ = cum_sum.max() - cum_sum.min()
uniform_norm = norm / n_
uniform_norm = uniform_norm.astype('int')
# flat histogram
image_eq = uniform_norm[image_flattened]
# reshaping the flattened matrix to its original shape
image_eq = np.reshape(a=image_eq, newshape=image_matrix.shape)
return image_eq
```

**Credits** - The above code is an inspiration from the article written by * Tory Walker*.

The above function returns an equalized image matrix when passed the original image matrix as an argument.

Let's write another function that computes the equalization for both the `RGB`

image and the `gray_scale`

image taking the above function in use.

```
def equalize_this(image_file, with_plot=False, gray_scale=False, bins=256):
image_src = read_this(image_file=image_file, gray_scale=gray_scale)
if not gray_scale:
r_image = image_src[:, :, 0]
g_image = image_src[:, :, 1]
b_image = image_src[:, :, 2]
r_image_eq = enhance_contrast(image_matrix=r_image)
g_image_eq = enhance_contrast(image_matrix=g_image)
b_image_eq = enhance_contrast(image_matrix=b_image)
image_eq = np.dstack(tup=(r_image_eq, g_image_eq, b_image_eq))
cmap_val = None
else:
image_eq = enhance_contrast(image_matrix=image_src)
cmap_val = 'gray'
if with_plot:
fig = plt.figure(figsize=(10, 20))
ax1 = fig.add_subplot(2, 2, 1)
ax1.axis("off")
ax1.title.set_text('Original')
ax2 = fig.add_subplot(2, 2, 2)
ax2.axis("off")
ax2.title.set_text("Equalized")
ax1.imshow(image_src, cmap=cmap_val)
ax2.imshow(image_eq, cmap=cmap_val)
return True
return image_eq
```

Let's test the above function -

```
equalize_this(image_file='lena_original.png', with_plot=True)
```

```
equalize_this(image_file='lena_original.png', with_plot=True, gray_scale=True)
```

The above plots are clear and we can say that the equalized images look better than the original images. This was implemented from scratch using the `NumPy`

library.

Let's compare the equalized image obtained from the `cv2`

library and the equalized image obtained from the code written from scratch.

We can notice there is a slight difference between the `library image`

and `scratch image`

. But both seem to be clear when compared with the `original image`

. Here I complete my article with my own takeaway.

Personally, I learned a lot by exploring and implementing different methods applied to increase the image intensity. Especially, trying to implement the code from scratch by both referring and learning.

It is always good to use the library methods as they seem to be more optimized and works 100 percent.

Image processing is a very crucial subject to learn and one really deserves to try out practicing with so much curiosity and ones' own exploration.

Do give a read to my other articles and let me know your thoughts -

]]>**Credits of Cover Image** - Photo by Michael Busch on Unsplash

**NumPy**- For matrix operations and manipulating the same.**OpenCV**- For reading the image and converting it into a 2D array (matrix).**Matplotlib**- For plotting the matrix as an image.

For this mini-project, I am using a famous Lena image which is mainly used for testing the computer vision models. Make sure to download this image and save it in the current working directory.

```
import cv2
import numpy as np
from matplotlib import pyplot as plt
```

First, we read the image file using the `imread()`

method from the module `cv2`

. To do that, we simply need to import the package and use so. Hence by doing this, we get the image in the form of a matrix.

By default, `imread()`

method reads the image in `BGR`

(`Blue`

, `Green`

, `Red`

) format. To convert the read image into regular format i.e., `RGB`

(`Red`

, `Green`

, `Blue`

) we use `cvtColor()`

method from the same module `cv2`

.

```
def read_this(image_file, gray_scale=False):
image_src = cv2.imread(image_file)
if gray_scale:
image_rgb = cv2.cvtColor(image_src, cv2.COLOR_BGR2GRAY)
else:
image_rgb = cv2.cvtColor(image_src, cv2.COLOR_BGR2RGB)
return image_rgb
```

The above function returns an image matrix from the image file that is passed. It consists of regular `if`

and `else`

condition if we want to get the image matrix either in `gray_scale`

or `RGB`

format.

To mirror the image basically, we need to reverse the matrix row-wise from left to right. For example, let's consider a matrix `A`

.

```
>>> A = [
[4, 1, 1],
[2, 8, 0],
[3, 8, 1]
]
```

If we want to mirror this matrix (row-wise), then it would be -

```
>>> import numpy as np
>>> mirror_ = np.fliplr(A)
>>> mirror_
[[1, 1, 4],
[0, 8, 2],
[1, 8, 3]]
```

We can also do this without using the `NumPy`

module. If so, we get to use loops and reverse each row. This would take a while if performed the same on image matrices as they are very large matrices and we don't want our code to perform very slow.

```
def mirror_this(image_file, gray_scale=False, with_plot=False):
image_rgb = read_this(image_file=image_file, gray_scale=gray_scale)
image_mirror = np.fliplr(image_rgb)
if with_plot:
fig = plt.figure(figsize=(10, 20))
ax1 = fig.add_subplot(2, 2, 1)
ax1.axis("off")
ax1.title.set_text('Original')
ax2 = fig.add_subplot(2, 2, 2)
ax2.axis("off")
ax2.title.set_text("Mirrored")
if not gray_scale:
ax1.imshow(image_rgb)
ax2.imshow(image_mirror)
else:
ax1.imshow(image_rgb, cmap='gray')
ax2.imshow(image_mirror, cmap='gray')
return True
return image_mirror
```

The above function returns an image matrix that is reversed or flipped row-wise from left to right.

Let's plot the same -

```
mirror_this(image_file="lena_original.png", with_plot=True)
```

```
mirror_this(image_file="lena_original.png", gray_scale=True, with_plot=True)
```

To flip the image basically, we need to reverse the matrix column-wise from up to down. For example, let's consider a matrix `B`

.

```
>>> B = [
[4, 1, 1],
[2, 8, 0],
[3, 8, 1]
]
```

If we want to flip this matrix (column-wise), then it would be -

```
>>> import numpy as np
>>> flip_= np.flipud(B)
>>> flip_
[[3, 8, 1],
[2, 8, 0],
[4, 1, 1]]
```

We use `NumPy`

for flipping the matrix in order to maintain the fastness of the code.

```
def flip_this(image_file, gray_scale=False, with_plot=False):
image_rgb = read_this(image_file=image_file, gray_scale=gray_scale)
image_flip = np.flipud(image_rgb)
if with_plot:
fig = plt.figure(figsize=(10, 20))
ax1 = fig.add_subplot(2, 2, 1)
ax1.axis("off")
ax1.title.set_text('Original')
ax2 = fig.add_subplot(2, 2, 2)
ax2.axis("off")
ax2.title.set_text("Flipped")
if not gray_scale:
ax1.imshow(image_rgb)
ax2.imshow(image_flip)
else:
ax1.imshow(image_rgb, cmap='gray')
ax2.imshow(image_flip, cmap='gray')
return True
return image_flip
```

The above function returns an image matrix that is reversed or flipped column-wise from up to down.

Let's plot the same -

```
flip_this(image_file='lena_original.png', with_plot=True)
```

```
flip_this(image_file='lena_original.png', gray_scale=True, with_plot=True)
```

```
class ImageOpsFromScratch(object):
def __init__(self, image_file):
self.image_file = image_file
def read_this(self, gray_scale=False):
image_src = cv2.imread(self.image_file)
if gray_scale:
image_rgb = cv2.cvtColor(image_src, cv2.COLOR_BGR2GRAY)
else:
image_rgb = cv2.cvtColor(image_src, cv2.COLOR_BGR2RGB)
return image_rgb
def mirror_this(self, with_plot=True, gray_scale=False):
image_rgb = self.read_this(gray_scale=gray_scale)
image_mirror = np.fliplr(image_rgb)
if with_plot:
self.plot_it(orig_matrix=image_rgb, trans_matrix=image_mirror, head_text='Mirrored', gray_scale=gray_scale)
return None
return image_mirror
def flip_this(self, with_plot=True, gray_scale=False):
image_rgb = self.read_this(gray_scale=gray_scale)
image_flip = np.flipud(image_rgb)
if with_plot:
self.plot_it(orig_matrix=image_rgb, trans_matrix=image_flip, head_text='Flipped', gray_scale=gray_scale)
return None
return image_flip
def plot_it(self, orig_matrix, trans_matrix, head_text, gray_scale=False):
fig = plt.figure(figsize=(10, 20))
ax1 = fig.add_subplot(2, 2, 1)
ax1.axis("off")
ax1.title.set_text('Original')
ax2 = fig.add_subplot(2, 2, 2)
ax2.axis("off")
ax2.title.set_text(head_text)
if not gray_scale:
ax1.imshow(orig_matrix)
ax2.imshow(trans_matrix)
else:
ax1.imshow(orig_matrix, cmap='gray')
ax2.imshow(trans_matrix, cmap='gray')
return True
```

```
imo = ImageOpsFromScratch(image_file='lena_original.png')
### Mirroring ###
imo.mirror_this()
imo.mirror_this(gray_scale=True)
### Flipping ###
imo.flip_this()
imo.flip_this(gray_scale=True)
```

The above image results will be displayed. Now that everything is sorted we can create other image operations like `equalize()`

, `solarize()`

etc. I have not covered that in this article but definitely in the next one.

- By implementing this, I have learned and understood the logic behind various image operations and how to put that in the form of code.

We can also do the same using a well-known image library `Pillow`

. But, as a developer, we need to know things work from start to end. This is something where everyone should be curious about.

Please do read my another article related to image processing and computer vision where I explain *image convolution from scratch*.

People who are experienced in writing code will laugh and say, "Dude! common, it is all the compiler or an interpreter that performs the magic in making the computer understand the code".

**Credits of Cover Image** - Photo by Joshua Sortino on Unsplash

Yes, the compiler or an interpreter plays a huge role in converting the code in `0`

s' and `1`

s'. You heard it right, the computer is only able to understand `0`

s' and `1`

s' which either way can be represented as `True`

for `1`

and `False`

for `0`

.

We can think of Compiler as a program that is intended to translate or convert the written code into machine code or byte code which is then used by the system to execute the program and display the output. The compiler converts the source code (written program) into machine code in one single go. Oftentimes, machine code is also termed as object code or byte code, or executable code.

Like the compiler, the interpreter is also a program whose main aim is to convert the written code into machine code. But, the conversion happens on the line by line basis, not in one single go.

**Note** - Both compiler and an interpreter perform the same job except that compiler converts the source code (written program) into machine code in one single go whereas an interpreter does it on a line-by-line basis.

To understand this, in more detail - we can take a real-life example that we might have experienced at some point in time. Suppose you are suffering from a fever, you go to a doctor for getting treated. The doctor will check and gives a prescription. Now, when you try to understand what he has prescribed, you will never be able to contemplate that as the handwriting of the doctor would be hard to understand. But, if you take the same prescription and consult the nearby chemist, he/she will be able to give you proper medicines and advice you when and how to consume them.

In this scenario,

- Doctor is the programmer.
- The doctor is writing a program (prescription) to solve your problem.

- You are the computer.
- You only understand English which is written normally as how computers only understand
`0`

s' and`1`

s'.

- You only understand English which is written normally as how computers only understand
- Chemist is the compiler or an interpreter.
- Chemist is helping you in the translation of the prescription into medicines.

- You will execute (consume) the medication as how computers execute the actions once the source code is compiled or interpreted.

Well, that's it. This article gives a brief idea of the terms **compiler** and an **interpreter** and how they both play a major role in converting or translating the source code into machine code, which is later on executed.

**Credits of Cover Image** - Photo by Nicolas Van Leekwijck on Unsplash

As a programmer or a developer, we usually think of solving this problem by considering a random `n`

number of nodes or cities and assign a random distance or cost value to each node or city respectively. We opt for a **Dynamic Programming** approach where a certain problem is broken down into sub-problems and utilizing the fact that the optimal solution to the overall problem depends upon the optimal solution to its sub-problems. Recursive methods are applied subsequently to solve each sub-problem and thus solving the original problem.

To understand how to implement this programmatically, please read my medium blog where I explain the procedure in detail. I have implemented the same but taken to next level by considering the authentic place coordinates and calculating `haversine distance`

for getting approximate distance between any two places.

The link to my python package is below. The current version of the package is `0.2.2`

.

You can install the package via `pip`

.

```
pip install dora-explorer --user
```

`import`

the package```
from dora_explorer.distance_locator import DistanceLocator
from dora_explorer.travel_places import GeoTraveller
from dora_explorer.tiny_explore import DoraTheExplorer
```

```
dis_loc = DistanceLocator()
geo_travel = GeoTraveller()
```

The default distance is taken in `km`

. Allowed options are -

- Miles
- Meters
- Feet and
- Yards

```
from_place = 'singapore'
to_place = 'new zealand'
```

Distance checking

```
## kilometers
from_to_dis_km = dis_loc.get_distance(from_=from_place, to_=to_place)
print("The distance in kms from {} to {} - {} kms".format(from_place, to_place, from_to_dis_km))
## miles
from_to_dis_miles = dis_loc.get_distance(from_=from_place, to_=to_place, in_miles=True)
print("The distance in miles from {} to {} - {} miles".format(from_place, to_place, from_to_dis_miles))
## meters
from_to_dis_meters = dis_loc.get_distance(from_=from_place, to_=to_place, in_meters=True)
print("The distance in meters from {} to {} - {} meters".format(from_place, to_place, from_to_dis_meters))
## feet
from_to_dis_feet = dis_loc.get_distance(from_=from_place, to_=to_place, in_feet=True)
print("The distance in feet from {} to {} - {} feet".format(from_place, to_place, from_to_dis_feet))
## yards
from_to_dis_yards = dis_loc.get_distance(from_=from_place, to_=to_place, in_yards=True)
print("The distance in yards from {} to {} - {} yards".format(from_place, to_place, from_to_dis_yards))
```

Output

```
'The distance in kms from singapore to new zealand - 8358.12 kms'
'The distance in miles from singapore to new zealand - 5194.6 miles'
'The distance in meters from singapore to new zealand - 8358116.45 meters'
'The distance in feet from singapore to new zealand - 27422980.08 feet'
'The distance in yards from singapore to new zealand - 9143779.4 yards'
```

We can also check the distance plot from `from_place`

to `to_place`

in the following way. But before doing this, it is preferred to have MapBox API which can be obtained from the official website. I have saved my API in a `mapbox_api.txt`

file and read the same for further usage.

`read()`

the MapBox API```
with open(file='mapbox_api.txt', mode='r') as api_file:
map_api = api_file.read()
```

There are two main conditions to get the appropriate distance plot.

If `geo_token=map_api`

then

- If
`from_place`

and`to_place`

belong to the**same country**, then`with_map=True`

and`with_directions=True`

.

```
## belong to the same country - India
from_place = 'delhi'
to_place = 'bengaluru'
dis_loc.get_distance_plot(
from_=from_place,
to_=to_place,
with_map=True,
with_directions=True,
geo_token=map_api
)
```

- If
`from_place`

and`to_place`

belong to**differernt countries**, then`with_map=True`

and`with_directions=False`

.

```
from_place = 'singapore'
to_place = 'new zealand'
dis_loc.get_distance_plot(
from_=from_place,
to_=to_place,
with_map=True,
with_directions=False,
geo_token=map_api
)
```

If `geo_token=None`

or `geo_token=""`

then `with_map=False`

and `with_directions=False`

.

- The distance plot will be simply a line plot from two coordinates (latitudes and longitudes).

```
from_place = 'delhi'
to_place = 'bengaluru'
dis_loc.get_distance_plot(from_=from_place, to_=to_place)
```

For this, a `list`

of places should be given as input to the package. There are two possible conditions to get the acquired result

- If the
`list`

of places belong to the**same country**considering`geo_token=map_api`

.

**Spain country example** -

```
place_list = ["madrid", "barcelona", "alicante", "palma"]
explore = DoraTheExplorer(place_list=place_list)
explore.get_path(
source_city='palma',
with_map=True,
with_directions=True,
geo_token=map_api
)
```

Output

```
'plot is saved successfully ...'
'palma >> barcelona >> madrid >> alicante >> palma'
```

**Note** - if `geo_token=None`

and all the places belong to the **same country**, then a line plot is plotted after joining the coordinates (latitudes and longitudes).

**Minimum distance to cover all the places**

```
## kilometers
min_distance_km = explore.get_min_dis(source_city='palma')
print("The minimun distance to cover {} in kms - {}".format(place_list, min_distance_km))
## miles
min_distance_miles = explore.get_min_dis(source_city='palma', in_miles=True)
print("The minimum distance to cover {} in miles - {}".format(place_list, min_distance_miles))
## meters
min_distance_meters = explore.get_min_dis(source_city='palma', in_meters=True)
print("The minimum distance to cover {} in meters - {}".format(place_list, min_distance_meters))
## feet
min_distance_feet = explore.get_min_dis(source_city='palma', in_feet=True)
print("The minimum distance to cover {} in feet - {}".format(place_list, min_distance_feet))
## yards
min_distance_yards = explore.get_min_dis(source_city='palma', in_yards=True)
print("The minimum distance to cover {} in yards - {}".format(place_list, min_distance_yards))
```

Output

```
"The minimun distance to cover ['madrid', 'barcelona', 'alicante', 'palma'] in kms - 1372.78"
"The minimum distance to cover ['madrid', 'barcelona', 'alicante', 'palma'] in miles - 853.19"
"The minimum distance to cover ['madrid', 'barcelona', 'alicante', 'palma'] in meters - 1372780.0"
"The minimum distance to cover ['madrid', 'barcelona', 'alicante', 'palma'] in feet - 4504091.18"
"The minimum distance to cover ['madrid', 'barcelona', 'alicante', 'palma'] in yards - 1501821.32"
```

- If the
`list`

of places belong to**different countries**considering`geo_token=map_api`

or`geo_token=None`

.

**Multi-country example** -

```
place_list = ['london', 'madrid', 'bengaluru', 'delhi']
explore = DoraTheExplorer(place_list=place_list)
explore.get_path(
source_city='london',
with_map=True,
with_directions=True,
geo_token=map_api
)
```

Output

```
"Cannot find the shortest path, as the cities ['london', 'madrid', 'bengaluru', 'delhi'] do not belong to the same country."
```

**Minimum distance to cover all the places**

```
min_distance = explore.get_min_dis(source_city='london')
print(min_distance)
```

Output

```
"Cannot find the minimum distance to cover the cities ['london', 'madrid', 'bengaluru', 'delhi'], as they do not belong to the same country."
```

- The package is still under development. Quite a few features and bugs need to be resolved.
- If you have liked my package, you can star it on the GitHub project page.
- If you would like to contribute and want to add new features, you are welcome.
- If you would like to report an issue, I would be happy to check and resolve the same.