Python: ค้นหาจำนวนลายมือในวิดีโอ
คุณรู้จักอัลกอริทึมที่สามารถดูว่ามีการเขียนด้วยลายมือบนรูปภาพหรือไม่? ฉันไม่สนใจที่จะรู้ว่าลายมือเขียนว่าอย่างไร แต่มีเพียงของขวัญชิ้นเดียวเท่านั้น?
ฉันมีวิดีโอของใครบางคนที่กรอกสไลด์ด้วยลายมือ เป้าหมายของฉันคือการกำหนดจำนวนสไลด์ที่เต็มไปด้วยลายมือแล้ว
สามารถดาวน์โหลดวิดีโอที่เป็นปัญหาได้ที่นี่: http://www.filedropper.com/00_6
สำหรับวิดีโอนี้มีการแนะนำวิธีแก้ปัญหาที่ยอดเยี่ยมไว้แล้วในQuantify ว่าสไลด์นั้นเต็มไปด้วยลายมือเท่าใด
วิธีแก้ปัญหาขึ้นอยู่กับการสรุปจำนวนสีเฉพาะที่ใช้สำหรับการเขียนด้วยลายมือ อย่างไรก็ตามหากลายมือไม่ได้เป็นสีน้ำเงิน แต่มีสีอื่นที่สามารถพบได้ในสิ่งที่ไม่ใช่ลายมือด้วยวิธีนี้จะไม่ได้ผล
ดังนั้นฉันจึงสนใจที่จะทราบว่ามีวิธีแก้ปัญหาทั่วไปเพิ่มเติมเพื่อตรวจสอบว่ามีลายมือปรากฏอยู่บนรูปภาพหรือไม่?
สิ่งที่ฉันได้ทำไปแล้ว:ฉันกำลังคิดที่จะแยกรูปทรงของภาพจากนั้นก็ตรวจจับส่วนที่เขียนด้วยลายมือตามความโค้งของรูปทรง (แต่ฉันไม่รู้ว่าจะทำส่วนนั้นอย่างไร) มันอาจจะไม่ใช่ความคิดที่ดีที่สุด แต่มันก็ไม่ถูกต้องเสมอไป ...
import cv2
import matplotlib.pyplot as plt
img = cv2.imread(PATH TO IMAGE)
print("img shape=", img.shape)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
cv2.imshow("image", gray)
cv2.waitKey(1)
#### extract all contours
# Find Canny edges
edged = cv2.Canny(gray, 30, 200)
cv2.waitKey(0)
# Finding Contours
# Use a copy of the image e.g. edged.copy()
# since findContours alters the image
contours, hierarchy = cv2.findContours(edged,
cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
cv2.imshow('Canny Edges After Contouring', edged)
cv2.waitKey(0)
print("Number of Contours found = " + str(len(contours)))
# Draw all contours
# -1 signifies drawing all contours
cv2.drawContours(img, contours, -1, (0, 255, 0), 3)
cv2.imshow('Contours', img)
cv2.waitKey(0)
คำตอบ
คุณสามารถระบุพื้นที่ที่ถ่ายด้วยการเขียนด้วยมือโดยการกำบังพิกเซลจากเทมเพลตจากนั้นทำเช่นเดียวกันเพื่อให้เกิดความแตกต่างระหว่างเฟรมต่อไปและเทมเพลต คุณสามารถใช้การขยายช่องเปิดและขีด จำกัด สำหรับสิ่งนี้
เริ่มต้น Let 's กับคุณแม่แบบ มาระบุส่วนที่เราจะปกปิด:
import cv2
import numpy as np
template = cv2.imread('template.jpg')
ตอนนี้เรามาขยายพิกเซลที่ถูกครอบครองเพื่อสร้างโซนที่เราจะมาสก์ (ซ่อน) ในภายหลัง:
template = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY)
kernel = np.ones((5, 5),np.uint8)
dilation = cv2.dilate(255 - template, kernel,iterations = 5)
_, thresh = cv2.threshold(dilation,25,255,cv2.THRESH_BINARY_INV)
ในเฟรมต่อมาเราจะลบมาสก์นี้ออกจากรูปภาพโดยเปลี่ยนพิกเซลทั้งหมดให้เป็นสีขาว ตัวอย่างเช่น:
import numpy as np
import cv2
vidcap = cv2.VideoCapture('0_0.mp4')
success,image = vidcap.read()
count = 0
frames = []
while count < 500:
frames.append(image)
success,image = vidcap.read()
count += 1
mask = np.where(thresh == 0)
example = frames[300]
example[mask] = [255, 255, 255]
cv2.imshow('', example)
cv2.waitKey(0)
ตอนนี้เราจะสร้างฟังก์ชันที่จะคืนค่าความแตกต่างระหว่างเทมเพลตและรูปภาพที่กำหนด นอกจากนี้เรายังจะใช้การเปิดเพื่อกำจัดพิกเซลที่เหลือเพียงพิกเซลเดียวที่จะทำให้น่าเกลียด
def difference_with_mask(image):
grayscale = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
kernel = np.ones((5, 5), np.uint8)
dilation = cv2.dilate(255 - grayscale, kernel, iterations=5)
_, thresh = cv2.threshold(dilation, 25, 255, cv2.THRESH_BINARY_INV)
thresh[mask] = 255
closing = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel)
return closing
cv2.imshow('', difference_with_mask(frames[400]))
cv2.waitKey(0)
เพื่อจัดการกับความจริงที่ว่าคุณไม่ต้องการให้มือถูกตรวจพบว่าเป็นการเขียนด้วยมือฉันขอแนะนำว่าแทนที่จะใช้มาสก์สำหรับแต่ละเฟรมคุณใช้เปอร์เซ็นไทล์ที่ 95 ของ 15 เฟรมที่ 30 สุดท้าย ... ดูนี่สิ:
results = []
for ix, frame in enumerate(frames):
if ix % 30 == 0:
history.append(frame)
results.append(np.quantile(history, 0.95, axis=0))
print(ix)
ตอนนี้เฟรมตัวอย่างกลายเป็นแบบนี้ (มือจะถูกลบออกเพราะส่วนใหญ่ไม่ได้อยู่ใน 15 เฟรมสุดท้ายที่ 30):
ดังที่คุณเห็นบางส่วนของการเขียนด้วยมือหายไป มันจะมาในภายหลังเนื่องจากการเปลี่ยนแปลงเปอร์เซ็นไทล์ขึ้นอยู่กับเวลาที่เรากำลังทำอยู่ คุณจะเห็นในภายหลัง: ในตัวอย่างของฉันที่มีเฟรม 18,400 ข้อความที่ขาดหายไปในภาพด้านบนจะปรากฏขึ้น จากนั้นคุณสามารถใช้ฟังก์ชันที่ฉันให้คุณและนี่จะเป็นผลลัพธ์:
และไปเลย! โปรดทราบว่าโซลูชันนี้ซึ่งไม่รวมถึงมือจะใช้เวลาคำนวณนานขึ้นเนื่องจากมีการคำนวณบางอย่างที่ต้องทำ การใช้เพียงภาพโดยไม่คำนึงถึงมือจะคำนวณได้ทันทีถึงขนาดที่คุณสามารถเรียกใช้บนฟีดเว็บแคมได้แบบเรียลไทม์
ตัวอย่างสุดท้าย:
นี่คือเฟรม 18,400:
ภาพสุดท้าย:
คุณสามารถเล่นกับฟังก์ชั่นนี้ได้หากต้องการให้มาสก์ล้อมรอบข้อความให้บางมากขึ้น:
รหัสเต็ม:
import os
import numpy as np
import cv2
vidcap = cv2.VideoCapture('0_0.mp4')
success,image = vidcap.read()
count = 0
from collections import deque
frames = deque(maxlen=700)
while count < 500:
frames.append(image)
success,image = vidcap.read()
count += 1
template = cv2.imread('template.jpg')
template = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY)
kernel = np.ones((5, 5),np.uint8)
dilation = cv2.dilate(255 - template, kernel,iterations = 5)
cv2.imwrite('dilation.jpg', dilation)
cv2.imshow('', dilation)
cv2.waitKey(0)
_, thresh = cv2.threshold(dilation,25,255,cv2.THRESH_BINARY_INV)
cv2.imwrite('thresh.jpg', thresh)
cv2.imshow('', thresh)
cv2.waitKey(0)
mask = np.where(thresh == 0)
example = frames[400]
cv2.imwrite('original.jpg', example)
cv2.imshow('', example)
cv2.waitKey(0)
example[mask] = 255
cv2.imwrite('example_masked.jpg', example)
cv2.imshow('', example)
cv2.waitKey(0)
def difference_with_mask(image):
grayscale = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
kernel = np.ones((5, 5), np.uint8)
dilation = cv2.dilate(255 - grayscale, kernel, iterations=5)
_, thresh = cv2.threshold(dilation, 25, 255, cv2.THRESH_BINARY_INV)
thresh[mask] = 255
closing = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel)
return closing
cv2.imshow('', difference_with_mask(frames[400]))
cv2.waitKey(0)
masked_example = difference_with_mask(frames[400])
cv2.imwrite('masked_example.jpg', masked_example)
from collections import deque
history = deque(maxlen=15)
results = []
for ix, frame in enumerate(frames):
if ix % 30 == 0:
history.append(frame)
results.append(np.quantile(history, 0.95, axis=0))
print(ix)
if ix > 500:
break
cv2.imshow('', frames[400])
cv2.waitKey(0)
cv2.imshow('', results[400].astype(np.uint8))
cv2.imwrite('percentiled_frame.jpg', results[400].astype(np.uint8))
cv2.waitKey(0)
cv2.imshow('', difference_with_mask(results[400].astype(np.uint8)))
cv2.imwrite('final.jpg', difference_with_mask(results[400].astype(np.uint8)))
cv2.waitKey(0)
คุณสามารถลองสร้างเทมเพลตก่อนที่จะตรวจพบซึ่งคุณสามารถใช้เพื่อหักออกจากเฟรมปัจจุบันของวิดีโอได้ วิธีหนึ่งที่คุณสามารถสร้างเทมเพลตดังกล่าวได้คือการวนซ้ำทุกพิกเซลของเฟรมและค้นหาว่ามีค่า (สีขาว) ในพิกัดนั้นสูงกว่าค่าที่เก็บไว้ในรายการหรือไม่
นี่คือตัวอย่างเทมเพลตดังกล่าวจากวิดีโอของคุณโดยการวนซ้ำในสองวินาทีแรก:
เมื่อคุณมีแล้วคุณสามารถตรวจจับข้อความได้อย่างง่ายดาย คุณสามารถใช้cv2.absdiff()ฟังก์ชันเพื่อสร้างความแตกต่างของเทมเพลตและเฟรม นี่คือตัวอย่าง:
เมื่อคุณมีภาพนี้แล้วการค้นหาการเขียน (threshold + contour search หรือสิ่งที่คล้ายกัน) เป็นเรื่องเล็กน้อย
นี่คือตัวอย่างรหัส:
import numpy as np
import cv2
cap = cv2.VideoCapture('0_0.mp4') # read video
bgr = cap.read()[1] # get first frame
frame = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY) # transform to grayscale
template = frame.copy() # make a copy of the grayscale
h, w = frame.shape[:2] # height, width
matrix = [] # a list for [y, x] coordinares
# fill matrix with all coordinates of the image (height x width)
for j in range(h):
for i in range(w):
matrix.append([j, i])
fps = cap.get(cv2.CAP_PROP_FPS) # frames per second of the video
seconds = 2 # How many seconds of the video you wish to look the template for
k = seconds * fps # calculate how many frames of the video is in that many seconds
i = 0 # some iterator to count the frames
lowest = [] # list that will store highest values of each pixel on the fram - that will build our template
# store the value of the first frame - just so you can compare it in the next step
for j in matrix:
y = j[0]
x = j[1]
lowest.append(template[y, x])
# loop through the number of frames calculated before
while(i < k):
bgr = cap.read()[1] # bgr image
frame = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY) # transform to grayscale
# iterate through every pixel (pixels are located in the matrix)
for l, j in enumerate(matrix):
y = j[0] # x coordinate
x = j[1] # y coordinate
temp = template[y, x] # value of pixel in template
cur = frame[y, x] # value of pixel in the current frame
if cur > temp: # if the current frame has higher value change the value in the "lowest" list
lowest[l] = cur
i += 1 # increment the iterator
# just for vizualization
cv2.imshow('frame', frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
i = 0 # new iteratir to increment position in the "lowest" list
template = np.ones((h, w), dtype=np.uint8)*255 # new empty white image
# iterate through the matrix and change the value of the new empty white image to that value
# in the "lowest" list
for j in matrix:
template[j[0], j[1]] = lowest[i]
i += 1
# just for visualization - template
cv2.imwrite("template.png", template)
cv2.imshow("template", template)
cv2.waitKey(0)
cv2.destroyAllWindows()
counter = 0 # counter of countours: logicaly if the number of countours would
# rapidly decrease than that means that a new template is in order
mean_compare = 0 # this is needed for a simple color checker if the contour is
# the same color as the oders
# this is the difference between the frame of the video and created template
while(cap.isOpened()):
bgr = cap.read()[1] # bgr image
frame = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY) # grayscale
img = cv2.absdiff(template, frame) # resulted difference
thresh = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)[1] # thresholded image
kernel = np.ones((5, 5), dtype=np.uint8) # simple kernel
thresh = cv2.dilate(thresh, kernel, iterations=1) # dilate thresholded image
cnts, h = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # contour search
if len(cnts) < counter*0.5 and counter > 50: # check if new template is in order
# search for new template again
break
else:
counter = len(cnts) # update counter
for cnt in cnts: # iterate through contours
size = cv2.contourArea(cnt) # size of contours - to filter out noise
if 20 < size < 30000: # noise criterion
mask = np.zeros(frame.shape, np.uint8) # empry mask - needed for color compare
cv2.drawContours(mask, [cnt], -1, 255, -1) # draw contour on mask
mean = cv2.mean(bgr, mask=mask) # the mean color of the contour
if not mean_compare: # first will set the template color
mean_compare = mean
else:
k1 = 0.85 # koeficient how much each channels value in rgb image can be smaller
k2 = 1.15 # koeficient how much each channels value in rgb image can be bigger
# condition
b = bool(mean_compare[0] * k1 < mean[0] < mean_compare[0] * k2)
g = bool(mean_compare[1] * k1 < mean[1] < mean_compare[1] * k2)
r = bool(mean_compare[2] * k1 < mean[2] < mean_compare[2] * k2)
if b and g and r:
cv2.drawContours(bgr, [cnt], -1, (0, 255, 0), 2) # draw on rgb image
# just for visualization
cv2.imshow('img', bgr)
if cv2.waitKey(1) & 0xFF == ord('s'):
cv2.imwrite(str(j)+".png", img)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
# release the video object and destroy window
cap.release()
cv2.destroyAllWindows()
ผลลัพธ์ที่เป็นไปได้อย่างหนึ่งด้วยตัวกรองขนาดและสีที่เรียบง่าย:
หมายเหตุ: อัลกอริทึมการค้นหาเทมเพลตนี้ช้ามากเนื่องจากลูปที่ซ้อนกันและอาจได้รับการปรับให้เหมาะสมเพื่อให้เร็วขึ้นคุณต้องมีความรู้ทางคณิตศาสตร์มากกว่าฉันเล็กน้อย นอกจากนี้คุณจะต้องตรวจสอบว่าเทมเพลตมีการเปลี่ยนแปลงในวิดีโอเดียวกันหรือไม่ฉันเดาว่าไม่น่าจะยากเกินไป
แนวคิดที่ง่ายกว่าในการทำให้เร็วขึ้นเล็กน้อยคือการปรับขนาดเฟรมเพื่อสมมติว่า 20% และทำการค้นหาเทมเพลตเดียวกัน หลังจากนั้นปรับขนาดกลับเป็นต้นฉบับและขยายเทมเพลต ผลลัพธ์จะไม่ดีเท่า แต่จะสร้างมาสก์ว่าข้อความและเส้นของเทมเพลตอยู่ที่ไหน จากนั้นวาดลงบนเฟรม
ฉันไม่คิดว่าคุณต้องการรหัสจริงๆในกรณีนี้และมันจะค่อนข้างนานถ้าคุณทำ แต่นี่คืออัลกอริทึมที่ต้องทำ
ใช้โมเดล EAST (ตัวตรวจจับข้อความฉากที่แม่นยำอย่างมีประสิทธิภาพ) ของ OpenCV ที่จุดเริ่มต้นเพื่อสร้างข้อความเริ่มต้นบนสไลด์ นั่นทำให้คุณมีกรอบขอบเขตของเปอร์เซ็นต์เริ่มต้นของสไลด์ที่ปกคลุมไปด้วยข้อความสไลด์ซึ่งต่างจากข้อความอธิบายที่เขียนด้วยลายมือ
ทุกครั้งพูด 1-5 วินาที (คนเขียนไม่ได้เร็วขนาดนั้น) เปรียบเทียบภาพพื้นฐานนั้นกับภาพปัจจุบันและภาพก่อนหน้า
หากรูปภาพปัจจุบันมีข้อความมากกว่ารูปภาพก่อนหน้า แต่กรอบขอบเขตเริ่มต้นไม่เหมือนกันแสดงว่าคุณมีสไลด์ใหม่และค่อนข้างยุ่ง
หากรูปภาพปัจจุบันมีข้อความมากกว่าภาพก่อนหน้า แต่กล่องขอบเขตเริ่มต้นเหมือนกันระบบจะเพิ่มข้อความเพิ่มเติม
หากรูปภาพปัจจุบันมีข้อความน้อยกว่ารูปภาพก่อนหน้า แต่กรอบขอบเขตเริ่มต้นไม่เหมือนกันคุณจะมีสไลด์ใหม่อีกครั้ง - เท่านั้นไม่ยุ่งและมีพื้นที่ว่างเหมือนภาพสุดท้ายที่จะเขียน
หากรูปภาพปัจจุบันมีข้อความน้อยกว่ารูปภาพก่อนหน้า แต่กรอบขอบเขตเริ่มต้นเหมือนกันคุณอาจมีสไลด์ที่ซ้ำกันซึ่งน่าจะเป็นข้อความมากกว่านั้นหรือครูกำลังลบส่วนเพื่อดำเนินการต่อหรือแก้ไขคำอธิบาย หมายความว่าคุณจะต้องมีวิธีจัดการกับสิ่งนี้
เมื่อคุณมีสไลด์ใหม่ให้นำรูปภาพก่อนหน้านี้และเปรียบเทียบกรอบขอบเขตของข้อความทั้งหมดลบกล่องสำหรับสถานะเริ่มต้น
ในทางคำนวณสิ่งนี้จะไม่ถูก (แน่นอนคุณจะไม่ทำชีวิตนี้อย่างน้อยก็ไม่เป็นเวลาหลายปี) แต่มันแข็งแกร่งและการสุ่มตัวอย่างข้อความทุก ๆ วินาทีจะช่วยได้
โดยส่วนตัวฉันจะเข้าใกล้สิ่งนี้เป็นวงดนตรี นั่นคือกล่องขอบเขตเริ่มต้นจากนั้นดูสีของข้อความ หากคุณสามารถหลีกเลี่ยงเปอร์เซ็นต์ของข้อความสีต่างๆได้ให้ทำ และเมื่อทำไม่ได้คุณก็ยังสบายดี
นอกจากคำตอบที่ดีที่ผู้คนให้มาแล้วฉันยังมีคำแนะนำอีกสองอย่าง
วิธีแรกคือวิธีการของ CNN การใช้รูทีนการตรวจจับวัตถุบางอย่างสามารถทำได้โดยสิ้นเชิงหรือแม้แต่วิธีการแบ่งส่วน (เช่น U-NET) เพื่อแยกความแตกต่างระหว่างข้อความ เป็นเรื่องง่ายเพราะคุณสามารถค้นหารูปภาพนับล้านจากหนังสือข้อความดิจิทัลและเอกสารที่เขียนด้วยลายมือเพื่อฝึกโมเดลของคุณ
แนวทางที่สองคือการค้นหาและแยกทุกสัญลักษณ์บนภาพแยกกัน (ด้วยวิธีง่ายๆเช่นเดียวกับที่คุณใช้จนถึงตอนนี้หรือด้วยconnectedcomponent) เนื่องจากตัวอักษรและสัญลักษณ์การพิมพ์มีรูปร่างและลักษณะที่เป็นเอกลักษณ์ (แบบอักษรที่คล้ายกัน - ไม่เหมือนกับตัวอักษรที่เขียนด้วยลายมือ) คุณจึงสามารถจับคู่ตัวอักษรที่พบทั้งหมดกับตัวอักษรพิมพ์ตัวอย่างที่คุณรวบรวมแยกกันเพื่อแยกความแตกต่างระหว่างตัวอักษรที่เขียนด้วยลายมือและตัวพิมพ์ การจับคู่ตามจุดคุณลักษณะ (เช่น SURF) อาจเป็นเครื่องมือที่ดีสำหรับแนวทางนี้