Curses Cam

Normally a shell lets you input via a text command and outputs via a text message. When using curses you can extend its capabilities and program for example games with limited graphics.
I wanted to go even further and built a python script that — using pygame’s camera module and curses — captures an image from an attached webcam (USB or built-in), transforms it and displays it on the shell.
Although both the resolution and the color depth are shrunk immensely, the resulting colored text on the shell often resembles the scene caught and has a nice visual effect.
There are two modes, camera and file viewer, which can be toggled by pressing F1. In camera mode you can see what the camera is seeing and snap a photo, which then will be saved to disk. In file viewer mode you can view the photos you took. The files will be saved in an out/ directory located in the current python file’s directory. Saved photos have the file extension .si (shell image).
Unfortunately pygame’s camera module does neither work on Mac OS X nor on Windows. Thus this program is only properly usable under Linux systems. You obviously also need a webcam or else you will not be able to take a picture.

Controls

  • F1 switches between camera and file viewer mode
  • Space saves the current photo as a .si file (only in camera mode)
  • Left or down arrow key decreases current file’s id (only in file viewer mode)
  • Right or up arrow key increases current file’s id (only in file viewer mode)

Blinky Graffiti Hand


# Python 2.7.7 Code
# Jonathan Frech 19th of August, 2016
#         edited 21st of August, 2016
#         edited 23rd of August, 2016
#         edited 26th of August, 2016

# import
import curses, pygame, pygame.camera, time, os

# init
def init():
	global MODE, PATH, RUNNING, KEYS, stdscr, cam, toclear
	
	MODE = ["cam", "view"][0]
	PATH = os.path.abspath(os.path.dirname(__file__)) + "/"
	RUNNING = True
	
	KEYS = {
		"quit":[curses.KEY_F12                   ],
		"snap":[ord(" ")                         ],
		"prev":[curses.KEY_LEFT , curses.KEY_DOWN],
		"next":[curses.KEY_RIGHT, curses.KEY_UP  ],
		"swch":[curses.KEY_F1                    ],
		
		"debg":[ord("+")                         ]
	}
	
	stdscr = curses.initscr()
	stdscr.keypad(True)

	curses.noecho()
	curses.cbreak()
	curses.curs_set(False)
	curses.start_color()
	curses.use_default_colors()
	colors()
	
	pygame.camera.init()
	cams = pygame.camera.list_cameras()
	cam = pygame.camera.Camera(cams[0], [640, 480])
	cam.start()
	
	toclear = 10

# cleanup
def cleanup():
	cam.stop()
	
	stdscr.keypad(False)
	stdscr.nodelay(False)
	
	curses.echo()
	curses.nocbreak()
	curses.curs_set(True)
	curses.endwin()

# colors
def colors():
	global COLORS, MIN, MAX
	
	colorpairs = [curses.COLOR_BLACK, curses.COLOR_RED, curses.COLOR_GREEN, curses.COLOR_BLUE, curses.COLOR_YELLOW, curses.COLOR_CYAN, curses.COLOR_MAGENTA, curses.COLOR_WHITE]
	for _ in range(0, len(colorpairs)):
		#curses.init_pair(_+1, curses.COLOR_BLACK, colorpairs[_])
		curses.init_pair(_+1, colorpairs[_], curses.COLOR_BLACK)
		#curses.init_pair(_+1, curses.COLOR_WHITE, curses.COLOR_BLACK)
	COLORS = ["000", "100", "010", "001", "110", "011", "101", "111"]

# save shell image (si)
def save(data):
	path = PATH + "out/"
	if not os.path.exists(path):
		try:
			os.mkdir(path)
		except:
			pass
	
	path += "img" + str(len(os.listdir(path))) + ".si"
	
	try:
		doc = open(path, "w")
		doc.write(data)
		doc.close()
	except:
		pass
def load(fileid):
	path = PATH + "out/img" + str(fileid) + ".si"
	try:
		doc = open(path, "r")
		data = doc.read().split()
		doc.close()
		
		return data
	except:
		pass
	
	return ["1", "1"]

# resize
def resize():
	global my, mx, off
	
	my, mx = stdscr.getmaxyx()
	off = [(my-siz[1])/2, (mx-siz[0]*2)/2]

