ساخت یک OCR ساده تشخیص اعداد انگلیسی به کمک OpenCV
این مطلب را میتوان به عنوان جمع بندی مطالبی که تاکنون بررسی شدند درنظر گرفت و در اساس مطلب جدیدی ندارد و صرفا ترکیب یک سری تکنیک است؛ برای مثال:
چطور یک تصویر را به نمونهی سیاه و سفید آن تبدیل کنیم؟
کار با متد Threshold جهت بهبود کیفیت یک تصویر جهت تشخیص اشیاء
تشخیص کانتورها (Contours) و اشیاء موجود در یک تصویر
آشنایی با نحوهی گروه بندی تصاویر مشابه و مفاهیمی مانند برچسبهای تصاویر که بیانگر یک گروه از تصاویر هستند.
تهیه تصاویر اعداد انگلیسی جهت آموزش دادن به الگوریتم CvKNearest
در اینجا نیز از یکی دیگر از الگوریتمهای machine learning موجود در OpenCV به نام CvKNearestبرای تشخیص اعداد انگلیسی استفاده خواهیم کرد. این الگوریتم نزدیکترین همسایهی اطلاعاتی مفروض را در گروهی از دادههای آموزش داده شدهی به آن پیدا میکند. خروجی آن شمارهی این گروه است. بنابراین نحوهی طبقهی بندی اطلاعات در اینجا چیزی شبیه به شکل زیر خواهد بود:
مجموعهای از تصاویر 0 تا 9 را جمع آوری کردهایم. هر کدام از پوشهها، بیانگر اعدادی از یک خانواده هستند. این تصویر را با فرمت ذیل جمع آوری میکنیم:
به این ترتیب
در متد خواندن تصاویر آموزشی، ابتدا پوشههای اصلی مسیر Numbers تصویر ابتدای بحث دریافت میشوند. سپس نام هر پوشه، شمارهی گروه تصاویر موجود در آن پوشه را تشکیل خواهد داد. به این نام در الگوریتمهای machine leaning، کلاس هم گفته میشود. سپس هر تصویر را با فرمت سیاه و سفید بارگذاری کرده و به لیست تصاویر موجود اضافه میکنیم. در اینجا از متد processTrainingImage نیز استفاده شدهاست. هدف از آن بهبود کیفیت تصویر دریافتی جهت کار تشخیص اشیاء است:
عملیات صورت گرفتهی در این متد را با تصویر ذیل بهتر میتوان توضیح داد:
ابتدا تصویر اصلی بارگذاری میشود؛ همان تصویر سمت چپ. سپس با استفاده از متد Threshold، شدت نور نواحی مختلف آن یکسان شده و آماده میشود برای تشخیص کانتورهای موجود در آن. در ادامه با استفاده از متد FindContours، شیء مرتبط با عدد جاری یافت میشود. سپس متد Cv2.BoundingRect مستطیل دربرگیرندهی این شیء را تشخیص میدهد (تصویر سمت راست). بر این اساس میتوان تصویر اصلی ورودی را به یک تصویر کوچکتر که صرفا شامل ناحیهی عدد مدنظر است، تبدیل کرد. در ادامه برای کار با الگوریتم CvKNearest نیاز است تا این تصویر بهبود یافته را تبدیل به یک ماتریس یک بعدی کردی که روش انجام کار توسط متد Reshape مشاهده میکنید.
از همین روش پردازش و بهبود تصویر ورودی، جهت پردازش اعداد یافت شدهی در یک تصویر با تعداد زیادی عدد نیز استفاده خواهیم کرد.
آموزش دادن به الگوریتم CvKNearest
تا اینجا تصاویر گروه بندی شدهای را خوانده و لیستی از آنها را مطابق فرمت الگوریتم CvKNearest تهیه کردیم. مرحلهی بعد، معرفی این لیست به متد Train این الگوریتم است:
متد Train دو ورودی دارد. ورودی اول آن یک تصویر است که باید از طریق متد PushBack کلاس Mat تهیه شود. بنابراین لیست تصاویر اصلی را تبدیل به لیستی از Matها خواهیم کرد.
سپس نیاز است لیست گروههای متناظر با تصاویر اعداد را تبدیل به فرمت مورد انتظار متد Train کنیم. در اینجا صرفا لیستی از اعداد صحیح را داریم. این لیست نیز باید تبدیل به یک Mat شود که روش انجام آن در متد فوق بیان شدهاست. کلاس Mat سازندهی مخصوصی را جهت تبدیل لیست اعداد، به همراه دارد. این Mat نیز باید تبدیل به یک ماتریس یک بعدی شود که برای این منظور از متد Reshape استفاده شدهاست.
انجام عملیات OCR نهایی
پس از تهیهی لیستی از تصاویر و آموزش دادن آنها به الگوریتم CvKNearest، تنها کاری که باید انجام دهیم، یافتن اعداد در تصویر نمونهی مدنظر و سپس معرفی آن به متد FindNearest الگوریتم CvKNearest است. روش انجام اینکار بسیار شبیه است به روش معرفی شده در متد processTrainingImage که پیشتر بررسی شد:
این عملیات به صورت خلاصه در تصویر ذیل مشخص شدهاست:
ابتدا تصویر اصلی که قرار است عملیات OCR روی آن صورت گیرد، بارگذاری میشود. سپس کانتورها و اعداد موجود در آن تشخیص داده میشوند. مستطیلهای قرمز رنگ در برگیرندهی این اعداد را در تصویر دوم مشاهده میکنید. سپس این کانتورهای یافت شده را که شامل یکی از اعداد تشخیص داده شدهاست، تبدیل به یک ماتریس یک بعدی کرده و به متد FindNearest ارسال میکنیم. خروجی آن نام گروه یا پوشهای است که این عدد در آن قرار دارد. در همینجا این خروجی را تبدیل به یک رشته کرده و در تصویر سوم با رنگ سبز رنگ نمایش میدهیم.
بنابراین در این تصویر، پنجرهی segmented image، همان اشیاء تشخیص داده شدهی از تصویر اصلی هستند.
پنجرهی با زمینهی سیاه رنگ، نتیجهی نهایی OCR است که نسبتا هم دقیق عمل کردهاست.
کدهای کامل این مثال را از اینجامیتوانید دریافت کنید.
این مطلب را میتوان به عنوان جمع بندی مطالبی که تاکنون بررسی شدند درنظر گرفت و در اساس مطلب جدیدی ندارد و صرفا ترکیب یک سری تکنیک است؛ برای مثال:
چطور یک تصویر را به نمونهی سیاه و سفید آن تبدیل کنیم؟
کار با متد Threshold جهت بهبود کیفیت یک تصویر جهت تشخیص اشیاء
تشخیص کانتورها (Contours) و اشیاء موجود در یک تصویر
آشنایی با نحوهی گروه بندی تصاویر مشابه و مفاهیمی مانند برچسبهای تصاویر که بیانگر یک گروه از تصاویر هستند.
تهیه تصاویر اعداد انگلیسی جهت آموزش دادن به الگوریتم CvKNearest
در اینجا نیز از یکی دیگر از الگوریتمهای machine learning موجود در OpenCV به نام CvKNearestبرای تشخیص اعداد انگلیسی استفاده خواهیم کرد. این الگوریتم نزدیکترین همسایهی اطلاعاتی مفروض را در گروهی از دادههای آموزش داده شدهی به آن پیدا میکند. خروجی آن شمارهی این گروه است. بنابراین نحوهی طبقهی بندی اطلاعات در اینجا چیزی شبیه به شکل زیر خواهد بود:
مجموعهای از تصاویر 0 تا 9 را جمع آوری کردهایم. هر کدام از پوشهها، بیانگر اعدادی از یک خانواده هستند. این تصویر را با فرمت ذیل جمع آوری میکنیم:
public class ImageInfo { public Mat Image { set; get; } public int ImageGroupId { set; get; } public int ImageId { set; get; } }
public IList<ImageInfo> ReadTrainingImages(string path, string ext) { var images = new List<ImageInfo>(); var imageId = 1; foreach (var dir in new DirectoryInfo(path).GetDirectories()) { var groupId = int.Parse(dir.Name); foreach (var imageFile in dir.GetFiles(ext)) { var image = processTrainingImage(new Mat(imageFile.FullName, LoadMode.GrayScale)); if (image == null) { continue; } images.Add(new ImageInfo { Image = image, ImageId = imageId++, ImageGroupId = groupId }); } } return images; }
private static Mat processTrainingImage(Mat gray) { var threshImage = new Mat(); Cv2.Threshold(gray, threshImage, Thresh, ThresholdMaxVal, ThresholdType.BinaryInv); // Threshold to find contour Point[][] contours; HiearchyIndex[] hierarchyIndexes; Cv2.FindContours( threshImage, out contours, out hierarchyIndexes, mode: ContourRetrieval.CComp, method: ContourChain.ApproxSimple); if (contours.Length == 0) { return null; } Mat result = null; var contourIndex = 0; while ((contourIndex >= 0)) { var contour = contours[contourIndex]; var boundingRect = Cv2.BoundingRect(contour); //Find bounding rect for each contour var roi = new Mat(threshImage, boundingRect); //Crop the image //Cv2.ImShow("src", gray); //Cv2.ImShow("roi", roi); //Cv.WaitKey(0); var resizedImage = new Mat(); var resizedImageFloat = new Mat(); Cv2.Resize(roi, resizedImage, new Size(10, 10)); //resize to 10X10 resizedImage.ConvertTo(resizedImageFloat, MatType.CV_32FC1); //convert to float result = resizedImageFloat.Reshape(1, 1); contourIndex = hierarchyIndexes[contourIndex].Next; } return result; }
ابتدا تصویر اصلی بارگذاری میشود؛ همان تصویر سمت چپ. سپس با استفاده از متد Threshold، شدت نور نواحی مختلف آن یکسان شده و آماده میشود برای تشخیص کانتورهای موجود در آن. در ادامه با استفاده از متد FindContours، شیء مرتبط با عدد جاری یافت میشود. سپس متد Cv2.BoundingRect مستطیل دربرگیرندهی این شیء را تشخیص میدهد (تصویر سمت راست). بر این اساس میتوان تصویر اصلی ورودی را به یک تصویر کوچکتر که صرفا شامل ناحیهی عدد مدنظر است، تبدیل کرد. در ادامه برای کار با الگوریتم CvKNearest نیاز است تا این تصویر بهبود یافته را تبدیل به یک ماتریس یک بعدی کردی که روش انجام کار توسط متد Reshape مشاهده میکنید.
از همین روش پردازش و بهبود تصویر ورودی، جهت پردازش اعداد یافت شدهی در یک تصویر با تعداد زیادی عدد نیز استفاده خواهیم کرد.
آموزش دادن به الگوریتم CvKNearest
تا اینجا تصاویر گروه بندی شدهای را خوانده و لیستی از آنها را مطابق فرمت الگوریتم CvKNearest تهیه کردیم. مرحلهی بعد، معرفی این لیست به متد Train این الگوریتم است:
public CvKNearest TrainData(IList<ImageInfo> trainingImages) { var samples = new Mat(); foreach (var trainingImage in trainingImages) { samples.PushBack(trainingImage.Image); } var labels = trainingImages.Select(x => x.ImageGroupId).ToArray(); var responses = new Mat(labels.Length, 1, MatType.CV_32SC1, labels); var tmp = responses.Reshape(1, 1); //make continuous var responseFloat = new Mat(); tmp.ConvertTo(responseFloat, MatType.CV_32FC1); // Convert to float var kNearest = new CvKNearest(); kNearest.Train(samples, responseFloat); // Train with sample and responses return kNearest; }
سپس نیاز است لیست گروههای متناظر با تصاویر اعداد را تبدیل به فرمت مورد انتظار متد Train کنیم. در اینجا صرفا لیستی از اعداد صحیح را داریم. این لیست نیز باید تبدیل به یک Mat شود که روش انجام آن در متد فوق بیان شدهاست. کلاس Mat سازندهی مخصوصی را جهت تبدیل لیست اعداد، به همراه دارد. این Mat نیز باید تبدیل به یک ماتریس یک بعدی شود که برای این منظور از متد Reshape استفاده شدهاست.
انجام عملیات OCR نهایی
پس از تهیهی لیستی از تصاویر و آموزش دادن آنها به الگوریتم CvKNearest، تنها کاری که باید انجام دهیم، یافتن اعداد در تصویر نمونهی مدنظر و سپس معرفی آن به متد FindNearest الگوریتم CvKNearest است. روش انجام اینکار بسیار شبیه است به روش معرفی شده در متد processTrainingImage که پیشتر بررسی شد:
public void DoOCR(CvKNearest kNearest, string path) { var src = Cv2.ImRead(path); Cv2.ImShow("Source", src); var gray = new Mat(); Cv2.CvtColor(src, gray, ColorConversion.BgrToGray); var threshImage = new Mat(); Cv2.Threshold(gray, threshImage, Thresh, ThresholdMaxVal, ThresholdType.BinaryInv); // Threshold to find contour Point[][] contours; HiearchyIndex[] hierarchyIndexes; Cv2.FindContours( threshImage, out contours, out hierarchyIndexes, mode: ContourRetrieval.CComp, method: ContourChain.ApproxSimple); if (contours.Length == 0) { throw new NotSupportedException("Couldn't find any object in the image."); } //Create input sample by contour finding and cropping var dst = new Mat(src.Rows, src.Cols, MatType.CV_8UC3, Scalar.All(0)); var contourIndex = 0; while ((contourIndex >= 0)) { var contour = contours[contourIndex]; var boundingRect = Cv2.BoundingRect(contour); //Find bounding rect for each contour Cv2.Rectangle(src, new Point(boundingRect.X, boundingRect.Y), new Point(boundingRect.X + boundingRect.Width, boundingRect.Y + boundingRect.Height), new Scalar(0, 0, 255), 2); var roi = new Mat(threshImage, boundingRect); //Crop the image var resizedImage = new Mat(); var resizedImageFloat = new Mat(); Cv2.Resize(roi, resizedImage, new Size(10, 10)); //resize to 10X10 resizedImage.ConvertTo(resizedImageFloat, MatType.CV_32FC1); //convert to float var result = resizedImageFloat.Reshape(1, 1); var results = new Mat(); var neighborResponses = new Mat(); var dists = new Mat(); var detectedClass = (int)kNearest.FindNearest(result, 1, results, neighborResponses, dists); //Console.WriteLine("DetectedClass: {0}", detectedClass); //Cv2.ImShow("roi", roi); //Cv.WaitKey(0); //Cv2.ImWrite(string.Format("det_{0}_{1}.png",detectedClass, contourIndex), roi); Cv2.PutText( dst, detectedClass.ToString(CultureInfo.InvariantCulture), new Point(boundingRect.X, boundingRect.Y + boundingRect.Height), 0, 1, new Scalar(0, 255, 0), 2); contourIndex = hierarchyIndexes[contourIndex].Next; } Cv2.ImShow("Segmented Source", src); Cv2.ImShow("Detected", dst); Cv2.ImWrite("dest.jpg", dst); Cv2.WaitKey(); }
ابتدا تصویر اصلی که قرار است عملیات OCR روی آن صورت گیرد، بارگذاری میشود. سپس کانتورها و اعداد موجود در آن تشخیص داده میشوند. مستطیلهای قرمز رنگ در برگیرندهی این اعداد را در تصویر دوم مشاهده میکنید. سپس این کانتورهای یافت شده را که شامل یکی از اعداد تشخیص داده شدهاست، تبدیل به یک ماتریس یک بعدی کرده و به متد FindNearest ارسال میکنیم. خروجی آن نام گروه یا پوشهای است که این عدد در آن قرار دارد. در همینجا این خروجی را تبدیل به یک رشته کرده و در تصویر سوم با رنگ سبز رنگ نمایش میدهیم.
بنابراین در این تصویر، پنجرهی segmented image، همان اشیاء تشخیص داده شدهی از تصویر اصلی هستند.
پنجرهی با زمینهی سیاه رنگ، نتیجهی نهایی OCR است که نسبتا هم دقیق عمل کردهاست.
کدهای کامل این مثال را از اینجامیتوانید دریافت کنید.