الواقع المعزز البسيط مع OpenCV و Three.js و WebSockets

نشرت: 2022-03-10
ملخص سريع ↬ في هذا البرنامج التعليمي ، سنستخدم OpenCV في Python لاكتشاف الكائنات على شكل دائرة في دفق كاميرا الويب واستبدالها بـ 3D Earth في Three.js في نافذة المتصفح أثناء استخدام WebSockets لدمج كل ذلك معًا.

يعتبر الواقع المعزز بشكل عام أمرًا صعبًا للغاية. ومع ذلك ، من الممكن إنشاء مشاريع رائعة بصريًا باستخدام مكتبات مفتوحة المصدر فقط. في هذا البرنامج التعليمي ، سنستخدم OpenCV في Python لاكتشاف الكائنات على شكل دائرة في دفق كاميرا الويب واستبدالها بـ 3D Earth في Three.js في نافذة متصفح أثناء استخدام WebSockets لدمجها معًا.

نريد فصل الواجهة الأمامية والخلفية بشكل صارم من أجل جعلها قابلة لإعادة الاستخدام. في تطبيق حقيقي ، يمكننا كتابة الواجهة الأمامية في Unity أو Unreal Engine أو Blender ، على سبيل المثال ، لجعلها تبدو رائعة حقًا. تعد الواجهة الأمامية للمتصفح هي الأسهل من حيث التنفيذ ويجب أن تعمل على كل تهيئة ممكنة تقريبًا.

لتبسيط الأمور ، سنقسم التطبيق إلى ثلاثة أجزاء أصغر:

  1. ستقوم Python الخلفية مع OpenCV OpenCV بقراءة تدفق كاميرا الويب وفتح نوافذ متعددة مع صورة الكاميرا بعد تمريرها عبر عوامل تصفية متعددة لتسهيل تصحيح الأخطاء وإعطائنا نظرة ثاقبة على ما تراه خوارزمية الكشف عن الدائرة بالفعل. سيكون ناتج هذا الجزء مجرد إحداثيات ثنائية الأبعاد ونصف قطر الدائرة المكتشفة.
  2. واجهة JavaScript أمامية مع Three.js في متصفح تنفيذ خطوة بخطوة لمكتبة Three.js لعرض الأرض المزخرفة مع دوران القمر حولها. الشيء الأكثر إثارة للاهتمام هنا هو تعيين إحداثيات الشاشة ثنائية الأبعاد في العالم ثلاثي الأبعاد. سنقوم أيضًا بتقريب الإحداثيات ونصف القطر لزيادة دقة OpenCV.
  3. ستقوم WebSockets في كل من الواجهة الأمامية والخلفية الخلفية مع خادم WebSockets بإرسال رسائل بشكل دوري مع إحداثيات الدائرة المكتشفة وأنصاف الأقطار إلى عميل المستعرض.
النتيجة النهائية
المزيد بعد القفز! أكمل القراءة أدناه ↓

1. Python Back-End مع OpenCV

ستكون خطوتنا الأولى هي استيراد مكتبة OpenCV في Python وفتح نافذة ببث مباشر من كاميرا الويب.

سنستخدم أحدث إصدار من OpenCV 3.0 (انظر ملاحظات التثبيت) مع Python 2.7. يرجى ملاحظة أن التثبيت على بعض الأنظمة قد يكون مشكلة وأن التوثيق الرسمي ليس مفيدًا للغاية. جربت بنفسي على Mac OS X الإصدار 3.0 من MacPorts وكان لدى الثنائي مشكلة تبعية لذلك اضطررت للتبديل إلى Homebrew بدلاً من ذلك. لاحظ أيضًا أن بعض حزم OpenCV قد لا تأتي مع ربط Python افتراضيًا (تحتاج إلى استخدام بعض خيارات سطر الأوامر).

مع Homebrew جريت:

 brew install opencv

يؤدي هذا إلى تثبيت OpenCV مع روابط Python افتراضيًا.

فقط لاختبار الأشياء ، أوصيك بتشغيل Python في الوضع التفاعلي (قم بتشغيل python في CLI بدون أي وسيطات) واكتب import cv2 . إذا تم تثبيت OpenCV بشكل صحيح وكانت المسارات إلى روابط Python صحيحة ، فلن يؤدي ذلك إلى حدوث أي أخطاء.

لاحقًا ، سنستخدم أيضًا لغة Python numpy لبعض العمليات البسيطة باستخدام المصفوفات حتى نتمكن من تثبيتها الآن أيضًا.

 pip install numpy

قراءة صورة الكاميرا

الآن يمكننا اختبار الكاميرا:

 import cv2 capture = cv2.VideoCapture(0) while True: ret, image = capture.read() cv2.imshow('Camera stream', image) if cv2.waitKey(1) & 0xFF == ord('q'): break

باستخدام cv2.VideoCapture(0) ، يمكننا الوصول إلى الكاميرا الموجودة في الفهرس 0 وهو الافتراضي (الكاميرا المدمجة عادةً). إذا كنت تريد استخدام رقم مختلف ، فحاول استخدام أرقام أكبر من الصفر ؛ ومع ذلك ، لا توجد طريقة سهلة لسرد جميع الكاميرات المتاحة بإصدار OpenCV الحالي.

عندما نطلق cv2.imshow('Camera stream', image) لأول مرة ، فإنه يتحقق من عدم وجود نافذة بهذا الاسم ويقوم بإنشاء نافذة جديدة لنا بها صورة من الكاميرا. سيتم إعادة استخدام نفس النافذة لكل تكرار للحلقة الرئيسية.

ثم استخدمنا capture.read() للانتظار والتقاط صورة الكاميرا الحالية. تقوم هذه الطريقة أيضًا بإرجاع خاصية ret المنطقي في حالة فصل الكاميرا أو عدم توفر الإطار التالي لسبب ما.

في النهاية ، لدينا cv2.waitKey(1) الذي يتحقق لمدة 1 مللي ثانية من الضغط على أي مفتاح وإرجاع رمزه. لذلك ، عندما نضغط على q ، نخرج من الحلقة ، نغلق النافذة وينتهي التطبيق.

إذا نجح كل هذا ، فقد مررنا أصعب جزء من التطبيق الخلفي وهو تشغيل الكاميرا.

تصفية صور الكاميرا

بالنسبة لاكتشاف الدائرة الفعلي ، سنستخدم دائرة Hough Transform التي يتم تنفيذها في طريقة cv2.HoughCircles() والآن هي الخوارزمية الوحيدة المتاحة في OpenCV. الشيء المهم بالنسبة لنا هو أنه يحتاج إلى صورة ذات تدرج رمادي كمدخل ويستخدم خوارزمية كاشف الحافة Canny بالداخل للعثور على الحواف في الصورة. نريد أن نكون قادرين على التحقق يدويًا مما تراه الخوارزمية ، لذلك سنقوم بتكوين صورة واحدة كبيرة من أربع صور أصغر مع تطبيق مرشح مختلف.

كاشف الحافة Canny عبارة عن خوارزمية تعالج الصورة عادةً في أربعة اتجاهات (عموديًا وأفقيًا وقطرين) وتبحث عن الحواف. يتم شرح الخطوات الفعلية التي تقوم بها هذه الخوارزمية بمزيد من التفصيل على ويكيبيديا أو باختصار في مستندات OpenCV.

على النقيض من مطابقة الأنماط ، تكتشف هذه الخوارزمية الأشكال الدائرية حتى نتمكن من استخدام أي كائنات علينا تسليمها دائرية. سأستخدم غطاءًا من جرة قهوة سريعة التحضير ثم كوب قهوة برتقالي.

لا نحتاج إلى العمل مع الصور بالحجم الكامل (يعتمد على دقة الكاميرا ، بالطبع) ، لذلك سنقوم بتغيير حجمها بين capture.read() و cv2.imshow إلى 640 بكسل للعرض والارتفاع وفقًا لذلك للحفاظ على نسبة العرض إلى الارتفاع:

 width, height = image.shape scale = 640.0 / width image = cv2.resize(image, (0,0), fx=scale, fy=scale)

ثم نريد تحويلها إلى صورة ذات تدرج رمادي وتطبيق ضباب متوسط ​​أول يزيل الضوضاء ويحتفظ بالحواف ، ثم كاشف حافة Canny لمعرفة ما ستعمل به خوارزمية الكشف عن الدائرة. لهذا السبب ، سنقوم بتكوين شبكة 2 × 2 مع جميع المعاينات الأربعة.

 t = 100 # threshold for Canny Edge Detection algorithm grey = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) blured = cv2.medianBlur(grey, 15) # Create 2x2 grid for all previews grid = np.zeros([2*h, 2*w, 3], np.uint8) grid[0:h, 0:w] = image # We need to convert each of them to RGB from greyscaled 8 bit format grid[h:2*h, 0:w] = np.dstack([cv2.Canny(grey, t / 2, t)] * 3) grid[0:h, w:2*w] = np.dstack([blured] * 3) grid[h:2*h, w:2*w] = np.dstack([cv2.Canny(blured, t / 2, t)] * 3) 
الشبكة مع المعاينات. أعلى اليسار: بيانات كاميرا الويب الخام ؛ أعلى اليمين: تدرج الرمادي بعد التمويه المتوسط ​​؛ أسفل اليسار: حافة رمادية وحافة ؛ أسفل اليمين: تدرج الرمادي بعد تمويه متوسط ​​وحافة Canny.

على الرغم من أن كاشف الحواف Canny يستخدم Gaussian blur لتقليل الضوضاء ، إلا أنه من واقع خبرتي لا يزال من المفيد استخدام ضباب متوسط ​​أيضًا. يمكنك مقارنة الصورتين السفليتين. الشخص الموجود على اليسار هو مجرد اكتشاف حافة Canny بدون أي مرشح آخر. الصورة الثانية هي أيضًا اكتشاف حافة Canny ولكن هذه المرة بعد تطبيق تمويه متوسط. لقد قلل من الكائنات في الخلفية مما سيساعد في اكتشاف الدائرة.

كشف الدوائر مع التدرج الكبير

داخليًا ، يستخدم OpenCV تنفيذًا أكثر كفاءة لتحويل Hough Circle يسمى Hough Gradient Method الذي يستخدم معلومات الحافة من كاشف الحافة Canny. تم وصف طريقة التدرج بعمق في كتاب Learning OpenCV و Circle Hough Transform على ويكيبيديا .

حان الوقت الآن لاكتشاف الدائرة الفعلية:

 sc = 1 # Scale for the algorithm md = 30 # Minimum required distance between two circles # Accumulator threshold for circle detection. Smaller numbers are more # sensitive to false detections but make the detection more tolerant. at = 40 circles = cv2.HoughCircles(blured, cv2.HOUGH_GRADIENT, sc, md, t, at)

هذا يعيد مصفوفة من جميع الدوائر المكتشفة. من أجل البساطة ، سنهتم فقط بالأول. Hough Gradient حساس جدًا للأشكال الدائرية حقًا ، لذا من غير المحتمل أن يؤدي ذلك إلى اكتشافات خاطئة. إذا حدث ذلك ، فقم بزيادة المعلمة at . هذا هو السبب في أننا استخدمنا تمويه متوسط ​​أعلاه ؛ أزال المزيد من الضوضاء حتى نتمكن من استخدام حد أدنى ، مما يجعل الاكتشاف أكثر تسامحًا مع عدم الدقة مع فرصة أقل لاكتشاف الدوائر الخاطئة.

سنقوم بطباعة مركز الدائرة ونصف قطرها على وحدة التحكم وكذلك رسم الدائرة الموجودة بمركزها على الصورة من الكاميرا في نافذة منفصلة. لاحقًا ، سنرسله عبر WebSocket إلى المتصفح. لاحظ أن x و y و radius كلها بالبكسل.

 if circles is not None: # We care only about the first circle found. circle = circles[0][0] x, y, radius = int(circle[0]), int(circle[1]), int(circle[2]) print(x, y, radius) # Highlight the circle cv2.circle(image, [x, y], radius, (0, 0, 255), 1) # Draw a dot in the center cv2.circle(image, [x, y], 1, (0, 0, 255), 1)

سيؤدي هذا إلى طباعة وحدة التحكم في المجموعات مثل:

 (251, 202, 74) (252, 203, 73) (250, 202, 74) (246, 202, 76) (246, 204, 74) (246, 205, 72) 
تم اكتشاف دفق كاميرا الويب مع الدوائر باستخدام Hough Gradient.

كما ترى في هذه الرسوم المتحركة ، فقد فشلت في العثور على أي دوائر على الإطلاق. تحتوي الكاميرا المدمجة على 15 إطارًا في الثانية فقط وعندما أحرك يدي بسرعة ، تكون الصورة غير واضحة حتى لا تجد حواف دائرية ، ولا حتى بعد تطبيق المرشحات.

في نهاية هذه المقالة ، سنعود إلى هذه المشكلة ونتحدث كثيرًا عن الإعدادات الخاصة بالكاميرا واختيار خوارزمية الكشف ، ولكن يمكننا القول بالفعل أنه على الرغم من أن الإعداد سيئ للغاية (فقط 15 إطارًا في الثانية ، الإضاءة السيئة ، الكثير من الضوضاء في الخلفية ، الكائن له تباين منخفض) ، والنتيجة جيدة بشكل معقول.

هذا كل شئ حتى الان. لدينا إحداثيات x و y radius بالبكسل لدائرة موجودة في صورة كاميرا الويب.

يمكنك رؤية الكود المصدري الكامل لهذا الجزء على gist.github.com.

2. الواجهة الأمامية لجافا سكريبت مع Three.js في المتصفحات

يعتمد الجزء الأمامي على مكتبة Three.js (الإصدار r72). سنبدأ فقط بإنشاء كرة دوارة محكم تمثل الأرض في وسط الشاشة ، ثم نضيف القمر الذي يدور حوله. في النهاية سنقوم بتعيين إحداثيات ماوس الشاشة ثنائية الأبعاد إلى الفضاء ثلاثي الأبعاد.

ستتألف صفحة HTML الخاصة بنا من عنصر <canvas> واحد فقط. راجع index.html على gist.github.com.

خلق الأرض

ستكون JavaScript أطول قليلاً لكنها مقسمة إلى وظائف تهيئة متعددة حيث لكل منها غرض واحد. قوام الأرض والقمر تأتي من planetpixelemporium.com. لاحظ أنه عند تحميل الزخارف ، يتم تطبيق قواعد CORS.

 var scene, camera, renderer, light, earthMesh, earthRotY = 0; function initScene(width, height) { scene = new THREE.Scene(); // Setup cameta with 45 deg field of view and same aspect ratio camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000); // Set the camera to 400 units along `z` axis camera.position.set(0, 0, 400); renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); renderer.setSize(width, height); renderer.shadowMap.enabled = true; document.body.appendChild(renderer.domElement); } function initLight() { light = new THREE.SpotLight(0xffffff); // Position the light slightly to a side to make shadows look better. light.position.set(400, 100, 1000); light.castShadow = true; scene.add(light); } function initEarth() { // Load Earth texture and create material from it var earthMaterial = new THREE.MeshLambertMaterial({ map: THREE.ImageUtils.loadTexture("/images/earthmap1k.jpg"), }); // Create a sphere 25 units in radius and 16 segments // both horizontally and vertically. var earthGeometry = new THREE.SphereGeometry(25, 16, 16); earthMesh = new THREE.Mesh(earthGeometry, earthMaterial); earthMesh.receiveShadow = true; earthMesh.castShadow = true; // Add Earth to the scene scene.add(earthMesh); } // Update position of objects in the scene function update() { earthRotY += 0.007; earthMesh.rotation.y = earthRotY; } // Redraw entire scene function render() { update(); renderer.setClearColor(0x000000, 0); renderer.render(scene, camera); // Schedule another frame requestAnimationFrame(render); } document.addEventListener('DOMContentLoaded', function(e) { // Initialize everything and start rendering initScene(window.innerWidth, window.innerHeight); initEarth(); initLight(); // Start rendering the scene requestAnimationFrame(render); });

شاهد عرض حي هنا.

Threejs- الأرض الغزل
كرة محكم بها Three.js. (رصيد نسيج الأرض)

كانت هذه في الغالب مجرد عناصر أساسية من Three.js. تعتبر أسماء الكائنات والطرق ذاتية الاستكشاف (مثل "" "" الظل "أو castShadow ولكن إذا لم تستخدمه مطلقًا قبل أن أوصيك بشدة بإلقاء نظرة على دروس لي receiveShadow .

اختياريًا ، يمكننا أيضًا رسم محور في وسط الشاشة لمساعدتنا في نظام الإحداثيات.

 var axes = new THREE.AxisHelper(60); axes.position.set(0, 0, 0); scene.add(axes);

مضيفا القمر

سيكون تكوين القمر متشابهًا جدًا. الاختلاف الرئيسي هو أننا نحتاج إلى ضبط موضع القمر بالنسبة إلى الأرض.

 function initMoon() { // The same as initEarth() with just different texture } // Update position of objects in the scene function update() { // Update Earth position // ... // Update Moon position moonRotY += 0.005; radY += 0.03; radZ += 0.0005; // Calculate position on a sphere x = moonDist * Math.cos(radZ) * Math.sin(radY); y = moonDist * Math.sin(radZ) * Math.sin(radY); z = moonDist * Math.cos(radY); var pos = earthMesh.position; // We can keep `z` as is because we're not moving the Earth // along z axis. moonMesh.position.set(x + earthMesh.pos.x, y + earthMesh.pos.y, z); moonMesh.rotation.y = moonRotY; }

شاهد عرض حي هنا.

الأرض والقمر مع Three.js. (رصيد قوام الأرض والقمر)

تعيين الإحداثيات ثنائية الأبعاد لعالم ثلاثي الأبعاد

حتى الآن ، كل شيء واضح جدًا. سيكون الجزء الأكثر إثارة للاهتمام هو كيفية إخفاء إحداثيات الشاشة ثنائية الأبعاد القادمة من OpenCV (انظر إخراج الكشف الدائري أعلاه) إلى عالم ثلاثي الأبعاد؟ عندما كنا نحدد نصف القطر والمواضع في Three.js ، استخدمنا بعض الوحدات ولكن لا علاقة لها بوحدات بكسل الشاشة الفعلية. في الواقع ، تعتمد أبعاد كل شيء نراه في المشهد بشكل كبير على إعدادات الكاميرا (مثل نسبة العرض إلى الارتفاع أو مجال الرؤية).

لهذا السبب ، سنصنع جسمًا مستويًا يكون كبيرًا بما يكفي لتغطية المشهد بأكمله بمركزه عند [0,0,0] . لأغراض التوضيح ، سنقوم بتعيين إحداثيات الماوس ثنائية الأبعاد إلى موضع الأرض بشكل ثلاثي الأبعاد باستخدام محور z ثابت. بعبارة أخرى ، سنحول x و y فقط ولا نقلق بشأن z ، وهي المسافة من الجسم إلى الكاميرا.

سنقوم بتحويل مواضع شاشة الماوس إلى نطاق من -1.0 إلى +1.0 مع مركزه عند [0,0] لأننا نحتاج إلى العمل مع المتجهات العادية.

سنستخدم لاحقًا هذه التقنية الدقيقة لتعيين موضع الدائرة المكتشفة إلى ثلاثي الأبعاد وأيضًا لمطابقة حجم الدائرة من ثنائي الأبعاد إلى ثلاثي الأبعاد.

 var mouse = {}; function initPlane() { // The plane needs to be large to always cover entire scene var tmpGeometry = new THREE.PlaneGeometry(1000, 1000, 1, 1); tmpGeometry.position = new THREE.Vector3(0, 0, 0); var tmpMesh = new THREE.Mesh(tmpGeometry); } function onDocumentMouseMove(event) { // Current mouse position with [0,0] in the center of the window // and ranging from -1.0 to +1.0 with `y` axis inverted. mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.y = - (event.clientY / window.innerHeight) * 2 + 1; } function update() { // ... the rest of the function // We need mouse x and y coordinates to set vector's direction var vector = new THREE.Vector3(mouse.x, mouse.y, 0.0); // Unproject camera distortion (fov, aspect ratio) vector.unproject(camera); var norm = vector.sub(camera.position).normalize(); // Cast a line from our camera to the tmpMesh and see where these // two intersect. That's our 2D position in 3D coordinates. var ray = new THREE.Raycaster(camera.position, norm); var intersects = ray.intersectObject(tmpMesh); earthMesh.position.x = intersects[0].point.x; earthMesh.position.y = intersects[0].point.y; }

شاهد عرض حي هنا.

Threejs- الأرض الغزل
تم تعيين موضع الأرض في ثلاثي الأبعاد إلى موضع الماوس ثنائي الأبعاد. (رصيد قوام الأرض والقمر)

نظرًا لأننا نتحقق من التقاطع بمستوى ، فإننا نعلم أنه سيكون هناك دائمًا واحدًا فقط.

هذا كل ما في هذا الجزء. في نهاية الجزء التالي ، سنضيف أيضًا WebSockets وعنصر <video> مع دفق الكاميرا الخاص بنا والذي سيتم تراكبه بواسطة المشهد ثلاثي الأبعاد في Three.js.

3. WebSockets في كل من الواجهة الأمامية والخلفية

يمكننا البدء بتنفيذ WebSockets في الواجهة الخلفية لـ Python عن طريق تثبيت مكتبات simple-websocket-server . هناك العديد من المكتبات المختلفة مثل Tornado أو Autobahn. simple-websocket-server لأنه سهل الاستخدام للغاية وليس له أي اعتماديات.

 pip install git+https://github.com/dpallot/simple-websocket-server.git

سنقوم بتشغيل خادم WebSocket في سلسلة منفصلة وسنتتبع جميع العملاء المتصلين.

 from SimpleWebSocketServer import SimpleWebSocketServer, WebSocket clients = [], server = None class SimpleWSServer(WebSocket): def handleConnected(self): clients.append(self) def handleClose(self): clients.remove(self) def run_server(): global server server = SimpleWebSocketServer(', 9000, SimpleWSServer, selectInterval=(1000.0 / 15) / 1000) server.serveforever() t = threading.Thread(target=run_server) t.start() # The rest of the OpenCV code ...

استخدمنا المعامل selectInterval في مُنشئ الخادم لجعله يتحقق بشكل دوري من أي رسائل معلقة. يرسل الخادم الرسائل فقط عند تلقي البيانات من العملاء ، أو يحتاج إلى الجلوس على السلسلة الرئيسية في حلقة. لا يمكننا السماح له بحظر الخيط الرئيسي لأن OpenCV يحتاجه أيضًا. نظرًا لأننا نعلم أن الكاميرا تعمل فقط بمعدل 15 إطارًا في الثانية ، يمكننا استخدام نفس الفاصل الزمني على خادم WebSocket.

بعد ذلك ، بعد اكتشاف الدوائر ، يمكننا تكرار جميع العملاء المتصلين وإرسال الموضع الحالي ونصف القطر بالنسبة لحجم الصورة.

 for client in clients: msg = json.dumps({'x': x / w, 'y': y / h, 'radius': radius / w}) client.sendMessage(unicode(msg))

يمكنك رؤية كود المصدر الكامل للخادم على gist.github.com.

سيحاكي جزء JavaScript نفس السلوك كما فعلنا مع موضع الماوس. سنتعقب أيضًا الرسائل القليلة ونحسب متوسط ​​القيمة لكل محور ونصف قطر لتحسين الدقة.

 var history = []; var ws = new WebSocket('ws://localhost:9000'); ws.onopen = function() { console.log('onopen'); }; ws.onmessage = function (event) { var m = JSON.parse(event.data); history.push({ x: mx * 2 - 1, y: -my * 2 + 1, radius: m.radius}); // ... rest of the function. };

بدلاً من تعيين موضع الأرض على موضع الماوس الحالي ، سنستخدم متغير msgHistory .

ربما ليس من الضروري لصق الكود بالكامل هنا ، لذا لا تتردد في إلقاء نظرة على تفاصيل التنفيذ على gist.gihtub.com.

ثم أضف عنصر <video> واحدًا مع تدفق كاميرا الويب الذي يملأ النافذة بأكملها والتي سيتم تراكبها بواسطة مشهدنا ثلاثي الأبعاد بخلفية شفافة.

 var videoElm = document.querySelector('video'); // Make sure the video fits the window. var constrains = { video: { mandatory: { minWidth: window.innerWidth }}}; if (navigator.getUserMedia) { navigator.getUserMedia(constrains, function(stream) { videoElm.src = window.URL.createObjectURL(stream); // When the webcam stream is ready get it's dimensions. videoElm.oncanplay = function() { init(videoElm.clientWidth, videoElm.clientHeight); // Init everything ... requestAnimationFrame(render); } }, function() {}); }

النتيجة النهائية:

تم تعيين التطبيق النهائي مع Earth للإحداثيات التي تم العثور عليها بواسطة OpenCV.

لتلخيص سريع لما فعلناه وما يظهره الفيديو أعلاه:

  1. تقوم لغة Python الخلفية بتشغيل خادم WebSocket.
  2. يكتشف الخادم دائرة باستخدام OpenCV من تدفق كاميرا الويب.
  3. يعرض عميل JavaScript نفس دفق كاميرا الويب باستخدام عنصر <video> .
  4. يعرض العميل مشهدًا ثلاثي الأبعاد باستخدام Three.js.
  5. يتصل العميل بالخادم عبر بروتوكول WebSocket ويستقبل موضع الدائرة ونصف القطر.

الكود الفعلي المستخدم لهذا العرض التوضيحي متاح على GitHub. إنه أكثر تعقيدًا إلى حد ما كما أنه يقحم الإحداثيات بين رسالتين من النهاية الخلفية لأن دفق كاميرا الويب يعمل فقط بمعدل 15 إطارًا في الثانية بينما يتم عرض المشهد ثلاثي الأبعاد بسرعة 60 إطارًا في الثانية. يمكنك مشاهدة الفيديو الأصلي على موقع يوتيوب.

تحفظات

هناك بعض النتائج الجديرة بالملاحظة:

كشف الدائرة ليس مثاليًا

إنه لأمر رائع أنه يعمل مع أي جسم دائري ولكنه حساس جدًا للضوضاء وتشوه الصورة ، على الرغم من أن النتيجة جيدة جدًا كما ترون أعلاه. أيضًا ، ربما لا توجد أمثلة عملية لاكتشاف الدائرة باستثناء الاستخدام الأساسي. قد يكون من الأفضل استخدام اكتشاف القطع الناقص ولكن لم يتم تنفيذه في OpenCV في الوقت الحالي.

كل شيء يعتمد على الإعداد الخاص بك

كاميرات الويب المدمجة سيئة بشكل عام. 15 إطارًا في الثانية ليس كافيًا وزيادته إلى 30 إطارًا في الثانية يقلل من ضبابية الحركة بشكل كبير ويجعل الكشف أكثر موثوقية. يمكننا تقسيم هذه النقطة إلى أربع نقاط أخرى:

  • تشوهات الكاميرا
    تقدم العديد من الكاميرات بعض تشويه الصورة ، وغالبًا ما يكون تأثير عين السمكة الذي له تأثير كبير على اكتشاف الشكل. تحتوي وثائق OpenCV على برنامج تعليمي مباشر للغاية حول كيفية تقليل التشويه عن طريق معايرة الكاميرا.
  • لا توجد قائمة رسمية بالأجهزة التي يدعمها OpenCV
    حتى إذا كان لديك بالفعل كاميرا جيدة ، فقد لا تعمل مع OpenCV دون مزيد من التوضيح. لقد قرأت أيضًا عن الأشخاص الذين يستخدمون بعض المكتبات الأخرى لالتقاط صورة الكاميرا (مثل libdc1394 للكاميرات المستندة إلى IEEE 1394) ثم استخدام OpenCV فقط لمعالجة الصور. يتيح لك مدير حزم Brew تجميع OpenCV مباشرةً مع دعم libdc1394.
  • تعمل بعض الكاميرات مع OpenCV بشكل أفضل من غيرها
    إذا كنت محظوظًا ، يمكنك تعيين بعض خيارات الكاميرا مثل الإطارات في الثانية مباشرة على الكاميرا ولكن قد لا يكون لها أي تأثير على الإطلاق إذا لم يكن OpenCV متوافقًا مع جهازك. مرة أخرى ، بدون أي تفسير.
  • تعتمد جميع المعلمات على استخدام العالم الحقيقي
    عند استخدامها في تثبيت في العالم الحقيقي ، يوصى بشدة باختبار الخوارزميات والفلاتر في البيئة الفعلية لأن أشياء مثل الأضواء أو لون الخلفية أو اختيار العنصر لها تأثيرات كبيرة على النتيجة. يتضمن هذا أيضًا الظلال من ضوء النهار والأشخاص الواقفين وما إلى ذلك.

عادة ما تكون مطابقة الأنماط خيارًا أفضل

إذا رأيت أي واقع معزز مستخدم في الممارسة ، فمن المحتمل أن يعتمد على مطابقة النمط. إنه أكثر موثوقية بشكل عام ولا يتأثر بالمشكلات الموضحة أعلاه.

الفلاتر حاسمة

أعتقد أن الاستخدام الصحيح للفلاتر يتطلب بعض الخبرة ودائمًا القليل من السحر. يعتمد وقت معالجة معظم المرشحات على معلماتها ، على الرغم من أنه في OpenCV 3.0 تمت إعادة كتابة بعض منها بالفعل في CUDA C (لغة تشبه C للبرمجة المتوازية للغاية مع بطاقات رسومات NVIDIA) مما يؤدي إلى تحسينات كبيرة في الأداء.

تصفية البيانات من OpenCV

لقد رأينا أن اكتشاف الدائرة به بعض عدم الدقة: في بعض الأحيان يفشل في العثور على أي دائرة أو يكتشف نصف القطر الخطأ. لتقليل هذا النوع من الخطأ ، سيكون من المفيد تنفيذ طريقة أكثر تعقيدًا لتحسين الدقة. في مثالنا ، استخدمنا الوسيط لـ x و y radius ، وهو أمر بسيط للغاية. المرشح الشائع الاستخدام مع نتائج جيدة هو مرشح كالمان ، الذي يستخدمه الطيارون الآليون للطائرات بدون طيار لتقليل عدم الدقة القادمة من أجهزة الاستشعار. ومع ذلك ، فإن تنفيذها ليس بسيطًا مثل استخدام math.mean() فقط من https://mathjs.org.

خاتمة

رأيت لأول مرة تطبيقًا مشابهًا في المتحف الوطني للتاريخ الطبيعي في مدريد منذ عامين وتساءلت عن مدى صعوبة صنع شيء مشابه.

كانت فكرتي الأساسية وراء هذا العرض التوضيحي هي استخدام الأدوات الشائعة على الويب (مثل WebSockets و Three.js) ولا تتطلب أي متطلبات مسبقة حتى يتمكن أي شخص من البدء في استخدامها على الفور. لهذا السبب أردت استخدام اكتشاف الدائرة فقط وليس مطابقة الأنماط ، الأمر الذي يتطلب طباعة أو الحصول على كائن معين في العالم الحقيقي.

أريد أن أقول إنني قللت بشدة من أهمية متطلبات الكاميرا الفعلية. الإطارات العالية في الثانية والإضاءة الجيدة أهم من الدقة. لم أتوقع أيضًا أن عدم توافق الكاميرا مع OpenCV سيكون مشكلة.