merged upstream master

This commit is contained in:
Brian Holt
2011-09-19 06:51:54 +01:00
17 changed files with 774 additions and 14 deletions
+3 -2
View File
@@ -25,6 +25,7 @@
- Tony Yu
Reading of paletted images; build, bug and doc fixes.
Code to generate scikits-image logo.
- Zachary Pincus
Tracing of low cost paths, FreeImage I/O plugin
@@ -58,7 +59,7 @@
From whom we borrowed the github+web tools / style.
- Kyle Mandli
CSV to ReST code for MATLAB comparison table.
CSV to ReST code for feature comparison table.
- Brian Holt
Histograms of Oriented Gradients
Histograms of Oriented Gradients
+5 -2
View File
@@ -2,10 +2,10 @@ How to make a new release of ``scikits.image``
==============================================
- Update the version number in setup.py and commit
- Update the docs and api, push upstream using the "push_github" script
- Update the docs and api, push upstream using "make gh-pages"
- Add the version number as a tag in git::
git tag v0.1
git tag v0.5
- Push the new meta-data to github::
@@ -19,3 +19,6 @@ How to make a new release of ``scikits.image``
- Increase the version number in the setup.py file to ``0.Xdev``.
- Update the web frontpage.
The webpage is kept in a separate repo: scikits.image-web
+11
View File
@@ -0,0 +1,11 @@
.PHONY: logo
logo: green_orange_snake.png snake_logo.svg
inkscape --export-png=scikits_image_logo.png --export-dpi=100 \
--export-area-drawing --export-background-opacity=1 \
snake_logo.svg
python shrink_logo.py
green_orange_snake.png:
python scikits_image_logo.py --no-plot
Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

