เพิ่มความเป็นจริงอย่างง่ายด้วย OpenCV, Three.js และ WebSockets
เผยแพร่แล้ว: 2022-03-10โดยทั่วไปแล้วเทคโนโลยี Augmented Reality นั้นสร้างได้ยากมาก อย่างไรก็ตาม มันเป็นไปได้ที่จะสร้างโปรเจ็กต์ที่ดูน่าประทับใจโดยใช้เพียงแค่ไลบรารีโอเพนซอร์ส ในบทช่วยสอนนี้ เราจะใช้ OpenCV ใน Python เพื่อตรวจจับวัตถุรูปวงกลมในสตรีมเว็บแคมและแทนที่ด้วย 3D Earth ใน Three.js ในหน้าต่างเบราว์เซอร์ในขณะที่ใช้ WebSockets เพื่อรวมสิ่งนี้เข้าด้วยกัน
เราต้องการแยกส่วนหน้าและส่วนหลังออกอย่างเคร่งครัดเพื่อให้นำกลับมาใช้ใหม่ได้ ในแอปพลิเคชันในโลกแห่งความเป็นจริง เราสามารถเขียน front-end ใน Unity, Unreal Engine หรือ Blender เพื่อให้ดูสวยงาม ส่วนหน้าของเบราว์เซอร์เป็นวิธีที่ง่ายที่สุดในการใช้งานและควรทำงานกับการกำหนดค่าที่เป็นไปได้เกือบทั้งหมด
เพื่อให้ง่ายขึ้น เราจะแบ่งแอปออกเป็นสามส่วนย่อยๆ:
- Python แบ็กเอนด์ด้วย OpenCV OpenCV จะอ่านสตรีมเว็บแคมและเปิดหน้าต่างหลายบานที่มีรูปภาพของกล้องหลังจากส่งผ่านตัวกรองหลายตัวเพื่อลดการดีบักและให้ข้อมูลเชิงลึกเล็กน้อยเกี่ยวกับสิ่งที่อัลกอริธึมการตรวจจับวงกลมมองเห็นจริง ผลลัพธ์ของส่วนนี้จะเป็นเพียงพิกัด 2 มิติและรัศมีของวงกลมที่ตรวจพบ
- JavaScript front-end พร้อม Three.js ในเบราว์เซอร์ การใช้งานไลบรารี Three.js ทีละขั้นตอนเพื่อแสดง Earth ที่มีพื้นผิวโดยมีดวงจันทร์หมุนรอบตัว สิ่งที่น่าสนใจที่สุดที่นี่คือการทำแผนที่พิกัดหน้าจอ 2 มิติเข้ากับโลก 3 มิติ นอกจากนี้เรายังจะประมาณพิกัดและรัศมีเพื่อเพิ่มความแม่นยำของ OpenCV
- WebSockets ทั้ง front-end และ back-end Back-end ที่มีเซิร์ฟเวอร์ WebSockets จะส่งข้อความเป็นระยะพร้อมพิกัดวงกลมที่ตรวจพบและรัศมีไปยังไคลเอนต์เบราว์เซอร์
1. Python Back-End ด้วย OpenCV
ขั้นตอนแรกของเราคือการนำเข้าไลบรารี OpenCV ใน Python และเปิดหน้าต่างด้วยสตรีมเว็บแคมแบบสด
เราจะใช้ OpenCV 3.0 ใหม่ล่าสุด (ดูบันทึกการติดตั้ง) กับ Python 2.7 โปรดทราบว่าการติดตั้งในบางระบบอาจมีปัญหาและเอกสารทางการไม่เป็นประโยชน์มากนัก ฉันลองใช้ Mac OS X เวอร์ชัน 3.0 จาก MacPorts และไบนารีมีปัญหาการพึ่งพา ดังนั้นฉันจึงต้องเปลี่ยนไปใช้ Homebrew แทน โปรดทราบด้วยว่าแพ็คเกจ OpenCV บางแพ็คเกจอาจไม่มาพร้อมกับ Python Binding โดยค่าเริ่มต้น (คุณต้องใช้ตัวเลือกบรรทัดคำสั่งบางตัว)
ด้วย Homebrew ฉันวิ่ง:
brew install opencv
สิ่งนี้จะติดตั้ง OpenCV ด้วยการผูก Python เป็นค่าเริ่มต้น
เพียงเพื่อทดสอบสิ่งต่าง ๆ ฉันแนะนำให้คุณเรียกใช้ Python ในโหมดโต้ตอบ (เรียกใช้ python
ใน CLI โดยไม่มีข้อโต้แย้งใด ๆ ) และเขียน import cv2
หากติดตั้ง OpenCV อย่างถูกต้องและพาธไปยัง Python โยงถูกต้อง ก็ไม่น่าจะเกิดข้อผิดพลาดใดๆ
ต่อมา เราจะใช้ Python's 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
เราแยกจากลูป ปิดหน้าต่างและแอปจะสิ้นสุด
หากวิธีนี้ใช้ได้ผล เราก็ผ่านส่วนที่ยากที่สุดของแอปแบ็คเอนด์ที่ทำให้กล้องทำงานได้
การกรองภาพจากกล้อง
สำหรับการตรวจจับวงกลมจริง เราจะใช้ Circle Hough Transform ซึ่งใช้งานใน cv2.HoughCircles()
และตอนนี้เป็นอัลกอริธึมเดียวที่มีอยู่ใน OpenCV สิ่งสำคัญสำหรับเราคือต้องมีภาพระดับสีเทาเป็นอินพุต และใช้อัลกอริธึมตัว ตรวจจับขอบ Canny เพื่อค้นหาขอบในภาพ เราต้องการตรวจสอบด้วยตนเองว่าอัลกอริธึมเห็นอะไร ดังนั้นเราจะสร้างภาพขนาดใหญ่หนึ่งภาพจากภาพที่เล็กกว่าสี่ภาพโดยใช้ตัวกรองที่แตกต่างกัน
เครื่องตรวจจับขอบ Canny เป็นอัลกอริธึมที่ประมวลผลภาพโดยทั่วไปสี่ทิศทาง (แนวตั้ง แนวนอน และแนวทแยงสองเส้น) และค้นหาขอบ ขั้นตอนจริงที่อัลกอริธึมนี้ทำขึ้นนั้นมีรายละเอียดมากขึ้นใน Wikipedia หรือโดยสังเขปในเอกสาร OpenCV
ตรงกันข้ามกับรูปแบบการจับคู่ อัลกอริธึมนี้จะตรวจจับรูปร่างทรงกลม เพื่อให้เราสามารถใช้วัตถุใดๆ ที่เราต้องถือด้วยมือที่เป็นวงกลมได้ ฉันจะใช้ฝาจากโถกาแฟสำเร็จรูป แล้วตามด้วยแก้วกาแฟสีส้ม
เราไม่จำเป็นต้องทำงานกับรูปภาพขนาดเต็ม (แน่นอนว่าขึ้นอยู่กับความละเอียดของกล้องของคุณ) ดังนั้นเราจะปรับขนาดให้เหมาะสมระหว่าง capture.read()
และ cv2.imshow
เป็น 640px ความกว้างและความสูงตามอัตราส่วนภาพ:
width, height = image.shape scale = 640.0 / width image = cv2.resize(image, (0,0), fx=scale, fy=scale)
จากนั้น เราต้องการแปลงเป็นภาพระดับสีเทาและใช้การ เบลอค่ามัธยฐาน แรก ซึ่งจะขจัดสัญญาณรบกวนและคงขอบไว้ จากนั้นจึงใช้ตัวตรวจจับขอบ Canny เพื่อดูว่าอัลกอริธึมการตรวจจับวงกลมจะทำงานด้วยอะไร ด้วยเหตุผลนี้ เราจะสร้างตารางขนาด 2x2 พร้อมการแสดงตัวอย่างทั้งสี่แบบ
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 โดยไม่มีตัวกรองอื่น ภาพที่สองยังเป็น Canny edge detection แต่คราวนี้หลังจากใช้ Median blur แล้ว มันลดวัตถุในพื้นหลังซึ่งจะช่วยในการตรวจจับวงกลม
การตรวจจับวงกลมด้วย Hough Gradient
ภายใน OpenCV ใช้การนำ Hough Circle Transform ไปใช้อย่างมีประสิทธิภาพมากขึ้นที่เรียกว่า Hough Gradient Method ซึ่งใช้ข้อมูลขอบจากตัวตรวจจับขอบ Canny วิธีการไล่ระดับได้อธิบายไว้อย่างลึกซึ้งในหนังสือ Learning OpenCV และ Circle Hough Transform บน Wikipedia
ถึงเวลาสำหรับการตรวจจับวงกลมจริงแล้ว:
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)
สิ่งนี้จะพิมพ์ไปยังคอนโซล tuples เช่น:
(251, 202, 74) (252, 203, 73) (250, 202, 74) (246, 202, 76) (246, 204, 74) (246, 205, 72)
ดังที่คุณเห็นในแอนิเมชั่นนี้ ไม่พบแวดวงเลย กล้องในตัวของฉันมีเพียง 15fps และเมื่อฉันขยับมืออย่างรวดเร็ว ภาพจะเบลอ ดังนั้นจึงไม่พบขอบวงกลม แม้จะใส่ฟิลเตอร์แล้วก็ตาม
ในตอนท้ายของบทความนี้ เราจะกลับมาที่ปัญหานี้และพูดคุยกันมากมายเกี่ยวกับการตั้งค่าเฉพาะของกล้องและทางเลือกของอัลกอริธึมการตรวจจับ แต่เราบอกได้เลยว่าแม้ว่าการตั้งค่าของฉันจะแย่มาก (เพียง 15fps, แสงไม่ดี, มีจุดรบกวนในพื้นหลังมาก วัตถุมีความเปรียบต่างต่ำ) ผลที่ได้คือดีพอสมควร
นั่นคือทั้งหมดที่สำหรับตอนนี้. เรามีพิกัด x
และ y
และ radius
เป็นพิกเซลของวงกลมที่พบในภาพเว็บแคม
คุณสามารถดูซอร์สโค้ดแบบเต็มสำหรับส่วนนี้ได้ที่ gist.github.com
2. JavaScript Front-End ด้วย Three.js ในเบราว์เซอร์
ส่วนหน้าจะขึ้นอยู่กับไลบรารี Three.js (เวอร์ชัน r72) เราจะเริ่มต้นด้วยการสร้างทรงกลมพื้นผิวที่หมุนได้ซึ่งเป็นตัวแทนของโลกที่อยู่ตรงกลางหน้าจอ จากนั้นเพิ่มดวงจันทร์ที่หมุนไปรอบๆ ในตอนท้าย เราจะแมปพิกัดเมาส์หน้าจอ 2 มิติกับพื้นที่ 3 มิติ
หน้า 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); });
ดูการสาธิตสดที่นี่


