mirror of
https://github.com/dnet/pySSTV.git
synced 2026-01-21 07:50:28 +01:00
Compare commits
112 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 | ||
|
|
e220d1964c | ||
|
|
4b95f8b5c3 | ||
|
|
99d8709053 | ||
|
|
509d162a0d | ||
|
|
03092f5549 | ||
|
|
b731112007 | ||
|
|
35de6c718e | ||
|
|
6d3d2143b7 | ||
|
|
c6504a7551 | ||
|
|
75d9722491 | ||
|
|
0c38059001 | ||
|
|
de0dc22cb8 | ||
|
|
da0c102090 | ||
|
|
9ff3f73d17 | ||
|
|
d51c00a04a | ||
|
|
a601f8e754 | ||
|
|
5eb8a468e4 | ||
|
|
81b45e7e4e | ||
|
|
9cbb0c50ff | ||
|
|
0d52546d81 | ||
|
|
ac0f89bfdf | ||
|
|
bf2efdabea | ||
|
|
d574ab1d49 | ||
|
|
8f8f431050 | ||
|
|
a43f64fd79 | ||
|
|
d177707233 | ||
|
|
72ffc80aad | ||
|
|
5d3b11a144 | ||
|
|
7eac9636a3 | ||
|
|
56743c7afb | ||
|
|
810f604a5a | ||
|
|
23a2e2bd00 | ||
|
|
ff15c3a63a | ||
|
|
5d38b3b52d | ||
|
|
c7fecff3e6 | ||
|
|
40768cb57a | ||
|
|
d540abd852 | ||
|
|
dee321d59e | ||
|
|
ec6ff85d06 | ||
|
|
43d1c53514 | ||
|
|
ff2fc58aa7 | ||
|
|
c185ea571b | ||
|
|
f295a07c83 | ||
|
|
3c3bb34b1b | ||
|
|
d7a05fc1d5 | ||
|
|
6418a9febc | ||
|
|
2f6c7ca871 | ||
|
|
f02f43e4b5 | ||
|
|
7d3c7c4683 | ||
|
|
8c10e0ce3b | ||
|
|
7335f72c91 | ||
|
|
45fbdbd7c8 | ||
|
|
2c23ae9632 | ||
|
|
2b0f482653 | ||
|
|
927f805771 | ||
|
|
1916cfd031 | ||
|
|
09df062547 | ||
|
|
9ef2a5a8ca | ||
|
|
d82357b45d | ||
|
|
b837723d93 | ||
|
|
6f5ea1a5ef | ||
|
|
e0a4563a56 | ||
|
|
2dd5eaa273 | ||
|
|
2e8148b029 | ||
|
|
e8f07e5bcb | ||
|
|
63b97f07cf | ||
|
|
7e9436adeb | ||
|
|
b74feb49ab | ||
|
|
bd3b84cfb6 | ||
|
|
3a25caeab9 | ||
|
|
9bf312ca85 | ||
|
|
4c020d61b9 | ||
|
|
4d56f34a72 |
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
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
stb_image.h
|
||||||
9
.travis.yml
Normal file
9
.travis.yml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
language: python
|
||||||
|
python:
|
||||||
|
- "3.6"
|
||||||
|
- "3.7"
|
||||||
|
- "3.8"
|
||||||
|
- "3.9"
|
||||||
|
sudo: false
|
||||||
|
install: "pip install -r requirements.txt"
|
||||||
|
script: nosetests
|
||||||
26
README.md
26
README.md
|
|
@ -14,8 +14,10 @@ Command line usage
|
||||||
|
|
||||||
$ python -m pysstv -h
|
$ python -m pysstv -h
|
||||||
usage: __main__.py [-h]
|
usage: __main__.py [-h]
|
||||||
[--mode {MartinM2,MartinM1,Robot24BW,ScottieS2,ScottieS1,Robot8BW}]
|
[--mode {MartinM1,MartinM2,ScottieS1,ScottieS2,ScottieDX,Robot36,PasokonP3,PasokonP5,PasokonP7,PD90,PD120,PD160,PD180,PD240,PD290,WraaseSC2120,WraaseSC2180,Robot8BW,Robot24BW}]
|
||||||
[--rate RATE] [--bits BITS]
|
[--rate RATE] [--bits BITS] [--vox] [--fskid FSKID]
|
||||||
|
[--chan CHAN] [--resize] [--keep-aspect-ratio]
|
||||||
|
[--keep-aspect] [--resample {nearest,bicubic,lanczos}]
|
||||||
image.png output.wav
|
image.png output.wav
|
||||||
|
|
||||||
Converts an image to an SSTV modulated WAV file.
|
Converts an image to an SSTV modulated WAV file.
|
||||||
|
|
@ -24,12 +26,23 @@ Command line usage
|
||||||
image.png input image file name
|
image.png input image file name
|
||||||
output.wav output WAV file name
|
output.wav output WAV file name
|
||||||
|
|
||||||
optional arguments:
|
options:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
--mode {MartinM2,MartinM1,Robot24BW,ScottieS2,ScottieS1,Robot8BW}
|
--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)
|
image mode (default: Martin M1)
|
||||||
--rate RATE sampling rate (default: 48000)
|
--rate RATE sampling rate (default: 48000)
|
||||||
--bits BITS bits per sample (default: 16)
|
--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
|
Python interface
|
||||||
----------------
|
----------------
|
||||||
|
|
@ -64,9 +77,10 @@ Useful links
|
||||||
|
|
||||||
- receive-only "counterpart": https://github.com/windytan/slowrx
|
- receive-only "counterpart": https://github.com/windytan/slowrx
|
||||||
- free SSTV handbook: http://www.sstv-handbook.com/
|
- free SSTV handbook: http://www.sstv-handbook.com/
|
||||||
|
- robot 36 encoder/decoder in C: https://github.com/xdsopl/robot36/
|
||||||
|
|
||||||
Dependencies
|
Dependencies
|
||||||
------------
|
------------
|
||||||
|
|
||||||
- Python 2.7 (tested on 2.7.5)
|
- Python 3.5 or later
|
||||||
- Python Imaging Library (Debian/Ubuntu package: `python-imaging`)
|
- 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"
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
from __future__ import print_function
|
from __future__ import print_function, division
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
from sys import stderr
|
from sys import stderr
|
||||||
import color
|
from pysstv import color, grayscale
|
||||||
import grayscale
|
|
||||||
|
|
||||||
SSTV_MODULES = [color, grayscale]
|
SSTV_MODULES = [color, grayscale]
|
||||||
|
|
||||||
|
|
@ -31,10 +30,53 @@ def main():
|
||||||
help='add FSKID at the end')
|
help='add FSKID at the end')
|
||||||
parser.add_argument('--chan', dest='chan', type=int,
|
parser.add_argument('--chan', dest='chan', type=int,
|
||||||
help='number of channels (default: mono)')
|
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()
|
args = parser.parse_args()
|
||||||
image = Image.open(args.img_file)
|
image = Image.open(args.img_file)
|
||||||
mode = module_map[args.mode]
|
mode = module_map[args.mode]
|
||||||
if not all(i >= m for i, m in zip(image.size, (mode.WIDTH, mode.HEIGHT))):
|
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))):
|
||||||
print(('Image must be at least {m.WIDTH} x {m.HEIGHT} pixels '
|
print(('Image must be at least {m.WIDTH} x {m.HEIGHT} pixels '
|
||||||
'for mode {m.__name__}').format(m=mode), file=stderr)
|
'for mode {m.__name__}').format(m=mode), file=stderr)
|
||||||
raise SystemExit(1)
|
raise SystemExit(1)
|
||||||
|
|
@ -48,6 +90,10 @@ def main():
|
||||||
|
|
||||||
|
|
||||||
def build_module_map():
|
def build_module_map():
|
||||||
|
try:
|
||||||
|
from collections import OrderedDict
|
||||||
|
module_map = OrderedDict()
|
||||||
|
except ImportError:
|
||||||
module_map = {}
|
module_map = {}
|
||||||
for module in SSTV_MODULES:
|
for module in SSTV_MODULES:
|
||||||
for mode in module.MODES:
|
for mode in module.MODES:
|
||||||
|
|
|
||||||
225
pysstv/color.py
225
pysstv/color.py
|
|
@ -1,34 +1,40 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
from __future__ import division
|
from __future__ import division
|
||||||
from sstv import byte_to_freq, FREQ_BLACK
|
from pysstv.sstv import byte_to_freq, FREQ_BLACK, FREQ_WHITE, FREQ_VIS_START
|
||||||
from grayscale import GrayscaleSSTV
|
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):
|
class ColorSSTV(GrayscaleSSTV):
|
||||||
|
def on_init(self):
|
||||||
|
self.pixels = self.image.convert('RGB').load()
|
||||||
|
|
||||||
def encode_line(self, line):
|
def encode_line(self, line):
|
||||||
msec_pixel = self.SCAN / self.WIDTH
|
msec_pixel = self.SCAN / self.WIDTH
|
||||||
image = self.image.load()
|
image = self.pixels
|
||||||
for index in self.COLOR_SEQ:
|
for color in self.COLOR_SEQ:
|
||||||
for item in self.before_channel(index):
|
yield from self.before_channel(color)
|
||||||
yield item
|
for col in range(self.WIDTH):
|
||||||
for col in xrange(self.WIDTH):
|
|
||||||
pixel = image[col, line]
|
pixel = image[col, line]
|
||||||
freq_pixel = byte_to_freq(pixel[index])
|
freq_pixel = byte_to_freq(pixel[color.value])
|
||||||
yield freq_pixel, msec_pixel
|
yield freq_pixel, msec_pixel
|
||||||
for item in self.after_channel(index):
|
yield from self.after_channel(color)
|
||||||
yield item
|
|
||||||
|
|
||||||
def before_channel(self, index):
|
def before_channel(self, color):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
after_channel = before_channel
|
after_channel = before_channel
|
||||||
|
|
||||||
|
|
||||||
class MartinM1(ColorSSTV):
|
class MartinM1(ColorSSTV):
|
||||||
COLOR_SEQ = (GREEN, BLUE, RED)
|
COLOR_SEQ = (Color.green, Color.blue, Color.red)
|
||||||
VIS_CODE = 0x2c
|
VIS_CODE = 0x2c
|
||||||
WIDTH = 320
|
WIDTH = 320
|
||||||
HEIGHT = 256
|
HEIGHT = 256
|
||||||
|
|
@ -36,11 +42,11 @@ class MartinM1(ColorSSTV):
|
||||||
SCAN = 146.432
|
SCAN = 146.432
|
||||||
INTER_CH_GAP = 0.572
|
INTER_CH_GAP = 0.572
|
||||||
|
|
||||||
def before_channel(self, index):
|
def before_channel(self, color):
|
||||||
if index == GREEN:
|
if color is Color.green:
|
||||||
yield FREQ_BLACK, self.INTER_CH_GAP
|
yield FREQ_BLACK, self.INTER_CH_GAP
|
||||||
|
|
||||||
def after_channel(self, index):
|
def after_channel(self, color):
|
||||||
yield FREQ_BLACK, self.INTER_CH_GAP
|
yield FREQ_BLACK, self.INTER_CH_GAP
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -59,10 +65,9 @@ class ScottieS1(MartinM1):
|
||||||
def horizontal_sync(self):
|
def horizontal_sync(self):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def before_channel(self, index):
|
def before_channel(self, color):
|
||||||
if index == ColorSSTV.RED:
|
if color is Color.red:
|
||||||
for item in MartinM1.horizontal_sync(self):
|
yield from MartinM1.horizontal_sync(self)
|
||||||
yield item
|
|
||||||
yield FREQ_BLACK, self.INTER_CH_GAP
|
yield FREQ_BLACK, self.INTER_CH_GAP
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -71,4 +76,180 @@ class ScottieS2(ScottieS1):
|
||||||
SCAN = 88.064 - ScottieS1.INTER_CH_GAP
|
SCAN = 88.064 - ScottieS1.INTER_CH_GAP
|
||||||
WIDTH = 160
|
WIDTH = 160
|
||||||
|
|
||||||
MODES = (MartinM1, MartinM2, ScottieS1, ScottieS2)
|
|
||||||
|
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
|
||||||
|
HEIGHT = 240
|
||||||
|
SYNC = 9
|
||||||
|
INTER_CH_GAP = 4.5
|
||||||
|
Y_SCAN = 88
|
||||||
|
C_SCAN = 44
|
||||||
|
PORCH = 1.5
|
||||||
|
SYNC_PORCH = 3
|
||||||
|
INTER_CH_FREQS = [None, FREQ_WHITE, FREQ_BLACK]
|
||||||
|
|
||||||
|
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)
|
||||||
|
y_pixel_time = self.Y_SCAN / self.WIDTH
|
||||||
|
uv_pixel_time = self.C_SCAN / self.WIDTH
|
||||||
|
return chain(
|
||||||
|
[(FREQ_BLACK, self.SYNC_PORCH)],
|
||||||
|
((byte_to_freq(p[0]), y_pixel_time) for p in pixels),
|
||||||
|
[(self.INTER_CH_FREQS[channel], self.INTER_CH_GAP),
|
||||||
|
(FREQ_VIS_START, self.PORCH)],
|
||||||
|
((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)
|
||||||
|
|
|
||||||
167
pysstv/examples/codegen.py
Normal file
167
pysstv/examples/codegen.py
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
#!/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()))
|
||||||
25
pysstv/examples/codeman.c
Normal file
25
pysstv/examples/codeman.c
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
#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;
|
||||||
|
}
|
||||||
|
|
@ -2,18 +2,19 @@
|
||||||
# -*- encoding: utf-8 -*-
|
# -*- encoding: utf-8 -*-
|
||||||
|
|
||||||
# copy to ~/.gimp-2.8/plug-ins/
|
# copy to ~/.gimp-2.8/plug-ins/
|
||||||
# dependencies: GIMP 2.8, python-imaging-tk
|
# dependencies: GIMP 2.8, python-imaging-tk, python-pyaudio
|
||||||
|
|
||||||
from gimpfu import register, main, pdb, PF_BOOL, PF_STRING, PF_RADIO, CLIP_TO_IMAGE
|
from gimpfu import register, main, pdb, PF_BOOL, PF_STRING, PF_RADIO, CLIP_TO_IMAGE
|
||||||
from PIL import Image, ImageTk
|
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 import __main__ as pysstv_main
|
||||||
from pysstv.examples.pyaudio_sstv import PyAudioSSTV
|
from pysstv.examples.pyaudio_sstv import PyAudioSSTV
|
||||||
from pysstv.sstv import SSTV
|
from pysstv.sstv import SSTV
|
||||||
from itertools import repeat
|
from itertools import repeat
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from Queue import Queue, Empty
|
from queue import Queue, Empty
|
||||||
import gimp
|
from time import sleep
|
||||||
|
import gimp, os
|
||||||
|
|
||||||
MODULE_MAP = pysstv_main.build_module_map()
|
MODULE_MAP = pysstv_main.build_module_map()
|
||||||
|
|
||||||
|
|
@ -41,7 +42,7 @@ class Sine1750(SSTV):
|
||||||
|
|
||||||
|
|
||||||
class Transmitter(object):
|
class Transmitter(object):
|
||||||
def __init__(self, sstv, root, progress):
|
def __init__(self, sstv, root, progress, set_ptt_pin, ptt_state):
|
||||||
def encode_line_hooked(line):
|
def encode_line_hooked(line):
|
||||||
progress.update_image(line)
|
progress.update_image(line)
|
||||||
return self.original_encode_line(line)
|
return self.original_encode_line(line)
|
||||||
|
|
@ -53,11 +54,23 @@ class Transmitter(object):
|
||||||
self.tx_enabled = IntVar()
|
self.tx_enabled = IntVar()
|
||||||
self.audio_thread = None
|
self.audio_thread = None
|
||||||
self.stopping = False
|
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):
|
def start_stop_tx(self):
|
||||||
if self.tx_enabled.get():
|
if self.tx_enabled.get():
|
||||||
self.stopping = False
|
self.stopping = False
|
||||||
self.audio_thread = AudioThread(self.sstv, self)
|
self.audio_thread = AudioThread(self.sstv, self)
|
||||||
|
self.set_ptt(True)
|
||||||
self.audio_thread.start()
|
self.audio_thread.start()
|
||||||
else:
|
else:
|
||||||
self.stop()
|
self.stop()
|
||||||
|
|
@ -68,9 +81,11 @@ class Transmitter(object):
|
||||||
if self.audio_thread is not None:
|
if self.audio_thread is not None:
|
||||||
self.stopping = True
|
self.stopping = True
|
||||||
self.audio_thread.stop()
|
self.audio_thread.stop()
|
||||||
|
self.set_ptt(False)
|
||||||
|
|
||||||
def audio_thread_ended(self):
|
def audio_thread_ended(self):
|
||||||
if not self.stopping:
|
if not self.stopping:
|
||||||
|
self.set_ptt(False)
|
||||||
self.tx_enabled.set(0)
|
self.tx_enabled.set(0)
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
|
|
@ -103,12 +118,12 @@ class ProgressCanvas(Canvas):
|
||||||
self.height_ratio = 1
|
self.height_ratio = 1
|
||||||
width, height = image.size
|
width, height = image.size
|
||||||
pixels = image.load()
|
pixels = image.load()
|
||||||
RED, GREEN, BLUE = range(3)
|
RED, GREEN, BLUE = list(range(3))
|
||||||
self.colors = ['#{0:02x}{1:02x}{2:02x}'.format(
|
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][RED] for x in range(width)) / width),
|
||||||
contrast(sum(pixels[x, y][GREEN] for x in xrange(width)) / width),
|
contrast(sum(pixels[x, y][GREEN] for x in range(width)) / width),
|
||||||
contrast(sum(pixels[x, y][BLUE] for x in xrange(width)) / width))
|
contrast(sum(pixels[x, y][BLUE] for x in range(width)) / width))
|
||||||
for y in xrange(height)]
|
for y in range(height)]
|
||||||
if height / float(width) > 1.5:
|
if height / float(width) > 1.5:
|
||||||
width *= 2
|
width *= 2
|
||||||
elif width < 200:
|
elif width < 200:
|
||||||
|
|
@ -137,14 +152,20 @@ def contrast(value):
|
||||||
else:
|
else:
|
||||||
return 255 - value
|
return 255 - value
|
||||||
|
|
||||||
def transmit_current_image(image, drawable, mode, vox, fskid):
|
def transmit_current_image(image, drawable, mode, vox, fskid, ptt_port, ptt_pin, ptt_state):
|
||||||
sstv = MODULE_MAP[mode]
|
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)
|
pil_img = match_image_with_sstv_mode(image_gimp_to_pil(image), sstv)
|
||||||
root = Tk()
|
root = Tk()
|
||||||
cu = CanvasUpdater(ProgressCanvas(root, pil_img))
|
cu = CanvasUpdater(ProgressCanvas(root, pil_img))
|
||||||
cu.start()
|
cu.start()
|
||||||
tm = Transmitter(init_sstv(sstv, pil_img, vox, fskid), root, cu)
|
tm = Transmitter(init_sstv(sstv, pil_img, vox, fskid), root, cu, set_ptt_pin, ptt_state)
|
||||||
tm1750 = Transmitter(Sine1750(None, 44100, 16), None, None)
|
tm1750 = Transmitter(Sine1750(None, 44100, 16), None, None, set_ptt_pin, ptt_state)
|
||||||
buttons = Frame(root)
|
buttons = Frame(root)
|
||||||
for text, tram in (('TX', tm), ('1750 Hz', tm1750)):
|
for text, tram in (('TX', tm), ('1750 Hz', tm1750)):
|
||||||
Checkbutton(buttons, text=text, indicatoron=False, padx=5, pady=5,
|
Checkbutton(buttons, text=text, indicatoron=False, padx=5, pady=5,
|
||||||
|
|
@ -188,6 +209,23 @@ def init_sstv(mode, image, vox, fskid):
|
||||||
s.add_fskid_text(fskid)
|
s.add_fskid_text(fskid)
|
||||||
return s
|
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(
|
register(
|
||||||
"pysstv_for_gimp",
|
"pysstv_for_gimp",
|
||||||
"PySSTV for GIMP",
|
"PySSTV for GIMP",
|
||||||
|
|
@ -199,9 +237,14 @@ register(
|
||||||
"*",
|
"*",
|
||||||
[
|
[
|
||||||
(PF_RADIO, "mode", "SSTV mode", "MartinM1",
|
(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_BOOL, "vox", "Include VOX tones", True),
|
||||||
(PF_STRING, "fskid", "FSK ID", ""),
|
(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
|
transmit_current_image
|
||||||
|
|
|
||||||
|
|
@ -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/
|
Tested on PyAudio 0.2.7 http://people.csail.mit.edu/hubert/pyaudio/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import division
|
|
||||||
from pysstv.sstv import SSTV
|
from pysstv.sstv import SSTV
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from itertools import islice
|
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.
|
receiver such as slowrx or QSSTV.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import print_function
|
|
||||||
from pyinotify import WatchManager, Notifier, ProcessEvent, IN_CREATE
|
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.color import MartinM1, MartinM2, ScottieS1, ScottieS2
|
||||||
from pysstv.grayscale import Robot8BW, Robot24BW
|
from pysstv.grayscale import Robot8BW, Robot24BW
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
@ -44,13 +44,13 @@ class EventHandler(ProcessEvent):
|
||||||
|
|
||||||
def get_module_for_filename(filename):
|
def get_module_for_filename(filename):
|
||||||
basename, _ = path.splitext(path.basename(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:
|
if mode in basename:
|
||||||
return module
|
return module
|
||||||
|
|
||||||
def get_module_for_image(image):
|
def get_module_for_image(image):
|
||||||
size = image.size
|
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))):
|
if all(i >= m for i, m in zip(size, (mode.WIDTH, mode.HEIGHT))):
|
||||||
return mode
|
return mode
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,24 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
from __future__ import division
|
from __future__ import division
|
||||||
from sstv import SSTV, byte_to_freq
|
from pysstv.sstv import SSTV, byte_to_freq
|
||||||
|
|
||||||
|
|
||||||
class GrayscaleSSTV(SSTV):
|
class GrayscaleSSTV(SSTV):
|
||||||
|
def on_init(self):
|
||||||
|
self.pixels = self.image.convert('LA').load()
|
||||||
|
|
||||||
def gen_image_tuples(self):
|
def gen_image_tuples(self):
|
||||||
for line in xrange(self.HEIGHT):
|
for line in range(self.HEIGHT):
|
||||||
for item in self.horizontal_sync():
|
yield from self.horizontal_sync()
|
||||||
yield item
|
yield from self.encode_line(line)
|
||||||
for item in self.encode_line(line):
|
|
||||||
yield item
|
|
||||||
|
|
||||||
def encode_line(self, line):
|
def encode_line(self, line):
|
||||||
msec_pixel = self.SCAN / self.WIDTH
|
msec_pixel = self.SCAN / self.WIDTH
|
||||||
image = self.image.load()
|
image = self.pixels
|
||||||
pixlen = len(image[0, line])
|
for col in range(self.WIDTH):
|
||||||
for col in xrange(self.WIDTH):
|
|
||||||
pixel = image[col, line]
|
pixel = image[col, line]
|
||||||
freq_pixel = byte_to_freq(sum(pixel) / pixlen)
|
freq_pixel = byte_to_freq(pixel[0])
|
||||||
yield freq_pixel, msec_pixel
|
yield freq_pixel, msec_pixel
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -35,7 +34,7 @@ class Robot24BW(GrayscaleSSTV):
|
||||||
VIS_CODE = 0x0A
|
VIS_CODE = 0x0A
|
||||||
WIDTH = 320
|
WIDTH = 320
|
||||||
HEIGHT = 240
|
HEIGHT = 240
|
||||||
SYNC = 12
|
SYNC = 7
|
||||||
SCAN = 93
|
SCAN = 93
|
||||||
|
|
||||||
MODES = (Robot8BW, Robot24BW)
|
MODES = (Robot8BW, Robot24BW)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ from __future__ import division, with_statement
|
||||||
from math import sin, pi
|
from math import sin, pi
|
||||||
from random import random
|
from random import random
|
||||||
from contextlib import closing
|
from contextlib import closing
|
||||||
from itertools import imap, izip, cycle, chain
|
from itertools import cycle, chain
|
||||||
from array import array
|
from array import array
|
||||||
import wave
|
import wave
|
||||||
|
|
||||||
|
|
@ -33,6 +33,10 @@ class SSTV(object):
|
||||||
self.vox_enabled = False
|
self.vox_enabled = False
|
||||||
self.fskid_payload = ''
|
self.fskid_payload = ''
|
||||||
self.nchannels = 1
|
self.nchannels = 1
|
||||||
|
self.on_init()
|
||||||
|
|
||||||
|
def on_init(self):
|
||||||
|
pass
|
||||||
|
|
||||||
BITS_TO_STRUCT = {8: 'b', 16: 'h'}
|
BITS_TO_STRUCT = {8: 'b', 16: 'h'}
|
||||||
|
|
||||||
|
|
@ -42,12 +46,12 @@ class SSTV(object):
|
||||||
data = array(fmt, self.gen_samples())
|
data = array(fmt, self.gen_samples())
|
||||||
if self.nchannels != 1:
|
if self.nchannels != 1:
|
||||||
data = array(fmt, chain.from_iterable(
|
data = array(fmt, chain.from_iterable(
|
||||||
izip(*([data] * self.nchannels))))
|
zip(*([data] * self.nchannels))))
|
||||||
with closing(wave.open(filename, 'wb')) as wav:
|
with closing(wave.open(filename, 'wb')) as wav:
|
||||||
wav.setnchannels(self.nchannels)
|
wav.setnchannels(self.nchannels)
|
||||||
wav.setsampwidth(self.bits // 8)
|
wav.setsampwidth(self.bits // 8)
|
||||||
wav.setframerate(self.samples_per_sec)
|
wav.setframerate(self.samples_per_sec)
|
||||||
wav.writeframes(data.tostring())
|
wav.writeframes(data)
|
||||||
|
|
||||||
def gen_samples(self):
|
def gen_samples(self):
|
||||||
"""generates discrete samples from gen_values()
|
"""generates discrete samples from gen_values()
|
||||||
|
|
@ -60,8 +64,8 @@ class SSTV(object):
|
||||||
amp = max_value // 2
|
amp = max_value // 2
|
||||||
lowest = -amp
|
lowest = -amp
|
||||||
highest = amp - 1
|
highest = amp - 1
|
||||||
alias_cycle = cycle((alias * (random() - 0.5) for _ in xrange(1024)))
|
alias_cycle = cycle((alias * (random() - 0.5) for _ in range(1024)))
|
||||||
for value, alias_item in izip(self.gen_values(), alias_cycle):
|
for value, alias_item in zip(self.gen_values(), alias_cycle):
|
||||||
sample = int(value * amp + alias_item)
|
sample = int(value * amp + alias_item)
|
||||||
yield (lowest if sample <= lowest else
|
yield (lowest if sample <= lowest else
|
||||||
sample if sample <= highest else highest)
|
sample if sample <= highest else highest)
|
||||||
|
|
@ -81,9 +85,9 @@ class SSTV(object):
|
||||||
samples += spms * msec
|
samples += spms * msec
|
||||||
tx = int(samples)
|
tx = int(samples)
|
||||||
freq_factor = freq * factor
|
freq_factor = freq * factor
|
||||||
for sample in xrange(tx):
|
for sample in range(tx):
|
||||||
yield sin(sample * freq_factor + offset)
|
yield sin(sample * freq_factor + offset)
|
||||||
offset += sample * freq_factor
|
offset += (sample + 1) * freq_factor
|
||||||
samples -= tx
|
samples -= tx
|
||||||
|
|
||||||
def gen_freq_bits(self):
|
def gen_freq_bits(self):
|
||||||
|
|
@ -100,7 +104,7 @@ class SSTV(object):
|
||||||
yield FREQ_SYNC, MSEC_VIS_BIT # start bit
|
yield FREQ_SYNC, MSEC_VIS_BIT # start bit
|
||||||
vis = self.VIS_CODE
|
vis = self.VIS_CODE
|
||||||
num_ones = 0
|
num_ones = 0
|
||||||
for _ in xrange(7):
|
for _ in range(7):
|
||||||
bit = vis & 1
|
bit = vis & 1
|
||||||
vis >>= 1
|
vis >>= 1
|
||||||
num_ones += bit
|
num_ones += bit
|
||||||
|
|
@ -109,10 +113,9 @@ class SSTV(object):
|
||||||
parity_freq = FREQ_VIS_BIT1 if num_ones % 2 == 1 else FREQ_VIS_BIT0
|
parity_freq = FREQ_VIS_BIT1 if num_ones % 2 == 1 else FREQ_VIS_BIT0
|
||||||
yield parity_freq, MSEC_VIS_BIT
|
yield parity_freq, MSEC_VIS_BIT
|
||||||
yield FREQ_SYNC, MSEC_VIS_BIT # stop bit
|
yield FREQ_SYNC, MSEC_VIS_BIT # stop bit
|
||||||
for freq_tuple in self.gen_image_tuples():
|
yield from self.gen_image_tuples()
|
||||||
yield freq_tuple
|
for fskid_byte in map(ord, self.fskid_payload):
|
||||||
for fskid_byte in imap(ord, self.fskid_payload):
|
for _ in range(6):
|
||||||
for _ in xrange(6):
|
|
||||||
bit = fskid_byte & 1
|
bit = fskid_byte & 1
|
||||||
fskid_byte >>= 1
|
fskid_byte >>= 1
|
||||||
bit_freq = FREQ_FSKID_BIT1 if bit == 1 else FREQ_FSKID_BIT0
|
bit_freq = FREQ_FSKID_BIT1 if bit == 1 else FREQ_FSKID_BIT0
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,9 @@
|
||||||
from os import path
|
from os import path
|
||||||
|
import pickle
|
||||||
|
|
||||||
def get_asset_filename(filename):
|
def get_asset_filename(filename):
|
||||||
return path.join(path.dirname(__file__), 'assets', 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)
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
import unittest
|
import unittest
|
||||||
from itertools import islice
|
from itertools import islice
|
||||||
import pickle
|
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from pysstv import color
|
from pysstv import color
|
||||||
from common import get_asset_filename
|
from pysstv.tests.common import get_asset_filename, load_pickled_asset
|
||||||
|
|
||||||
|
|
||||||
class TestMartinM1(unittest.TestCase):
|
class TestMartinM1(unittest.TestCase):
|
||||||
|
|
@ -17,12 +16,12 @@ class TestMartinM1(unittest.TestCase):
|
||||||
self.lena = color.MartinM1(lena, 48000, 16)
|
self.lena = color.MartinM1(lena, 48000, 16)
|
||||||
|
|
||||||
def test_gen_freq_bits(self):
|
def test_gen_freq_bits(self):
|
||||||
expected = pickle.load(open(get_asset_filename("MartinM1_freq_bits.p")))
|
expected = load_pickled_asset("MartinM1_freq_bits")
|
||||||
actual = list(islice(self.s.gen_freq_bits(), 0, 1000))
|
actual = list(islice(self.s.gen_freq_bits(), 0, 1000))
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
def test_gen_freq_bits_lena(self):
|
def test_gen_freq_bits_lena(self):
|
||||||
expected = pickle.load(open(get_asset_filename("MartinM1_freq_bits_lena.p")))
|
expected = load_pickled_asset("MartinM1_freq_bits_lena")
|
||||||
actual = list(islice(self.lena.gen_freq_bits(), 0, 1000))
|
actual = list(islice(self.lena.gen_freq_bits(), 0, 1000))
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
|
|
@ -40,7 +39,6 @@ class TestMartinM1(unittest.TestCase):
|
||||||
self.maxDiff = None
|
self.maxDiff = None
|
||||||
line_numbers = [1, 10, 100]
|
line_numbers = [1, 10, 100]
|
||||||
for line in line_numbers:
|
for line in line_numbers:
|
||||||
file = open(get_asset_filename("MartinM1_encode_line_lena%d.p" % line))
|
expected = load_pickled_asset("MartinM1_encode_line_lena{0}".format(line))
|
||||||
expected = pickle.load(file)
|
|
||||||
actual = list(self.lena.encode_line(line))
|
actual = list(self.lena.encode_line(line))
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
import unittest
|
import unittest
|
||||||
from itertools import islice, izip
|
from io import BytesIO
|
||||||
import pickle
|
from itertools import islice
|
||||||
import mock
|
import mock
|
||||||
from mock import MagicMock
|
from mock import MagicMock
|
||||||
from StringIO import StringIO
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
from pysstv import sstv
|
from pysstv import sstv
|
||||||
from pysstv.sstv import SSTV
|
from pysstv.sstv import SSTV
|
||||||
from common import get_asset_filename
|
from pysstv.tests.common import load_pickled_asset
|
||||||
|
|
||||||
|
|
||||||
class TestSSTV(unittest.TestCase):
|
class TestSSTV(unittest.TestCase):
|
||||||
|
|
@ -21,7 +20,7 @@ class TestSSTV(unittest.TestCase):
|
||||||
def test_horizontal_sync(self):
|
def test_horizontal_sync(self):
|
||||||
horizontal_sync = self.s.horizontal_sync()
|
horizontal_sync = self.s.horizontal_sync()
|
||||||
expected = (1200, self.s.SYNC)
|
expected = (1200, self.s.SYNC)
|
||||||
actual = horizontal_sync.next()
|
actual = next(iter(horizontal_sync))
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
def test_gen_freq_bits(self):
|
def test_gen_freq_bits(self):
|
||||||
|
|
@ -45,8 +44,8 @@ class TestSSTV(unittest.TestCase):
|
||||||
# FIXME: Instead of using a test fixture, 'expected' should be synthesized?
|
# FIXME: Instead of using a test fixture, 'expected' should be synthesized?
|
||||||
def test_gen_values(self):
|
def test_gen_values(self):
|
||||||
gen_values = self.s.gen_values()
|
gen_values = self.s.gen_values()
|
||||||
expected = pickle.load(open(get_asset_filename("SSTV_gen_values.p")))
|
expected = load_pickled_asset("SSTV_gen_values")
|
||||||
for e, g in izip(expected, gen_values):
|
for e, g in zip(expected, gen_values):
|
||||||
self.assertAlmostEqual(e, g, delta=0.000000001)
|
self.assertAlmostEqual(e, g, delta=0.000000001)
|
||||||
|
|
||||||
def test_gen_samples(self):
|
def test_gen_samples(self):
|
||||||
|
|
@ -57,20 +56,20 @@ class TestSSTV(unittest.TestCase):
|
||||||
# and having different results.
|
# and having different results.
|
||||||
# https://en.wikipedia.org/wiki/Quantization_%28signal_processing%29
|
# https://en.wikipedia.org/wiki/Quantization_%28signal_processing%29
|
||||||
sstv.random = MagicMock(return_value=0.4) # xkcd:221
|
sstv.random = MagicMock(return_value=0.4) # xkcd:221
|
||||||
expected = pickle.load(open(get_asset_filename("SSTV_gen_samples.p")))
|
expected = load_pickled_asset("SSTV_gen_samples")
|
||||||
actual = list(islice(gen_values, 0, 1000))
|
actual = list(islice(gen_values, 0, 1000))
|
||||||
for e, a in izip(expected, actual):
|
for e, a in zip(expected, actual):
|
||||||
self.assertAlmostEqual(e, a, delta=1)
|
self.assertAlmostEqual(e, a, delta=1)
|
||||||
|
|
||||||
def test_write_wav(self):
|
def test_write_wav(self):
|
||||||
self.maxDiff = None
|
self.maxDiff = None
|
||||||
sio = StringIO()
|
bio = BytesIO()
|
||||||
sio.close = MagicMock() # ignore close() so we can .getvalue()
|
bio.close = MagicMock() # ignore close() so we can .getvalue()
|
||||||
mock_open = MagicMock(return_value=sio)
|
mock_open = MagicMock(return_value=bio)
|
||||||
with mock.patch('__builtin__.open', mock_open):
|
with mock.patch('builtins.open', mock_open):
|
||||||
self.s.write_wav('unittest.wav')
|
self.s.write_wav('unittest.wav')
|
||||||
expected = '8aa1d52b222b411e032ce2bce77d203a'
|
expected = 'dd7eed880ab3360fb79ce09c469deee2'
|
||||||
data = sio.getvalue()
|
data = bio.getvalue()
|
||||||
actual = hashlib.md5(data).hexdigest()
|
actual = hashlib.md5(data).hexdigest()
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
PIL==1.1.7
|
Pillow==10.3.0
|
||||||
mock==1.0.1
|
mock==1.0.1
|
||||||
nose==1.3.0
|
nose==1.3.0
|
||||||
|
|
|
||||||
12
setup.py
12
setup.py
|
|
@ -4,14 +4,19 @@ from setuptools import setup
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='PySSTV',
|
name='PySSTV',
|
||||||
version='0.2.2',
|
version='0.5.7',
|
||||||
description='Python classes for generating Slow-scan Television transmissions',
|
description='Python classes for generating Slow-scan Television transmissions',
|
||||||
author=u'András Veres-Szentkirályi',
|
author=u'András Veres-Szentkirályi',
|
||||||
author_email='vsza@vsza.hu',
|
author_email='vsza@vsza.hu',
|
||||||
url='https://github.com/dnet/pySSTV',
|
url='https://github.com/dnet/pySSTV',
|
||||||
packages=['pysstv', 'pysstv.tests', 'pysstv.examples'],
|
packages=['pysstv', 'pysstv.tests', 'pysstv.examples'],
|
||||||
keywords='HAM SSTV slow-scan television Scottie Martin Robot',
|
entry_points={
|
||||||
install_requires = ['PIL',],
|
'console_scripts': [
|
||||||
|
'pysstv = pysstv.__main__:main',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
keywords='HAM SSTV slow-scan television Scottie Martin Robot Pasokon',
|
||||||
|
install_requires = ['Pillow'],
|
||||||
license='MIT',
|
license='MIT',
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Development Status :: 4 - Beta',
|
'Development Status :: 4 - Beta',
|
||||||
|
|
@ -21,4 +26,5 @@ setup(
|
||||||
'Operating System :: OS Independent',
|
'Operating System :: OS Independent',
|
||||||
],
|
],
|
||||||
long_description=open('README.md').read(),
|
long_description=open('README.md').read(),
|
||||||
|
long_description_content_type="text/markdown",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue