I'll just write the part where the pre-processing could be improved. Again, the colour of the text is very easy to select and threshold on it with cv2.inRange:
im = cv2.imread("text.png") # read image
lower = (60, 60, 0) # define lower limit
upper = (100, 100, 40) # and upper limit
mask = cv2.inRange(im, lower, upper) # use cv2.inRange
maskClosed = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, np.ones((3,3), dtype = np.uint8)) # use morph close to fill the holes in the mask
plt.imshow(maskClosed) # show mask
The result:

You can use this instead of your current pre-processing part. The letters should be very easily separated from each other.
Answer from Tino D on Stack OverflowI'll just write the part where the pre-processing could be improved. Again, the colour of the text is very easy to select and threshold on it with cv2.inRange:
im = cv2.imread("text.png") # read image
lower = (60, 60, 0) # define lower limit
upper = (100, 100, 40) # and upper limit
mask = cv2.inRange(im, lower, upper) # use cv2.inRange
maskClosed = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, np.ones((3,3), dtype = np.uint8)) # use morph close to fill the holes in the mask
plt.imshow(maskClosed) # show mask
The result:

You can use this instead of your current pre-processing part. The letters should be very easily separated from each other.
As you can see in the output below the letters 'S' and 'T' in the word 'STAND' are bounded together and I can't understand what i've done wrong, will be glad if you could help me guys.
The problem can be fixed.
Change iterations=3 on line 73:
dilated = cv2.dilate(eroded, kernel, iterations=3)
To:
dilated = cv2.dilate(eroded, kernel, iterations=1) #Change index to 1
Screenshot:

A great tutorial on the first step you described is available at pyimagesearch (and they have great tutorials in general)
In short, as described by Ella, you would have to use cv2.CHAIN_APPROX_SIMPLE. A slightly more robust method would be to use cv2.RETR_LIST instead of cv2.RETR_EXTERNAL and then sort the areas, as it should decently work even in white backgrounds/if the page inscribes a bigger shape in the background, etc.
Coming to the second part of your question, a good way to segment the characters would be to use the Maximally stable extremal region extractor available in OpenCV. A complete implementation in CPP is available here in a project I was helping out in recently. The Python implementation would go along the lines of (Code below works for OpenCV 3.0+. For the OpenCV 2.x syntax, check it up online)
import cv2
img = cv2.imread('test.jpg')
mser = cv2.MSER_create()
#Resize the image so that MSER can work better
img = cv2.resize(img, (img.shape[1]*2, img.shape[0]*2))
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
vis = img.copy()
regions = mser.detectRegions(gray)
hulls = [cv2.convexHull(p.reshape(-1, 1, 2)) for p in regions[0]]
cv2.polylines(vis, hulls, 1, (0,255,0))
cv2.namedWindow('img', 0)
cv2.imshow('img', vis)
while(cv2.waitKey()!=ord('q')):
continue
cv2.destroyAllWindows()
This gives the output as

Now, to eliminate the false positives, you can simply cycle through the points in hulls, and calculate the perimeter (sum of distance between all adjacent points in hulls[i], where hulls[i] is a list of all points in one convexHull). If the perimeter is too large, classify it as not a character.
The diagnol lines across the image are coming because the border of the image is black. that can simply be removed by adding the following line as soon as the image is read (below line 7)
img = img[5:-5,5:-5,:]
which gives the output

The option on the top of my head requires the extractions of 4 corners of the skewed image. This is done by using cv2.CHAIN_APPROX_SIMPLE instead of cv2.CHAIN_APPROX_NONE when finding contours. Afterwards, you could use cv2.approxPolyDP and hopefully remain with the 4 corners of the receipt (If all your images are like this one then there is no reason why it shouldn't work).
Now use cv2.findHomography and cv2.wardPerspective to rectify the image according to source points which are the 4 points extracted from the skewed image and destination points that should form a rectangle, for example the full image dimensions.
Here you could find code samples and more information: OpenCV-Geometric Transformations of Images
Also this answer may be useful - SO - Detect and fix text skew
EDIT: Corrected the second chain approx to cv2.CHAIN_APPROX_NONE.
I think you should follow this where show that Sorting Contours using Python and OpenCV.
The basic steps that I follow are:
- Blur the image and if necessary convert to gray scale first.
- Apply the Canny edge detection algorithm to find the outline of every character.
- Pass the edge detected image to the Adaptive Algorithm which works better considering the neighbouring points.
- Perform dilation which performs better on line segmentation.
- Perform the line segmentation on the copy of dilated image which produces the segments randomly.
- Finally sort the segment in a order "Top to bottom".
If you want to send all the sorted (assuming sorting means: top-left to bottom-right) cropped-images of the input image given by the cv2.findContours() to the recognition model, you do not need to pass the ctrs through the sorted function. cv2.findContours() gives all the ctrs in bottom-right to top-left order. Just reverse it using reversed function or you can loop over all the ctrs in a reverse way. After this, use cv2.boundingrect() followed cropping with python slicing and save it in a list. Finally, send the list to the recognition model to recognition line-by-line.












This also works with handwritten text images with lines that are not perfectly horizontal: