HZK
@HZK (Ritoku Sakamae)

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

tkinter: 時計の針の回転をどれだけ高速にできるか

解決したいこと

バーサライタという器具があります。

これは、一列に並べたLEDを高速で動かすことで残像を起こし、画像を表示するものです。回転数は大体秒速10~30回転(600~1800rpm)が必要と言われています。
参考:https://homemadegarbage.com/tag/%e3%83%90%e3%83%bc%e3%82%b5%e3%83%a9%e3%82%a4%e3%82%bf

これを、tkinter上で表現しようと試みています。
まず、円を1度ずつ360の座標に分割しました。次に回転速度を1度/1ミリ秒とすると360度/360ミリ秒となり、1秒では2.78回転となります。
そこで、回転速度をあげて、1秒30回転以上にできる方法はないか、考えています。

ただし角度を増やすと座標が飛んで残像が得られないため、秒数を短くする必要があります。しかし、after()メソッドは1ミリ秒以下にはできません。

試したこと

以下のコードで、残像が得られるかどうか、試してみました。
円を、角度36度の扇、10枚に分割して、扇の中で往復させて、扇を重ねてみました。数値的には1秒あたり27.8回転していることになります。
結果は、残像らしきものはありますが、目標とはほど遠いものです。

そこで、「針の動きを1秒間30回以上することが可能なのか」が質問です。
after()メソッドでは、不可能なのか?
プログラム的に他の方法があるのか?
以下のコードに問題があるのか?

それとも、LEDではなくモニター画面だから、人間の視覚的に無理なのか。
何でも良いので、助言を頂けたら幸いです。

該当するソースコード

# -*- coding: utf-8 -*-
import tkinter as tk
import math
import random

# キャンバスのサイズの設定
CANVAS_WIDTH = 400
CANVAS_HEIGHT = CANVAS_WIDTH
CANVAS_SIZE = CANVAS_WIDTH

# 時計の前面と背景の色の設定
BG_COLOR = "black"

#時計の外枠
FG_COLOR = "gray"

# 盤面を表す円の半径の設定
CLOCK_OVAL_RADIUS = CANVAS_SIZE / 2

R = CLOCK_OVAL_RADIUS*0.95
X0 = CANVAS_HEIGHT/2
Y0 = CANVAS_WIDTH/2

Color_Lst =["blue", "orange", "red", "green", "purple"]

