This is also based on the difference of gray levels of noise pixels and the text. You can adjust threshold manually. May be you can arrive at a reasonable auto-threshold by taking into account the distribution of the noise and text pixels values. Below I use such threshold, which is the mean - std
. It works for the given image, but not sure if it'll work in general.
im = cv2.imread('XKRut.jpg')
gray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)
# calculate auto-threshold value
# noise and text pixels
_, bw = cv2.threshold(cv2.GaussianBlur(gray, (3, 3), 0), 0, 255, cv2.THRESH_BINARY_INV|cv2.THRESH_OTSU)
# find mean and std of noise and text pixel values
mu, sd = cv2.meanStdDev(gray, mask=bw)
simple = gray.copy()
# apply the auto-threshold to clean the image
thresh = mu - sd
simple[simple > thresh] = 255
simple[simple <= thresh] = 0
simple2 = simple.copy()
# clean it further considering text-like pixel density in a 3x3 window
# using avg = ~cv2.blur(simple, (3, 3)) is more intuitive, but Gaussian
# kernel gives more weight to center pixel
avg = ~cv2.GaussianBlur(simple, (3, 3), 0)
# need more than 3 text-like pixels in the 3x3 window to classify the center pixel
# as text. otherwise it is noise. this definition is more relevant to box kernel
thresh2 = 255*3/9
simple2[avg < thresh2] = 255
Cleaned image:

Clean image taking into account the pixel density:

If your images don't have a huge variation in their pixel values, you can pre-calculate a best threshold, or an alpha
, beta
pair for zindarod's solution.