# camera
def runcam():
	global siz, toclear
	
	# rgb min and max
	MIN = [0, 0, 0]
	MAX = [255, 255, 255]
	
	# char used to display image
	CHR = "#"
	
	# sleep time
	SLP = .05
	
	# while loop
	while True:
		# get image
		img = cam.get_image()
		
		# resize image
		siz = [4*15, 3*15]
		#siz = [4*25, 3*25]
		img = pygame.transform.scale(img, siz)
		
		# calculate offset
		resize()
		
		# get image's size
		rct = img.get_rect()
		
		# new min and max
		min = [255, 255, 255]
		max = [0, 0, 0]
		
		# data to save the image
		data = ""
		
		# go through x and y
		for y in range(0, rct.bottom):
			for x in range(0, rct.right):
				# range
				if y+off[0] >= 0 and y+off[0] < my-1 and x*2+off[1] >= 0 and x*2+off[1] < mx-1:
					# get color
					c = img.get_at([x, y])
					
					# transform color
					C = ""
					for _ in range(0, 3):
						# update new min and max
						if c[_] < min[_]:
							min[_] = c[_]
						if c[_] > max[_]:
							max[_] = c[_]
						
						# new color
						if c[_] < MIN[_]+(MAX[_]-MIN[_])/2:
							C += "0"
						else:
							C += "1"
					
					# save data
					data += str(COLORS.index(C))
					
					# character determined by gray value
					#n = (c[0]+c[1]+c[2])/3.
					#chs = [".", ",", ":", ";", "-", "~", "*", "+", "/", "&", "=", "#", "@"]
					#CHR = chs[int(n/256.*len(chs))]
					
					# render
					stdscr.addstr(y+off[0], x*2+off[1], CHR*2, curses.color_pair(COLORS.index(C)+1))
			
			# next line
			data += "\n"
		
		# update min and max
		MIN = min
		MAX = max
		
		# handle characters
		char = stdscr.getch()
		
		# quit
		if char in KEYS["quit"]:
			return "//quit"
		
		# save
		elif char in KEYS["snap"]:
			save(data[:-1])
		
		# switch
		elif char in KEYS["swch"]:
			return "view"
		
		# resize
		if curses.is_term_resized(my, mx):
			resize()
		
		# refresh and wait
		stdscr.refresh()
		time.sleep(SLP)
		
		# clear
		toclear -= 1
		if toclear <= 0:
			toclear = 10
			stdscr.clear()

# file viewer
def runview():
	global siz
	
	fileid = 0
	data = load(fileid)
	siz = [len(data[0]), len(data)]
	resize()
	
	showfileid = True
	
	# while loop
	while True:
		# go through data and display it
		for y in range(0, len(data)):
			for x in range(0, len(data[y])):
				# range
				if y+off[0] >= 0 and y+off[0] < my-1 and x*2+off[1] >= 0 and x*2+off[1] < mx-1:
					stdscr.addstr(y+off[0], x*2+off[1], "##", curses.color_pair(int(data[y][x])+1))
		
		# display current file
		if showfileid:
			stdscr.addstr(my-1, 0, "File #" + str(fileid))
		
		# refresh
		stdscr.refresh()
		
		# handle
		char = stdscr.getch()
		
		# quit
		if char in KEYS["quit"]:
			return "//quit"
		
		# previous file
		elif char in KEYS["prev"]:
			fileid -= 1
			data = load(fileid)
			siz = [len(data[0]), len(data)]
			resize()
		
		# next file
		elif char in KEYS["next"]:
			fileid += 1
			data = load(fileid)
			siz = [len(data[0]), len(data)]
			resize()
		
		# toggle if file id is shown
		elif char in KEYS["debg"]:
			showfileid = not showfileid
		
		# switch
		elif char in KEYS["swch"]:
			return "cam"
		
		# resize
		if curses.is_term_resized(my, mx):
			resize()
		
		# clear
		stdscr.clear()

# init
init()

# run
while RUNNING:
	if MODE == "cam":
		stdscr.nodelay(True)
		MODE = runcam()
	elif MODE == "view":
		stdscr.nodelay(False)
		MODE = runview()
	
	if MODE == "//quit":
		RUNNING = False

# cleanup
cleanup()
Advertisements

2 thoughts on “Curses Cam

  1. Pingback: Second Anniversary – J-Blog

  2. Pingback: Mandelbrot Set ASCII Viewer – J-Blog

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s