class POV(tk.Frame):
    def __init__(self, master=None):
        super().__init__(master)
  
        self.frame0= tk.Frame(self.master, bg="white") 
        self.frame0.pack()
        
        # キャンバスの中心座標を覚えておく
        self.center_x = CANVAS_WIDTH / 2
        self.center_y = CANVAS_HEIGHT / 2     
        
        # 変数定義
        self.timer_on = False
        self.ovalhand_lst = []
        self.after_id = 0    
        self.rad =0
    
        self.create_widget()
        self.create_button()
        self.draw_hand10()
        
    def create_widget(self):
        self.canvas = tk.Canvas(self.frame0, width = CANVAS_WIDTH, height = CANVAS_HEIGHT, 
                                     bg=BG_COLOR, highlightthickness=0)
        #gridは3x3       
        self.canvas.grid(column=0, row=0, columnspan=3, padx=0, pady=0,
                         sticky=tk.NSEW,
                         )  

        # 時計の盤面を表す円を描画する
        x1 = self.center_x - CLOCK_OVAL_RADIUS
        y1 = self.center_y - CLOCK_OVAL_RADIUS
        x2 = self.center_x + CLOCK_OVAL_RADIUS
        y2 = self.center_y + CLOCK_OVAL_RADIUS

        self.canvas.create_oval(
            x1, y1, x2, y2,
            fill="black",
            width=2,
            outline="gold"
        )

    def draw_hand10(self):
        for j in range(10):
            rad_s = (math.pi/180*36)*j
    
            #20個のLEDを模した円を並べる
            for i in range(1,21):
                xa = X0 + R*math.cos(rad_s + self.rad)*0.05*i
                ya = Y0 + R*math.sin(rad_s + self.rad)*0.05*i
                #
                xb = xa-5
                yb = ya-5
                xc = xa+5
                yc = ya+5

                oval_hand =self.canvas.create_oval(xb, yb, xc, yc, fill = "white", outline = "", width = 3)
                self.ovalhand_lst.append(oval_hand)

    def rotate_hand10(self): 
        #円を10個に分割
        for j in range(10):
            rad_s = (math.pi/180*36)*j

            for i in range(20): 
                xa = X0 + R*math.cos(rad_s + self.rad)*0.05*(i+1)
                ya = Y0 + R*math.sin(rad_s + self.rad)*0.05*(i+1)
                #
                xb = xa-5
                yb = ya-5
                xc = xa+5
                yc = ya+5
                #
                k = j%5
                rcol = Color_Lst[k]
                its = j*20 +i
                
                self.canvas.coords(self.ovalhand_lst[its], xb, yb, xc, yc)
                self.canvas.itemconfig(self.ovalhand_lst[its], fill =rcol, outline = "")
        
            #1度ずつふえる 36度まで。その後0度に戻す
            if self.rad < math.pi/180*36:
                self.rad += (math.pi/180)
            else:
                self.rad =0

        #afterメソッドで、1ミリ秒後に繰り返す。5とか6の方がそれっぽいか?
        self.after_id = self.after(1, self.rotate_hand10)

    def create_button(self):
        # スタート/ストップボタン
        self.start_button = tk.Button(self.frame0, width=20, height=1, text="スタート/ストップ", fg = "gray20",
                                 font=("MSゴシック体", "14"), command=self.start_button_clicked)
        self.start_button.grid(column=0, row=2, columnspan=3, sticky=tk.NSEW)        

    #startボタンを押した時
    def start_button_clicked(self):
        if self.timer_on == False:
            self.timer_on = True

            self.rotate_hand10()
 
        elif self.timer_on == True:
            self.timer_on = False
            #更新をキャンセル
            self.after_cancel(self.after_id)

if __name__ == "__main__":
    app = tk.Tk()
    POV(app)
    app.mainloop()
0

3Answer

素人的考えですが
そもそも、PCモニターで回転速度を1度/1ミリ秒の表示が難しいと思います。

理由
残像を発生させるためにはモニターに画像が表示される必要があると思うのですが、PCのモニター画面の更新周期は、リフレッシュレートで決まります。
1msで画面を表示するには1000Hzのリフレッシュレートが必要です。普通のモニターが60 Hzなので、PC側がいくら早くてもリフレッシュレートを超えた速度で画面の更新ができないです。(高速ゲーミング用で500Hzがありますが・・・)

60Hzの場合
     1度が16.66・・msなので、360*16.66・・ms=6s
     キッチリ表示させた場合で0.166・・回転/sとなります。
     これを超えた速度で表示させた場合は、表示が抜けるので角度を飛ばした状態になるかと・・・思います。

500Hz
     1度が2msなので、360*2ms=0.72s/回転
     キッチリ表示させた場合で1.388・・回転/sとなります。
     500Hzのモニターと500Hzで画像出力できるPCで処理しても1秒30回転は無理っぽい

間違えていたらすみません。

2Like

液晶ディスプレイが60Hz(リフレッシュレート)で60fps(フレームレート)の場合、人間の目は60枚のパラパラ漫画を動画と勘違いして認識します。(静止画を動画に錯覚させる技術)

液晶ディスプレイに扇風機を映しても60枚のパラパラ漫画が表示されているに過ぎません。(静止画を動画に錯覚させる技術)

一方、バーサライタは動画を静止画と錯覚させる技術です。1周360度のタイミングでLEDを発光させ、静止画を浮かびあがらせます。
時計の秒針を60枚作成して1回転としても2.78回転に及びません。

錯覚させる技術が真逆のため液晶ディスプレイを60Hz(リフレッシュレート)、60fps(フレームレート)から2何倍の8k相当にあげても難しいとおもいます。

一つ提案ですがバーサライタの動画を1枚1枚静止画として分析してはどうでしょう。
擬似的に表現できるかもしれません。

2Like

「現在時刻にどこの位置に描画するか」の発想でコードを書いてみました。
Tkinterより高速に描画できると思われるPygameを使っています。

scr.png

コードの先頭にある変数RPS⭐️を大きくするほど、高速な回転になります。
自分の環境では、毎秒120回を超える頻度で更新が可能ですが、RPSが十数を超えると、もはや回転しているようには見えません。
これは、人間の視力と、モニターのリフレッシュレートなどが関係していると思います。

毎秒120回程度の更新では、1周60個をプロットするなら RPS=2を超えると、間引かれていきます。

RPSを1から少しずつ増やして見てください。

import pygame
from pygame.locals import *
import sys
import datetime
import math

RPS = 1 #revolutions per second ⭐️
Arm = 210 
Center = (240, 240)

Green = Color(0, 255, 0)
Red = Color(255, 0, 0)
Black = Color(0, 0, 0)

pygame.init()
Surface = pygame.display.set_mode((480, 480))
Clock = pygame.time.Clock()
pygame.display.set_caption("Exsample 480x480")
font = pygame.font.Font(None, 24) 
rps_f = font.render(f"RPS: {RPS}", True, Black) 

def main():
    count = 0
    while True:
        count += 1
        Surface.fill(Green)

        ticks = pygame.time.get_ticks()
        seconds = ticks / 1000
        rotates = RPS * seconds
        radian = rotates * (2 * math.pi)
        position = (Center[0] + Arm * math.sin(radian), Center[1] - Arm * math.cos(radian))

        tick_f = font.render(f"ticks: {ticks} ms", True, Black) 
        counter_f = font.render(f"counter: {count}", True, Black) 
        rotates_f = font.render(f"rotates: {rotates:.3f}", True, Black) 
        Surface.blit(rps_f, [4, 2])
        Surface.blit(tick_f, [4, 480 - 20])
        Surface.blit(counter_f, [150, 480 - 20])
        Surface.blit(rotates_f, [300, 480 - 20])

        #pygame.draw.circle(Surface, Red, position, 20)
        pygame.draw.line(Surface, Red, Center, position, width=4)

        pygame.display.update()
        Clock.tick_busy_loop()

        for event in pygame.event.get():
            if event.type == QUIT:
                pygame.quit()
                sys.exit()

        if ticks % (60.0 * 1000) < 1.0:
            pygame.time.delay(3 * 1000) # delay 3s per minutes 🚏


if __name__ == '__main__':
    main()

🚏 1分ごとに3秒間停止するようにしてあります

2Like

Comments

  1. @HZK

    Questioner

    みなさま、お返事ありがとうございます。

    頂いた助言をすべて咀嚼できていませんが、まずは現時点で、一旦感謝の意を表明させて頂きます。
    リフレッシュレートからは、円座標を1度ずつ進ませる場合は、一周6秒だと全ての座標をきっちり描画でき、これが限界となります。
    もっとも、そこから回転速度を上げた場合、例えば一周3秒位までは、円の動きを追随できている感覚はあります。
    このあたりは、錯覚ということになりますが、あらためて人間の視覚の不思議さを実感しています。

  2. @HZK

    Questioner

    @YearCentury
    ご指摘のとおり、リフレッシュレートの問題でした。
    今まで深く考えてきませんでしたが、このような場面で問題になることを実例をもって理解できました。簡潔なご指摘、感謝しています。

    @HalHarada
    錯覚させる技術が真逆、との本質を突いた発言が全てでした。
    提案にあるように、液晶ディスプレイで錯覚させる別の方法を考えてみたいと思います。ありがとうございました。

    @nak435
    まだpygameについて自身の理解が不足している状況でした。
    しかし、別のモジュールを用いて、座標指定とRPS変数を立てて検討するという方法論はとても参考になりました。
    わざわざ検証用のコードを提示して頂いたことは励みになります。ありがとうございました。

Your answer might help someone💌