diff --git a/.gitignore b/.gitignore
index b6e47617de110dea7ca47e087ff1347cc2646eda..a00b2abf9952eeed21d378bc090daa96b2821e9c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -127,3 +127,9 @@ dmypy.json
# Pyre type checker
.pyre/
+
+#my
+/pythontest.py
+/testmedia
+/tmp
+*.exe
\ No newline at end of file
diff --git a/README.md b/README.md
index 1366d67b86a0873851ec4b9d79274acc88cebbad..a89883c090a26e7c10394ab87b113b51dc5ecf3e 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,42 @@
-# ShellPlayer
-You can watch video in shell !!!
+![image](./imgs/logo_char.png)
+# ShellPlayer
+| English | [中文版](./README_CN.md) |
+You can play colorful & soundful video in shell !!!
+You can play colorful & soundful video in shell !!!
+You can play colorful & soundful video in shell !!!
+
+## Getting Started
+### Prerequisites
+* Linux
+* python3
+* [ffmpeg](http://ffmpeg.org/)
+```bash
+sudo apt-get install ffmpeg
+```
+### Dependencies
+This code depends on opencv-python, available via pip install.
+```bash
+pip install opencv-python
+```
+### Clone this repo
+```bash
+git clone https://github.com/HypoX64/ShellPlayer.git
+cd ShellPlayer
+```
+### Run program
+```bash
+python play.py -m "your_video_or_image_path"
+```
+![image](./imgs/kun.gif)
+## More parameters
+
+| Option | Description | Default |
+| :----------: | :------------------------: | :-------------------------------------: |
+| -m | your video or image path | './imgs/test.jpg' |
+| -g | if specified, play gray video | |
+| -f | playing fps, 0-> auto | 0 |
+| -c | charstyle: style of output 1 \| 2 \| 3 | 3 |
+| -s | size of shell, 1:80X24 2:132X43 3:203X55 | 1 |
+| --frame_num | how many frames want to play 0->all | 0 |
+| --char_scale | character aspect ratio in shell | 2.0 |
+
diff --git a/README_CN.md b/README_CN.md
new file mode 100644
index 0000000000000000000000000000000000000000..b684bf315c67613f1e1c4a82a3a9fb14fe23f430
--- /dev/null
+++ b/README_CN.md
@@ -0,0 +1,41 @@
+![image](./imgs/logo_char.png)
+# ShellPlayer
+| [English](./README.md) | 中文版 |
+你能在终端中看视频,色彩斑斓且富有声音的!!!
+你能在终端中看视频,色彩斑斓且富有声音的!!!
+你能在终端中看视频,色彩斑斓且富有声音的!!!
+## 入门
+### 前提要求
+* Linux
+* python3
+* [ffmpeg](http://ffmpeg.org/)
+```bash
+sudo apt-get install ffmpeg
+```
+### 依赖
+代码依赖于 opencv-python, 可以通过 pip install安装
+```bash
+pip install opencv-python
+```
+### 克隆这个仓库
+```bash
+git clone https://github.com/HypoX64/ShellPlayer.git
+cd ShellPlayer
+```
+### 运行程序
+```bash
+python play.py -m "视频或者图片的路径"
+```
+![image](./imgs/kun.gif)
+## 更多的参数
+
+| 选项 | 描述 | 默认值 |
+| :----------: | :------------------------: | :-------------------------------------: |
+| -m | 视频或者图片的路径 | './imgs/test.jpg' |
+| -g | 如果输入则播放黑白的视频 | |
+| -f | 播放帧速率, 0-> 自动 | 0 |
+| -c | charstyle: 字符输出效果 1 \| 2 \| 3 | 3 |
+| -s | 终端尺寸, 1:80X24 2:132X43 3:203X55 | 1 |
+| --frame_num | 播放总帧数 0->播放所有的帧 | 0 |
+| --char_scale | 字符长宽比 | 2.0 |
+
diff --git a/img2shell.py b/img2shell.py
new file mode 100644
index 0000000000000000000000000000000000000000..1f9068c7d880fcf01752799b9fb182e4b04f8bd6
--- /dev/null
+++ b/img2shell.py
@@ -0,0 +1,172 @@
+import numpy as np
+import cv2
+import time
+
+'''
+#norm
+red 31m 204,0,0
+green 32m 78,145,6
+brown 33m 196,160,0
+blue 34m 52,101,164
+cyan-blue 36m 6,152,154
+
+#highlight
+#gray 30m 85, 87, 83
+red 31m 239,41 ,41
+green 32m 138,226,52
+yellow 33m 253,233,79
+blue 34m 114,159,207
+purple 35m 173,127,168
+blue_sky 36m 52 ,226,226
+white 37m 238,238,238
+'''
+def char_add_color(char,color_num):
+ if color_num > 4:
+ return '\033[1;3'+str(color_num+1-5)+'m'+char+'\033[0m'
+ elif color_num == 4:
+ return '\033[36m'+char+'\033[0m'
+ elif color_num == 3:
+ return '\033[34m'+char+'\033[0m'
+ elif color_num == 2:
+ return '\033[33m'+char+'\033[0m'
+ elif color_num == 1:
+ return '\033[32m'+char+'\033[0m'
+ elif color_num == 0:
+ return '\033[31m'+char+'\033[0m'
+
+
+class Transformer(object):
+ def __init__(self, strshape,scshape,charstyle=3):
+ super(Transformer, self).__init__()
+ self.strshape = strshape
+ self.scshape = scshape
+ self.strh,self.strw = self.strshape[:2]
+ self.sch,self.scw = self.scshape[:2]
+ self.ord = 2
+ if charstyle == 1:
+ self.chars=[' ', ',', '+', '1', 'n','D','&','M','@']
+ elif charstyle == 2:
+ self.chars=[' ', '▏', '▎', '▍', '▌','▋','▊','▉','█']
+ elif charstyle == 3:
+ self.chars=[' ', '▏', '▂', '▍', '▅','▋','▇','▉','█']
+
+ self.char_length = len(self.chars)
+ #self.colors = np.array([[204,0,0],[78,145,6],[196,160,0],[52,101,164],[6,152,154],[239,41 ,41],[138,226,52],[253,233,79],[114,159,207],[173,127,168],[52 ,226,226],[238,238,238]])
+ self.colors = np.array([[204,0,0],[78,145,6],[196,160,0],[52,101,164],[6,152,154],[239,41 ,70],[170,226,52],[253,233,100],[114,159,207],[173,127,168],[52 ,226,226],[238,238,238]])
+ self.color_length = len(self.colors)
+
+ self.brightness = self.colors[:,0]*0.299+self.colors[:,1]*0.587+self.colors[:,2]*0.114
+ self.brightness_divisor = self.brightness/8
+
+ self.colors_hue = self.colors.astype(np.float64)
+
+ for i in range(self.color_length):
+ #self.colors_hue[i] = self.colors_hue[i]/np.mean(self.colors_hue[i])
+ self.colors_hue[i] = self.colors_hue[i]/self.brightness[i]
+
+ self.color_chars=[]
+ for i in range(self.char_length):
+ tmp=[]
+ for j in range(self.color_length):
+ tmp.append(char_add_color(self.chars[i],j))
+ self.color_chars.append(tmp)
+
+ self.norm_matrix = np.zeros((self.strh,self.strw,3))
+ self.color_img_matrix = np.zeros((self.color_length,self.strh,self.strw,3))
+ self.color_contrast_matrix = np.zeros((self.color_length,self.strh,self.strw,3))
+ for i in range(self.color_length):
+ self.color_contrast_matrix[i,:,:] = self.colors_hue[i]
+ self.color_contrast_distance = np.zeros((self.color_length,self.strh,self.strw))
+
+ def blank(self,string):
+ if self.sch>self.strh:
+ for i in range((self.sch-self.strh)//2):
+ string = '\n'+string+'\n'
+ return string
+
+ def gray(self,img):
+ img = cv2.resize(img,(self.strw,self.strh),interpolation=cv2.INTER_AREA)
+ if img.ndim == 3:
+ if img.shape[2] == 4:
+ img = img[:,:,:2]
+ img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
+ string = ''
+ for i in range(self.strh):
+ for j in range(self.strw):
+ string += self.chars[img[i][j]//32]
+ if i != self.strh-1:
+ string += '\n'
+ string = self.blank(string)
+
+ return string
+
+
+ def pixel_color(self,Y,dis):
+ if Y<24:
+ return ' '
+
+ # color
+ color_num = np.where(dis==np.min(dis))[0][0]
+
+ # brightness
+ #Y = pixel[0]*0.299+pixel[1]*0.587+pixel[2]*0.114
+ brightness_level = int(Y/self.brightness_divisor[color_num])
+ if brightness_level>8:
+ brightness_level = 8
+
+ char = self.color_chars[brightness_level][color_num]
+ return char
+
+ def color(self,img):
+ img = cv2.resize(img,(self.strw,self.strh),interpolation=cv2.INTER_AREA)
+ if img.shape[2] == 4:
+ img = img[:,:,:2]
+ img = img[:,:,::-1]
+ img = img.astype(np.float64)
+ img = np.clip(img, 1, 255)
+
+ # get color distance matrix
+
+ bright = img[:,:,0]*0.299+img[:,:,1]*0.587+img[:,:,2]*0.114
+ for i in range(3):self.norm_matrix[:,:,i] = bright
+ self.color_img_matrix[:] = img/self.norm_matrix
+
+ # for i in range(3):self.norm_matrix[:,:,i] = np.mean(img,axis=2)
+ # self.color_img_matrix[:] = img/self.norm_matrix
+
+ self.color_contrast_distance = np.linalg.norm(self.color_img_matrix-self.color_contrast_matrix,ord=self.ord,axis=3)
+
+ string = ''
+ for i in range(self.strh):
+ for j in range(self.strw):
+ string += self.pixel_color(bright[i,j],self.color_contrast_distance[:,i,j])
+ if i != self.strh-1:
+ string += '\n'
+
+ string = self.blank(string)
+
+ return string
+
+ def convert(self,img,isgray):
+ if isgray:
+ return self.gray(img)
+ else :
+ return self.color(img)
+
+ def eval_performance(self,isgray):
+ t1 = time.time()
+ img = cv2.imread('./imgs/logo.png')
+ print(self.convert(img,isgray))
+ for i in range(10):
+ img = cv2.imread('./imgs/test.jpg')
+ img = cv2.resize(img,(1280,720))
+ self.convert(img,isgray)
+ t2 = time.time()
+ recommend_fps = int(1/((t2-t1)/10))-1
+ return recommend_fps
+
+def main():
+ pass
+
+if __name__ == '__main__':
+ main()
diff --git a/imgs/colorbar.png b/imgs/colorbar.png
new file mode 100644
index 0000000000000000000000000000000000000000..486fa8e577a11e63a08d8d851346d0a421d55335
Binary files /dev/null and b/imgs/colorbar.png differ
diff --git a/imgs/colorbar_highlight.png b/imgs/colorbar_highlight.png
new file mode 100644
index 0000000000000000000000000000000000000000..4a2d98e0ef53bb184c8688dcb7ec37effba4b725
Binary files /dev/null and b/imgs/colorbar_highlight.png differ
diff --git a/imgs/kun.gif b/imgs/kun.gif
new file mode 100644
index 0000000000000000000000000000000000000000..2c7825b97da496b5ce0bc6c24b3171be2b405fa7
Binary files /dev/null and b/imgs/kun.gif differ
diff --git a/imgs/logo.png b/imgs/logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..3b3a3c7dc3bd645d092d5aee835f745b7f76ad13
Binary files /dev/null and b/imgs/logo.png differ
diff --git a/imgs/logo.psd b/imgs/logo.psd
new file mode 100644
index 0000000000000000000000000000000000000000..f08b49ae290670b78741a8a98e51febc0ca1d453
Binary files /dev/null and b/imgs/logo.psd differ
diff --git a/imgs/logo_char.png b/imgs/logo_char.png
new file mode 100644
index 0000000000000000000000000000000000000000..14626732c20aa8f539ab6d47148fd151bd5a9247
Binary files /dev/null and b/imgs/logo_char.png differ
diff --git a/imgs/test.jpg b/imgs/test.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..42575b4c054cab5c790a218d467a5a4a22c3ea78
Binary files /dev/null and b/imgs/test.jpg differ
diff --git a/imgs/test_char.png b/imgs/test_char.png
new file mode 100644
index 0000000000000000000000000000000000000000..5af68611c729bf076e8ef0e9cea87f557fb72651
Binary files /dev/null and b/imgs/test_char.png differ
diff --git a/options.py b/options.py
new file mode 100644
index 0000000000000000000000000000000000000000..6433c33d734ae678039c5778fa8c01e0aa6a37be
--- /dev/null
+++ b/options.py
@@ -0,0 +1,27 @@
+import argparse
+
+class Options():
+ def __init__(self):
+ self.parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
+ self.initialized = False
+
+ def initialize(self):
+ self.parser.add_argument('-m','--media', type=str, default='./imgs/test.jpg',help='your video or image path')
+ self.parser.add_argument('-g','--gray', action='store_true', help='if specified, play gray video')
+ self.parser.add_argument('-f','--fps', type=int, default=0,help='playing fps, 0-> auto')
+ self.parser.add_argument('-c','--charstyle', type=int, default=3,help='style of output')
+ self.parser.add_argument('-s','--screen', type=int, default=1,help='size of shell 1:80*24 2:132*43 3:203*55')
+
+ self.parser.add_argument('--ori_fps', type=int, default=0,help='original fps for video, 0-> auto')
+ self.parser.add_argument('--frame_num', type=int, default=0,help='how many frames want to play 0->all')
+ self.parser.add_argument('--char_scale', type=float, default=2.0,help='')
+
+
+ self.initialized = True
+
+ def getparse(self):
+ if not self.initialized:
+ self.initialize()
+ self.opt = self.parser.parse_args()
+ return self.opt
+
diff --git a/play.py b/play.py
new file mode 100644
index 0000000000000000000000000000000000000000..0773877c8ec1d69a5aa951fb7e504e93124598d8
--- /dev/null
+++ b/play.py
@@ -0,0 +1,96 @@
+import os
+import sys
+import time
+from multiprocessing import Process, Queue
+import threading
+import subprocess
+import numpy as np
+import cv2
+
+from util import util,ffmpeg
+from img2shell import Transformer
+from options import Options
+
+def readvideo(opt,imgQueue):
+ cap = cv2.VideoCapture(opt.media)
+ play_index = np.linspace(0, opt.frame_num-1,num=int(opt.frame_num*opt.fps/opt.ori_fps),dtype=np.int64)
+ frame_cnt = 0; play_cnt =0
+ while(cap.isOpened()):
+ _ , frame = cap.read()
+ if frame_cnt == play_index[play_cnt]:
+ imgQueue.put(frame)
+ if play_cnt < len(play_index)-1:
+ play_cnt+= 1
+ frame_cnt += 1
+
+def timer(opt,timerQueueime):
+ while True:
+ t = 1.0/opt.fps
+ time.sleep(t)
+ timerQueueime.put(True)
+
+opt = Options().getparse()
+
+#-------------------------------Media Init-------------------------------
+if util.is_img(opt.media):
+ img = cv2.imread(opt.media)
+ h_media,w_media = img.shape[:2]
+elif util.is_video(opt.media):
+ fps,endtime,h_media,w_media = ffmpeg.get_video_infos(opt.media)
+ if opt.frame_num == 0:
+ opt.frame_num = int(endtime*fps-5)
+ if opt.ori_fps == 0:
+ opt.ori_fps = fps
+ util.clean_tempfiles(tmp_init=True)
+else:
+ print('Can not load this file!')
+
+#-------------------------------Image Shape Init-------------------------------
+if opt.screen==1:
+ limw = 80;limh = 24
+if opt.screen==2:
+ limw = 132;limh = 43
+if opt.screen==3:
+ limw = 203;limh = 55
+screen_scale = limh/limw
+
+img_scale = h_media/w_media/opt.char_scale
+if img_scale >= screen_scale:
+ strshape = (limh,int(limh/img_scale))
+else:
+ strshape = (int(limw*img_scale),limw)
+
+#-------------------------------img2shell Init-------------------------------
+transformer = Transformer(strshape,(limh,limw),opt.charstyle)
+if util.is_video(opt.media):
+ recommend_fps = transformer.eval_performance(opt.gray)
+ if opt.fps == 0:
+ opt.fps = np.clip(recommend_fps,1,opt.ori_fps)
+ else:
+ opt.fps = np.clip(opt.fps,1,opt.ori_fps)
+ ffmpeg.video2voice(opt.media,'-ar 16000 ./tmp/tmp.wav')
+
+#-------------------------------main-------------------------------
+if util.is_img(opt.media):
+ print(transformer.convert(img,opt.gray))
+elif util.is_video(opt.media):
+ imgQueue = Queue(1)
+ timerQueue = Queue()
+
+ imgload_p = Process(target=readvideo,args=(opt,imgQueue))
+ imgload_p.daemon = True
+ imgload_p.start()
+
+ timer_p = Process(target=timer,args=(opt,timerQueue))
+ timer_p.daemon = True
+ timer_p.start()
+
+ time.sleep(0.5)
+ subprocess.Popen('paplay ./tmp/tmp.wav', shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+
+ for i in range(int(opt.frame_num*opt.fps/opt.ori_fps)-1):
+ timerQueue.get()
+ img = imgQueue.get()
+ string = transformer.convert(img,opt.gray)
+ t=threading.Thread(target=print,args=(string,))
+ t.start()
diff --git a/util/ffmpeg.py b/util/ffmpeg.py
new file mode 100644
index 0000000000000000000000000000000000000000..9d0694e5b512724606cc1c3025beda790955b33f
--- /dev/null
+++ b/util/ffmpeg.py
@@ -0,0 +1,34 @@
+import os,json
+import subprocess
+# ffmpeg 3.4.6
+
+def run(cmd_str):
+ #out_string = os.popen(cmd_str).read()
+ #For chinese path in Windows
+ #https://blog.csdn.net/weixin_43903378/article/details/91979025
+ stream = os.popen(cmd_str)._stream
+ out_string = stream.buffer.read().decode(encoding='utf-8')
+ return out_string
+
+
+def video2voice(videopath,voicepath):
+ p = subprocess.Popen('ffmpeg -i "'+videopath+'" '+voicepath, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ sout = p.stdout.readlines()
+ # run(cmd_str)
+
+def get_video_infos(videopath):
+ cmd_str = 'ffprobe -v quiet -print_format json -show_format -show_streams -i "' + videopath + '"'
+ out_string = run(cmd_str)
+ infos = json.loads(out_string)
+ try:
+ fps = eval(infos['streams'][0]['avg_frame_rate'])
+ endtime = float(infos['format']['duration'])
+ width = int(infos['streams'][0]['width'])
+ height = int(infos['streams'][0]['height'])
+ except Exception as e:
+ fps = eval(infos['streams'][1]['r_frame_rate'])
+ endtime = float(infos['format']['duration'])
+ width = int(infos['streams'][1]['width'])
+ height = int(infos['streams'][1]['height'])
+
+ return fps,endtime,height,width
\ No newline at end of file
diff --git a/util/util.py b/util/util.py
new file mode 100644
index 0000000000000000000000000000000000000000..814ae262acf179e48ef53862b6e2f70686823c2c
--- /dev/null
+++ b/util/util.py
@@ -0,0 +1,108 @@
+import os
+import shutil
+
+def Traversal(filedir):
+ file_list=[]
+ for root,dirs,files in os.walk(filedir):
+ for file in files:
+ file_list.append(os.path.join(root,file))
+ for dir in dirs:
+ Traversal(dir)
+ return file_list
+
+def is_img(path):
+ ext = os.path.splitext(path)[1]
+ ext = ext.lower()
+ if ext in ['.jpg','.png','.jpeg','.bmp']:
+ return True
+ else:
+ return False
+
+def is_video(path):
+ ext = os.path.splitext(path)[1]
+ ext = ext.lower()
+ if ext in ['.mp4','.flv','.avi','.mov','.mkv','.wmv','.rmvb','.mts']:
+ return True
+ else:
+ return False
+
+def is_imgs(paths):
+ tmp = []
+ for path in paths:
+ if is_img(path):
+ tmp.append(path)
+ return tmp
+
+def is_videos(paths):
+ tmp = []
+ for path in paths:
+ if is_video(path):
+ tmp.append(path)
+ return tmp
+
+def is_dirs(paths):
+ tmp = []
+ for path in paths:
+ if os.path.isdir(path):
+ tmp.append(path)
+ return tmp
+
+def writelog(path,log,isprint=False):
+ f = open(path,'a+')
+ f.write(log+'\n')
+ f.close()
+ if isprint:
+ print(log)
+
+def makedirs(path):
+ if os.path.isdir(path):
+ pass
+ # print(path,'existed')
+ else:
+ os.makedirs(path)
+ # print('makedir:',path)
+
+def clean_tempfiles(tmp_init=True):
+ if os.path.isdir('./tmp'):
+ shutil.rmtree('./tmp')
+ if tmp_init:
+ os.makedirs('./tmp')
+
+def second2stamp(s):
+ h = int(s/3600)
+ s = int(s%3600)
+ m = int(s/60)
+ s = int(s%60)
+ return "%02d:%02d:%02d" % (h, m, s)
+
+def counttime(t1,t2,now_num,all_num):
+ '''
+ t1,t2: time.time()
+ '''
+ used_time = int(t2-t1)
+ all_time = int(used_time/now_num*all_num)
+ return second2stamp(used_time)+'/'+second2stamp(all_time)
+
+def get_bar(percent,num = 25):
+ bar = '['
+ for i in range(num):
+ if i < round(percent/(100/num)):
+ bar += '#'
+ else:
+ bar += '-'
+ bar += ']'
+ return bar+' '+"%.2f"%percent+'%'
+
+def copyfile(src,dst):
+ try:
+ shutil.copyfile(src, dst)
+ except Exception as e:
+ print(e)
+
+def opt2str(opt):
+ message = ''
+ message += '---------------------- Options --------------------\n'
+ for k, v in sorted(vars(opt).items()):
+ message += '{:>25}: {:<35}\n'.format(str(k), str(v))
+ message += '----------------- End -------------------'
+ return message