ImagePRO-Python 1.2.2__py3-none-any.whl → 1.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. ImagePRO/__init__.py +2 -2
  2. ImagePRO/human_analysis/body_analysis/__init__.py +4 -0
  3. ImagePRO/human_analysis/body_analysis/body_pose_estimation.py +26 -5
  4. ImagePRO/human_analysis/body_analysis/hand_tracking.py +28 -6
  5. ImagePRO/human_analysis/face_analysis/__init__.py +5 -1
  6. ImagePRO/human_analysis/face_analysis/eye_status_analysis.py +22 -4
  7. ImagePRO/human_analysis/face_analysis/face_comparison.py +15 -3
  8. ImagePRO/human_analysis/face_analysis/face_detection.py +5 -2
  9. ImagePRO/human_analysis/face_analysis/face_mesh_analysis.py +36 -18
  10. ImagePRO/human_analysis/face_analysis/head_pose_estimation.py +14 -4
  11. ImagePRO/object_analysis/__init__.py +3 -0
  12. ImagePRO/object_analysis/object_detection.py +24 -13
  13. ImagePRO/pre_processing/__init__.py +4 -1
  14. ImagePRO/pre_processing/blur.py +10 -10
  15. ImagePRO/pre_processing/contrast.py +4 -7
  16. ImagePRO/pre_processing/crop.py +4 -5
  17. ImagePRO/pre_processing/dataset_generator.py +16 -10
  18. ImagePRO/pre_processing/histogram.py +36 -48
  19. ImagePRO/pre_processing/resize.py +2 -2
  20. ImagePRO/pre_processing/rotate.py +6 -7
  21. ImagePRO/pre_processing/sharpen.py +16 -13
  22. ImagePRO/utils/image.py +7 -6
  23. ImagePRO/utils/result.py +10 -7
  24. {imagepro_python-1.2.2.dist-info → imagepro_python-1.3.1.dist-info}/METADATA +46 -18
  25. imagepro_python-1.3.1.dist-info/RECORD +31 -0
  26. {imagepro_python-1.2.2.dist-info → imagepro_python-1.3.1.dist-info}/WHEEL +1 -1
  27. imagepro_python-1.2.2.dist-info/RECORD +0 -31
  28. {imagepro_python-1.2.2.dist-info → imagepro_python-1.3.1.dist-info}/licenses/LICENSE +0 -0
  29. {imagepro_python-1.2.2.dist-info → imagepro_python-1.3.1.dist-info}/top_level.txt +0 -0
@@ -60,9 +60,9 @@ def apply_average_blur(
60
60
  ):
61
61
  raise ValueError("'kernel_size' must be a tuple of two positive integers.")
62
62
 
63
- # Apply box filter blur
64
- blurred = cv2.blur(image._data.copy(), kernel_size)
65
-
63
+ # Apply box filter blur (cv2.blur never mutates its input)
64
+ blurred = cv2.blur(image._data, kernel_size)
65
+
66
66
  return Result(
67
67
  image=blurred,
68
68
  meta={
@@ -112,8 +112,8 @@ def apply_gaussian_blur(
112
112
  raise ValueError("'kernel_size' must be a tuple of two odd positive integers.")
113
113
 
114
114
  # Apply Gaussian blur with automatic sigma calculation
115
- blurred = cv2.GaussianBlur(image._data.copy(), kernel_size, 0)
116
-
115
+ blurred = cv2.GaussianBlur(image._data, kernel_size, 0)
116
+
117
117
  return Result(
118
118
  image=blurred,
119
119
  meta={
@@ -160,8 +160,8 @@ def apply_median_blur(
160
160
  raise ValueError("'filter_size' must be an odd integer greater than 1.")
161
161
 
162
162
  # Apply median filtering
163
- blurred = cv2.medianBlur(image._data.copy(), filter_size)
164
-
163
+ blurred = cv2.medianBlur(image._data, filter_size)
164
+
165
165
  return Result(
166
166
  image=blurred,
167
167
  meta={
@@ -203,7 +203,7 @@ def apply_bilateral_blur(
203
203
  Result: Result object with smoothed image.
204
204
  - image (np.ndarray): Edge-preserving smoothed output
205
205
  - data (None): No additional data
206
- - meta (dict): Contains filter *,parameters and operation info
206
+ - meta (dict): Contains filter parameters and operation info
207
207
 
208
208
  Raises:
209
209
  TypeError: If image is not an Image instance
@@ -221,12 +221,12 @@ def apply_bilateral_blur(
221
221
 
222
222
  # Apply bilateral filter
223
223
  blurred = cv2.bilateralFilter(
224
- image._data.copy(),
224
+ image._data,
225
225
  filter_size,
226
226
  sigma_color,
227
227
  sigma_space
228
228
  )
229
-
229
+
230
230
  return Result(
231
231
  image=blurred,
232
232
  meta={
@@ -13,14 +13,11 @@ import cv2
13
13
 
14
14
  from ImagePRO.utils.image import Image
15
15
  from ImagePRO.utils.result import Result
16
- from ImagePRO.pre_processing.grayscale import convert_to_grayscale
17
16
 
18
17
 
19
18
  # Constants for contrast enhancement
20
19
  DEFAULT_CLIP_LIMIT = 2.0 # For CLAHE
21
20
  DEFAULT_TILE_GRID_SIZE = (8, 8) # For CLAHE
22
- DEFAULT_ALPHA = 1.5 # For linear stretching
23
- DEFAULT_BETA = 10 # For linear stretching
24
21
 
25
22
 
26
23
  def apply_clahe_contrast(
@@ -65,8 +62,8 @@ def apply_clahe_contrast(
65
62
  ):
66
63
  raise TypeError("'tile_grid_size' must be a tuple of two positive integers")
67
64
 
68
- # Convert and apply CLAHE
69
- grayscale = convert_to_grayscale(image=Image.from_array(image._data.copy()))
65
+ # Convert and apply CLAHE (cvtColor returns a new array; input untouched)
66
+ grayscale = cv2.cvtColor(image._data, cv2.COLOR_BGR2GRAY)
70
67
  clahe = cv2.createCLAHE(clipLimit=clip_limit, tileGridSize=tile_grid_size)
71
68
  enhanced = clahe.apply(grayscale)
72
69
 
@@ -105,7 +102,7 @@ def apply_histogram_equalization(
105
102
  raise TypeError("'image' must be an Image instance")
106
103
 
107
104
  # Convert and apply global histogram equalization
108
- grayscale = convert_to_grayscale(image=Image.from_array(image._data.copy()))
105
+ grayscale = cv2.cvtColor(image._data, cv2.COLOR_BGR2GRAY)
109
106
  enhanced = cv2.equalizeHist(grayscale)
110
107
 
111
108
  return Result(
@@ -156,7 +153,7 @@ def apply_contrast_stretching(
156
153
  raise ValueError("'beta' must be an integer between 0 and 255")
157
154
 
158
155
  # Convert and apply linear stretching
159
- grayscale = convert_to_grayscale(image=Image.from_array(image._data.copy()))
156
+ grayscale = cv2.cvtColor(image._data, cv2.COLOR_BGR2GRAY)
160
157
  enhanced = cv2.convertScaleAbs(grayscale, alpha=alpha, beta=beta)
161
158
 
162
159
  return Result(
@@ -12,10 +12,6 @@ if str(_src_path) not in sys.path:
12
12
  from ImagePRO.utils.image import Image
13
13
  from ImagePRO.utils.result import Result
14
14
 
15
- # Constants
16
- DEFAULT_START_POINT = (0, 0)
17
- DEFAULT_END_POINT = (100, 100)
18
-
19
15
 
20
16
  def crop_image(
21
17
  image: Image,
@@ -59,7 +55,10 @@ def crop_image(
59
55
  x2, y2 = end_point
60
56
 
61
57
  if x1 < 0 or y1 < 0 or x2 <= x1 or y2 <= y1:
62
- raise ValueError("Invalid crop coordinates: ensure (x1, y1) is top-left and (x2, y2) is bottom-right")
58
+ raise ValueError(
59
+ "Invalid crop coordinates: ensure (x1, y1) is top-left "
60
+ "and (x2, y2) is bottom-right"
61
+ )
63
62
 
64
63
  height, width = image.shape[:2]
65
64
 
@@ -1,6 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import random
3
4
  import sys
5
+ import time
4
6
  from pathlib import Path
5
7
 
6
8
  # Add src directory to path for absolute imports
@@ -9,16 +11,12 @@ _src_path = _file_path.parents[2] # Go up to src directory
9
11
  if str(_src_path) not in sys.path:
10
12
  sys.path.insert(0, str(_src_path))
11
13
 
14
+ import cv2
15
+
12
16
  from ImagePRO.human_analysis.face_analysis.face_detection import detect_faces
13
- from ImagePRO.pre_processing import blur, grayscale, rotate, sharpen, resize
17
+ from ImagePRO.pre_processing import blur, grayscale, resize, rotate, sharpen
14
18
  from ImagePRO.utils.image import Image
15
19
 
16
- import random
17
- import time
18
-
19
- import cv2
20
- import mediapipe as mp
21
-
22
20
  # Constants
23
21
  DEFAULT_NUM_IMAGES = 200
24
22
  DEFAULT_START_INDEX = 0
@@ -40,7 +38,7 @@ def capture_bulk_pictures(
40
38
  apply_grayscale: bool = False,
41
39
  apply_sharpen: bool = False,
42
40
  apply_rotate: bool = False,
43
- apply_resize: tuple = False,
41
+ apply_resize: tuple[int, int] | bool = False,
44
42
  delay: float = DEFAULT_DELAY
45
43
  ) -> None:
46
44
  """Generate a dataset by capturing faces from webcam.
@@ -50,7 +48,7 @@ def capture_bulk_pictures(
50
48
  face recognition datasets with data augmentation.
51
49
 
52
50
  Processing pipeline (if enabled):
53
- average blur → laplacian sharpen → grayscale → resize → random rotate
51
+ median blur → laplacian sharpen → grayscale → resize → random rotate
54
52
 
55
53
  Args:
56
54
  folder_path: Base directory for dataset.
@@ -96,6 +94,14 @@ def capture_bulk_pictures(
96
94
  if not isinstance(delay, (int, float)) or delay < 0:
97
95
  raise ValueError("'delay' must be a non-negative number")
98
96
 
97
+ try:
98
+ import mediapipe as mp
99
+ except ImportError as err:
100
+ raise ImportError(
101
+ "The optional 'mediapipe' dependency is required for dataset "
102
+ 'generation. Install it with: pip install "ImagePRO-Python[mediapipe]"'
103
+ ) from err
104
+
99
105
  # Setup output directory structure
100
106
  base_dir = Path(folder_path)
101
107
  face_folder = base_dir / str(face_id)
@@ -135,7 +141,7 @@ def capture_bulk_pictures(
135
141
 
136
142
  if apply_blur:
137
143
  # Reduce noise while preserving facial features
138
- processed = blur.apply_average_blur(
144
+ processed = blur.apply_median_blur(
139
145
  image=Image.from_array(processed),
140
146
  filter_size=3
141
147
  ).image
@@ -15,73 +15,61 @@ import matplotlib.pyplot as plt
15
15
  from ImagePRO.utils.image import Image
16
16
  from ImagePRO.utils.result import Result
17
17
 
18
+
18
19
  def show_histogram(image: Image) -> Result:
19
20
  """
20
- Displays the histogram of an image with customizable options.
21
-
22
- This function visualizes the intensity distribution of an image. It handles both grayscale
23
- and color images, with automatic detection of image type. For color images, it displays
24
- histograms for each color channel (BGR or RGB) with appropriate colors.
21
+ Builds a matplotlib histogram figure for an image.
22
+
23
+ This function creates the histogram plot of the intensity distribution of
24
+ an image. It handles both grayscale and color images, with automatic
25
+ detection of image type. For color images, it plots a histogram for each
26
+ color channel (BGR or RGB) with appropriate colors. The pyplot module is
27
+ returned in ``data``; call ``matplotlib.pyplot.show()`` to display the
28
+ figure.
25
29
 
26
30
  Args:
27
31
  image (Image):
28
- Input image to convert. Must be BGR, RGB or Grayscale format.
32
+ Input image to analyze. Must be BGR, RGB or Grayscale format.
29
33
 
30
34
  Returns:
31
35
  Result: Result object with histogram plot.
32
- - image (np.ndarray): None
33
- - data (None): matplotlib.pyplot object
36
+ - image: None
37
+ - data: The matplotlib.pyplot module (figure is created, not shown)
34
38
  - meta (dict): Contains source object and operation info
35
39
 
36
40
  Raises:
37
41
  TypeError: If image is not an Image instance
38
- ValueError: If image colorspace is not defined.
42
+ ValueError: If image colorspace is not supported.
39
43
  """
40
44
  if not isinstance(image, Image):
41
45
  raise TypeError("'image' must be an Image instance.")
42
46
 
43
- if image.colorspace == "BGR":
44
- colors = ('blue', 'green', 'red')
45
- plt.figure(figsize=(10, 6))
47
+ if image.colorspace in ("BGR", "RGB"):
48
+ if image.colorspace == "BGR":
49
+ colors = ("blue", "green", "red")
50
+ labels = ("Blue Channel", "Green Channel", "Red Channel")
51
+ else:
52
+ colors = ("red", "green", "blue")
53
+ labels = ("Red Channel", "Green Channel", "Blue Channel")
54
+ channels = list(enumerate(colors))
55
+ elif image.colorspace == "GRAY":
56
+ channels = [(0, "black")]
57
+ labels = None
58
+ else:
59
+ raise ValueError(f"Unknown colorspace: {image.colorspace}")
46
60
 
47
- for i, color in enumerate(colors):
48
- hist = cv2.calcHist([image._data], [i], None, [256], [0, 256])
49
- plt.plot(hist, color=color)
50
- plt.xlim([0, 256])
51
-
52
-
53
- plt.title('Histogram of BGR Channels')
54
- plt.xlabel('Pixel Intensity')
55
- plt.ylabel('Frequency')
56
- plt.legend(['Blue Channel', 'Green Channel', 'Red Channel'])
57
-
58
-
59
- elif image.colorspace == "RGB":
60
- colors = ('red', 'green', 'blue')
61
- plt.figure(figsize=(10, 6))
61
+ plt.figure(figsize=(10, 6))
62
+ for channel, color in channels:
63
+ hist = cv2.calcHist([image._data], [channel], None, [256], [0, 256])
64
+ plt.plot(hist, color=color)
62
65
 
63
- for i, color in enumerate(colors):
64
- hist = cv2.calcHist([image._data], [i], None, [256], [0, 256])
65
- plt.plot(hist, color=color)
66
- plt.xlim([0, 256])
67
-
68
-
69
- plt.title('Histogram of RGB Channels')
70
- plt.xlabel('Pixel Intensity')
71
- plt.ylabel('Frequency')
72
- plt.legend(['Red Channel', 'Green Channel', 'Blue Channel'])
73
-
74
-
75
- elif image.colorspace == 'GRAY':
76
- plt.figure(figsize=(10, 6))
66
+ if labels is not None:
67
+ plt.title(f"Histogram of {image.colorspace} Channels")
68
+ plt.legend(labels)
69
+ plt.xlabel("Pixel Intensity")
70
+ plt.ylabel("Frequency")
77
71
 
78
- hist = cv2.calcHist([image._data], [0], None, [256], [0, 256])
79
- plt.plot(hist, color='black')
80
- plt.xlim([0, 256])
81
-
82
- else:
83
- return ValueError('Unknown colorspace')
84
-
72
+ plt.xlim([0, 256])
85
73
 
86
74
  return Result(
87
75
  image=None,
@@ -22,7 +22,7 @@ def resize_image(
22
22
  ) -> Result:
23
23
  """Resize an image to specified dimensions.
24
24
 
25
- Changes image size while maintaining aspect ratio.
25
+ Changes image size to the exact (width, height) given.
26
26
  Uses bilinear interpolation for smooth resizing.
27
27
 
28
28
  Args:
@@ -52,7 +52,7 @@ def resize_image(
52
52
 
53
53
  # Resize using bilinear interpolation
54
54
  resized = cv2.resize(image._data, new_size, interpolation=cv2.INTER_LINEAR)
55
-
55
+
56
56
  return Result(
57
57
  image=resized,
58
58
  meta={
@@ -10,7 +10,6 @@ if str(_src_path) not in sys.path:
10
10
  sys.path.insert(0, str(_src_path))
11
11
 
12
12
  import cv2
13
- import numpy as np
14
13
 
15
14
  from ImagePRO.utils.image import Image
16
15
  from ImagePRO.utils.result import Result
@@ -18,7 +17,6 @@ from ImagePRO.utils.result import Result
18
17
 
19
18
  # Constants for rotation operations
20
19
  DEFAULT_SCALE = 1.0
21
- DEFAULT_ANGLE = 45.0
22
20
 
23
21
 
24
22
  def rotate_image_90(image: Image) -> Result:
@@ -39,7 +37,7 @@ def rotate_image_90(image: Image) -> Result:
39
37
  if not isinstance(image, Image):
40
38
  raise TypeError("'image' must be an Image instance")
41
39
 
42
- rotated = cv2.rotate(image._data.copy(), cv2.ROTATE_90_CLOCKWISE)
40
+ rotated = cv2.rotate(image._data, cv2.ROTATE_90_CLOCKWISE)
43
41
  return Result(
44
42
  image=rotated,
45
43
  meta={
@@ -68,7 +66,7 @@ def rotate_image_180(image: Image) -> Result:
68
66
  if not isinstance(image, Image):
69
67
  raise TypeError("'image' must be an Image instance")
70
68
 
71
- rotated = cv2.rotate(image._data.copy(), cv2.ROTATE_180)
69
+ rotated = cv2.rotate(image._data, cv2.ROTATE_180)
72
70
  return Result(
73
71
  image=rotated,
74
72
  meta={
@@ -97,7 +95,7 @@ def rotate_image_270(image: Image) -> Result:
97
95
  if not isinstance(image, Image):
98
96
  raise TypeError("'image' must be an Image instance")
99
97
 
100
- rotated = cv2.rotate(image._data.copy(), cv2.ROTATE_90_COUNTERCLOCKWISE)
98
+ rotated = cv2.rotate(image._data, cv2.ROTATE_90_COUNTERCLOCKWISE)
101
99
  return Result(
102
100
  image=rotated,
103
101
  meta={
@@ -117,7 +115,8 @@ def rotate_image_custom(
117
115
  """Rotate image by custom angle with optional scaling.
118
116
 
119
117
  Rotates around image center. Positive angles are counter-clockwise.
120
- Image is resized to contain full rotated content.
118
+ The output canvas keeps the original image dimensions, so corners of
119
+ the rotated image may be clipped.
121
120
 
122
121
  Args:
123
122
  image: Input image to rotate.
@@ -144,7 +143,7 @@ def rotate_image_custom(
144
143
 
145
144
  h, w = image.shape[:2]
146
145
  matrix = cv2.getRotationMatrix2D((w/2, h/2), angle, scale)
147
- rotated = cv2.warpAffine(image._data.copy(), matrix, (w, h))
146
+ rotated = cv2.warpAffine(image._data, matrix, (w, h))
148
147
 
149
148
  return Result(
150
149
  image=rotated,
@@ -14,7 +14,7 @@ import numpy as np
14
14
 
15
15
  from ImagePRO.utils.image import Image
16
16
  from ImagePRO.utils.result import Result
17
- from ImagePRO.pre_processing.blur import apply_average_blur
17
+ from ImagePRO.pre_processing.blur import DEFAULT_KERNEL_SIZE
18
18
 
19
19
  # Constants
20
20
  DEFAULT_LAPLACIAN_COEFFICIENT = 3.0
@@ -52,13 +52,16 @@ def apply_laplacian_sharpening(
52
52
  if not isinstance(coefficient, (int, float)) or coefficient < 0:
53
53
  raise ValueError("'coefficient' must be a non-negative number")
54
54
 
55
- # Apply Laplacian edge detection
56
- laplacian = cv2.Laplacian(image._data.copy(), cv2.CV_64F)
57
- laplacian = np.uint8(np.absolute(laplacian))
55
+ # Apply Laplacian edge detection, then enhance edges while keeping
56
+ # float arithmetic identical to: image + coefficient * |laplacian|
57
+ laplacian = cv2.Laplacian(image._data, cv2.CV_64F)
58
+ np.absolute(laplacian, out=laplacian)
59
+ laplacian = laplacian.astype(np.uint8)
58
60
 
59
- # Enhance edges
60
- sharpened = image._data + coefficient * laplacian
61
- sharpened = np.uint8(np.clip(sharpened, 0, 255))
61
+ sharpened = laplacian * coefficient
62
+ sharpened += image._data
63
+ np.clip(sharpened, 0, 255, out=sharpened)
64
+ sharpened = sharpened.astype(np.uint8)
62
65
 
63
66
  return Result(
64
67
  image=sharpened,
@@ -102,15 +105,15 @@ def apply_unsharp_masking(
102
105
  raise ValueError("'coefficient' must be a non-negative number")
103
106
 
104
107
  # Create the mask from original vs blurred difference
105
- blurred = apply_average_blur(image=Image.from_array(image._data.copy()))
108
+ blurred = cv2.blur(image._data, DEFAULT_KERNEL_SIZE)
106
109
  mask = cv2.subtract(image._data, blurred)
107
-
110
+
108
111
  # Apply unsharp masking
109
112
  sharpened = cv2.addWeighted(
110
- image._data,
111
- 1 + coefficient,
112
- mask,
113
- -coefficient,
113
+ image._data,
114
+ 1 + coefficient,
115
+ mask,
116
+ -coefficient,
114
117
  0
115
118
  )
116
119
 
ImagePRO/utils/image.py CHANGED
@@ -18,8 +18,9 @@ class Image:
18
18
  """
19
19
  Lightweight, immutable image wrapper for ImagePRO.
20
20
 
21
- Use factory constructors to create instances. All methods return new Image objects.
22
- Images are handled in BGR format by default to match OpenCV conventions.
21
+ Use factory constructors to create instances; instances are lightweight
22
+ wrappers and are not modified by ImagePRO operations. Images are handled
23
+ in BGR format by default to match OpenCV conventions.
23
24
 
24
25
  Attributes:
25
26
  _data (np.ndarray):
@@ -35,7 +36,7 @@ class Image:
35
36
  >>> # Load from file (default BGR)
36
37
  >>> img = Image.from_path('input.jpg', colorspace="BGR")
37
38
  >>> print(img.shape) # (H, W, 3)
38
- >>>
39
+ >>>
39
40
  >>> # Or wrap a NumPy array
40
41
  >>> img = Image.from_array(np_array, colorspace="RGB")
41
42
  """
@@ -47,7 +48,7 @@ class Image:
47
48
 
48
49
  @classmethod
49
50
  def from_path(
50
- cls,
51
+ cls,
51
52
  path: str | Path,
52
53
  colorspace: Colorspace = "BGR"
53
54
  ) -> Image:
@@ -58,7 +59,8 @@ class Image:
58
59
  path (str | Path):
59
60
  Path to the image file.
60
61
  colorspace (Colorspace, optional):
61
- Colorspace of the image array in that path ("BGR", "RGB", or "GRAY"). Defaults to "BGR".
62
+ Colorspace of the image array in that path
63
+ ("BGR", "RGB", or "GRAY"). Defaults to "BGR".
62
64
 
63
65
  Returns:
64
66
  Image: New Image instance loaded from disk.
@@ -124,4 +126,3 @@ class Image:
124
126
  np.dtype: Data type of the image array.
125
127
  """
126
128
  return self._data.dtype
127
-
ImagePRO/utils/result.py CHANGED
@@ -143,22 +143,25 @@ class Result:
143
143
  raise IOError(f"Failed to write CSV to {out_path}: {exc}") from exc
144
144
  return self
145
145
 
146
- def flatten_rows(data):
146
+
147
+ def flatten_rows(data: Any) -> list[list[Any]]:
147
148
  """
148
149
  Flatten nested lists/tuples into rows suitable for CSV writing.
149
-
150
+
150
151
  Recursively processes nested data structures to create a flat list of rows
151
152
  that can be written to a CSV file. Handles mixed nesting levels gracefully.
152
-
153
+
153
154
  Args:
154
155
  data: Nested list/tuple structure containing data to flatten.
155
-
156
+
156
157
  Returns:
157
158
  list[list[Any]]: Flattened list of rows, where each row is a list of values.
158
-
159
+
159
160
  Example:
160
- >>> flatten_rows([[1, 2], [3, [4, 5]]])
161
- [[1, 2], [3, 4, 5]]
161
+ >>> flatten_rows([[1, 2], [3, 4]])
162
+ [[1, 2], [3, 4]]
163
+ >>> flatten_rows([[[1, 2], [3, 4]], [5]])
164
+ [[1, 2], [3, 4], [5]]
162
165
  """
163
166
  rows = []
164
167
  for item in data: