Compare commits

..

No commits in common. "master" and "v0.2.3" have entirely different histories.

19 changed files with 97 additions and 597 deletions

View file

@ -1,40 +0,0 @@
# 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
.gitignore vendored
View file

@ -1 +0,0 @@
stb_image.h

View file

@ -1,9 +0,0 @@
language: python
python:
- "3.6"
- "3.7"
- "3.8"
- "3.9"
sudo: false
install: "pip install -r requirements.txt"
script: nosetests

View file

@ -14,10 +14,8 @@ Command line usage
$ python -m pysstv -h
usage: __main__.py [-h]
[--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] [--resize] [--keep-aspect-ratio]
[--keep-aspect] [--resample {nearest,bicubic,lanczos}]
[--mode {MartinM2,MartinM1,Robot24BW,ScottieS2,ScottieS1,Robot8BW}]
[--rate RATE] [--bits BITS]
image.png output.wav
Converts an image to an SSTV modulated WAV file.
@ -26,23 +24,12 @@ Command line usage
image.png input image file name
output.wav output WAV file name
options:
optional arguments:
-h, --help show this help message and exit
--mode {MartinM1,MartinM2,ScottieS1,ScottieS2,ScottieDX,Robot36,PasokonP3,PasokonP5,PasokonP7,PD90,PD120,PD160,PD180,PD240,PD290,WraaseSC2120,WraaseSC2180,Robot8BW,Robot24BW}
--mode {MartinM2,MartinM1,Robot24BW,ScottieS2,ScottieS1,Robot8BW}
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
----------------
@ -82,5 +69,5 @@ Useful links
Dependencies
------------
- Python 3.5 or later
- Python Imaging Library (Debian/Ubuntu package: `python3-pil`)
- Python 2.7 (tested on 2.7.5)
- Python Imaging Library (Debian/Ubuntu package: `python-imaging`)

View file

@ -1,3 +0,0 @@
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"

View file

@ -1,10 +1,11 @@
#!/usr/bin/env python
from __future__ import print_function, division
from __future__ import print_function
from PIL import Image
from argparse import ArgumentParser
from sys import stderr
from pysstv import color, grayscale
import color
import grayscale
SSTV_MODULES = [color, grayscale]
@ -30,53 +31,10 @@ def main():
help='add FSKID at the end')
parser.add_argument('--chan', dest='chan', type=int,
help='number of channels (default: mono)')
parser.add_argument('--resize', dest='resize', action='store_true',
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)')
args = parser.parse_args()
image = Image.open(args.img_file)
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 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:
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:
h = mode.HEIGHT
w = int(orig_ratio * h)
else:
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
image = image.crop((x, y, mode.WIDTH + x, mode.HEIGHT + y))
elif not all(i >= m for i, m in zip(image.size, (mode.WIDTH, mode.HEIGHT))):
if not all(i >= m for i, m in zip(image.size, (mode.WIDTH, mode.HEIGHT))):
print(('Image must be at least {m.WIDTH} x {m.HEIGHT} pixels '
'for mode {m.__name__}').format(m=mode), file=stderr)
raise SystemExit(1)
@ -90,11 +48,7 @@ def main():
def build_module_map():
try:
from collections import OrderedDict
module_map = OrderedDict()
except ImportError:
module_map = {}
module_map = {}
for module in SSTV_MODULES:
for mode in module.MODES:
module_map[mode.__name__] = mode

View file

@ -1,40 +1,38 @@
#!/usr/bin/env python
from __future__ import division
from pysstv.sstv import byte_to_freq, FREQ_BLACK, FREQ_WHITE, FREQ_VIS_START
from pysstv.grayscale import GrayscaleSSTV
from sstv import byte_to_freq, FREQ_BLACK, FREQ_WHITE, FREQ_VIS_START
from grayscale import GrayscaleSSTV
from itertools import chain
from enum import Enum
class Color(Enum):
red = 0
green = 1
blue = 2
RED, GREEN, BLUE = range(3)
class ColorSSTV(GrayscaleSSTV):
def on_init(self):
self.pixels = self.image.convert('RGB').load()
self.pixels = self.image.load()
def encode_line(self, line):
msec_pixel = self.SCAN / self.WIDTH
image = self.pixels
for color in self.COLOR_SEQ:
yield from self.before_channel(color)
for col in range(self.WIDTH):
for index in self.COLOR_SEQ:
for item in self.before_channel(index):
yield item
for col in xrange(self.WIDTH):
pixel = image[col, line]
freq_pixel = byte_to_freq(pixel[color.value])
freq_pixel = byte_to_freq(pixel[index])
yield freq_pixel, msec_pixel
yield from self.after_channel(color)
for item in self.after_channel(index):
yield item
def before_channel(self, color):
def before_channel(self, index):
return []
after_channel = before_channel
class MartinM1(ColorSSTV):
COLOR_SEQ = (Color.green, Color.blue, Color.red)
COLOR_SEQ = (GREEN, BLUE, RED)
VIS_CODE = 0x2c
WIDTH = 320
HEIGHT = 256
@ -42,11 +40,11 @@ class MartinM1(ColorSSTV):
SCAN = 146.432
INTER_CH_GAP = 0.572
def before_channel(self, color):
if color is Color.green:
def before_channel(self, index):
if index == GREEN:
yield FREQ_BLACK, self.INTER_CH_GAP
def after_channel(self, color):
def after_channel(self, index):
yield FREQ_BLACK, self.INTER_CH_GAP
@ -65,9 +63,10 @@ class ScottieS1(MartinM1):
def horizontal_sync(self):
return []
def before_channel(self, color):
if color is Color.red:
yield from MartinM1.horizontal_sync(self)
def before_channel(self, index):
if index == ColorSSTV.RED:
for item in MartinM1.horizontal_sync(self):
yield item
yield FREQ_BLACK, self.INTER_CH_GAP
@ -77,12 +76,6 @@ 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
@ -93,14 +86,14 @@ class Robot36(ColorSSTV):
C_SCAN = 44
PORCH = 1.5
SYNC_PORCH = 3
INTER_CH_FREQS = [None, FREQ_WHITE, FREQ_BLACK]
INTER_CH_FREQS = [None, FREQ_BLACK, FREQ_WHITE]
def on_init(self):
self.yuv = self.image.convert('YCbCr').load()
def encode_line(self, line):
pixels = [self.yuv[col, line] for col in range(self.WIDTH)]
channel = 2 - (line % 2)
pixels = [self.yuv[col, line] for col in xrange(self.WIDTH)]
channel = (line % 2) + 1
y_pixel_time = self.Y_SCAN / self.WIDTH
uv_pixel_time = self.C_SCAN / self.WIDTH
return chain(
@ -111,145 +104,4 @@ class Robot36(ColorSSTV):
((byte_to_freq(p[channel]), uv_pixel_time) for p in pixels))
class PasokonP3(ColorSSTV):
"""
[ VIS code or horizontal sync here ]
Back porch - 5 time units of black (1500 Hz).
Red component - 640 pixels of 1 time unit each.
Gap - 5 time units of black.
Green component - 640 pixels of 1 time unit each.
Gap - 5 time units of black.
Blue component - 640 pixels of 1 time unit each.
Front porch - 5 time units of black.
Horizontal Sync - 25 time units of 1200 Hz.
"""
TIMEUNIT = 1000/4800. # ms
COLOR_SEQ = (Color.red, Color.green, Color.blue)
VIS_CODE = 0x71
WIDTH = 640
HEIGHT = 480+16
SYNC = 25 * TIMEUNIT
SCAN = WIDTH * TIMEUNIT
INTER_CH_GAP = 5 * TIMEUNIT
def before_channel(self, color):
if color is Color.red:
yield FREQ_BLACK, self.INTER_CH_GAP
def after_channel(self, color):
yield FREQ_BLACK, self.INTER_CH_GAP
class PasokonP5(PasokonP3):
TIMEUNIT = 1000/3200. # ms
VIS_CODE = 0x72
SYNC = 25 * TIMEUNIT
SCAN = PasokonP3.WIDTH * TIMEUNIT
INTER_CH_GAP = 5 * TIMEUNIT
class PasokonP7(PasokonP3):
TIMEUNIT = 1000/2400. # ms
VIS_CODE = 0xF3
SYNC = 25 * TIMEUNIT
SCAN = PasokonP3.WIDTH * TIMEUNIT
INTER_CH_GAP = 5 * TIMEUNIT
class PD90(ColorSSTV):
VIS_CODE = 0x63
WIDTH = 320
HEIGHT = 256
SYNC = 20
PORCH = 2.08
PIXEL = 0.532
def gen_image_tuples(self):
yuv = self.image.convert('YCbCr').load()
for line in range(0, self.HEIGHT, 2):
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)]
for p in pixels0:
yield byte_to_freq(p[0]), self.PIXEL
for p0, p1 in zip(pixels0, pixels1):
yield byte_to_freq((p0[2] + p1[2]) / 2), self.PIXEL
for p0, p1 in zip(pixels0, pixels1):
yield byte_to_freq((p0[1] + p1[1]) / 2), self.PIXEL
for p in pixels1:
yield byte_to_freq(p[0]), self.PIXEL
class PD120(PD90):
VIS_CODE = 0x5f
WIDTH = 640
HEIGHT = 496
PIXEL = 0.19
class PD160(PD90):
VIS_CODE = 0x62
WIDTH = 512
HEIGHT = 400
PIXEL = 0.382
class PD180(PD120):
VIS_CODE = 0x60
PIXEL = 0.286
class PD240(PD120):
VIS_CODE = 0x61
PIXEL = 0.382
class PD290(PD240):
VIS_CODE = 0x5e
WIDTH = 800
HEIGHT = 616
PIXEL = 0.286
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)
MODES = (MartinM1, MartinM2, ScottieS1, ScottieS2, Robot36)

View file

@ -1,167 +0,0 @@
#!/usr/bin/env python
class Image(object):
def __init__(self, content):
self.content = content
def load(self):
return self
def __getitem__(self, item):
if isinstance(item, tuple):
x, y = item
return Image('{0}[(ROW({1}) + COL({2})) * 3'.format(self.content, y, x))
elif isinstance(item, int):
return Image('{0} + RGB({1})]'.format(self.content, item))
else:
raise NotImplementedError()
def __rmul__(self, n):
return Image('({1} * {0})'.format(self.content, float(n)))
def __mul__(self, n):
return Image('({0} * {1})'.format(self.content, float(n)))
def __rtruediv__(self, n):
return Image('({1} / {0})'.format(self.content, n))
def __truediv__(self, n):
return Image('({0} / {1})'.format(self.content, n))
def __radd__(self, n):
return Image('({1} + {0})'.format(self.content, n))
def __add__(self, n):
return Image('({0} + {1})'.format(self.content, n))
def __str__(self):
return self.content
from pysstv.color import MartinM1, MartinM2, PasokonP3, PasokonP5, PasokonP7
import re
supported = [MartinM1, MartinM2, PasokonP3, PasokonP5, PasokonP7]
ROW_RE = re.compile(r'ROW\(\d+\)')
def main(sstv_class=None):
if sstv_class is None:
sstv_class = MartinM1
elif sstv_class not in supported:
raise NotImplementedError()
sstv = sstv_class(Image('img'), 44100, 16)
n = 0
yield '#define ROW(x) x'
yield '#define COL(x) x'
yield '#define RGB(x) x'
yield 'void convert(unsigned char *img, float *freqs, float *msecs, const int width) {\nint frq = 0;'
history = []
lut = {}
same_as = {}
for freq, msec in sstv.gen_freq_bits():
printed = 'freqs[frq] = {1}; msecs[frq++] = {2};'.format(n, freq, msec)
key = ROW_RE.sub('row', printed)
old = lut.get(key)
if old is not None:
same_as[n] = old
else:
lut[key] = n
history.append((printed, key))
n += 1
del lut
m_start, m_len = gen_matches(same_as, history, n)
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 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)
def gen_matches(same_as, history, n):
cur_start = None
cur_len = None
cur_end = None
for i in range(n):
if cur_start is None:
tmp = same_as.get(i)
if tmp is not None:
cur_len = 1
cur_start = i
cur_end = tmp
else:
tmp = same_as.get(i)
if tmp is not None and history[tmp][1] == history[cur_end + 1][1] and cur_start > cur_end:
cur_len += 1
cur_end += 1
else:
if tmp is not None and history[tmp][1] == history[cur_end + 1][1]:
return cur_start, cur_len
tmp = same_as.get(i)
if tmp is None:
cur_start = None
else:
cur_len = 1
cur_start = i
cur_end = tmp
def test(img_file):
from subprocess import Popen, PIPE, check_output
from os import remove, path
from PIL import Image
from datetime import datetime
import struct
exe = './codegen-test-executable'
if not path.exists('stb_image.h'):
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)
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)
start = datetime.now()
gen = check_output([exe, img_file])
native_elapsed = datetime.now() - start
print(' - native took', native_elapsed)
img = Image.open(img_file)
sstv = sstv_class(img, 44100, 16)
start = datetime.now()
try:
for n, (freq, msec) in enumerate(sstv.gen_freq_bits()):
assert gen[n * 8:(n + 1) * 8] == struct.pack('ff', freq, msec)
except AssertionError:
mode_name = sstv_class.__name__
with open('/tmp/{0}-c.bin'.format(mode_name), 'wb') as f:
f.write(gen)
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 open('/tmp/{0}.c'.format(mode_name), 'w') as f:
f.write(c_src)
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))
python_elapsed = datetime.now() - start
print(' - python took', python_elapsed)
print(' - speedup:', python_elapsed.total_seconds() / native_elapsed.total_seconds())
print('OK')
finally:
try:
remove(exe)
except OSError:
pass
if __name__ == '__main__':
from sys import argv
if len(argv) > 2 and argv[1] == 'test':
test(argv[2])
else:
print('\n'.join(main()))

View file

@ -1,25 +0,0 @@
#include <stdio.h>
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#include "codegen.c"
int main(int argc, char **argv) {
int x, y, n;
unsigned char *img = stbi_load(argv[1], &x, &y, &n, 0);
if (n != 3) {
fprintf(stderr, "Only RGB images are supported\n");
return 1;
}
float freqs[FREQ_COUNT], msecs[FREQ_COUNT];
convert(img, freqs, msecs, x);
for (int i = 0; i < FREQ_COUNT; i++) {
fwrite(&(freqs[i]), 4, 1, stdout);
fwrite(&(msecs[i]), 4, 1, stdout);
}
return 0;
}

View file

@ -2,19 +2,18 @@
# -*- encoding: utf-8 -*-
# copy to ~/.gimp-2.8/plug-ins/
# dependencies: GIMP 2.8, python-imaging-tk, python-pyaudio
# dependencies: GIMP 2.8, python-imaging-tk
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 time import sleep
import gimp, os
from Queue import Queue, Empty
import gimp
MODULE_MAP = pysstv_main.build_module_map()
@ -42,7 +41,7 @@ class Sine1750(SSTV):
class Transmitter(object):
def __init__(self, sstv, root, progress, set_ptt_pin, ptt_state):
def __init__(self, sstv, root, progress):
def encode_line_hooked(line):
progress.update_image(line)
return self.original_encode_line(line)
@ -54,23 +53,11 @@ class Transmitter(object):
self.tx_enabled = IntVar()
self.audio_thread = None
self.stopping = False
self.set_ptt_pin = set_ptt_pin
self.ptt_state = ptt_state
def set_ptt(self, state):
if self.set_ptt_pin is None:
return
if not state:
sleep(0.2)
self.set_ptt_pin(state != self.ptt_state)
if state:
sleep(0.2)
def start_stop_tx(self):
if self.tx_enabled.get():
self.stopping = False
self.audio_thread = AudioThread(self.sstv, self)
self.set_ptt(True)
self.audio_thread.start()
else:
self.stop()
@ -81,11 +68,9 @@ class Transmitter(object):
if self.audio_thread is not None:
self.stopping = True
self.audio_thread.stop()
self.set_ptt(False)
def audio_thread_ended(self):
if not self.stopping:
self.set_ptt(False)
self.tx_enabled.set(0)
def close(self):
@ -118,12 +103,12 @@ class ProgressCanvas(Canvas):
self.height_ratio = 1
width, height = image.size
pixels = image.load()
RED, GREEN, BLUE = list(range(3))
RED, GREEN, BLUE = range(3)
self.colors = ['#{0:02x}{1:02x}{2:02x}'.format(
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)]
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)]
if height / float(width) > 1.5:
width *= 2
elif width < 200:
@ -152,20 +137,14 @@ def contrast(value):
else:
return 255 - value
def transmit_current_image(image, drawable, mode, vox, fskid, ptt_port, ptt_pin, ptt_state):
def transmit_current_image(image, drawable, mode, vox, fskid):
sstv = MODULE_MAP[mode]
if ptt_port is not None:
from serial import Serial
set_ptt_pin = getattr(Serial(ptt_port), 'set' + ptt_pin)
set_ptt_pin(ptt_state)
else:
set_ptt_pin = None
pil_img = match_image_with_sstv_mode(image_gimp_to_pil(image), sstv)
root = Tk()
cu = CanvasUpdater(ProgressCanvas(root, pil_img))
cu.start()
tm = Transmitter(init_sstv(sstv, pil_img, vox, fskid), root, cu, set_ptt_pin, ptt_state)
tm1750 = Transmitter(Sine1750(None, 44100, 16), None, None, set_ptt_pin, ptt_state)
tm = Transmitter(init_sstv(sstv, pil_img, vox, fskid), root, cu)
tm1750 = Transmitter(Sine1750(None, 44100, 16), None, None)
buttons = Frame(root)
for text, tram in (('TX', tm), ('1750 Hz', tm1750)):
Checkbutton(buttons, text=text, indicatoron=False, padx=5, pady=5,
@ -209,23 +188,6 @@ def init_sstv(mode, image, vox, fskid):
s.add_fskid_text(fskid)
return s
def get_serial_ports():
try:
if os.name == 'nt':
from serial.tools.list_ports_windows import comports
elif os.name == 'posix':
from serial.tools.list_ports_posix import comports
else:
raise ImportError("Sorry: no implementation for your"
"platform ('%s') available" % (os.name,))
except ImportError:
yield "(couldn't import PySerial)", None
else:
yield "(disabled)", None
for port, desc, _ in comports():
yield '{0} ({1})'.format(port, desc.strip()), port
register(
"pysstv_for_gimp",
"PySSTV for GIMP",
@ -237,14 +199,9 @@ register(
"*",
[
(PF_RADIO, "mode", "SSTV mode", "MartinM1",
tuple((n, n) for n in sorted(MODULE_MAP.keys()))),
tuple((n, n) for n in sorted(MODULE_MAP.iterkeys()))),
(PF_BOOL, "vox", "Include VOX tones", True),
(PF_STRING, "fskid", "FSK ID", ""),
(PF_RADIO, "ptt_port", "PTT port", None,
tuple(get_serial_ports())),
(PF_RADIO, "ptt_pin", "PTT pin", "RTS",
tuple((n, n) for n in ("RTS", "DTR"))),
(PF_BOOL, "ptt_state", "PTT inversion", False),
],
[],
transmit_current_image

View file

@ -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

View file

@ -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.items():
for mode, module in MODE_MAP.iteritems():
if mode in basename:
return module
def get_module_for_image(image):
size = image.size
for mode in MODE_MAP.values():
for mode in MODE_MAP.itervalues():
if all(i >= m for i, m in zip(size, (mode.WIDTH, mode.HEIGHT))):
return mode

View file

@ -1,7 +1,7 @@
#!/usr/bin/env python
from __future__ import division
from pysstv.sstv import SSTV, byte_to_freq
from sstv import SSTV, byte_to_freq
class GrayscaleSSTV(SSTV):
@ -9,14 +9,16 @@ class GrayscaleSSTV(SSTV):
self.pixels = self.image.convert('LA').load()
def gen_image_tuples(self):
for line in range(self.HEIGHT):
yield from self.horizontal_sync()
yield from self.encode_line(line)
for line in xrange(self.HEIGHT):
for item in self.horizontal_sync():
yield item
for item in self.encode_line(line):
yield item
def encode_line(self, line):
msec_pixel = self.SCAN / self.WIDTH
image = self.pixels
for col in range(self.WIDTH):
for col in xrange(self.WIDTH):
pixel = image[col, line]
freq_pixel = byte_to_freq(pixel[0])
yield freq_pixel, msec_pixel

View file

@ -4,7 +4,7 @@ from __future__ import division, with_statement
from math import sin, pi
from random import random
from contextlib import closing
from itertools import cycle, chain
from itertools import imap, izip, cycle, chain
from array import array
import wave
@ -46,12 +46,12 @@ class SSTV(object):
data = array(fmt, self.gen_samples())
if self.nchannels != 1:
data = array(fmt, chain.from_iterable(
zip(*([data] * self.nchannels))))
izip(*([data] * self.nchannels))))
with closing(wave.open(filename, 'wb')) as wav:
wav.setnchannels(self.nchannels)
wav.setsampwidth(self.bits // 8)
wav.setframerate(self.samples_per_sec)
wav.writeframes(data)
wav.writeframes(data.tostring())
def gen_samples(self):
"""generates discrete samples from gen_values()
@ -64,8 +64,8 @@ class SSTV(object):
amp = max_value // 2
lowest = -amp
highest = amp - 1
alias_cycle = cycle((alias * (random() - 0.5) for _ in range(1024)))
for value, alias_item in zip(self.gen_values(), alias_cycle):
alias_cycle = cycle((alias * (random() - 0.5) for _ in xrange(1024)))
for value, alias_item in izip(self.gen_values(), alias_cycle):
sample = int(value * amp + alias_item)
yield (lowest if sample <= lowest else
sample if sample <= highest else highest)
@ -85,7 +85,7 @@ class SSTV(object):
samples += spms * msec
tx = int(samples)
freq_factor = freq * factor
for sample in range(tx):
for sample in xrange(tx):
yield sin(sample * freq_factor + offset)
offset += (sample + 1) * freq_factor
samples -= tx
@ -104,7 +104,7 @@ class SSTV(object):
yield FREQ_SYNC, MSEC_VIS_BIT # start bit
vis = self.VIS_CODE
num_ones = 0
for _ in range(7):
for _ in xrange(7):
bit = vis & 1
vis >>= 1
num_ones += bit
@ -113,9 +113,10 @@ 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
yield from self.gen_image_tuples()
for fskid_byte in map(ord, self.fskid_payload):
for _ in range(6):
for freq_tuple in self.gen_image_tuples():
yield freq_tuple
for fskid_byte in imap(ord, self.fskid_payload):
for _ in xrange(6):
bit = fskid_byte & 1
fskid_byte >>= 1
bit_freq = FREQ_FSKID_BIT1 if bit == 1 else FREQ_FSKID_BIT0

View file

@ -1,9 +1,4 @@
from os import path
import pickle
def get_asset_filename(filename):
return path.join(path.dirname(__file__), 'assets', filename)
def load_pickled_asset(filename):
with open(get_asset_filename(filename + '.p'), 'rb') as f:
return pickle.load(f)

View file

@ -1,10 +1,11 @@
import unittest
from itertools import islice
import pickle
from PIL import Image
from pysstv import color
from pysstv.tests.common import get_asset_filename, load_pickled_asset
from common import get_asset_filename
class TestMartinM1(unittest.TestCase):
@ -16,12 +17,12 @@ class TestMartinM1(unittest.TestCase):
self.lena = color.MartinM1(lena, 48000, 16)
def test_gen_freq_bits(self):
expected = load_pickled_asset("MartinM1_freq_bits")
expected = pickle.load(open(get_asset_filename("MartinM1_freq_bits.p")))
actual = list(islice(self.s.gen_freq_bits(), 0, 1000))
self.assertEqual(expected, actual)
def test_gen_freq_bits_lena(self):
expected = load_pickled_asset("MartinM1_freq_bits_lena")
expected = pickle.load(open(get_asset_filename("MartinM1_freq_bits_lena.p")))
actual = list(islice(self.lena.gen_freq_bits(), 0, 1000))
self.assertEqual(expected, actual)
@ -39,6 +40,7 @@ class TestMartinM1(unittest.TestCase):
self.maxDiff = None
line_numbers = [1, 10, 100]
for line in line_numbers:
expected = load_pickled_asset("MartinM1_encode_line_lena{0}".format(line))
file = open(get_asset_filename("MartinM1_encode_line_lena%d.p" % line))
expected = pickle.load(file)
actual = list(self.lena.encode_line(line))
self.assertEqual(expected, actual)

View file

@ -1,13 +1,14 @@
import unittest
from io import BytesIO
from itertools import islice
from itertools import islice, izip
import pickle
import mock
from mock import MagicMock
from StringIO import StringIO
import hashlib
from pysstv import sstv
from pysstv.sstv import SSTV
from pysstv.tests.common import load_pickled_asset
from common import get_asset_filename
class TestSSTV(unittest.TestCase):
@ -20,7 +21,7 @@ class TestSSTV(unittest.TestCase):
def test_horizontal_sync(self):
horizontal_sync = self.s.horizontal_sync()
expected = (1200, self.s.SYNC)
actual = next(iter(horizontal_sync))
actual = horizontal_sync.next()
self.assertEqual(expected, actual)
def test_gen_freq_bits(self):
@ -44,8 +45,8 @@ class TestSSTV(unittest.TestCase):
# FIXME: Instead of using a test fixture, 'expected' should be synthesized?
def test_gen_values(self):
gen_values = self.s.gen_values()
expected = load_pickled_asset("SSTV_gen_values")
for e, g in zip(expected, gen_values):
expected = pickle.load(open(get_asset_filename("SSTV_gen_values.p")))
for e, g in izip(expected, gen_values):
self.assertAlmostEqual(e, g, delta=0.000000001)
def test_gen_samples(self):
@ -56,20 +57,20 @@ class TestSSTV(unittest.TestCase):
# and having different results.
# https://en.wikipedia.org/wiki/Quantization_%28signal_processing%29
sstv.random = MagicMock(return_value=0.4) # xkcd:221
expected = load_pickled_asset("SSTV_gen_samples")
expected = pickle.load(open(get_asset_filename("SSTV_gen_samples.p")))
actual = list(islice(gen_values, 0, 1000))
for e, a in zip(expected, actual):
for e, a in izip(expected, actual):
self.assertAlmostEqual(e, a, delta=1)
def test_write_wav(self):
self.maxDiff = None
bio = BytesIO()
bio.close = MagicMock() # ignore close() so we can .getvalue()
mock_open = MagicMock(return_value=bio)
with mock.patch('builtins.open', mock_open):
sio = StringIO()
sio.close = MagicMock() # ignore close() so we can .getvalue()
mock_open = MagicMock(return_value=sio)
with mock.patch('__builtin__.open', mock_open):
self.s.write_wav('unittest.wav')
expected = 'dd7eed880ab3360fb79ce09c469deee2'
data = bio.getvalue()
data = sio.getvalue()
actual = hashlib.md5(data).hexdigest()
self.assertEqual(expected, actual)

View file

@ -1,3 +1,3 @@
Pillow==10.3.0
PIL==1.1.7
mock==1.0.1
nose==1.3.0

View file

@ -4,19 +4,14 @@ from setuptools import setup
setup(
name='PySSTV',
version='0.5.7',
version='0.2.3',
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'],
keywords='HAM SSTV slow-scan television Scottie Martin Robot',
install_requires = ['PIL',],
license='MIT',
classifiers=[
'Development Status :: 4 - Beta',
@ -26,5 +21,4 @@ setup(
'Operating System :: OS Independent',
],
long_description=open('README.md').read(),
long_description_content_type="text/markdown",
)