به نام خدا

سلام خدمت هموطنان عزیز و دوستان گرامی!

قصد دارم به توضیح کد خواندن بارکد با OpenCV در C++ بپردازم.

منبع من این سایت بود. بعد از اینکه مطالبش رو خوندم و کدش رو تست کردم شروع کردم برای خودم کد زدن و قصد دارم کد خودم رو توضیح بدم! به نظر میرسه کد من عملکرد بهتری در خواندن بار کد ها داره!

به این نکته توجه کنید که بار کدی که ما میخونیم بارکد UPC هست که روی کالا ها زده میشه و 12 رقم داره!

خوب ابتدا یه سری توضیح در مورد بارکد UPC بدم بعد به سراغ کد و پیاده سازی میریم:

بار کد از سمت چپ به راست شروع میشه و یک سفیدی کنار بارکد به طور خودکار وجود داره! یعنی اون حاشیه ای که داره جزو بار کد هست!

بارکد به صورت بیتی عمل میکنه یعنی مثلا هر n پیکسل نشون دهنده 1 بیت هست! اگه n پیکسل سیاه باشه اون بیت 0 و اگه سفید باشه اون بیت 1 به حساب میاد!

اما نکته مهم اینجاست که مقدار n چه قدره؟! 

باید توجه کنید که مقدار n به تصویر بستگی داره! یه موقع تصویر بزرگ هست مثلا n مقدارش 50 پیکسل هست و یه موقع تصویر کوچک هست 12 پیکسل میشه! پس مقدار n برای هر تصویر متغیر هست. اما از کجا میشه این مقدار رو بدست آورد؟

کاری که انجام دادن اینه که در بار کد 3 میله اول رو بهش میگن شروع (S) که یک الگو ثابت داره! سیاه-سفید-سیاه ما میدونیم که هر کدوم از اینها یک میله هستند یا به عبارتی هر کدام از اینها یک بیت هستند پس با اندازه گیری یکی از این سه میله میتوانیم بگوییم هر میله یا بیت چند پیکسل است!

توجه کنید که به طور ایده آل باید تمامی پیکسل های یک میله به یک رنگ باشند! یعنی تمامی پیکسل ها یا سفید و یا سیاه باشند!

بعد از آن سه بیت آغازی به سراغ اصل مطلب یعنی عدد ها میرویم!

هر عدد در بارکد شامل 7 بیت هست مثلا 0001101 نشان دهنده عدد 0 میباشد.

جدول زیر این کد ها رو نشان میدهد

Digit L-code R-code
0 0001101 1110010
1 0011001 1100110
2 0010011 1101100
3 0111101 1000010
4 0100011 1011100
5 0110001 1001110
6 0101111 1010000
7 0111011 1000100
8 0110111 1001000
9 0001011 1110100

نکته ای که نباید نادیده گرفته شود این است که کد های ارقام سمت راست با کد های ارقام سمت چپ متفاوت است در نتیجه اگر عدد 2 سمت چپ باشد با کد 0010011 نشان داده میشود و اگر سمت راست باشد با کد 1101100 نشان داده میشود!

برای بدست آوردن اعداد درون بار کد باید 7 بیت را بخوانیم و سپس از جدول مشخص کنیم که این هفت بیت بیانگر چه عددی (از 0 تا 9) میباشد!

اجزاء یک بارکد به صورت زیر است:

barcode detail

 

مواردی که ما از این تصویر میخواهیم تنها 6 عدد سمت چپ و 6 عدد سمت راست هستند!

حال به سراغ پیاده سازی پردازش بارکد میرویم.

 

» توضیح کد : خواندن بارکد

خوب در ابتدا گفتیم که یک فضای سفید دور بارکد هست! برای شروع یک تابع مینویسیم که این فضا رو رد کنیم و به اولین میله سیاه برسیم:

void ignore_begin(const cv::Mat_<uchar>& image, cv::Point& cur){
	while (image(cur) == SPACE)
		cur.x++;
}

 

تابع بعدی تعداد پیکسل برای هر بیت (یا میله) را به ما میدهد! (به کمک سه میله آغازین)

int calc_unit_size(const cv::Mat_<uchar>& image, cv::Point& cur){
	int counter = 0;
	int pattern[3] = { BAR, SPACE, BAR };
	for (int i = 0; i < 3; i++){
		while (image(cur) == pattern[i]){
			cur.x++;
			counter++;
		}
	}
	return counter/3;
}

 

