Compare commits

...

112 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
András Veres-Szentkirályi 0c38059001 bumped version to v0.3.1 2017-10-24 14:46:20 +02:00
András Veres-Szentkirályi de0dc22cb8 setup.py: added Pasokon to keywords 2017-10-24 14:46:16 +02:00
Blaine Murphy da0c102090 Robot 36 color correction (#12)
The color sequence for Robot 36 is listed as YCrCb in the SSTV Handbook: http://www.sstv-handbook.com/download/sstv_04.pdf (Table 4.3) and the Dayton Paper: https://web.archive.org/web/20120104184535/http://www.barberdsp.com/files/Dayton%20Paper.pdf

* Corrected inter-channel frequency order for Robot 36 mode.
* Switched Robot 36 channel order to CrCb.
2017-10-24 14:42:17 +02:00
András Veres-Szentkirályi 9ff3f73d17 requirements.txt: bumped Pillow version to 4.3.0 2017-10-24 14:36:22 +02:00
András Veres-Szentkirályi d51c00a04a bumped version to v0.3 2017-01-12 12:58:24 +01:00
András Veres-Szentkirályi a601f8e754 updated README regarding Python 3 support (#10) 2017-01-12 12:58:11 +01:00
András Veres-Szentkirályi 5eb8a468e4 extracted method load_pickled_asset 2017-01-12 12:54:07 +01:00
András Veres-Szentkirályi 81b45e7e4e Python 3 uses builtins.open instead of io.open 2017-01-12 12:48:50 +01:00
András Veres-Szentkirályi 9cbb0c50ff use BytesIO on Python 3 2017-01-12 12:37:58 +01:00
András Veres-Szentkirályi 0d52546d81 test_sstv: select open namespace depending on Python major version 2017-01-12 12:26:01 +01:00
András Veres-Szentkirályi ac0f89bfdf test_sstv: open further files in explicit 'rb' mode 2017-01-12 12:25:34 +01:00
András Veres-Szentkirályi bf2efdabea test_sstv: handle iterables in a more portable way 2017-01-12 12:25:03 +01:00
András Veres-Szentkirályi d574ab1d49 use absolute imports in test_sstv as well 2017-01-12 12:16:33 +01:00
András Veres-Szentkirályi 8f8f431050 StringIO was also moved in Python 3 2017-01-12 11:14:54 +01:00
András Veres-Szentkirályi a43f64fd79 modified tests for Python 2 + 3 compatibility 2017-01-12 11:06:21 +01:00
András Veres-Szentkirályi d177707233 Python 2.6 still doesn't support delta in assertAlmostEqual 2017-01-12 10:58:48 +01:00
András Veres-Szentkirályi 72ffc80aad Travis CI: extended Python version coverage 2017-01-12 10:53:44 +01:00
KM4YRI 5d3b11a144 added Python six compatibility layer (#10)
added dependency to `six` for better Python 2/3 compatibility
2017-01-12 10:49:13 +01:00
András Veres-Szentkirályi 7eac9636a3 bumped version to v0.2.8 2017-01-03 15:08:29 +01:00
KM4YRI 56743c7afb Python 3 compatibility: imap->map, izip->zip, xrange->range (#9) 2017-01-03 15:04:11 +01:00
András Veres-Szentkirályi 810f604a5a codegen: added Pasokon modes to test harness 2016-04-20 19:49:43 +02:00
András Veres-Szentkirályi 23a2e2bd00 codegen: check for non-RGB images 2016-04-20 19:36:24 +02:00
András Veres-Szentkirályi ff15c3a63a codegen: removed hardcoded test image file name 2016-04-20 14:07:35 +02:00
András Veres-Szentkirályi 5d38b3b52d codegen: avoid defining test image in multiple places 2016-04-20 14:04:57 +02:00
András Veres-Szentkirályi c7fecff3e6 codegen: save outputs and C code in case of failed tests 2016-04-19 11:36:08 +02:00
András Veres-Szentkirályi 40768cb57a codegen: added MartinM2 to the test harness 2016-04-19 11:21:31 +02:00
András Veres-Szentkirályi d540abd852 codegen: pass real image width as parameter 2016-04-19 11:20:14 +02:00
András Veres-Szentkirályi dee321d59e codegen: replaced BMP with RGB array and STBI 2016-04-19 11:13:57 +02:00
András Veres-Szentkirályi ec6ff85d06 codegen: avoid masking exceptions if exe doesn't exist yet 2016-04-19 11:01:17 +02:00
András Veres-Szentkirályi 43d1c53514 codegen: added timing stats 2016-02-23 16:36:32 +01:00
András Veres-Szentkirályi ff2fc58aa7 codegen: optimized gen_matches invocation 2016-02-23 16:21:39 +01:00
András Veres-Szentkirályi c185ea571b codegen: use NotImplementedError 2016-02-23 16:21:30 +01:00
András Veres-Szentkirályi f295a07c83 codegen: removed unused variable 2016-02-23 16:17:53 +01:00
András Veres-Szentkirályi 3c3bb34b1b codegen: added simple test suite 2016-02-22 17:35:25 +01:00
András Veres-Szentkirályi d7a05fc1d5 codegen: return 0 in stub main() 2016-02-22 17:35:09 +01:00
András Veres-Szentkirályi 6418a9febc codegen: provide generator instead of printing 2016-02-22 17:34:44 +01:00
András Veres-Szentkirályi 2f6c7ca871 codegen: use configurable SSTV subclass 2016-02-22 17:34:21 +01:00
András Veres-Szentkirályi f02f43e4b5 added experimental C code generator 2016-02-22 15:02:00 +01:00
András Veres-Szentkirályi 7d3c7c4683 bumped version to v0.2.7 2016-02-22 14:52:18 +01:00
András Veres-Szentkirályi 8c10e0ce3b updated Pillow dependency in setup.py as well 2016-02-22 14:52:12 +01:00
András Veres-Szentkirályi 7335f72c91 bumped version to v0.2.6 2016-02-22 14:47:04 +01:00
András Veres-Szentkirályi 45fbdbd7c8 README: added build status 2015-10-20 10:31:33 +02:00
András Veres-Szentkirályi 2c23ae9632 use container-based Travis infrastructure 2015-10-20 10:28:50 +02:00
András Veres-Szentkirályi 2b0f482653 fixed supported Python versions 2015-10-20 10:17:37 +02:00
András Veres-Szentkirályi 927f805771 replaced PIL with Pillow 2015-10-20 10:09:41 +02:00
András Veres-Szentkirályi 1916cfd031 added Travis CI 2015-10-20 10:06:22 +02:00
András Veres-Szentkirályi 09df062547 use absolute imports for Python 3 compatibility 2014-06-07 11:48:04 +02:00
András Veres-Szentkirályi 9ef2a5a8ca added serial port PTT support to GIMP plugin 2014-05-14 23:26:54 +02:00
András Veres-Szentkirályi d82357b45d bumped version to v0.2.5 2014-04-23 10:25:03 +02:00
András Veres-Szentkirályi b837723d93 Merge pull request #6 from DominikAuras/pasokon_modes
Pasokon modes
2014-04-23 09:31:52 +02:00
Dominik Auras 6f5ea1a5ef Update README 2014-04-22 23:17:34 +02:00
Dominik Auras e0a4563a56 Add "high"-resolution modes Pasokon P3, P5 and P7
Tested against MMSSTV and HamRadio software
2014-04-22 23:11:40 +02:00
András Veres-Szentkirályi 2dd5eaa273 bumped version to v0.2.4 2014-01-14 09:42:51 +01:00
András Veres-Szentkirályi 2e8148b029 Merge pull request #5 from CodeBlock/patch-1
RED isn't a member of ColorSSTV since 105fa7b
2014-01-14 00:41:11 -08:00
Ricky Elrod e8f07e5bcb RED isn't a member of ColorSSTV (anymore?) 2014-01-13 21:12:24 -05:00
András Veres-Szentkirályi 63b97f07cf bumped version to v0.2.3 2013-11-23 12:39:11 +01:00
András Veres-Szentkirályi 7e9436adeb grayscale: adjusted Robot24BW sync timing 2013-11-22 18:40:25 +01:00
András Veres-Szentkirályi b74feb49ab GrayscaleSSTV: use PIL for grayscale conversion 2013-11-22 18:36:58 +01:00
András Veres-Szentkirályi bd3b84cfb6 ColorSSTV: load raster data only once 2013-11-22 18:33:39 +01:00
András Veres-Szentkirályi 3a25caeab9 optimized Robot36 to use PIL YUV converter 2013-11-22 18:19:56 +01:00
András Veres-Szentkirályi 9bf312ca85 added SSTV.on_init() hook 2013-11-22 18:19:00 +01:00
András Veres-Szentkirályi 4c020d61b9 added Robot 36 encoder 2013-11-22 17:05:13 +01:00
András Veres-Szentkirályi 4d56f34a72 gen_values: fixed FM sample repetition bug
In the previous version, the FM modulator issued the last sample of the
previous frequency-time tuple twice, once as the last sample of the
previous tuple, once as the first sample (sample = 0) of the next one.
2013-11-22 17:00:53 +01:00
19 changed files with 639 additions and 100 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

1
.gitignore vendored Normal file
View file

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

9
.travis.yml Normal file
View 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

View file

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

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

View file

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

View file

@ -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
View 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
View 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;
}

View file

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

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,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)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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",
) )