mirror of
https://github.com/dnet/pySSTV.git
synced 2026-01-21 16:00:27 +01:00
Compare commits
39 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4c840177c | ||
|
|
f25ecac2c5 | ||
|
|
02795a403b | ||
|
|
b43208287f | ||
|
|
bc74dc98d6 | ||
|
|
b22e81e65f | ||
|
|
c454aa3a4b | ||
|
|
0855fc6a0c | ||
|
|
75b0cd46e3 | ||
|
|
cb00cf20ad | ||
|
|
5d3d6a2584 | ||
|
|
456040aa6b | ||
|
|
ab0560b71e | ||
|
|
f4b4ca7b19 | ||
|
|
65241a6b71 | ||
|
|
011d9cbcae | ||
|
|
c978ca0515 | ||
|
|
ff73d8c309 | ||
|
|
3b8607f130 | ||
|
|
bb193334ff | ||
|
|
af5c2dff21 | ||
|
|
8d93d64bc5 | ||
|
|
89254d73ec | ||
|
|
39abaf1a17 | ||
|
|
2bf26ed432 | ||
|
|
2f7a11abc1 | ||
|
|
30294ee096 | ||
|
|
5a596df09c | ||
|
|
10476d013a | ||
|
|
3c2e4d0a5c | ||
|
|
323812bc59 | ||
|
|
9f4c435450 | ||
|
|
8f9151f0c1 | ||
|
|
0c09c84360 | ||
|
|
71ddabcb1d | ||
|
|
9143aaec97 | ||
|
|
7b06a5bb48 | ||
|
|
c784e9df15 | ||
|
|
1b6884ee63 |
40
.github/workflows/python-package.yml
vendored
Normal file
40
.github/workflows/python-package.yml
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
|
||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
|
||||
|
||||
name: Python package
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10", "3.11"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install flake8 pytest
|
||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
# stop the build if there are Python syntax errors or undefined names
|
||||
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
||||
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||
- name: Test with pytest
|
||||
run: |
|
||||
pytest
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
language: python
|
||||
python:
|
||||
- "2.7"
|
||||
- "3.2"
|
||||
- "3.3"
|
||||
- "3.4"
|
||||
- "3.5"
|
||||
- "3.6"
|
||||
- "3.7"
|
||||
- "3.8"
|
||||
- "3.9"
|
||||
sudo: false
|
||||
install: "pip install -r requirements.txt"
|
||||
script: nosetests
|
||||
|
|
|
|||
24
README.md
24
README.md
|
|
@ -1,8 +1,6 @@
|
|||
SSTV generator in pure Python
|
||||
=============================
|
||||
|
||||
[](https://travis-ci.org/dnet/pySSTV)
|
||||
|
||||
PySSTV generates SSTV modulated WAV files from any image that PIL can open
|
||||
(PNG, JPEG, GIF, and many others). These WAV files then can be played by any
|
||||
audio player connected to a shortwave radio for example.
|
||||
|
|
@ -16,9 +14,10 @@ Command line usage
|
|||
|
||||
$ python -m pysstv -h
|
||||
usage: __main__.py [-h]
|
||||
[--mode {MartinM1,MartinM2,ScottieS1,ScottieS2,Robot36,PasokonP3,PasokonP5,PasokonP7,PD90,PD120,PD160,PD180,PD240,Robot8BW,Robot24BW}]
|
||||
[--mode {MartinM1,MartinM2,ScottieS1,ScottieS2,ScottieDX,Robot36,PasokonP3,PasokonP5,PasokonP7,PD90,PD120,PD160,PD180,PD240,PD290,WraaseSC2120,WraaseSC2180,Robot8BW,Robot24BW}]
|
||||
[--rate RATE] [--bits BITS] [--vox] [--fskid FSKID]
|
||||
[--chan CHAN]
|
||||
[--chan CHAN] [--resize] [--keep-aspect-ratio]
|
||||
[--keep-aspect] [--resample {nearest,bicubic,lanczos}]
|
||||
image.png output.wav
|
||||
|
||||
Converts an image to an SSTV modulated WAV file.
|
||||
|
|
@ -27,15 +26,23 @@ Command line usage
|
|||
image.png input image file name
|
||||
output.wav output WAV file name
|
||||
|
||||
optional arguments:
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--mode {MartinM1,MartinM2,ScottieS1,ScottieS2,Robot36,PasokonP3,PasokonP5,PasokonP7,PD90,PD120D160,PD180,PD240,Robot8BW,Robot24BW}
|
||||
--mode {MartinM1,MartinM2,ScottieS1,ScottieS2,ScottieDX,Robot36,PasokonP3,PasokonP5,PasokonP7,PD90,PD120,PD160,PD180,PD240,PD290,WraaseSC2120,WraaseSC2180,Robot8BW,Robot24BW}
|
||||
image mode (default: Martin M1)
|
||||
--rate RATE sampling rate (default: 48000)
|
||||
--bits BITS bits per sample (default: 16)
|
||||
--vox add VOX tones at the beginning
|
||||
--fskid FSKID add FSKID at the end
|
||||
--chan CHAN number of channels (default: mono)
|
||||
--resize resize the image to the correct size
|
||||
--keep-aspect-ratio keep the original aspect ratio when resizing
|
||||
(and cut off excess pixels)
|
||||
--keep-aspect keep the original aspect ratio when resizing
|
||||
(not cut off excess pixels)
|
||||
--resample {nearest,bicubic,lanczos}
|
||||
which resampling filter to use for resizing
|
||||
(see Pillow documentation)
|
||||
|
||||
Python interface
|
||||
----------------
|
||||
|
|
@ -75,6 +82,5 @@ Useful links
|
|||
Dependencies
|
||||
------------
|
||||
|
||||
- Python 2.7 (tested on 2.7.9; 2.6 might work, but test suite lacks support)
|
||||
or 3.x (tested on 3.2, 3.3, 3.4 and 3.5)
|
||||
- Python Imaging Library (Debian/Ubuntu package: `python-imaging`)
|
||||
- Python 3.5 or later
|
||||
- Python Imaging Library (Debian/Ubuntu package: `python3-pil`)
|
||||
|
|
|
|||
3
pyproject.toml
Normal file
3
pyproject.toml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[build-system]
|
||||
requires = ["setuptools"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
|
@ -34,6 +34,8 @@ def main():
|
|||
help='resize the image to the correct size')
|
||||
parser.add_argument('--keep-aspect-ratio', dest='keep_aspect_ratio', action='store_true',
|
||||
help='keep the original aspect ratio when resizing (and cut off excess pixels)')
|
||||
parser.add_argument('--keep-aspect', dest='keep_aspect', action='store_true',
|
||||
help='keep the original aspect ratio when resizing (not cut off excess pixels)')
|
||||
parser.add_argument('--resample', dest='resample', default='lanczos',
|
||||
choices=('nearest', 'bicubic', 'lanczos'),
|
||||
help='which resampling filter to use for resizing (see Pillow documentation)')
|
||||
|
|
@ -42,14 +44,17 @@ def main():
|
|||
mode = module_map[args.mode]
|
||||
if args.resize and any(i != m for i, m in zip(image.size, (mode.WIDTH, mode.HEIGHT))):
|
||||
resample = getattr(Image, args.resample.upper())
|
||||
if args.keep_aspect_ratio:
|
||||
if args.keep_aspect_ratio or args.keep_aspect:
|
||||
orig_ratio = image.width / image.height
|
||||
mode_ratio = mode.WIDTH / mode.HEIGHT
|
||||
crop = orig_ratio != mode_ratio
|
||||
else:
|
||||
crop = False
|
||||
if crop:
|
||||
if orig_ratio < mode_ratio:
|
||||
t = orig_ratio < mode_ratio
|
||||
if args.keep_aspect:
|
||||
t = orig_ratio > mode_ratio
|
||||
if t:
|
||||
w = mode.WIDTH
|
||||
h = int(w / orig_ratio)
|
||||
else:
|
||||
|
|
@ -59,6 +64,14 @@ def main():
|
|||
w = mode.WIDTH
|
||||
h = mode.HEIGHT
|
||||
image = image.resize((w, h), resample)
|
||||
if args.keep_aspect:
|
||||
newbg = Image.new('RGB', (mode.WIDTH, mode.HEIGHT))
|
||||
if t:
|
||||
newbg.paste(image, (0, int((mode.HEIGHT/2)-(h/2))))
|
||||
else:
|
||||
newbg.paste(image, (int((mode.WIDTH/2)-(w/2)), 0))
|
||||
image = newbg.copy()
|
||||
crop = False
|
||||
if crop:
|
||||
x = (image.width - mode.WIDTH) / 2
|
||||
y = (image.height - mode.HEIGHT) / 2
|
||||
|
|
|
|||
107
pysstv/color.py
107
pysstv/color.py
|
|
@ -1,39 +1,40 @@
|
|||
#!/usr/bin/env python
|
||||
from __future__ import division
|
||||
from six.moves import range, zip
|
||||
from pysstv.sstv import byte_to_freq, FREQ_BLACK, FREQ_WHITE, FREQ_VIS_START
|
||||
from pysstv.grayscale import GrayscaleSSTV
|
||||
from itertools import chain
|
||||
from enum import Enum
|
||||
|
||||
|
||||
RED, GREEN, BLUE = range(3)
|
||||
class Color(Enum):
|
||||
red = 0
|
||||
green = 1
|
||||
blue = 2
|
||||
|
||||
|
||||
class ColorSSTV(GrayscaleSSTV):
|
||||
def on_init(self):
|
||||
self.pixels = self.image.load()
|
||||
self.pixels = self.image.convert('RGB').load()
|
||||
|
||||
def encode_line(self, line):
|
||||
msec_pixel = self.SCAN / self.WIDTH
|
||||
image = self.pixels
|
||||
for index in self.COLOR_SEQ:
|
||||
for item in self.before_channel(index):
|
||||
yield item
|
||||
for color in self.COLOR_SEQ:
|
||||
yield from self.before_channel(color)
|
||||
for col in range(self.WIDTH):
|
||||
pixel = image[col, line]
|
||||
freq_pixel = byte_to_freq(pixel[index])
|
||||
freq_pixel = byte_to_freq(pixel[color.value])
|
||||
yield freq_pixel, msec_pixel
|
||||
for item in self.after_channel(index):
|
||||
yield item
|
||||
yield from self.after_channel(color)
|
||||
|
||||
def before_channel(self, index):
|
||||
def before_channel(self, color):
|
||||
return []
|
||||
|
||||
after_channel = before_channel
|
||||
|
||||
|
||||
class MartinM1(ColorSSTV):
|
||||
COLOR_SEQ = (GREEN, BLUE, RED)
|
||||
COLOR_SEQ = (Color.green, Color.blue, Color.red)
|
||||
VIS_CODE = 0x2c
|
||||
WIDTH = 320
|
||||
HEIGHT = 256
|
||||
|
|
@ -41,11 +42,11 @@ class MartinM1(ColorSSTV):
|
|||
SCAN = 146.432
|
||||
INTER_CH_GAP = 0.572
|
||||
|
||||
def before_channel(self, index):
|
||||
if index == GREEN:
|
||||
def before_channel(self, color):
|
||||
if color is Color.green:
|
||||
yield FREQ_BLACK, self.INTER_CH_GAP
|
||||
|
||||
def after_channel(self, index):
|
||||
def after_channel(self, color):
|
||||
yield FREQ_BLACK, self.INTER_CH_GAP
|
||||
|
||||
|
||||
|
|
@ -64,10 +65,9 @@ class ScottieS1(MartinM1):
|
|||
def horizontal_sync(self):
|
||||
return []
|
||||
|
||||
def before_channel(self, index):
|
||||
if index == RED:
|
||||
for item in MartinM1.horizontal_sync(self):
|
||||
yield item
|
||||
def before_channel(self, color):
|
||||
if color is Color.red:
|
||||
yield from MartinM1.horizontal_sync(self)
|
||||
yield FREQ_BLACK, self.INTER_CH_GAP
|
||||
|
||||
|
||||
|
|
@ -77,6 +77,12 @@ class ScottieS2(ScottieS1):
|
|||
WIDTH = 160
|
||||
|
||||
|
||||
class ScottieDX(ScottieS1):
|
||||
VIS_CODE = 0x4c
|
||||
# http://www.barberdsp.com/downloads/Dayton%20Paper.pdf
|
||||
SCAN = 345.6000 - ScottieS1.INTER_CH_GAP
|
||||
|
||||
|
||||
class Robot36(ColorSSTV):
|
||||
VIS_CODE = 0x08
|
||||
WIDTH = 320
|
||||
|
|
@ -118,7 +124,7 @@ class PasokonP3(ColorSSTV):
|
|||
Horizontal Sync - 25 time units of 1200 Hz.
|
||||
"""
|
||||
TIMEUNIT = 1000/4800. # ms
|
||||
COLOR_SEQ = (RED, GREEN, BLUE)
|
||||
COLOR_SEQ = (Color.red, Color.green, Color.blue)
|
||||
VIS_CODE = 0x71
|
||||
WIDTH = 640
|
||||
HEIGHT = 480+16
|
||||
|
|
@ -126,11 +132,11 @@ class PasokonP3(ColorSSTV):
|
|||
SCAN = WIDTH * TIMEUNIT
|
||||
INTER_CH_GAP = 5 * TIMEUNIT
|
||||
|
||||
def before_channel(self, index):
|
||||
if index == self.COLOR_SEQ[0]:
|
||||
def before_channel(self, color):
|
||||
if color is Color.red:
|
||||
yield FREQ_BLACK, self.INTER_CH_GAP
|
||||
|
||||
def after_channel(self, index):
|
||||
def after_channel(self, color):
|
||||
yield FREQ_BLACK, self.INTER_CH_GAP
|
||||
|
||||
|
||||
|
|
@ -160,8 +166,7 @@ class PD90(ColorSSTV):
|
|||
def gen_image_tuples(self):
|
||||
yuv = self.image.convert('YCbCr').load()
|
||||
for line in range(0, self.HEIGHT, 2):
|
||||
for item in self.horizontal_sync():
|
||||
yield item
|
||||
yield from self.horizontal_sync()
|
||||
yield FREQ_BLACK, self.PORCH
|
||||
pixels0 = [yuv[col, line] for col in range(self.WIDTH)]
|
||||
pixels1 = [yuv[col, line + 1] for col in range(self.WIDTH)]
|
||||
|
|
@ -195,6 +200,56 @@ class PD240(PD120):
|
|||
VIS_CODE = 0x61
|
||||
PIXEL = 0.382
|
||||
|
||||
class PD290(PD240):
|
||||
VIS_CODE = 0x5e
|
||||
WIDTH = 800
|
||||
HEIGHT = 616
|
||||
PIXEL = 0.286
|
||||
|
||||
MODES = (MartinM1, MartinM2, ScottieS1, ScottieS2, Robot36,
|
||||
PasokonP3, PasokonP5, PasokonP7, PD90, PD120, PD160, PD180, PD240)
|
||||
|
||||
class WraaseSC2180(ColorSSTV):
|
||||
VIS_CODE = 0x37
|
||||
WIDTH = 320
|
||||
HEIGHT = 256
|
||||
COLOR_SEQ = (Color.red, Color.green, Color.blue)
|
||||
|
||||
SYNC = 5.5225
|
||||
PORCH = 0.5
|
||||
SCAN = 235.0
|
||||
|
||||
def before_channel(self, color):
|
||||
if color is Color.red:
|
||||
yield FREQ_BLACK, self.PORCH
|
||||
else:
|
||||
return []
|
||||
|
||||
def after_channel(self, color):
|
||||
return []
|
||||
|
||||
|
||||
class WraaseSC2120(WraaseSC2180):
|
||||
VIS_CODE = 0x3f
|
||||
|
||||
# NB: there are "authoritative" sounding documents that will tell you SC-2
|
||||
# 120 uses red and blue channels that have half the line width of the
|
||||
# green channel. Having spent several hours trying to nut out why SC2-120
|
||||
# images weren't decoding in anything else, I can say this is utter
|
||||
# bunkum. The line width is the same for all three channels, just
|
||||
# shorter.
|
||||
|
||||
SCAN = 156.0
|
||||
|
||||
def before_channel(self, color):
|
||||
# Not sure why, but SC2-120 decoding seems to need an extra few sync
|
||||
# pulses to decode in QSSTV and slowrx. Take the extra pulse out, and
|
||||
# it slants something chronic and QSSTV loses sync regularly even on
|
||||
# DX mode. Put it in, and both decode reliably. Go figure. SC2-180
|
||||
# works just fine without this extra pulse at the start of each
|
||||
# channel.
|
||||
yield FREQ_BLACK, self.PORCH
|
||||
yield from super().before_channel(color)
|
||||
|
||||
|
||||
MODES = (MartinM1, MartinM2, ScottieS1, ScottieS2, ScottieDX, Robot36,
|
||||
PasokonP3, PasokonP5, PasokonP7, PD90, PD120, PD160, PD180, PD240,
|
||||
PD290, WraaseSC2120, WraaseSC2180)
|
||||
|
|
|
|||
|
|
@ -69,10 +69,10 @@ def main(sstv_class=None):
|
|||
n += 1
|
||||
del lut
|
||||
m_start, m_len = gen_matches(same_as, history, n)
|
||||
for i in xrange(same_as[m_start]):
|
||||
for i in range(same_as[m_start]):
|
||||
yield history[i][0]
|
||||
yield 'for (int row = 0; row < width * {0}; row += width) {{'.format(sstv.HEIGHT)
|
||||
for i in xrange(same_as[m_start], same_as[m_start] + m_len - 1):
|
||||
for i in range(same_as[m_start], same_as[m_start] + m_len - 1):
|
||||
yield ' ' + history[i][1]
|
||||
yield '}'
|
||||
yield '}}\n\n#define FREQ_COUNT {0}'.format(n)
|
||||
|
|
@ -83,7 +83,7 @@ def gen_matches(same_as, history, n):
|
|||
cur_start = None
|
||||
cur_len = None
|
||||
cur_end = None
|
||||
for i in xrange(n):
|
||||
for i in range(n):
|
||||
if cur_start is None:
|
||||
tmp = same_as.get(i)
|
||||
if tmp is not None:
|
||||
|
|
@ -114,22 +114,22 @@ def test(img_file):
|
|||
import struct
|
||||
exe = './codegen-test-executable'
|
||||
if not path.exists('stb_image.h'):
|
||||
from urllib import urlretrieve
|
||||
from urllib.request import urlretrieve
|
||||
urlretrieve('https://raw.githubusercontent.com/nothings/stb/master/stb_image.h', 'stb_image.h')
|
||||
try:
|
||||
for sstv_class in supported:
|
||||
print 'Testing', sstv_class
|
||||
print('Testing', sstv_class)
|
||||
gcc = Popen(['gcc', '-xc', '-lm', '-o', exe, '-'], stdin=PIPE)
|
||||
start = datetime.now()
|
||||
with open(path.join(path.dirname(__file__), 'codeman.c')) as cm:
|
||||
c_src = cm.read().replace('#include "codegen.c"', '\n'.join(main(sstv_class)))
|
||||
gcc.communicate(c_src)
|
||||
gen_elapsed = datetime.now() - start
|
||||
print ' - gengcc took', gen_elapsed
|
||||
print(' - gengcc took', gen_elapsed)
|
||||
start = datetime.now()
|
||||
gen = check_output([exe, img_file])
|
||||
native_elapsed = datetime.now() - start
|
||||
print ' - native took', native_elapsed
|
||||
print(' - native took', native_elapsed)
|
||||
img = Image.open(img_file)
|
||||
sstv = sstv_class(img, 44100, 16)
|
||||
start = datetime.now()
|
||||
|
|
@ -138,20 +138,20 @@ def test(img_file):
|
|||
assert gen[n * 8:(n + 1) * 8] == struct.pack('ff', freq, msec)
|
||||
except AssertionError:
|
||||
mode_name = sstv_class.__name__
|
||||
with file('/tmp/{0}-c.bin'.format(mode_name), 'wb') as f:
|
||||
with open('/tmp/{0}-c.bin'.format(mode_name), 'wb') as f:
|
||||
f.write(gen)
|
||||
with file('/tmp/{0}-py.bin'.format(mode_name), 'wb') as f:
|
||||
with open('/tmp/{0}-py.bin'.format(mode_name), 'wb') as f:
|
||||
for n, (freq, msec) in enumerate(sstv.gen_freq_bits()):
|
||||
f.write(struct.pack('ff', freq, msec))
|
||||
with file('/tmp/{0}.c'.format(mode_name), 'w') as f:
|
||||
with open('/tmp/{0}.c'.format(mode_name), 'w') as f:
|
||||
f.write(c_src)
|
||||
print (" ! Outputs are different, they've been saved to "
|
||||
print((" ! Outputs are different, they've been saved to "
|
||||
"/tmp/{0}-{{c,py}}.bin, along with the C source code "
|
||||
"in /tmp/{0}.c").format(mode_name)
|
||||
"in /tmp/{0}.c").format(mode_name))
|
||||
python_elapsed = datetime.now() - start
|
||||
print ' - python took', python_elapsed
|
||||
print ' - speedup:', python_elapsed.total_seconds() / native_elapsed.total_seconds()
|
||||
print 'OK'
|
||||
print(' - python took', python_elapsed)
|
||||
print(' - speedup:', python_elapsed.total_seconds() / native_elapsed.total_seconds())
|
||||
print('OK')
|
||||
finally:
|
||||
try:
|
||||
remove(exe)
|
||||
|
|
@ -164,4 +164,4 @@ if __name__ == '__main__':
|
|||
if len(argv) > 2 and argv[1] == 'test':
|
||||
test(argv[2])
|
||||
else:
|
||||
print '\n'.join(main())
|
||||
print('\n'.join(main()))
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@
|
|||
|
||||
from gimpfu import register, main, pdb, PF_BOOL, PF_STRING, PF_RADIO, CLIP_TO_IMAGE
|
||||
from PIL import Image, ImageTk
|
||||
from Tkinter import Tk, Canvas, Button, Checkbutton, IntVar, Frame, LEFT, NW
|
||||
from tkinter import Tk, Canvas, Button, Checkbutton, IntVar, Frame, LEFT, NW
|
||||
from pysstv import __main__ as pysstv_main
|
||||
from pysstv.examples.pyaudio_sstv import PyAudioSSTV
|
||||
from pysstv.sstv import SSTV
|
||||
from itertools import repeat
|
||||
from threading import Thread
|
||||
from Queue import Queue, Empty
|
||||
from queue import Queue, Empty
|
||||
from time import sleep
|
||||
import gimp, os
|
||||
|
||||
|
|
@ -118,12 +118,12 @@ class ProgressCanvas(Canvas):
|
|||
self.height_ratio = 1
|
||||
width, height = image.size
|
||||
pixels = image.load()
|
||||
RED, GREEN, BLUE = range(3)
|
||||
RED, GREEN, BLUE = list(range(3))
|
||||
self.colors = ['#{0:02x}{1:02x}{2:02x}'.format(
|
||||
contrast(sum(pixels[x, y][RED] for x in xrange(width)) / width),
|
||||
contrast(sum(pixels[x, y][GREEN] for x in xrange(width)) / width),
|
||||
contrast(sum(pixels[x, y][BLUE] for x in xrange(width)) / width))
|
||||
for y in xrange(height)]
|
||||
contrast(sum(pixels[x, y][RED] for x in range(width)) / width),
|
||||
contrast(sum(pixels[x, y][GREEN] for x in range(width)) / width),
|
||||
contrast(sum(pixels[x, y][BLUE] for x in range(width)) / width))
|
||||
for y in range(height)]
|
||||
if height / float(width) > 1.5:
|
||||
width *= 2
|
||||
elif width < 200:
|
||||
|
|
@ -237,7 +237,7 @@ register(
|
|||
"*",
|
||||
[
|
||||
(PF_RADIO, "mode", "SSTV mode", "MartinM1",
|
||||
tuple((n, n) for n in sorted(MODULE_MAP.iterkeys()))),
|
||||
tuple((n, n) for n in sorted(MODULE_MAP.keys()))),
|
||||
(PF_BOOL, "vox", "Include VOX tones", True),
|
||||
(PF_STRING, "fskid", "FSK ID", ""),
|
||||
(PF_RADIO, "ptt_port", "PTT port", None,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ Demonstrates playing the generated samples directly using PyAudio
|
|||
Tested on PyAudio 0.2.7 http://people.csail.mit.edu/hubert/pyaudio/
|
||||
"""
|
||||
|
||||
from __future__ import division
|
||||
|
||||
from pysstv.sstv import SSTV
|
||||
from time import sleep
|
||||
from itertools import islice
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ simply copying/linking images to the directory or suing an SSTV
|
|||
receiver such as slowrx or QSSTV.
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
from pyinotify import WatchManager, Notifier, ProcessEvent, IN_CREATE
|
||||
from pyaudio_sstv import PyAudioSSTV
|
||||
from .pyaudio_sstv import PyAudioSSTV
|
||||
from pysstv.color import MartinM1, MartinM2, ScottieS1, ScottieS2
|
||||
from pysstv.grayscale import Robot8BW, Robot24BW
|
||||
from PIL import Image
|
||||
|
|
@ -44,13 +44,13 @@ class EventHandler(ProcessEvent):
|
|||
|
||||
def get_module_for_filename(filename):
|
||||
basename, _ = path.splitext(path.basename(filename))
|
||||
for mode, module in MODE_MAP.iteritems():
|
||||
for mode, module in MODE_MAP.items():
|
||||
if mode in basename:
|
||||
return module
|
||||
|
||||
def get_module_for_image(image):
|
||||
size = image.size
|
||||
for mode in MODE_MAP.itervalues():
|
||||
for mode in MODE_MAP.values():
|
||||
if all(i >= m for i, m in zip(size, (mode.WIDTH, mode.HEIGHT))):
|
||||
return mode
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import division
|
||||
from six.moves import range
|
||||
from pysstv.sstv import SSTV, byte_to_freq
|
||||
|
||||
|
||||
|
|
@ -11,10 +10,8 @@ class GrayscaleSSTV(SSTV):
|
|||
|
||||
def gen_image_tuples(self):
|
||||
for line in range(self.HEIGHT):
|
||||
for item in self.horizontal_sync():
|
||||
yield item
|
||||
for item in self.encode_line(line):
|
||||
yield item
|
||||
yield from self.horizontal_sync()
|
||||
yield from self.encode_line(line)
|
||||
|
||||
def encode_line(self, line):
|
||||
msec_pixel = self.SCAN / self.WIDTH
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import division, with_statement
|
||||
from six.moves import range
|
||||
from six.moves import map
|
||||
from six.moves import zip
|
||||
from math import sin, pi
|
||||
from random import random
|
||||
from contextlib import closing
|
||||
|
|
@ -54,7 +51,7 @@ class SSTV(object):
|
|||
wav.setnchannels(self.nchannels)
|
||||
wav.setsampwidth(self.bits // 8)
|
||||
wav.setframerate(self.samples_per_sec)
|
||||
wav.writeframes(data.tostring())
|
||||
wav.writeframes(data)
|
||||
|
||||
def gen_samples(self):
|
||||
"""generates discrete samples from gen_values()
|
||||
|
|
@ -116,8 +113,7 @@ class SSTV(object):
|
|||
parity_freq = FREQ_VIS_BIT1 if num_ones % 2 == 1 else FREQ_VIS_BIT0
|
||||
yield parity_freq, MSEC_VIS_BIT
|
||||
yield FREQ_SYNC, MSEC_VIS_BIT # stop bit
|
||||
for freq_tuple in self.gen_image_tuples():
|
||||
yield freq_tuple
|
||||
yield from self.gen_image_tuples()
|
||||
for fskid_byte in map(ord, self.fskid_payload):
|
||||
for _ in range(6):
|
||||
bit = fskid_byte & 1
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
import unittest
|
||||
from io import BytesIO
|
||||
from itertools import islice
|
||||
from six.moves import zip
|
||||
import mock
|
||||
from mock import MagicMock
|
||||
from six import BytesIO
|
||||
from six import PY2
|
||||
import hashlib
|
||||
|
||||
from pysstv import sstv
|
||||
|
|
@ -68,8 +66,7 @@ class TestSSTV(unittest.TestCase):
|
|||
bio = BytesIO()
|
||||
bio.close = MagicMock() # ignore close() so we can .getvalue()
|
||||
mock_open = MagicMock(return_value=bio)
|
||||
ns = '__builtin__' if PY2 else 'builtins'
|
||||
with mock.patch('{0}.open'.format(ns), mock_open):
|
||||
with mock.patch('builtins.open', mock_open):
|
||||
self.s.write_wav('unittest.wav')
|
||||
expected = 'dd7eed880ab3360fb79ce09c469deee2'
|
||||
data = bio.getvalue()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
six==1.10.0
|
||||
Pillow==4.3.0
|
||||
Pillow==10.3.0
|
||||
mock==1.0.1
|
||||
nose==1.3.0
|
||||
|
|
|
|||
10
setup.py
10
setup.py
|
|
@ -4,14 +4,19 @@ from setuptools import setup
|
|||
|
||||
setup(
|
||||
name='PySSTV',
|
||||
version='0.4.1',
|
||||
version='0.5.7',
|
||||
description='Python classes for generating Slow-scan Television transmissions',
|
||||
author=u'András Veres-Szentkirályi',
|
||||
author_email='vsza@vsza.hu',
|
||||
url='https://github.com/dnet/pySSTV',
|
||||
packages=['pysstv', 'pysstv.tests', 'pysstv.examples'],
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'pysstv = pysstv.__main__:main',
|
||||
],
|
||||
},
|
||||
keywords='HAM SSTV slow-scan television Scottie Martin Robot Pasokon',
|
||||
install_requires = ['Pillow', 'six'],
|
||||
install_requires = ['Pillow'],
|
||||
license='MIT',
|
||||
classifiers=[
|
||||
'Development Status :: 4 - Beta',
|
||||
|
|
@ -21,4 +26,5 @@ setup(
|
|||
'Operating System :: OS Independent',
|
||||
],
|
||||
long_description=open('README.md').read(),
|
||||
long_description_content_type="text/markdown",
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue