Compare commits

...

49 commits

Author SHA1 Message Date
András Veres-Szentkirályi a4c840177c bumped version to v0.5.7 2024-07-24 17:16:50 +02:00
András Veres-Szentkirályi f25ecac2c5 README.md: updated command line usage 2024-07-24 17:16:19 +02:00
dependabot[bot] 02795a403b
Bump pillow from 10.0.1 to 10.3.0 (#36)
Bumps [pillow](https://github.com/python-pillow/Pillow) from 10.0.1 to 10.3.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/10.0.1...10.3.0)

---
updated-dependencies:
- dependency-name: pillow
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-24 17:10:47 +02:00
Stuart Longland b43208287f
color: Implement Scottie DX, Wraase SC-2 120 and Wraase SC-2 180. (#37)
* color: Implement Wraase SC-2 120 and 180.

* color: Implement Scottie DX

* color: Re-factor Wraase modes

`ColorSSTV` parent class actually implements the scan-line encoding we
need, however the challenge is that it seems the sync pulse requirements
differ for SC2-120 and SC2-180 just slightly.

I haven't figured out why, partially because there seems to be little in
the way of clear (and correct!) docs as to how SC2-120 is supposed to work.

* color: Fix Scottie DX timing

Using actual reference timing values from N7CXI Dayton paper.
2024-07-24 17:09:27 +02:00
András Veres-Szentkirályi bc74dc98d6 bumped version to v0.5.6 2023-11-06 08:51:14 +01:00
Rehtt b22e81e65f
Add to scale the picture proportionally to the mode size (fill the vacant part with black pixels) (#26) 2023-11-06 08:49:37 +01:00
András Veres-Szentkirályi c454aa3a4b added pyproject.toml 2023-11-06 08:44:59 +01:00
András Veres-Szentkirályi 0855fc6a0c bumped version to v0.5.5 2023-11-06 08:30:05 +01:00
András Veres-Szentkirályi 75b0cd46e3 README.md: removed Travis CI build status 2023-11-06 08:29:42 +01:00
András Veres-Szentkirályi cb00cf20ad codegen.py: use open() instead of file() 2023-11-06 08:28:13 +01:00
András Veres-Szentkirályi 5d3d6a2584 ran 2to3 on examples 2023-11-06 08:26:56 +01:00
András Veres-Szentkirályi 456040aa6b updated Debian package name 2023-11-06 08:22:42 +01:00
András Veres-Szentkirályi ab0560b71e
Create python-package.yml 2023-11-06 08:20:16 +01:00
dependabot[bot] f4b4ca7b19 Bump pillow from 9.0.0 to 10.0.1
Bumps [pillow](https://github.com/python-pillow/Pillow) from 9.0.0 to 10.0.1.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/9.0.0...10.0.1)

---
updated-dependencies:
- dependency-name: pillow
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-06 08:18:42 +01:00
András Veres-Szentkirályi 65241a6b71 bumped version to v0.5.4 2022-01-13 09:12:45 +01:00
dependabot[bot] 011d9cbcae Bump pillow from 8.3.2 to 9.0.0
Bumps [pillow](https://github.com/python-pillow/Pillow) from 8.3.2 to 9.0.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/8.3.2...9.0.0)

---
updated-dependencies:
- dependency-name: pillow
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-13 09:11:31 +01:00
András Veres-Szentkirályi c978ca0515 bumped version to v0.5.3 2021-10-04 09:10:05 +02:00
dependabot[bot] ff73d8c309 Bump pillow from 8.2.0 to 8.3.2
Bumps [pillow](https://github.com/python-pillow/Pillow) from 8.2.0 to 8.3.2.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/8.2.0...8.3.2)

---
updated-dependencies:
- dependency-name: pillow
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-04 09:09:12 +02:00
András Veres-Szentkirályi 3b8607f130 bumped version to v0.5.2 2021-06-08 21:13:03 +02:00
dependabot[bot] bb193334ff Bump pillow from 8.1.1 to 8.2.0
Bumps [pillow](https://github.com/python-pillow/Pillow) from 8.1.1 to 8.2.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/8.1.1...8.2.0)

---
updated-dependencies:
- dependency-name: pillow
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-08 21:12:12 +02:00
András Veres-Szentkirályi af5c2dff21 bumped version to v0.5.1 2021-05-19 09:02:05 +02:00
András Veres-Szentkirályi 8d93d64bc5 README: added PD290 to cmdline usage 2021-05-19 09:01:40 +02:00
András Veres-Szentkirályi 89254d73ec removed trailing whitespace 2021-05-19 08:59:31 +02:00
clemibunge 39abaf1a17
Add mode PD290 2021-05-15 21:21:35 +02:00
András Veres-Szentkirályi 2bf26ed432 use enum for colors 2021-04-19 16:28:05 +02:00
András Veres-Szentkirályi 2f7a11abc1 use yield from instead of explicit version 2021-04-19 16:26:34 +02:00
András Veres-Szentkirályi 30294ee096 removed six 2021-04-19 16:23:40 +02:00
András Veres-Szentkirályi 5a596df09c removed support for older Python versions 2021-04-19 16:10:25 +02:00
András Veres-Szentkirályi 10476d013a bumped version to v0.5 2021-04-19 16:07:40 +02:00
dependabot[bot] 3c2e4d0a5c Bump pillow from 4.3.0 to 8.1.1
Bumps [pillow](https://github.com/python-pillow/Pillow) from 4.3.0 to 8.1.1.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/4.3.0...8.1.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-19 16:05:27 +02:00
Andrew Simmons 323812bc59
Add entry_point (#23) 2021-04-19 16:04:53 +02:00
András Veres-Szentkirályi 9f4c435450 bumped version to v0.4.4 2021-03-02 11:31:47 +01:00
András Veres-Szentkirályi 8f9151f0c1 Travis CI: updated Python versions 2021-03-02 11:22:39 +01:00
András Veres-Szentkirályi 0c09c84360 fixed Python 2 compatibility
Python 2 returns the string representation with just calling
writeframes(data) instead of writeframes(data.tostring())

/tmp/post.wav
0000 0000: 52 49 46 46 C4 AA 00 00  57 41 56 45 66 6D 74 20  RIFF.... WAVEfmt
0000 0010: 10 00 00 00 01 00 01 00  80 BB 00 00 00 77 01 00  ........ .....w..
0000 0020: 02 00 10 00 64 61 74 61  A0 AA 00 00 61 72 72 61  ....data ....arra
0000 0030: 79 28 27 68 27 2C 20 5B  30 2C 20 38 30 36 35 2C  y('h', [ 0, 8065,
0000 0040: 20 31 35 36 33 35 2C 20  32 32 32 34 32 2C 20 32   15635,  22242, 2

This change adds an escape hatch that could work for both major Python
versions, except Python 3.0 and 3.1 but those should be rare and
unsupported anyway.
2021-03-02 11:18:58 +01:00
Edmond Belliveau 71ddabcb1d Removed tostring() in write_wav. Python 3.9+ has removed the tostring() methods from array.array and other primitives. 2021-03-01 18:07:38 -04:00
András Veres-Szentkirályi 9143aaec97 bumped version to v0.4.3 2019-07-12 22:01:40 +02:00
András Veres-Szentkirályi 7b06a5bb48 set correct content type for Markdown README 2019-07-12 21:59:48 +02:00
András Veres-Szentkirályi c784e9df15 bumped version to v0.4.2 2019-07-12 21:55:20 +02:00
András Veres-Szentkirályi 1b6884ee63 fixed RGB modes silently depending on RGB images
Thanks to @Chris_J_Baird on Twitter reporting the issue:

> ColorSSTV.encode_line assuming its gets a tuple from __getpixel__
> had to make the Image.open() do a convert('RGB')

https://twitter.com/i/web/status/1149331458189737988
2019-07-12 21:53:45 +02:00
András Veres-Szentkirályi e220d1964c bumped version to v0.4.1 2019-01-22 14:43:26 +01:00
András Veres-Szentkirályi 4b95f8b5c3 added image resizing to the CLI
fixes gh-18
2019-01-22 14:41:44 +01:00
András Veres-Szentkirályi 99d8709053 gimp-plugin: documented pyaudio dependency 2018-04-30 16:25:09 +02:00
András Veres-Szentkirályi 509d162a0d gimp-plugin: removed dead code 2018-04-30 16:24:46 +02:00
András Veres-Szentkirályi 03092f5549 bumped version to v0.4 2018-02-25 13:20:40 +01:00
András Veres-Szentkirályi b731112007 added PD modes (#16) 2018-02-25 13:20:37 +01:00
András Veres-Szentkirályi 35de6c718e README: added missing options 2018-02-25 13:20:34 +01:00
András Veres-Szentkirályi 6d3d2143b7 build_module_map: use OrderedDict if available
this results in a much nicer "usage" screen by argparse
2018-02-24 21:28:46 +01:00
András Veres-Szentkirályi c6504a7551 bumped version to v0.3.2 2018-02-22 09:21:15 +01:00
Matt Molyneaux 75d9722491 Add six to setup.py (#17)
It's required by `pysstv.sstv`
2018-02-22 09:19:31 +01:00
15 changed files with 287 additions and 93 deletions

40
.github/workflows/python-package.yml vendored Normal file
View 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

View file

@ -1,10 +1,9 @@
language: python language: python
python: python:
- "2.7" - "3.6"
- "3.2" - "3.7"
- "3.3" - "3.8"
- "3.4" - "3.9"
- "3.5"
sudo: false sudo: false
install: "pip install -r requirements.txt" install: "pip install -r requirements.txt"
script: nosetests script: nosetests

View file

@ -1,8 +1,6 @@
SSTV generator in pure Python SSTV generator in pure Python
============================= =============================
[![Build Status](https://travis-ci.org/dnet/pySSTV.svg?branch=master)](https://travis-ci.org/dnet/pySSTV)
PySSTV generates SSTV modulated WAV files from any image that PIL can open 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 (PNG, JPEG, GIF, and many others). These WAV files then can be played by any
audio player connected to a shortwave radio for example. audio player connected to a shortwave radio for example.
@ -16,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,PasokonP3,PasokonP5,PasokonP7}] [--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.
@ -26,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,PasokonP3,PasokonP5,PasokonP7} --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
---------------- ----------------
@ -71,6 +82,5 @@ Useful links
Dependencies Dependencies
------------ ------------
- Python 2.7 (tested on 2.7.9; 2.6 might work, but test suite lacks support) - Python 3.5 or later
or 3.x (tested on 3.2, 3.3, 3.4 and 3.5) - Python Imaging Library (Debian/Ubuntu package: `python3-pil`)
- Python Imaging Library (Debian/Ubuntu package: `python-imaging`)

3
pyproject.toml Normal file
View file

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

View file

@ -1,6 +1,6 @@
#!/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
@ -30,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)
@ -47,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:

View file

@ -1,39 +1,40 @@
#!/usr/bin/env python #!/usr/bin/env python
from __future__ import division from __future__ import division
from six.moves import range
from pysstv.sstv import byte_to_freq, FREQ_BLACK, FREQ_WHITE, FREQ_VIS_START from pysstv.sstv import byte_to_freq, FREQ_BLACK, FREQ_WHITE, FREQ_VIS_START
from pysstv.grayscale import GrayscaleSSTV from pysstv.grayscale import GrayscaleSSTV
from itertools import chain 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): def on_init(self):
self.pixels = self.image.load() 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.pixels 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 range(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
@ -41,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
@ -64,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 == 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
@ -77,6 +77,12 @@ class ScottieS2(ScottieS1):
WIDTH = 160 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): class Robot36(ColorSSTV):
VIS_CODE = 0x08 VIS_CODE = 0x08
WIDTH = 320 WIDTH = 320
@ -118,7 +124,7 @@ class PasokonP3(ColorSSTV):
Horizontal Sync - 25 time units of 1200 Hz. Horizontal Sync - 25 time units of 1200 Hz.
""" """
TIMEUNIT = 1000/4800. # ms TIMEUNIT = 1000/4800. # ms
COLOR_SEQ = (RED, GREEN, BLUE) COLOR_SEQ = (Color.red, Color.green, Color.blue)
VIS_CODE = 0x71 VIS_CODE = 0x71
WIDTH = 640 WIDTH = 640
HEIGHT = 480+16 HEIGHT = 480+16
@ -126,11 +132,11 @@ class PasokonP3(ColorSSTV):
SCAN = WIDTH * TIMEUNIT SCAN = WIDTH * TIMEUNIT
INTER_CH_GAP = 5 * TIMEUNIT INTER_CH_GAP = 5 * TIMEUNIT
def before_channel(self, index): def before_channel(self, color):
if index == self.COLOR_SEQ[0]: if color is Color.red:
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
@ -149,4 +155,101 @@ class PasokonP7(PasokonP3):
INTER_CH_GAP = 5 * TIMEUNIT INTER_CH_GAP = 5 * TIMEUNIT
MODES = (MartinM1, MartinM2, ScottieS1, ScottieS2, Robot36, PasokonP3, PasokonP5, PasokonP7) 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)

View file

@ -69,10 +69,10 @@ def main(sstv_class=None):
n += 1 n += 1
del lut del lut
m_start, m_len = gen_matches(same_as, history, n) 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 history[i][0]
yield 'for (int row = 0; row < width * {0}; row += width) {{'.format(sstv.HEIGHT) 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 ' ' + history[i][1]
yield '}' yield '}'
yield '}}\n\n#define FREQ_COUNT {0}'.format(n) yield '}}\n\n#define FREQ_COUNT {0}'.format(n)
@ -83,7 +83,7 @@ def gen_matches(same_as, history, n):
cur_start = None cur_start = None
cur_len = None cur_len = None
cur_end = None cur_end = None
for i in xrange(n): for i in range(n):
if cur_start is None: if cur_start is None:
tmp = same_as.get(i) tmp = same_as.get(i)
if tmp is not None: if tmp is not None:
@ -114,22 +114,22 @@ def test(img_file):
import struct import struct
exe = './codegen-test-executable' exe = './codegen-test-executable'
if not path.exists('stb_image.h'): 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') urlretrieve('https://raw.githubusercontent.com/nothings/stb/master/stb_image.h', 'stb_image.h')
try: try:
for sstv_class in supported: for sstv_class in supported:
print 'Testing', sstv_class print('Testing', sstv_class)
gcc = Popen(['gcc', '-xc', '-lm', '-o', exe, '-'], stdin=PIPE) gcc = Popen(['gcc', '-xc', '-lm', '-o', exe, '-'], stdin=PIPE)
start = datetime.now() start = datetime.now()
with open(path.join(path.dirname(__file__), 'codeman.c')) as cm: with open(path.join(path.dirname(__file__), 'codeman.c')) as cm:
c_src = cm.read().replace('#include "codegen.c"', '\n'.join(main(sstv_class))) c_src = cm.read().replace('#include "codegen.c"', '\n'.join(main(sstv_class)))
gcc.communicate(c_src) gcc.communicate(c_src)
gen_elapsed = datetime.now() - start gen_elapsed = datetime.now() - start
print ' - gengcc took', gen_elapsed print(' - gengcc took', gen_elapsed)
start = datetime.now() start = datetime.now()
gen = check_output([exe, img_file]) gen = check_output([exe, img_file])
native_elapsed = datetime.now() - start native_elapsed = datetime.now() - start
print ' - native took', native_elapsed print(' - native took', native_elapsed)
img = Image.open(img_file) img = Image.open(img_file)
sstv = sstv_class(img, 44100, 16) sstv = sstv_class(img, 44100, 16)
start = datetime.now() start = datetime.now()
@ -138,20 +138,20 @@ def test(img_file):
assert gen[n * 8:(n + 1) * 8] == struct.pack('ff', freq, msec) assert gen[n * 8:(n + 1) * 8] == struct.pack('ff', freq, msec)
except AssertionError: except AssertionError:
mode_name = sstv_class.__name__ 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) 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()): for n, (freq, msec) in enumerate(sstv.gen_freq_bits()):
f.write(struct.pack('ff', freq, msec)) 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) 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 " "/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 python_elapsed = datetime.now() - start
print ' - python took', python_elapsed print(' - python took', python_elapsed)
print ' - speedup:', python_elapsed.total_seconds() / native_elapsed.total_seconds() print(' - speedup:', python_elapsed.total_seconds() / native_elapsed.total_seconds())
print 'OK' print('OK')
finally: finally:
try: try:
remove(exe) remove(exe)
@ -164,4 +164,4 @@ if __name__ == '__main__':
if len(argv) > 2 and argv[1] == 'test': if len(argv) > 2 and argv[1] == 'test':
test(argv[2]) test(argv[2])
else: else:
print '\n'.join(main()) print('\n'.join(main()))

View file

@ -2,17 +2,17 @@
# -*- 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
from time import sleep from time import sleep
import gimp, os import gimp, os
@ -118,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:
@ -152,9 +152,6 @@ def contrast(value):
else: else:
return 255 - value return 255 - value
def set_ptt_pin(port, pin, state):
getattr(port, 'set' + pin)(state)
def transmit_current_image(image, drawable, mode, vox, fskid, ptt_port, ptt_pin, ptt_state): 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: if ptt_port is not None:
@ -240,7 +237,7 @@ 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, (PF_RADIO, "ptt_port", "PTT port", None,

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

View file

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

View file

@ -1,7 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
from __future__ import division from __future__ import division
from six.moves import range
from pysstv.sstv import SSTV, byte_to_freq from pysstv.sstv import SSTV, byte_to_freq
@ -11,10 +10,8 @@ class GrayscaleSSTV(SSTV):
def gen_image_tuples(self): def gen_image_tuples(self):
for line in range(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

View file

@ -1,9 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
from __future__ import division, with_statement 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 math import sin, pi
from random import random from random import random
from contextlib import closing from contextlib import closing
@ -54,7 +51,7 @@ class SSTV(object):
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()
@ -116,8 +113,7 @@ 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 map(ord, self.fskid_payload):
for _ in range(6): for _ in range(6):
bit = fskid_byte & 1 bit = fskid_byte & 1

View file

@ -1,10 +1,8 @@
import unittest import unittest
from io import BytesIO
from itertools import islice from itertools import islice
from six.moves import zip
import mock import mock
from mock import MagicMock from mock import MagicMock
from six import BytesIO
from six import PY2
import hashlib import hashlib
from pysstv import sstv from pysstv import sstv
@ -68,8 +66,7 @@ class TestSSTV(unittest.TestCase):
bio = BytesIO() bio = BytesIO()
bio.close = MagicMock() # ignore close() so we can .getvalue() bio.close = MagicMock() # ignore close() so we can .getvalue()
mock_open = MagicMock(return_value=bio) mock_open = MagicMock(return_value=bio)
ns = '__builtin__' if PY2 else 'builtins' with mock.patch('builtins.open', mock_open):
with mock.patch('{0}.open'.format(ns), mock_open):
self.s.write_wav('unittest.wav') self.s.write_wav('unittest.wav')
expected = 'dd7eed880ab3360fb79ce09c469deee2' expected = 'dd7eed880ab3360fb79ce09c469deee2'
data = bio.getvalue() data = bio.getvalue()

View file

@ -1,4 +1,3 @@
six==1.10.0 Pillow==10.3.0
Pillow==4.3.0
mock==1.0.1 mock==1.0.1
nose==1.3.0 nose==1.3.0

View file

@ -4,14 +4,19 @@ from setuptools import setup
setup( setup(
name='PySSTV', name='PySSTV',
version='0.3.1', 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'],
entry_points={
'console_scripts': [
'pysstv = pysstv.__main__:main',
],
},
keywords='HAM SSTV slow-scan television Scottie Martin Robot Pasokon', keywords='HAM SSTV slow-scan television Scottie Martin Robot Pasokon',
install_requires = ['Pillow',], 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",
) )