นี่เป็นเพียงสิ่งพื้นฐานทั่วไปของ Three.js ชื่ออ็อบเจ็กต์และเมธอดสามารถอธิบายตนเองได้ (เช่น receiveShadow
หรือ castShadow
) แต่ถ้าคุณไม่เคยใช้มาก่อน เราขอแนะนำให้คุณดูบทช่วยสอนของ Lee Stemkoski
ทางเลือก เราสามารถวาดแกนตรงกลางหน้าจอเพื่อช่วยเราเกี่ยวกับระบบพิกัด
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; }
ดูการสาธิตสดที่นี่
การทำแผนที่พิกัด 2 มิติไปยังโลก 3 มิติ
จนถึงตอนนี้ทุกอย่างค่อนข้างชัดเจน ส่วนที่น่าสนใจที่สุดคือวิธีการแอบแฝงพิกัดหน้าจอ 2D ที่มาจาก OpenCV (ดูผลลัพธ์ของการตรวจจับแบบวงกลมด้านบน) ไปยังโลก 3 มิติ? เมื่อเรากำหนดรัศมีและตำแหน่งใน Three.js เราใช้บางหน่วย แต่สิ่งเหล่านี้ไม่เกี่ยวข้องกับพิกเซลหน้าจอจริง อันที่จริง ขนาดของทุกสิ่งที่เราเห็นในฉากนั้นขึ้นอยู่กับการตั้งค่ากล้องของเราเป็นอย่างมาก (เช่น อัตราส่วนภาพหรือระยะการมองเห็น)
ด้วยเหตุผลนี้ เราจะสร้างวัตถุระนาบแบนที่มีขนาดใหญ่พอที่จะครอบคลุมทั้งฉากโดยให้จุดศูนย์กลางอยู่ที่ [0,0,0]
เพื่อจุดประสงค์ในการสาธิต เราจะแมปพิกัดของเมาส์ 2 มิติกับตำแหน่งของโลกในแบบ 3 มิติด้วยแกน z
คงที่ กล่าวอีกนัยหนึ่ง เราจะแปลงเฉพาะ x
และ y
และไม่ต้องกังวลกับ z
ซึ่งเป็นระยะห่างจากวัตถุไปยังกล้องของเรา
เราจะแปลงตำแหน่งหน้าจอเมาส์เป็นช่วงตั้งแต่ -1.0
ถึง +1.0
โดยที่จุดศูนย์กลางอยู่ที่ [0,0]
เนื่องจากเราต้องทำงานกับเวกเตอร์ที่ทำให้เป็นมาตรฐาน
ต่อมา เราจะใช้เทคนิคนี้เพื่อจับคู่ตำแหน่งของวงกลมที่ตรวจพบกับ 3 มิติ และเพื่อให้ตรงกับขนาดวงกลมจาก 2 มิติเป็น 3 มิติ
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; }
ดูการสาธิตสดที่นี่

เนื่องจากเรากำลังตรวจสอบทางแยกด้วยระนาบ เรารู้ว่าจะมีเพียงจุดเดียวเสมอ
นั่นคือทั้งหมดสำหรับส่วนนี้ ในตอนท้ายของส่วนถัดไป เรายังจะเพิ่ม WebSockets และองค์ประกอบ <video>
ด้วยสตรีมกล้องของเราที่จะถูกซ้อนทับด้วยฉาก 3 มิติใน Three.js
3. WebSockets ทั้ง Front-End และ Back-End
เราสามารถเริ่มต้นด้วยการนำ WebSockets ไปใช้ใน Python back-end โดยติดตั้ง 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
ใน Constructor ของเซิร์ฟเวอร์ เพื่อตรวจสอบข้อความที่ค้างอยู่เป็นระยะ เซิร์ฟเวอร์จะส่งข้อความเมื่อได้รับข้อมูลจากลูกค้าเท่านั้น หรือจำเป็นต้องนั่งบนเธรดหลักในลูป เราปล่อยให้มันบล็อกเธรดหลักไม่ได้เพราะ OpenCV ก็ต้องการเช่นกัน เนื่องจากเรารู้ว่ากล้องทำงานที่ 15fps เท่านั้น เราจึงสามารถใช้ช่วงเวลาเดียวกันบนเซิร์ฟเวอร์ 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. };
แทนที่จะกำหนดตำแหน่งของ Earth ให้อยู่ในตำแหน่งเมาส์ปัจจุบันของฉัน เราจะใช้ตัวแปร msgHistory
อาจไม่จำเป็นต้องวางโค้ดทั้งหมดที่นี่ ดังนั้นโปรดดูรายละเอียดการใช้งานได้ที่ gist.gihtub.com
จากนั้นเพิ่มองค์ประกอบ <video>
หนึ่งรายการโดยมีสตรีมเว็บแคมเต็มหน้าต่างซึ่งจะถูกซ้อนทับด้วยฉาก 3 มิติของเราด้วยพื้นหลังโปร่งใส
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() {}); }
ผลลัพธ์สุดท้าย:
สรุปสิ่งที่เราทำและสิ่งที่วิดีโอด้านบนแสดงได้อย่างรวดเร็ว:
- Python back-end ใช้งานเซิร์ฟเวอร์ WebSocket
- เซิร์ฟเวอร์ตรวจพบวงกลมโดยใช้ OpenCV จากสตรีมเว็บแคม
- ไคลเอนต์ JavaScript แสดงสตรีมเว็บแคมเดียวกันโดยใช้องค์ประกอบ
<video>
- ไคลเอนต์แสดงฉาก 3 มิติโดยใช้ Three.js
- ลูกค้าเชื่อมต่อกับเซิร์ฟเวอร์ผ่านโปรโตคอล WebSocket และรับตำแหน่งวงกลมและรัศมี
รหัสจริงที่ใช้สำหรับการสาธิตนี้มีอยู่ใน GitHub มันซับซ้อนกว่าเล็กน้อยและยังสอดแทรกพิกัดระหว่างสองข้อความจากแบ็กเอนด์เพราะสตรีมเว็บแคมทำงานที่ 15fps เท่านั้นในขณะที่ฉาก 3D แสดงผลที่ 60fps คุณสามารถดูวิดีโอต้นฉบับบน YouTube
คำเตือน
มีข้อน่าสังเกตบางประการ:
การตรวจจับวงกลมไม่เหมาะ
ใช้งานได้ดีกับวัตถุทรงกลมใดๆ แต่มีความไวต่อสัญญาณรบกวนและการเสียรูปของภาพมาก อย่างที่คุณเห็นด้านบน ผลลัพธ์ของเราค่อนข้างดี นอกจากนี้ยังอาจไม่มีตัวอย่างในทางปฏิบัติของการตรวจจับวงกลมที่มีอยู่นอกเหนือจากการใช้งานพื้นฐานที่สุด มันอาจจะดีกว่าถ้าใช้การตรวจจับวงรี แต่ยังไม่ได้ใช้งานใน OpenCV ในตอนนี้
ทุกอย่างขึ้นอยู่กับการตั้งค่าของคุณ
เว็บแคมในตัวมักจะค่อนข้างแย่ 15fps ไม่เพียงพอและเพียงแค่เพิ่มเป็น 30fps จะช่วยลดความเบลอของการเคลื่อนไหวได้อย่างมากและทำให้การตรวจจับมีความน่าเชื่อถือมากขึ้น เราสามารถแบ่งจุดนี้ออกเป็นสี่จุดเพิ่มเติม:
- การบิดเบือนของกล้อง
กล้องหลายตัวมีความผิดเพี้ยนของภาพ ซึ่งโดยทั่วไปแล้วจะเป็นเอฟเฟกต์ตาปลาซึ่งมีอิทธิพลอย่างมากต่อการตรวจจับรูปร่าง เอกสารประกอบของ 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