+226
View File
@@ -0,0 +1,226 @@
"""
Script to draw scikits.image logo using Scipy logo as stencil. The easiest
starting point is the `plot_colorized_logo`; the "if-main" demonstrates its use.
Original snake image from pixabay [1]_
.. [1] http://pixabay.com/en/snake-green-toxic-close-yellow-3237/
"""
import numpy as np
import matplotlib.pyplot as plt
import scipy.misc
import scikits.image.io as sio
import scikits.image.filter as imfilt
import scipy_logo
# Utility functions
# =================
def get_edges(img):
edge = np.empty(img.shape)
if len(img.shape) == 3:
for i in range(3):
edge[:, :, i] = imfilt.sobel(img[:, :, i])
else:
edge = imfilt.sobel(img)
edge = rescale_intensity(edge)
return edge
def rescale_intensity(img):
i_range = float(img.max() - img.min())
img = (img - img.min()) / i_range * 255
return np.uint8(img)
def colorize(img, color, whiten=False):
"""Return colorized image from gray scale image
Parameters
----------
img : N x M array
grayscale image
color : length-3 sequence of floats
RGB color spec. Float values should be between 0 and 1.
whiten : bool
If True, a color value less than 1 increases the image intensity.
"""
color = np.asarray(color)[np.newaxis, np.newaxis, :]
img = img[:, :, np.newaxis]
if whiten:
# truncate and stretch intensity range to enhance contrast
img = np.clip(img, 80, 255)
img = rescale_intensity(img)
return np.uint8(color * (255 - img) + img)
else:
return np.uint8(img * color)
def prepare_axes(ax):
plt.sca(ax)
ax.xaxis.set_visible(False)
ax.yaxis.set_visible(False)
for spine in ax.spines.itervalues():
spine.set_visible(False)
_rgb_stack = np.ones((1, 1, 3), dtype=bool)
def gray2rgb(arr):
"""Return RGB image from a grayscale image.
Expand h x w image to h x w x 3 image where color channels are simply copies
of the grayscale image.
"""
return arr[:, :, np.newaxis] * _rgb_stack
# Logo generating classes
# =======================
class LogoBase(object):
def __init__(self):
self.logo = scipy_logo.ScipyLogo(radius=self.radius)
self.mask_1 = self.logo.get_mask(self.img.shape, 'upper left')
self.mask_2 = self.logo.get_mask(self.img.shape, 'lower right')
self.edges = get_edges(self.img)
# truncate and stretch intensity range to enhance contrast
self.edges = np.clip(self.edges, 0, 100)
self.edges = rescale_intensity(self.edges)
def _crop_image(self, img):
w = 2 * self.radius
x, y = self.origin
return img[y:y+w, x:x+w]
def get_canvas(self):
return 255 * np.ones(self.img.shape, dtype=np.uint8)
def plot_curve(self, **kwargs):
self.logo.plot_snake_curve(**kwargs)
class SnakeLogo(LogoBase):
def __init__(self):
self.radius = 250
self.origin = (420, 0)
img = sio.imread('data/snake_pixabay.jpg')
img = self._crop_image(img)
img = img.astype(float) * 1.1
img[img > 255] = 255
self.img = img.astype(np.uint8)
LogoBase.__init__(self)
snake_color = SnakeLogo()
snake = SnakeLogo()
# turn RGB image into gray image
snake.img = np.mean(snake.img, axis=2)
snake.edges = np.mean(snake.edges, axis=2)
# Demo plotting functions
# =======================
def plot_colorized_logo(logo, color, edges='light', switch=False, whiten=False):
"""Convenience function to plot artificially colored logo.
Parameters
----------
logo : subclass of LogoBase
color : length-3 sequence of floats
RGB color spec. Float values should be between 0 and 1.
edges : {'light'|'dark'}
Specifies whether Sobel edges are drawn light or dark
switch : bool
If False, the image is drawn on the southeast half of the Scipy curve
and the edge image is drawn on northwest half.
whiten : bool
If True, a color value less than 1 increases the image intensity.
"""
if not hasattr(color[0], '__iter__'):
color = [color] * 2
if not hasattr(whiten, '__iter__'):
whiten = [whiten] * 2
img = gray2rgb(logo.get_canvas())
mask_img = gray2rgb(logo.mask_2)
mask_edge = gray2rgb(logo.mask_1)
if switch:
mask_img, mask_edge = mask_edge, mask_img
if edges == 'dark':
lg_edge = colorize(255 - logo.edges, color[0], whiten=whiten[0])
else:
lg_edge = colorize(logo.edges, color[0], whiten=whiten[0])
lg_img = colorize(logo.img, color[1], whiten=whiten[1])
img[mask_img] = lg_img[mask_img]
img[mask_edge] = lg_edge[mask_edge]
logo.plot_curve(lw=5, color='w')
plt.imshow(img)
def red_light_edges(logo, **kwargs):
plot_colorized_logo(logo, (1, 0, 0), edges='light', **kwargs)
def red_dark_edges(logo, **kwargs):
plot_colorized_logo(logo, (1, 0, 0), edges='dark', **kwargs)
def blue_light_edges(logo, **kwargs):
plot_colorized_logo(logo, (0.35, 0.55, 0.85), edges='light', **kwargs)
def blue_dark_edges(logo, **kwargs):
plot_colorized_logo(logo, (0.35, 0.55, 0.85), edges='dark', **kwargs)
def green_orange_light_edges(logo, **kwargs):
colors = ((0.6, 0.8, 0.3), (1, 0.5, 0.1))
plot_colorized_logo(logo, colors, edges='light', **kwargs)
def green_orange_dark_edges(logo, **kwargs):
colors = ((0.6, 0.8, 0.3), (1, 0.5, 0.1))
plot_colorized_logo(logo, colors, edges='dark', **kwargs)
if __name__ == '__main__':
import sys
plot = False
if len(sys.argv) < 2 or sys.argv[1] != '--no-plot':
plot = True
print "Run with '--no-plot' flag to generate logo silently."
def plot_all():
plotters = (red_light_edges, red_dark_edges,
blue_light_edges, blue_dark_edges,
green_orange_light_edges, green_orange_dark_edges)
f, axes_array = plt.subplots(nrows=2, ncols=len(plotters))
for plot, ax_col in zip(plotters, axes_array.T):
prepare_axes(ax_col[0])
plot(snake)
prepare_axes(ax_col[1])
plot(snake, whiten=True)
plt.tight_layout()
def plot_snake():
f, ax = plt.subplots()
prepare_axes(ax)
green_orange_dark_edges(snake, whiten=(False, True))
plt.savefig('green_orange_snake.png', bbox_inches='tight')
if plot:
plot_all()
plot_snake()
if plot:
plt.show()
+262
View File
@@ -0,0 +1,262 @@
"""
Code used to trace Scipy logo.
"""
import numpy as np
import matplotlib.pyplot as plt
import scikits.image.io as imgio
from scipy.misc import lena
import matplotlib.nxutils as nx
class SymmetricAnchorPoint(object):
"""Anchor point in a parametric curve with symmetric handles
Parameters
----------
pt : length-2 sequence
(x, y) coordinates of anchor point
theta : float
angle of control handle
length : float
half-length of symmetric control handle. Each control point is `length`
distance away from the anchor point.
use_degrees : bool
If True, convert input `theta` from degrees to radians.
"""
def __init__(self, pt, theta, length, use_degrees=False):
self.pt = pt
if use_degrees:
theta = theta * np.pi / 180
self.theta = theta
self.length = length
def control_points(self):
"""Return control points for symmetric handles
The first point is in the direction of theta and the second is directly
opposite. For example, if `theta = 0`, then the first `p1` will be
directly to the right of the anchor point, and `p2` will be directly
to the left.
"""
theta = self.theta
offset = self.length * np.array([np.cos(theta), np.sin(theta)])
p1 = self.pt + offset
p2 = self.pt - offset
return p1, p2
def __repr__(self):
v = (self.pt, self.theta * 180/np.pi, self.length)
return 'SymmetricAnchorPoint(pt={0}, theta={1}, length={2})'.format(*v)
def curve_from_anchor_points(pts):
"""Return curve from a list of SymmetricAnchorPoints"""
assert len(pts) > 1
bezier_pts = []
for anchor in pts:
c1, c2 = anchor.control_points()
bezier_pts.extend([c2, anchor.pt, c1])
# clip control points from ends
bezier_pts = bezier_pts[1:-1]
x, y = [], []
# every third point is an anchor point
for i in range(0, len(bezier_pts)-1, 3):
xi, yi = cubic_curve(*bezier_pts[i:i+4])
x.append(xi)
y.append(yi)
return np.hstack(x), np.hstack(y)
def cubic_curve(p0, p1, p2, p3, npts=20):
"""Return points on a cubic Bezier curve
Parameters
----------
p0, p3 : length-2 sequences
end points of curve
p1, p2 : length-2 sequences
control points of curve
npts : int
number of points to return (including end points)
Returns
-------
x, y : arrays
points on cubic curve
"""
t = np.linspace(0, 1, npts)[:, np.newaxis]
# cubic bezier curve from http://en.wikipedia.org/wiki/Bezier_curve
b = (1-t)**3 * p0 + 3*t*(1-t)**2 * p1 + 3*t**2*(1-t) * p2 + t**3 * p3
x, y = b.transpose()
return x, y
class Circle(object):
def __init__(self, center, radius):
self.center = center
self.radius = radius
def point_from_angle(self, angle):
r = self.radius
# `angle` can be a scalar or 1D array: transpose twice for best results
pts = r * np.array((np.cos(angle), np.sin(angle))).T + self.center
return pts.T
def plot(self, **kwargs):
ax = kwargs.pop('ax', plt.gca())
fc = kwargs.pop('fc', 'none')
c = plt.Circle(self.center, self.radius, fc=fc, **kwargs)
ax.add_patch(c)
class ScipyLogo(object):
"""Object to generate scipy logo
Parameters
----------
center : length-2 array
the Scipy logo will be centered on this point.
radius : float
radius of logo
"""
CENTER = np.array((254, 246))
RADIUS = 252.0
THETA_START = 2.58
THETA_END = -0.368
def __init__(self, center=None, radius=None):
if center is None:
if radius is None:
center = self.CENTER
else:
center = np.array((radius, radius))
self.center = center
if radius is None:
radius = self.RADIUS
self.radius = radius
# calculate end points of curve so that it lies exactly on circle
logo_circle = Circle(self.CENTER, self.RADIUS)
s_start = logo_circle.point_from_angle(self.THETA_START)
s_end = logo_circle.point_from_angle(self.THETA_END)
self.circle = Circle(self.center, self.radius)
# note that angles are clockwise because of inverted y-axis
self._anchors = [SymmetricAnchorPoint(*t, use_degrees=True)
for t in [(s_start, -37, 90),
((144, 312), 7, 20),
((205, 375), 52, 50),
((330, 380), -53, 60),
((290, 260),-168, 50),
((217, 245),-168, 50),
((182, 118), -50, 60),
((317, 125), 53, 60),
((385, 198), 10, 20),
(s_end, -25, 60)]]
# normalize anchors so they have unit radius and are centered at origin
for a in self._anchors:
a.pt = (a.pt - self.CENTER) / self.RADIUS
a.length = a.length / self.RADIUS
def snake_anchors(self):
"""Return list of SymmetricAnchorPoints defining snake curve"""
anchors = []
for a in self._anchors:
pt = self.radius * a.pt + self.center
length = self.radius * a.length
anchors.append(SymmetricAnchorPoint(pt, a.theta, length))
return anchors
def snake_curve(self):
"""Return x, y coordinates of snake curve"""
return curve_from_anchor_points(self.snake_anchors())
def plot_snake_curve(self, **kwargs):
ax = kwargs.pop('ax', plt.gca())
x, y = self.snake_curve()
ax.plot(x, y, 'k', **kwargs)
def plot_circle(self, **kwargs):
self.circle.plot(**kwargs)
def plot_image(self, **kwargs):
ax = kwargs.pop('ax', plt.gca())
img = imgio.imread('data/scipy.png')
ax.imshow(img, **kwargs)
def get_mask(self, shape, region):
"""
Parameters
----------
region : {'upper left', 'lower right'}
"""
if region == 'upper left':
theta = np.linspace(self.THETA_END, self.THETA_START - 2 * np.pi)
elif region == 'lower right':
theta = np.linspace(self.THETA_END, self.THETA_START)
else:
msg = "Expected 'upper left' or 'lower right'; got %s" % region
raise ValueError(msg)
xy_circle = self.circle.point_from_angle(theta).T
x, y = self.snake_curve()
xy_curve = np.array((x, y)).T
xy_poly = np.vstack((xy_curve, xy_circle))
h, w = shape[:2]
y_img, x_img = np.mgrid[:h, :w]
xy_points = np.column_stack((x_img.flat, y_img.flat))
mask = nx.points_inside_poly(xy_points, xy_poly)
return mask.reshape((h, w))
def plot_scipy_trace():
plt.figure()
logo = ScipyLogo()
logo.plot_snake_curve()
logo.plot_circle()
logo.plot_image()
plot_anchors(logo.snake_anchors())
def plot_anchors(anchors, color='r', alpha=0.7):
for a in anchors:
c = a.control_points()
x, y = np.transpose(c)
plt.plot(x, y, 'o-', color=color, mfc='w', mec=color, alpha=alpha)
plt.plot(a.pt[0], a.pt[1], 'o', color=color, alpha=alpha)
def plot_snake_overlay():
plt.figure()
logo = ScipyLogo((670, 250), 250)
logo.plot_snake_curve()
logo.plot_circle()
img = imgio.imread('data/snake_pixabay.jpg')
#mask = logo.get_mask(img.shape, 'upper left')
#img[mask] = 255
plt.imshow(img)
def plot_lena_overlay():
plt.figure()
logo = ScipyLogo((300, 300), 180)
logo.plot_snake_curve()
logo.plot_circle()
img = lena()
#mask = logo.get_mask(img.shape, 'upper left')
#img[mask] = 255
plt.imshow(img)
if __name__ == '__main__':
plot_scipy_trace()
plot_snake_overlay()
plot_lena_overlay()
plt.show()
+16
View File
@@ -0,0 +1,16 @@
from scikits.image import io, transform
s = 0.7
img = io.imread('scikits_image_logo.png')
h, w, c = img.shape
print "\nScaling down logo by %.1fx..." % s
img = transform.homography(img, [[s, 0, 0],
[0, s, 0],
[0, 0, 1]],
output_shape=(int(h*s), int(w*s), 4),
order=3)
io.imsave('scikits_image_logo_small.png', img)
+92
View File
@@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="744.09448819"
height="1052.3622047"
id="svg2"
version="1.1"
inkscape:version="0.48.1 r9760"
sodipodi:docname="snake_logo.svg">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.98994949"
inkscape:cx="161.85782"
inkscape:cy="843.24881"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1280"
inkscape:window-height="722"
inkscape:window-x="0"
inkscape:window-y="24"
inkscape:window-maximized="1" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<image
sodipodi:absref="/home/stefan/src/scikits_image_logo/green_orange_snake.png"
xlink:href="green_orange_snake.png"
y="6.4703732"
x="8.8462782"
id="image3147"
height="132.30446"
width="132.30446" />
<g
id="g3158"
transform="translate(-105.05586,-195.43585)">
<text
sodipodi:linespacing="125%"
id="text3150"
y="278.58533"
x="261.22247"
style="font-size:24px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#4d4d4d;fill-opacity:1;stroke:none;font-family:Verdana;-inkscape-font-specification:Verdana"
xml:space="preserve"><tspan
style="font-size:70px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu"
y="278.58533"
x="261.22247"
id="tspan3152"
sodipodi:role="line">scikits-image</tspan></text>
<text
sodipodi:linespacing="125%"
id="text3154"
y="306.86957"
x="262.52374"
style="font-size:24px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#4d4d4d;fill-opacity:1;stroke:none;font-family:Verdana;-inkscape-font-specification:Verdana"
xml:space="preserve"><tspan
style="font-size:26.5px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu"
y="306.86957"
x="262.52374"
id="tspan3156"
sodipodi:role="line">image processing in python</tspan></text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

+1 -1
View File
@@ -123,7 +123,7 @@ html_title = 'scikits.image v%s docs' % version
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
html_logo = "scikits_image_logo_small.png"
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+1 -1
View File
@@ -584,7 +584,7 @@ pre {
background-color: {{ theme_codebgcolor }};
color: {{ theme_codetextcolor }};
line-height: 120%;
border: 1px solid #ac9;
border: 0px solid #ac9;
border-left: none;
border-right: none;
}
+4 -4
View File
@@ -1,8 +1,8 @@
User Guide
==========
.. contents::
:local:
.. include:: user_guide/plugins.txt
.. toctree::
:maxdepth: 1
user_guide/data_types
user_guide/plugins
+61
View File
@@ -0,0 +1,61 @@
Image data types and what they mean
===================================
.. note::
This is a preliminary design document.
In ``scikits.image``, we assume the following dtype ranges:
=========== ================
Data type Range
=========== ================
uint8 0 to 255
uint16 0 to 65535
float 0 to 1
int8 -128 to 127
int16 -32768 to 32767
=========== ================
Input types
-----------
Functions may choose to support only a subset of these data-types. In
such a case, the input will be converted to the required type (if
possible) and a warning message printed to the log if a memory copy is
needed. Type requirements should be noted in the docstrings.
The following utility functions are available to developers and
users:
(*Tentative list only -- not implemented yet*)
=============== =======================================
Function name Description
=============== =======================================
img_as_float Convert to 64-bit floating point repr.
Scaled to 0/1.
img_as_uint Convert to 16-bit uint repr.
Scaled to 0-65535.
img_as_int Convert to 16-bit int repr.
Scaled to -32767 to 32768.
=============== =======================================
Wherever possible, functions should try to handle input
without explicit conversion (e.g., there is no need to
force values to a specific type for doing a convolution;
a plotting function, on the other hand, needs to know the
range of the input).
Output types
------------
The output type of a function is determined by the function author,
and documented for the benefit of the user. While this requires the
user to explicitly convert the output to whichever format is needed,
it ensures that no unnecessary data copies take place.
A user that requires a specific type of output (e.g., for display
purposes), may do::
out = img_as_uint(sobel(img))
plt.imshow(out)
+4 -4
View File
@@ -1,4 +1,4 @@
from hough_transform import *
from finite_radon_transform import *
from project import *
from .hough_transform import *
from .finite_radon_transform import *
from .project import *
from .sum_area_table import *
+60
View File
@@ -0,0 +1,60 @@
def sat(x):
"""Summed area table / integral image.
The integral image contains the sum of all elements above and to the
left of it, i.e.:
.. math::
S[m, n] = \sum_{i \leq m} \sum_{j \leq n} X[i, j]
Parameters
----------
X : ndarray
Input image.
Returns
-------
S : ndarray
Summed area table.
References
----------
.. [1] F.C. Crow, "Summed-area tables for texture mapping,"
ACM SIGGRAPH Computer Graphics, vol. 18, 1984, pp. 207-212.
"""
return x.cumsum(1).cumsum(0)
def sat_sum(sat, r0, c0, r1, c1):
"""Use a summed area table / integral image to sum over a given window.
Parameters
----------
sat : ndarray
Summed area table / integral image.
r0, c0 : int
Top-left corner of block to be summed.
r1, c1 : int
Bottom-right corner of block to be summed.
Returns
-------
S : int
Sum over the given window.
"""
S = 0
S += sat[r1, c1]
if (r0 - 1 >= 0) and (c0 - 1 >= 0):
S += sat[r0 - 1, c0 - 1]
if (r0 - 1 >= 0):
S -= sat[r0 - 1, c1]
if (c0 - 1 >= 0):
S -= sat[r1, c0 - 1]
return S
+28
View File
@@ -0,0 +1,28 @@
import numpy as np
from numpy.testing import *
from scikits.image.transform import sat, sat_sum
x = (np.random.random((50, 50)) * 255).astype(np.uint8)
s = sat(x)
def test_validity():
y = np.arange(12).reshape((4,3))
y = (np.random.random((50, 50)) * 255).astype(np.uint8)
assert_equal(sat(y)[-1, -1],
y.sum())
def test_basic():
assert_equal(x[12:24, 10:20].sum(), sat_sum(s, 12, 10, 23, 19))
assert_equal(x[:20, :20].sum(), sat_sum(s, 0, 0, 19, 19))
assert_equal(x[:20, 10:20].sum(), sat_sum(s, 0, 10, 19, 19))
assert_equal(x[10:20, :20].sum(), sat_sum(s, 10, 0, 19, 19))
def test_single():
assert_equal(x[0, 0], sat_sum(s, 0, 0, 0, 0))
assert_equal(x[10, 10], sat_sum(s, 10, 10, 10, 10))
if __name__ == '__main__':
run_module_suite()