حال که حاشیه رو رد کردیم و سه بیت ابتدایی رو هم رد کردیم و مقدار پیکسل در هر بیت (یا میله) رو بدست آوردیم نوبت میرسه و خواندن ارقام موجود در بار کد!

برای اینکار یک تابع مینویسیم که کد بگیرد و عدد رو به ما بدهد

int get_digit(int code, int direction){
	if (direction == LEFT)
		switch (code){
			case 13:
				return 0;
			case 25:
				return 1;
			case 19:
				return 2;
			case 61:
				return 3;
			case 35:
				return 4;
			case 49:
				return 5;
			case 47:
				return 6;
			case 59:
				return 7;
			case 55:
				return 8;
			case 11:
				return 9;
		}
	else 
		switch (code){
		case 114:
			return 0;
		case 102:
			return 1;
		case 108:
			return 2;
		case 66:
			return 3;
		case 92:
			return 4;
		case 78:
			return 5;
		case 80:
			return 6;
		case 68:
			return 7;
		case 72:
			return 8;
		case 116:
			return 9;
	}
	return 0;

}

این تابع کار سختی انجام نمیدهد فقط کد هایی که به باینری در جدول بالا بود را به دسیمال تبدیل کردم و مطابق هر کد، عدد مربوطه برگردانده میشود.

 

حال تابع اصلی که تابع خواندن یک رقم میباشد را مرور میکنیم! به دلیل کمی پیچیدگی آنرا کوچک کوچک توضیح میدهم

int read_digit(const cv::Mat_<uchar>& image, cv::Point& cur,int usize , int direction){
	int bit[7] = { 0 };
	int code = 0;
	for (int i = 0; i < 7; i++){
		for (int j = 0; j < usize; j++){
			if (image(cur) == BAR)
				bit[i]++;
			cur.x++;
		}
		bit[i] = bit[i] > usize / 2 ? 1 : 0;

آرایه ی bit که شامل 7 عدد صحیح هست در واقع کد عدد ماست که هر عنصر از آن یا صفر است یا یک! مثلا بعد از انجام پردازش آرایه ی بیت به صورت زیر در میاید

0,1,0,1,1,1,1

که نشان دهنده عدد 6 میباشد (در ادامه این آرایه را به کد تبدیل میکنیم و سپس کد را به کمک تابع get_digit به عدد متناظر تبدیل میکنیم)

اما برای افزایش دقت و رفع مشکلات تصویر بارکد ما هر عنصر از آرایه به جای صفر و یک تعداد پیکسل های آن بیت (یا میله) ذخیره میشود! گفتیم که در حالت ایده آل باید این عدد برابر طول واحد (تعداد پیکسل در هر بیت یا میله) باشد چون هر بیت یا کاملا سفید است یا کاملا سیاه است اما این تنها یک حالت ایده آل است و ما نمیتوانیم به این موضوع تکیه کنیم چون در اکثر مواقع تصاویری که به برنامه میرسد انقدر دقیق نیستند در نتیجه ما باید برای جلوگیری از بروز مشکل و رفع برخی از اشکالات تصویر راه چاره ای بیاندیشیم.

کاری که در کد بالا انجام دادیم این است که به اندازه تعداد پیکسل در هر بیت به جلو میرویم و پیکسل های مشکی را میشماریم و درون عنصر آرایه قرار میدهیم و پس از آن چک میکنیم که آیا تعداد پیکسل های مشکی بیشتر از نصف تعداد پیکسل ها در هر بیت (یا میله) بود یا خیر؟ اگر بیشتر بود یعنی به احتمال زیاد کل میله سیاه بوده و مشکل از تصویر است که دقیق نیست و اگر کمتر از نصف تعداد پیکسل ها در هربیت (یا میله) بود یعنی احتمال زیاد این میله سفید بوده و به دلیل نواقص تصویر شاید چند پیکسل سیاه هم خوانده شده! پس ما این میله را کلا صفر به معنی سفید در نظر میگیریم!

برای آنکه بهتر متوجه بشوید مشکب دقیقا چیست به تصویر زیر نگاه کنید

این بخشی از یک بارکد است. میبینید که تا قبل از آنجایی که خط کشیدم همه چیز تراز است اما در یک جا به اندازه 1 پیکسل مقدار میله سفید کم است و دیگر از این جا به بعد تراز بهم میخورد و اگر به فکر چاره نباشیم قطعا به مشکل بر میخوریم.

فکری که من برای رفع این مشکل کردم این بود که بعد از خواندن هر میله یا بیت اشاره گر را دقیقا در اول میله بعد قرار دهیم که تراز همیشه به طور خودکار درست باشد!

مثلا در شکل بالا 

برای آنکه در ادامه به مشکل بر نخوریم بعد از اینکه میله 2 را خواندیم باید تراز انجام دهیم (این مثال تنها برای میله 2 است و عمومیت ندارد. برای درک آسان اینکار رو کردم) برای اینکار تعداد پیکسل های سفید بعد از خانه 2 را میخوانیم اگر دیدیم که در آنجا نصف یک میله سفید دیگر جا میشود یعنی آنجا واقعا جای یک میله سفید است ولی تصویر مشکل دارد پس هیچ کاری انجام نمیدهیم و حلقه را بدون هیچ دستکاری (البته دستکاری کردیم و دوباره برگرداندیم) ادامه میدهیم. 

کد ما میاد و میله 3 رو میخونه در حالی که سه چهارم اون یعنی بیش از نصفش در منطقه سفید قرار گرفته پس میاد و اون رو کلا سفید در نظر میگیره و حالا میخواد تراز انجام بده اینبار میاد و میبینه با وجود اینکه این میله سفید است اما الان در منطقه سیاه قرار داریم (یعنی میله 3 از منطقه سفید بیرون زده) در نتیجه کد ما میاد و تا منطقه سفید به عقب برمگیرده که دیگه مشکل شکل بالا به وجود نیاد و حلقه رو دوباره برای بدست آوردن رنگ میله بعد ادامه میده! شکلی که این کد بعد از انجام تراز انجام میده به صورت زیر میشه که خیلی بهتر از بالاست:

همچنین کد ما برای وقتی که سفیدی به اندازه کمی (کمتر از نصف طول واحد میله (تعداد پیکسل در هر بیت یا میله)) بیشتر باشد اشاره گر را به جلو میبرد که تراز بندی درست باشد!

کدی که این همه تعریفش را کردیم به صورت زیر است:

		int x = cur.x; int c = 0;

		if (image(cur) == bit[i] * 255){ // آیا در جایی که هستی هنوز همرنگ مقدار بدست آمده هستی؟ اگر هستی برو جلو که به رنگ مخالف برسی (در هنگامی که خط بزرگتر است)
			while (image(cur) == bit[i] * 255){
				cur.x++;
				c++;
			}
			if (c >= usize / 2)
				cur.x = x;
		}
		else { // خط کوچکتر است آنرا تراز میکنیم!
			while (image(cur) != bit[i] * 255){
				cur.x--;
				c++;
			}

			c--;
			cur.x++;

			if (c >= usize / 2)
				cur.x = x;
		}

	}

 

در ادامه باید آرایه ای که بدست آمده به کد تبدیل کنیم که بعد بتوانیم این کد رو به عدد (0 تا 9) تبدیل کنیم

	for (int i = 0; i < 7; i++){
		code *= 2;
		code += bit[i];
	}

خوب تابع خواندن یک رقم با خط زیر به پایان میرسد

return get_digit(code,direction);

 

حال به وسط بارکد رسیدیم و 6 رقم را خواندیم! 5 بیت میانی رو باید رد کنیم بنابراین از تابع زیر استفاده میکنیم:

void skip_middle(const cv::Mat_<uchar>& image, cv::Point& cur){
	int pattern[5] = { SPACE, BAR, SPACE, BAR, SPACE };
	for (int i = 0; i < 5; i++){
		while (image(cur) == pattern[i])
			cur.x++;
	}
}

و در آخر 6 رقم آخر رو هم میخوانیم و کار تمام است!

 

تابع زیر تمام کار های بالا را که گفتیم به ترتیب انجام میدهد و در نهایت مقدار بار کد را نشان میدهد

void read_barcode(cv::Mat& image){
	cv::bitwise_not(image, image);
	cv::threshold(image, image, 128, 255, cv::THRESH_BINARY);
	cv::Size size = image.size();
	cv::Point cur = cv::Point(0, size.height / 2);

	ignore_begin(image,cur);

	int unit_size = calc_unit_size(image, cur);
	std::cout << "unit size = " << unit_size << std::endl;

	unsigned long int serial = 0;

	for (int i = 0; i < 6; i++)
		 std::cout << read_digit(image, cur, unit_size, LEFT) % 10;
	

	skip_middle(image, cur);

	for (int i = 0; i < 6; i++)
		 std::cout << read_digit(image, cur, unit_size, RIGHT) % 10;
}

 

کد کامل برنامه را میتوانید از این لینک دانلود کنید.

امیدوارم مفید بوده باشد.

موفق باشید

یا علی مدد...!