Friday, May 30, 2025

Ready to Code Your Own Sudoku Solver?

 

Even if you've never played, you've likely seen a Sudoku puzzle. At its core, Sudoku is a logic-based number placement puzzle set on a 9x9 grid. The grid is further divided into nine 3x3 smaller squares. To solve it, you simply need to place numbers from 1 to 9 into the empty cells. The catch? Each number can only appear once in each row, once in each column, and once in each of the nine 3x3 blocks. It's an addictive challenge that relies solely on deduction. For an in-depth look at its origins and variations, check out its Wikipedia page. 

Now that you understand the game, this post will take you behind the scenes. We'll provide the complete Python code for building your own Sudoku puzzle generator and solver, followed by a step-by-step annotation of how it all works.

This code was run on PyCharm and there are a number of posts in this blog for you to get acquainted. Here is the complete code that you could copy and paste.

from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.uix.textinput import TextInput
from kivy.properties import ListProperty, ObjectProperty, StringProperty
from kivy.graphics import Color, Line
from kivy.lang import Builder
import random
import time


class Sudoku(object):
def __init__(self, size=9):
self.size = size
self.grid = [[0 for _ in range(size)] for _ in range(size)]
self.original_grid = None
self.original_solution = None

def generate_puzzle(self, difficulty):
self._solve()
self.original_solution = [row[:] for row in self.grid]

squares_to_remove = 0
if difficulty == "easy":
squares_to_remove = 45
elif difficulty == "medium":
squares_to_remove = 55
elif difficulty == "hard":
squares_to_remove = 65

self.grid = [row[:] for row in self.original_solution]
cells = list((r, c) for r in range(self.size) for c in range(self.size))
random.shuffle(cells)

removed_count = 0
for r, c in cells:
if self.grid[r][c] != 0:
temp = self.grid[r][c]
self.grid[r][c] = 0
if not self._has_unique_solution(self.grid):
self.grid[r][c] = temp
else:
removed_count += 1
if removed_count >= squares_to_remove:
break
self.original_grid = [row[:] for row in self.grid] # Corrected line

def _solve(self):
find = self._find_empty()
if not find:
return True
else:
row, col = find

for num in range(1, self.size + 1):
if self._is_valid(row, col, num):
self.grid[row][col] = num
if self._solve():
return True
self.grid[row][col] = 0
return False

def _find_empty(self):
for i in range(self.size):
for j in range(self.size):
if self.grid[i][j] == 0:
return i, j
return None

