You can filter using contour area then apply morpholgical closing to fill the small holes in the image. Here's the result:

import cv2
# Load image, convert to grayscale, Gaussian blur, Otsu's threshold
image = cv2.imread('1.jpg')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (3,3), 0)
thresh = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
# Filter using contour area and remove small noise
cnts = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
for c in cnts:
area = cv2.contourArea(c)
if area < 5500:
cv2.drawContours(thresh, [c], -1, (0,0,0), -1)
# Morph close and invert image
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5,5))
close = 255 - cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel, iterations=2)
cv2.imshow('thresh', thresh)
cv2.imshow('close', close)
cv2.waitKey()
Answer from nathancy on Stack OverflowYou can filter using contour area then apply morpholgical closing to fill the small holes in the image. Here's the result:

import cv2
# Load image, convert to grayscale, Gaussian blur, Otsu's threshold
image = cv2.imread('1.jpg')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (3,3), 0)
thresh = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
# Filter using contour area and remove small noise
cnts = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
for c in cnts:
area = cv2.contourArea(c)
if area < 5500:
cv2.drawContours(thresh, [c], -1, (0,0,0), -1)
# Morph close and invert image
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5,5))
close = 255 - cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel, iterations=2)
cv2.imshow('thresh', thresh)
cv2.imshow('close', close)
cv2.waitKey()
From the Documentation for bwareaopen you can find the algorithm that is used in the method, which is:
Determine the connected components:
CC = bwconncomp(BW, conn);
Compute the area of each component:
S = regionprops(CC, 'Area');
Remove small objects:
L = labelmatrix(CC);
BW2 = ismember(L, find([S.Area] >= P));
You could simply follow these steps to get to the result.
Removing small objects
python - Remove small objects from binary image with skimage - Stack Overflow
Mailman 3 Can morphology.remove_small_objects be used to remove small noise? - scikit-image - python.org
python - How to remove small connected objects using OpenCV - Stack Overflow
Use cv2.connectedComponentsWithStats (doc):
import cv2
import numpy as np
# Start by finding all of the connected components (white blobs in your image).
# 'im' needs to be grayscale and 8bit.
nb_blobs, im_with_separated_blobs, stats, _ = cv2.connectedComponentsWithStats(im)
# im_with_separated_blobs is an image where each detected blob has a different pixel value ranging from 1 to nb_blobs - 1.
# The background pixels have value 0.
im_with_separated_blobs looks like this :

# 'stats' (and the silenced output 'centroids') provides information about the blobs. See the docs for more information.
# Here, we're interested only in the size of the blobs :
sizes = stats[:, cv2.CC_STAT_AREA]
# You can also directly index the column with '-1' instead of 'cv2.CC_STAT_AREA' as it's the last column.
# A small gotcha is that the background is considered as a blob, and so its stats are included in the stats vector at position 0.
# minimum size of particles we want to keep (number of pixels).
# here, it's a fixed value, but you can set it as you want, eg the mean of the sizes or whatever.
min_size = 150
# create empty output image with will contain only the biggest composents
im_result = np.zeros_like(im_with_separated_blobs)
# for every component in the image, keep it only if it's above min_size.
# we start at 1 to avoid considering the background
for index_blob in range(1, nb_blobs):
if sizes[index_blob] >= min_size:
im_result[im_with_separated_blobs == index_blob] = 255
im_result looks like this : 
Bonus : An alternative way of coding the same process, taking advantage of indexing a numpy array with another array and of the numpy.where function. It's probably more Pythonic insofar that it avoids the for loop, but is also possibly less flexible rearding other filtering conditions (and perhaps less readable) :
_, im_with_separated_blobs, stats, _ = cv2.connectedComponentsWithStats(im)
sizes = stats[:, cv2.CC_STAT_AREA]
im_result = np.where(sizes[im_with_separated_blobs] >= min_size, im, 0)
The key here is to rely on the indexing of a numpy array with a 2D array. sizes[im_with_separated_blobs] is a 2D numpy array with the same shape as im_with_separated_blobs, but where every value has been replaced by sizes[value], i.e. the size of that blob.
Removing small connected components by area is called area opening. OpenCV does not have this as a function, it can be implemented as shown in other answers. But most other image processing packages will have an area opening function.
For example using scikit-image:
import skimage
import imageio.v3 as iio
img = iio.imread('cQMZm.png')[:,:,0]
out = skimage.morphology.area_opening(img, area_threshold=150, connectivity=2)
For example using DIPlib:
import diplib as dip
out = dip.AreaOpening(img, filterSize=150, connectivity=2)
PS: The DIPlib implementation is noticeably faster. Disclaimer: I'm an author of DIPlib.