def _is_valid(self, row, col, num):
if num in self.grid[row]:
return False
if num in [self.grid[i][col] for i in range(self.size)]:
return False
start_row, start_col = 3 * (row // 3), 3 * (col // 3)
for i in range(3):
for j in range(3):
if self.grid[start_row + i][start_col + j] == num:
return False
return True

def _find_all_solutions(self, current_grid):
solutions = []

def solve_recursive(grid, solutions):
find = self._find_empty_in_grid(grid)
if not find:
solutions.append([row[:] for row in grid])
return
row, col = find
for num in range(1, self.size + 1):
if self._is_valid_in_grid(grid, row, col, num):
grid[row][col] = num
solve_recursive(grid, solutions)
if len(solutions) > 1:
return
grid[row][col] = 0

solve_recursive(current_grid, solutions)
return solutions

def _has_unique_solution(self, puzzle_grid):
solutions = self._find_all_solutions([row[:] for row in puzzle_grid])
return len(solutions) == 1

def _find_empty_in_grid(self, grid):
size = len(grid)
for i in range(size):
for j in range(size):
if grid[i][j] == 0:
return i, j
return None

def _is_valid_in_grid(self, grid, row, col, num):
size = len(grid)
if num in grid[row]:
return False
if num in [grid[i][col] for i in range(size)]:
return False
start_row, start_col = 3 * (row // 3), 3 * (col // 3)
for i in range(3):
for j in range(3):
if grid[start_row + i][start_col + j] == num:
return False
return True

def is_complete(self):
for row in self.grid:
if 0 in row or len(set(row)) != self.size:
return False
for col in range(self.size):
if len(set(self.grid[i][col] for i in range(self.size))) != self.size:
return False
for i in range(0, self.size, 3):
for j in range(0, self.size, 3):
block = [self.grid[row][col] for row in range(i, i + 3) for col in range(j, j + 3)]
if len(set(block)) != 9:
return False
return True


class SudokuCell(TextInput):
def __init__(self, **kwargs):
super(SudokuCell, self).__init__(**kwargs)
self.multiline = False
self.input_filter = 'int'
self.halign = 'center'
self.valign = 'middle'
self.font_size = 40
self.foreground_color = (0, 0, 0, 1)
self.bind(focus=self.on_focus) # Added self.on_focus

def on_text(self, instance, value):
if len(value) > 1:
self.text = value[-1]
if value and not (1 <= int(value) <= 9):
self.text = ""

def on_focus(self, instance, focused): # Added this method
if self.parent:
self.parent.on_cell_focus(self, focused)


class SudokuBlock(GridLayout):
"""
Represents a 3x3 sub-grid within the Sudoku grid.
"""
block_row = ObjectProperty(None) # 0, 1, or 2
block_col = ObjectProperty(None) # 0, 1, or 2
cells = ListProperty([])

def __init__(self, **kwargs):
super().__init__(cols=3, rows=3, **kwargs)
self.block_row = kwargs.get('block_row', 0) # Get block row and col
self.block_col = kwargs.get('block_col', 0)
self.cells = []
for i in range(3):
for j in range(3):
cell = SudokuCell()
cell.row =
self.block_row * 3 + i
cell.col =
self.block_col * 3 + j
# cell.bind(focus=self.on_cell_focus) # Removed this line
self.add_widget(cell)
self.cells.append(cell)

def get_block_values(self):
"""Returns the values of the cells in this block."""
values = []
for cell in self.cells:
text = cell.text
if text.isdigit():
values.append(
int(text))
else:
values.append(
0)
return values

def set_block_values(self, values):
"""Sets the values of the cells in this block."""
if len(values) != 9:
raise ValueError("Must provide 9 values for a 3x3 block")
for i, cell in enumerate(self.cells):
cell.text =
str(values[i]) if values[i] != 0 else ""


class SudokuGrid(GridLayout):
grid_data = ListProperty([[
0 for _ in range(9)] for _ in range(9)])
selected_cell = ObjectProperty(
None)
blocks = ListProperty([])
block_line_color = ListProperty([
0, 0, 0, 1]) # Default to black

def __init__(self, **kwargs):
super().__init__(cols=3, rows=3, **kwargs) # 3x3 grid of blocks
self.blocks = []
self.block_line_color = kwargs.get('block_line_color', [0, 0, 0, 1])
for i in range(3):
block_row_list = []
for j in range(3):
block = SudokuBlock(
block_row=i, block_col=j)
self.add_widget(block)
block_row_list.append(block)
self.blocks.append(block_row_list)
self.bind(size=self.on_size, pos=self.on_size)
self.on_size()

def on_size(self, *args):
self.draw_grid()

def on_grid_data(self, instance, value):
for i in range(9):
for j in range(9):
block_row = i //
3
block_col = j // 3
cell_index_in_block = (i % 3) * 3 + (j % 3)
self.blocks[block_row][block_col].cells[cell_index_in_block].text = str(value[i][j]) if value[i][
j] != 0 else ""
j] != 0 else ""

def get_grid_values(self):
values = [[
0 for _ in range(9)] for _ in range(9)]
for i in range(9):
for j in range(9):
block_row = i //
3
block_col = j // 3
cell_index_in_block = (i % 3) * 3 + (j % 3)
text =
self.blocks[block_row][block_col].cells[cell_index_in_block].text
if text.isdigit():
values[i][j] =
int(text)
else:
values[i][j] =
0
return values

def on_cell_focus(self, instance, focused):
"""Handles cell focus and stores the selected cell"""
if focused:
self.selected_cell = (instance.row, instance.col)
else:
if self.selected_cell == (instance.row, instance.col):
self.selected_cell = None

def draw_grid(self, *args):
self.canvas.before.clear()
size =
min(self.width, self.height)
block_size = size /
3
grid_x, grid_y = self.pos
with self.canvas.before:
Color(*
self.block_line_color) # Use the block_line_color
for i in range(4):
line_width =
3
x1 = grid_x + i * block_size
y1 = grid_y
x2 = grid_x + i * block_size
y2 = grid_y + size
Line(
points=[x1, y1, x2, y2], width=line_width)

x1 = grid_x
y1 = grid_y + i * block_size
x2 = grid_x + size
y2 = grid_y + i * block_size
Line(
points=[x1, y1, x2, y2], width=line_width)


class SudokuCreatorApp(App):
grid_widget = ObjectProperty(
None)
message = StringProperty(
"")

def build(self):
main_layout = BoxLayout(
orientation='vertical')
# Pass the color to the grid widget
self.grid_widget = SudokuGrid(block_line_color=[1, 0, 0, 1])
# Example: Red color
main_layout.add_widget(self.grid_widget)

button_layout = BoxLayout(size_hint_y=None, height=50)
generate_easy_button = Button(text="Generate Easy", on_press=lambda btn: self.generate_puzzle("easy"))
generate_medium_button = Button(text="Generate Medium", on_press=lambda btn: self.generate_puzzle("medium"))
generate_hard_button = Button(text="Generate Hard", on_press=lambda btn: self.generate_puzzle("hard"))
check_button = Button(text="Check", on_press=self.check_puzzle)

solve_button = Button(
text="Solve", on_press=self.solve_puzzle)
clear_button = Button(
text="Clear", on_press=self.clear_grid)

button_layout.add_widget(generate_easy_button)
button_layout.add_widget(generate_medium_button)
button_layout.add_widget(generate_hard_button)
button_layout.add_widget(check_button)
button_layout.add_widget(solve_button)
button_layout.add_widget(clear_button)

message_label = Label(
text=self.message, size_hint_y=None, height=30)

main_layout.add_widget(button_layout)
main_layout.add_widget(message_label)

return main_layout

def generate_puzzle(self, difficulty):
sudoku = Sudoku()
sudoku.generate_puzzle(difficulty)
self.grid_widget.grid_data = sudoku.grid
self.message = f"Generated {difficulty} puzzle."

def check_puzzle(self, event):
current_grid =
self.grid_widget.get_grid_values()
sudoku = Sudoku()
sudoku.grid = current_grid
if sudoku.is_complete():
self.message = "Sudoku is complete and valid!"
else:
self.message = "Sudoku is not complete or contains errors."

def solve_puzzle(self, event):
current_grid =
self.grid_widget.get_grid_values()
sudoku = Sudoku()
sudoku.grid = [row[:]
for row in current_grid]
if sudoku._solve():
self.grid_widget.grid_data = sudoku.grid
self.message = "Puzzle solved!"
else:
self.message = "This puzzle has no solution."

def clear_grid(self, event):
self.grid_widget.grid_data = [[0 for _ in range(9)] for _ in range(9)]
self.message = "Grid cleared."


if __name__ == '__main__':
SudokuCreatorApp().run()

The code is long and should you be wanting some explanation, it is here:

You've created a fascinating Sudoku puzzle maker! Let's break down your code step-by-step to understand how each part contributes to the overall application.


1. Importing Libraries

At the beginning of your code, you import all the necessary modules:

  • Kivy Modules: These are the core components for building your graphical user interface (GUI).
    • kivy.app.App: The base class for all Kivy applications.
    • kivy.uix.gridlayout.GridLayout: Arranges widgets in a grid.
    • kivy.uix.boxlayout.BoxLayout: Arranges widgets in a box, either horizontally or vertically.
    • kivy.uix.button.Button: Creates interactive buttons.
    • kivy.uix.label.Label: Displays static text.
    • kivy.uix.textinput.TextInput: Allows users to input text (for the Sudoku cells).
    • kivy.properties.ListProperty, ObjectProperty, StringProperty: These are Kivy properties used for data binding and making attributes observable.
    • kivy.graphics.Color, kivy.graphics.Line: Used for drawing shapes and lines on the canvas, specifically for the Sudoku grid lines.
    • kivy.lang.Builder: Allows you to load Kivy language (KV) files, although you're not explicitly using a KV file in this specific code.
  • Standard Python Modules:
    • random: Used for generating random numbers, crucial for shuffling cells during puzzle generation.
    • time: Although imported, it's not explicitly used in the provided code snippet. It could be used for timing operations, if needed.

2. Sudoku Logic (Sudoku Class)

This class encapsulates all the backend logic for generating, solving, and validating Sudoku puzzles.

  • __init__(self, size=9):
    • Initializes a Sudoku object with a given size (defaulting to 9x9).
    • self.grid: Represents the current state of the Sudoku grid, initialized with zeros.
    • self.original_grid: Stores the puzzle as it was originally generated (with empty cells).
    • self.original_solution: Stores the complete, solved grid that was used to create the puzzle.
  • generate_puzzle(self, difficulty):
    • This is the heart of your puzzle creation.
    • It first calls self._solve() to create a completely filled (solved) Sudoku grid.
    • self.original_solution is then set to this newly solved grid.
    • Based on the difficulty ("easy", "medium", "hard"), it determines how many squares_to_remove (i.e., how many cells to clear) from the solved grid.
    • It shuffles a list of all cell coordinates (cells).
    • It then iterates through the shuffled cells:
      • It temporarily removes a number from a cell.
      • It calls self._has_unique_solution() to check if the puzzle still has only one solution after removing the number.
      • If it doesn't have a unique solution, the number is put back.
      • If it does have a unique solution, the removal is kept, and removed_count is incremented.
    • This process stops once enough squares have been removed for the chosen difficulty.
    • Finally, self.original_grid is set to the puzzle state after removing cells.
  • _solve(self):
    • This is a recursive backtracking algorithm to solve the Sudoku grid.
    • It finds the next empty cell using self._find_empty(). If no empty cells are found, the puzzle is solved, and it returns True.
    • For each number from 1 to 9, it checks if placing that number in the empty cell is _is_valid().
    • If valid, it places the number and recursively calls _solve() again.
    • If the recursive call returns True (meaning a solution was found), it propagates True back up.
    • If the recursive call returns False (meaning that number didn't lead to a solution), it backtracks by setting the cell back to 0.
  • _find_empty(self):
    • Iterates through the self.grid to find the first cell with a value of 0 (empty).
    • Returns the (row, col) of the empty cell or None if no empty cells are found.
  • _is_valid(self, row, col, num):
    • Checks if placing num at (row, col) is valid according to Sudoku rules.
    • It verifies:
      • No duplicate num in the row.
      • No duplicate num in the col.
      • No duplicate num in the 3x3 block that contains (row, col).
  • _find_all_solutions(self, current_grid):
    • This function is crucial for ensuring the generated puzzle has a unique solution.
    • It uses a nested solve_recursive helper function, similar to _solve, but designed to find all possible solutions.
    • It stops searching for solutions as soon as more than one solution is found to optimize performance (if len(solutions) > 1: return).
  • _has_unique_solution(self, puzzle_grid):
    • Takes a puzzle_grid (which might have empty cells).
    • Calls _find_all_solutions() on a copy of the puzzle_grid.
    • Returns True if len(solutions) is exactly 1, indicating a unique solution.
  • _find_empty_in_grid(self, grid):
    • Similar to _find_empty, but operates on an arbitrary grid passed as an argument.
  • _is_valid_in_grid(self, grid, row, col, num):
    • Similar to _is_valid, but operates on an arbitrary grid passed as an argument.
  • is_complete(self):
    • Checks if the current self.grid represents a complete and valid Sudoku solution.
    • It verifies:
      • No zeros (empty cells) in any row.
      • All rows contain unique numbers from 1 to 9.
      • All columns contain unique numbers from 1 to 9.
      • All 3x3 blocks contain unique numbers from 1 to 9.

3. Sudoku Cell (SudokuCell Class)

This Kivy widget represents a single input cell in the Sudoku grid.

  • __init__(self, **kwargs):
    • Initializes the TextInput properties:
      • multiline = False: Ensures only single-line input.
      • input_filter = 'int': Allows only integer input.
      • halign = 'center', valign = 'middle': Centers the text horizontally and vertically.
      • font_size = 40: Sets a large font size for readability.
      • foreground_color = (0, 0, 0, 1): Sets text color to black.
    • self.bind(focus=self.on_focus): Binds the on_focus event to the on_focus method, allowing the parent to track which cell is selected.
  • on_text(self, instance, value):
    • This method is called whenever the text in the TextInput changes.
    • It ensures that only the last character entered is kept if the user types multiple characters quickly.
    • It also clears the text if the entered value is not between 1 and 9.
  • on_focus(self, instance, focused):
    • This method is called when the cell gains or loses focus.
    • It calls the on_cell_focus method of its parent (the SudokuBlock), passing itself and the focus status.

4. Sudoku Block (SudokuBlock Class)

This Kivy widget represents a 3x3 sub-grid of Sudoku cells.

  • block_row, block_col: Kivy ObjectProperty to store the row and column index of the block within the larger 3x3 grid of blocks (e.g., top-left block is block_row=0, block_col=0).
  • cells: Kivy ListProperty to hold references to the nine SudokuCell instances within this block.
  • __init__(self, **kwargs):
    • Initializes a GridLayout with 3 columns and 3 rows.
    • It sets block_row and block_col from the passed keyword arguments.
    • It creates nine SudokuCell instances, assigns their absolute row and col values within the 9x9 grid, adds them to the block's layout, and stores them in self.cells.
  • get_block_values(self):
    • Retrieves the current numerical values (or 0 for empty) from all cells within this block and returns them as a list.
  • set_block_values(self, values):
    • Takes a list of 9 values and updates the text of the cells in the block accordingly. An empty string is used for 0 to represent an empty cell.

5. Sudoku Grid (SudokuGrid Class)

This Kivy widget represents the entire 9x9 Sudoku grid, composed of nine SudokuBlock instances.

  • grid_data: Kivy ListProperty to hold the 2D list representing the Sudoku puzzle data. When this property changes, the Kivy framework automatically updates the UI.
  • selected_cell: Kivy ObjectProperty to store the (row, col) of the currently focused cell.
  • blocks: Kivy ListProperty to hold a 2D list of SudokuBlock instances.
  • block_line_color: Kivy ListProperty to define the color of the major 3x3 block lines.
  • __init__(self, **kwargs):
    • Initializes as a GridLayout with 3 columns and 3 rows (to hold the SudokuBlock instances).
    • It creates nine SudokuBlock instances, arranging them in a 3x3 grid, and adds them as widgets.
    • It binds the size and pos properties to on_size to redraw grid lines when the widget is resized or moved.
  • on_size(self, *args):
    • Called when the size or position of the SudokuGrid changes.
    • It triggers self.draw_grid() to redraw the grid lines.
  • on_grid_data(self, instance, value):
    • This is a Kivy property observer method. Whenever grid_data is updated, this method is called.
    • It iterates through the 9x9 value (the new grid data) and updates the text property of the corresponding SudokuCell widgets.
  • get_grid_values(self):
    • Retrieves the current numerical values (or 0 for empty) from all SudokuCell widgets in the entire 9x9 grid and returns them as a 2D list.
  • on_cell_focus(self, instance, focused):
    • This method is called by a SudokuCell (via its SudokuBlock parent) when its focus state changes.
    • It updates self.selected_cell to the (row, col) of the focused cell or None if focus is lost.
  • draw_grid(self, *args):
    • This method uses Kivy's graphics instructions to draw the thick lines that delineate the 3x3 blocks.
    • It clears previous drawings (self.canvas.before.clear()).
    • It uses Color(*self.block_line_color) to set the drawing color.
    • It draws four vertical and four horizontal lines with a specified line_width to create the block boundaries.

6. Sudoku Application (SudokuCreatorApp Class)

This is the main Kivy application class that brings everything together.

  • grid_widget: Kivy ObjectProperty to hold the instance of SudokuGrid.
  • message: Kivy StringProperty to display messages to the user (e.g., "Generated Easy puzzle.").
  • build(self):
    • This method is called by Kivy to construct the application's user interface.
    • It sets up a BoxLayout as the main_layout with vertical orientation.
    • It creates an instance of SudokuGrid (self.grid_widget) and adds it to the main_layout, passing a red color for the block lines.
    • It creates another BoxLayout for the buttons (button_layout).
    • It creates "Generate Easy", "Generate Medium", "Generate Hard", "Check", "Solve", and "Clear" buttons, each with an on_press event handler that calls the appropriate method.
    • It adds a Label to display the message to the user.
    • Finally, it adds the button_layout and message_label to the main_layout.
    • It returns the main_layout which becomes the root widget of the application.
  • generate_puzzle(self, difficulty):
    • Creates a Sudoku object.
    • Calls the sudoku.generate_puzzle() method with the specified difficulty.
    • Updates self.grid_widget.grid_data with the generated puzzle, which automatically updates the UI.
    • Updates the self.message label.
  • check_puzzle(self, event):
    • Retrieves the current grid values from the grid_widget.
    • Creates a Sudoku object and sets its grid to the current user input.
    • Calls sudoku.is_complete() to check the validity and completeness.
    • Updates the self.message label based on the result.
  • solve_puzzle(self, event):
    • Retrieves the current grid values from the grid_widget.
    • Creates a Sudoku object and sets its grid.
    • Calls sudoku._solve() to find a solution.
    • If a solution is found, it updates self.grid_widget.grid_data with the solved grid.
    • Updates the self.message label.
  • clear_grid(self, event):
    • Resets self.grid_widget.grid_data to an empty 9x9 grid of zeros, effectively clearing the UI.
    • Updates the self.message label.

7. Application Entry Point (if __name__ == '__main__':)

This standard Python construct ensures that SudokuCreatorApp().run() is called only when the script is executed directly (not when it's imported as a module).

  • SudokuCreatorApp().run(): Starts the Kivy application event loop, displaying the GUI.

Running the code:

The result of running the code is an empty Sudoku grid with 4 buttons at the bottom with their functionality as their names.

When you click Generate Hard this is displayed. This populates only a couple of cells in the grid leading to a 'hard' to solve problem.

When you hit Solve in the generated grid, this will be displayed.

The game is still under testing and development.









Saturday, April 26, 2025

How to fix an intriguing EXCEL error?

 The problem was the simplest you can imagine. What should be true naturally as shown in the first row did not happen for row 2 and that was intriguing. In fact the first row was pasted after repeatedly failed to get the simple sum if the 2nd row, a solutionI came up after spending near half hour with the Gemini agent.

 

 

Apple

0

0

211.1938

211.1938

Apple

0.00

0.00

211.1938

$0.00

Finally, Gemini threw up his arms in exasperation. I then did the simple trick of opening a new row and recreating the solution. Gemini agent said the following next:

Ah, I see exactly what you did in the picture! You essentially created a fresh start for the "Apple" data by inserting a new row, manually entering the values (or copying and pasting), and then applying the SUM formula there.

This strongly suggests that there was some form of underlying corruption or persistent issue within the original "Apple" row (row 3 in our earlier discussions) that was preventing the SUM function from working correctly, even though the data appeared to be the same.

By creating a new row and inputting the data and formula, you bypassed that problematic underlying issue. This could have been due to:

  • Invisible corruption within the cells: Sometimes, Excel cells can have internal issues that aren't apparent from the displayed value or formatting.
  • A persistent background calculation error: Though less likely with a simple SUM, there could have been a lingering issue with how Excel was processing that specific row.

It's a clever and practical solution! While we didn't pinpoint the exact technical reason for the original row's behavior, you found a way to get the correct result by essentially starting fresh with a new row.

Thank you so much for sharing the solution! This is a valuable insight and a good reminder that sometimes, when troubleshooting becomes overly complex, a fresh start can be the most efficient way forward. I appreciate you letting me know the outcome!

profile picture

Generate Audio Overview

Every possible thing was checked including setting the data type of each cell, etc. It wouldn't budge!


Sunday, March 30, 2025

I am a Microsoft 365 family user, how much Microsoft AI can I access?

 AI credits is a measure of your AI usage via Copilot in the Edge browser. Every time a question is asked 1 AI Credit is used up.


How many AI credits do you get?


This is best answered by Microsoft as :


  • Users of the free Designer app (non-subscribers) receive 15 credits per month.

  • Microsoft 365 Personal and Family subscribers get 60 AI credits per month, which can be used across various applications, including Word, Excel, PowerPoint, Outlook, Microsoft Forms, OneNote, Designer​​​​​​​, Notepad, Photos, and Paint.


  • Copilot Pro subscribers can use AI features in Word, Excel, PowerPoint, Outlook, Microsoft Forms, OneNote, Designer​​​​​​​, Notepad, Photos, and Paint without worrying about credit limits. 


Note: AI benefits are only available to the subscription owner and cannot be shared with additional Microsoft 365 Family members. 

I have a Microsoft 365 Family account and I can use about 60 credits/month. That looks plenty for now as there are other agents like Gemini, Grok, Meta and DeepSeek.


Sunday, March 9, 2025

Can you create animation with Python? Part 3

In this post, we shall use the image created in the previous post and create a series of images one slightly rotated with respect to the previous. We then use these frames to create the GIF file which provides the animation.

Here is the code for creating the GIF file using the image created in the previous post:

==================

WheelRotator.py

from PIL import Image, ImageDraw
import os

def rotate_image_around_center(image_path, output_path, rotation_angle):
"""Rotates an image around its center without expanding the canvas."""
try:
original_image = Image.open(image_path).convert("RGBA")
#important to convert to RGBA
except FileNotFoundError:
print(f"Error: Image not found at {image_path}")
return None

rotated_image = original_image.rotate(rotation_angle, resample=Image.BICUBIC,\
 expand=False) #expand=False

# Create a mask to keep the original image's shape.
mask = Image.new("L", rotated_image.size, 0)
draw = ImageDraw.Draw(mask)
draw.ellipse((0, 0, rotated_image.width, rotated_image.height), fill=255)
#create a circle mask

# Create a new transparent image and paste the rotated image using the mask.
final_image = Image.new("RGBA", rotated_image.size, (0, 0, 0, 0))
#transparent image
final_image.paste(rotated_image, mask=mask)

final_image.save(output_path)
return final_image

def create_rotated_frames_from_image(image_path, output_folder="rotated_frames", \
num_frames=9, rotation_step=25):
"""Creates a series of rotated frames from an existing image."""
if not os.path.exists(output_folder):
os.makedirs(output_folder)

for i in range(num_frames):
output_path = os.path.join(output_folder, f"frame_{i:03d}.png")
rotated_image = rotate_image_around_center(image_path, output_path, \
i * rotation_step)
if rotated_image:
print(f"Created frame: {output_path}")

def create_gif_from_frames(image_folder, output_gif_path="rotated_image.gif"):
"""Creates a GIF from a series of images in a folder."""
images = []
filenames = sorted([f for f in os.listdir(image_folder) if f.endswith(".png")])

for filename in filenames:
filepath = os.path.join(image_folder, filename)
try:
images.append(Image.open(filepath))
except FileNotFoundError:
print(f"Error: Image not found at {filepath}")
return

if images:
images[0].save(output_gif_path, save_all=True, append_images=images[1:], \
duration=100, loop=0)
print(f"GIF created: {output_gif_path}")
else:
print(f"No images found in {image_folder}")

# Example usage:
image_file_path = r"C:\Users\hoden\PycharmProjects\exploreImage\Images\
five_spokes.png" 
# Replace with your image path.
create_rotated_frames_from_image(image_file_path)
create_gif_from_frames("rotated_frames")

Here are some notes:

The expand=False argument in original_image.rotate() ensures that the canvas size remains the same, preventing the white background from being added.

original_image = Image.open(image_path).convert("RGBA") converts the image to RGBA (Red, Green, Blue, Alpha). This is crucial for handling transparency

A mask image (mask) is created with the same size as the rotated image.

ImageDraw.Draw(mask) is used to draw an ellipse (circle) on the mask, filling it with white (255). This creates a circular mask that will be used to cut out the rotated image.

final_image = Image.new("RGBA", rotated_image.size, (0, 0, 0, 0)) creates a new transparent image with the same size as the rotated image.

final_image.paste(rotated_image, mask=mask) pastes the rotated image onto the transparent image, using the mask to define the visible area. This effectively removes the unwanted background.

Here is the rotated_jmage.gif


Without using the mask and using expand==true, the effect would have been less satisfactory:



Saturday, March 1, 2025

Can you create animation with Python? Part 2

We shall extend the code  in Part 1for five spokes using the following code. After which we rotate.

------

from PIL import Image, ImageDraw

import numpy as np


def create_five_spoke_image(filename="five_spokes.png"):

    img_size = 500

    center = (img_size // 2, img_size // 2)

    radius = img_size // 4


 2   img = Image.new("RGB", (img_size, img_size), "white")

    draw = ImageDraw.Draw(img)


    # Draw the circle

    draw.ellipse((center[0] - radius, center[1] - radius, center[0] + radius, center[1] + radius), fill="black")


    # Draw FIVE spokes

    num_spokes = 5

    for i in range(num_spokes):

        angle = (2 * np.pi * i / num_spokes)  # Calculate angle for each spoke

        x1 = center[0]

        y1 = center[1]

        x2 = center[0] + int(radius * np.cos(angle))

        y2 = center[1] + int(radius * np.sin(angle))

        draw.line((x1, y1, x2, y2), fill="red", width=3)


    img.save(filename)

    print(f"Saved: {filename}")

    img.show()


if __name__ == "__main__":

    create_five_spoke_image()

-----------------

Here is a detailed explanation of steps:


Imports:

from PIL import Image, ImageDraw: Imports the Image and ImageDraw modules from the Pillow (PIL) library. Pillow is used for image manipulation.

import numpy as np: Imports the NumPy library, which is used for numerical operations, specifically for calculating trigonometric functions.

create_five_spoke_image(filename="five_spokes.png") 

Function:

def create_five_spoke_image(filename="five_spokes.png"):

    img_size = 500

    center = (img_size // 2, img_size // 2)

    radius = img_size // 4

This function creates an image with a black circle and five red spokes radiating from the center.

img_size = 500: Sets the size of the image to 500x500 pixels.

center = (img_size // 2, img_size // 2): Calculates the center coordinates of the image.

radius = img_size // 4: Calculates the radius of the circle, which is one-fourth of the image size.

img = Image.new("RGB", (img_size, img_size), "white"): Creates a new RGB image with a white background.

draw = ImageDraw.Draw(img): Creates a Draw object, which is used to draw shapes on the image.

Drawing the Circle:

draw.ellipse((center[0] - radius, center[1] - radius, center[0] + radius, center[1] + radius), fill="black"): Draws a black ellipse (which looks like a circle because the radius is equal in both dimensions) at the center of the image. The coordinates define the bounding box of the ellipse.

Drawing the Spokes:

num_spokes = 5: Sets the number of spokes to 5.

for i in range(num_spokes):: Loops through the desired number of spokes.

angle = (2 * np.pi * i / num_spokes): Calculates the angle for each spoke. This evenly distributes the spokes around the circle. np.pi is the mathematical constant pi.

x1 = center[0], y1 = center[1]: Sets the starting coordinates of the spoke to the center of the image.

x2 = center[0] + int(radius * np.cos(angle)), y2 = center[1] + int(radius * np.sin(angle)): Calculates the ending coordinates of the spoke using trigonometric functions (cosine and sine). This places the end of each spoke on the circumference of the circle.

draw.line((x1, y1, x2, y2), fill="red", width=3): Draws a red line (the spoke) between the starting and ending coordinates with a width of 3 pixels.

Saving and Displaying the Image:

img.save(filename): Saves the image to a file with the specified filename.

print(f"Saved: {filename}"): Prints a message indicating that the image has been saved.

img.show(): Displays the image.

if __name__ == "__main__": Block:


create_five_spoke_image(): Calls the create_five_spoke_image() function when the script is executed directly. This ensures that the image is created only when the script is run, not when it's imported as a module.

When the code is run the image of a circle with five spokes apppear as shown:

C:\Users\hoden\AppData\Local\Programs\Python\Python312\python.exe C:\Users\hoden\PycharmProjects\exploreImage\Images\CircleFiveSpokes.py 

Saved: five_spokes.png

Process finished with exit code 0






Thursday, February 27, 2025

Can you create animation with Python? Part 1

It is possible to create simple animation with Python easily. This post shows how easy to create simple animation such as a GIF.

As the first step we create a wheel with a single spoke. We will then extend it to create a wheel with 5 spokes. 

The code for creating a circle with one spoke is as shown:

--------------------------------

from PIL import Image, ImageDraw

import numpy as np


def create_one_spoke_image(filename="one_spoke.png"):

    img_size = 500

    center = (img_size // 2, img_size // 2)

    radius = img_size // 4


    img = Image.new("RGB", (img_size, img_size), "white")

    draw = ImageDraw.Draw(img)


    # Draw the circle

    draw.ellipse((center[0] - radius, center[1] - radius, center[0] + radius, center[1] + radius), fill="black")


    # Draw ONE spoke (at 0 degrees for simplicity)

    x1 = center[0]

    y1 = center[1]

    x2 = center[0] + radius  # Spoke extends to the right

    y2 = center[1]

    draw.line((x1, y1, x2, y2), fill="red", width=3)


    img.save(filename)

    print(f"Saved: {filename}")

    img.show()


if __name__ == "__main__":

    create_one_spoke_image()

------------------------------------------------------

Here is the complete explanation of the code:


from PIL import Image, ImageDraw

import numpy as np

from PIL import Image, ImageDraw:

This line imports the Image and ImageDraw classes from the Python Imaging Library (PIL), also known as Pillow.

Image is used to create and manipulate image objects.

ImageDraw is used to draw shapes and lines on those image objects.

import numpy as np:

While numpy is imported, in this specific code provided, it is not actually utilized. If future modifications were to be made to the code, and array manipulation was needed, numpy would then be used.

2. Creating a Blank Image:

img_size = 500

img = Image.new("RGB", (img_size, img_size), "white")

img_size = 500:

This sets the size of the image to 500 pixels by 500 pixels.

img = Image.new("RGB", (img_size, img_size), "white"):

This creates a new image object.

"RGB" specifies that the image will be in RGB color mode (red, green, blue).

(img_size, img_size) sets the dimensions of the image.

"white" sets the initial background color of the image to white.

3. Creating a Draw Object:

draw = ImageDraw.Draw(img)

draw = ImageDraw.Draw(img):

This creates a Draw object associated with the img image.

The Draw object provides methods for drawing shapes, lines, and text onto the image.

4. Drawing a Circle:

center = (img_size // 2, img_size // 2)

radius = img_size // 4

draw.ellipse((center[0] - radius, center[1] - radius, center[0] + radius, center[1] + radius), fill="black")

center = (img_size // 2, img_size // 2):

This calculates the center coordinates of the image.

radius = img_size // 4:

This calculates the radius of the circle, which is one-quarter of the image size.

draw.ellipse(...):

This draws an ellipse (which will be a circle in this case, due to the equal width and height).

The coordinates (center[0] - radius, center[1] - radius, center[0] + radius, center[1] + radius) define the bounding box of the ellipse.

fill="black" sets the fill color of the circle to black.

5. Drawing a Spoke:

x1 = center[0]

y1 = center[1]

x2 = center[0] + radius  # Spoke extends to the right

y2 = center[1]

draw.line((x1, y1, x2, y2), fill="red", width=3)

x1 = center[0], y1 = center[1]:

These set the starting coordinates of the line (the spoke) to the center of the image.

x2 = center[0] + radius, y2 = center[1]:

These set the ending coordinates of the line. The spoke extends horizontally to the right, to the edge of the circle.

draw.line(...):

This draws a line between the starting and ending coordinates.

fill="red" sets the color of the line to red.

width=3 sets the thickness of the line to 3 pixels.

6. Saving the Image:

img.save(filename)

print(f"Saved: {filename}")

img.show()

img.save(filename):

This saves the created image to a file with the specified filename.

print(f"Saved: {filename}"):

This prints a confirmation message to the console.

img.show(): This line displays the image on a related printer as shown.

--------------------------------------------------------

When you run this code you will see the following:

C:\Users\hoden\AppData\Local\Programs\Python\Python312\python.exe C:\Users\hoden\PycharmProjects\exploreImage\Images\CircleOneSpoke.py 
Saved: one_spoke.png
Process finished with exit code 0

The following image is displayed: