نقوم بتحليل روائع الرسم بمساعدة ML الكلاسيكية

تحية للجميع! يدرس أحد أصدقائي كفنان ويتحدث بانتظام عن هذه التحفة أو تلك ، حول التقنيات التركيبية الفريدة ، حول إدراك اللون ، حول تطور الرسم والفنانين الرائعين. على خلفية هذا التأثير المستمر ، قررت التحقق مما إذا كانت معرفتي ومهاراتي الهندسية مناسبة لتحليل التراث الثقافي العالمي.

مسلحًا بمحلل مؤقت تحت غطاء الليل ، اقتحمت معرض الصور على الإنترنت وأخرجت ما يقرب من 50 ألف لوحة من هناك. دعونا نرى ما هو مثير للاهتمام للقيام بذلك ، باستخدام أدوات ML الكلاسيكية فقط (الحذر ، حركة المرور).

تحول ساذج


كما يتذكر الكثير منا من دروس علوم الكمبيوتر ، يتم تمثيل الصورة كمجموعة من وحدات البايت المسؤولة عن لون كل بكسل على حدة. كقاعدة ، يتم استخدام مخطط RGB ، حيث يتم تقسيم اللون إلى ثلاثة مكونات (أحمر / أخضر / أزرق) ، والتي عند تلخيصها بخلفية سوداء ، تعطي اللون الأصلي الذي يراه الشخص.

نظرًا لأن جميع التحف الفنية بالنسبة لنا قد أصبحت مؤقتًا فقط صفائف من الأرقام على القرص ، سنحاول وصف هذه الصفائف من خلال إنشاء الرسوم البيانية لتوزيع ترددات الكثافة لكل قناة.

سنستخدم numpy للحسابات ، وتصور باستخدام matplotlib.

مصدر
#      
def load_image_by_index(i):
    image_path = paintings_links.iloc[i].img_path
    img = cv2.imdecode(np.fromfile(str(Path.cwd()/image_path), np.uint8), cv2.IMREAD_UNCHANGED)
    return img    
#    
def get_hist_data_by_index(img_index):
    bin_div = 5 
    img = load_image_by_index(img_index)
    b, bins=  np.histogram(img[:,:,0], bins=255//bin_div, range=(0,255), density=True)
    g = np.histogram(img[:,:,1], bins=255//bin_div, range=(0,255), density=True)[0]
    r = np.histogram(img[:,:,2], bins=255//bin_div, range=(0,255), density=True)[0]
    return bins, r, g, b
#       
def plot_image_with_hist_by_index(img_index, height=6):
    bins, r, g, b = get_hist_data_by_index(img_index)
    img = load_image_by_index(img_index)
    fig = plt.figure(constrained_layout=True)

    if img.shape[0] < img.shape[1]:
        width_ratios = [3,1]
    else:
        width_ratios = [1,1]
        
    gs = GridSpec(3, 2, figure=fig, 
                  width_ratios = [3,1]
                 )
    ax_img = fig.add_subplot(gs[:,0])

    ax_r = fig.add_subplot(gs[0, 1])
    ax_g = fig.add_subplot(gs[1, 1], sharey=ax_r)
    ax_b = fig.add_subplot(gs[2, 1], sharey=ax_r)

    ax_img.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB),aspect = 'equal')
    ax_img.axis('off')
    
    ax_r.bar(bins[:-1], r, width = 5, color='red',alpha=0.7)
    ax_g.bar(bins[:-1], g, width = 5, color='green',alpha=0.7)
    ax_b.bar(bins[:-1], b, width = 5, color='blue',alpha=0.7)

    ax_r.axes.get_xaxis().set_ticks([])
    ax_r.axes.get_yaxis().set_ticks([])
    ax_g.axes.get_xaxis().set_ticks([])
    ax_g.axes.get_yaxis().set_ticks([])
    ax_b.axes.get_xaxis().set_ticks([])
    ax_b.axes.get_yaxis().set_ticks([])
    fig.suptitle("{} - {}".format(paintings_links.iloc[img_index].artist_name, 
                                 paintings_links.iloc[img_index].picture_name),ha= "left")
    
    fig.set_figheight(height)
    plt.axis('tight')
    if img.shape[0] < img.shape[1]:
        fig.set_figwidth(img.shape[1] *height / img.shape[0] *1.25)
    else:
        fig.set_figwidth(img.shape[1] *height / img.shape[0] *1.5)
    plt.show()


أمثلة على الأعمال:









بعد النظر بعناية في الرسوم البيانية لصور مختلفة ، يمكننا ملاحظة أن شكلها محدد للغاية ويختلف كثيرًا من عمل لآخر.

في هذا الصدد ، نفترض أن المدرج التكراري هو نوع من إلقاء الصورة ، مما يسمح بتمييزها إلى حد ما.

النموذج الأول


نجمع كل الرسوم البيانية في مجموعة بيانات واحدة كبيرة ونحاول البحث عن بعض "الشذوذ" فيها. سريعة ومريحة وعموما الخوارزمية المفضلة لهذه الأغراض هي فئة واحدة svm. سنستخدم تطبيقه من مكتبة sklearn

مصدر
#       ,       
res = []
error = []
for img_index in tqdm(range(paintings_links.shape[0])):
    try:
        bins, r, g, b = get_hist_data_by_index(img_index)
        res.append(np.hstack([r,g,b]))
    except:
        res.append(np.zeros(153,))
        error.append(img_index)
        
np_res = np.vstack(res)
#    
pd.DataFrame(np_res).to_pickle("histograms.pkl")
histograms = pd.read_pickle("histograms.pkl")
#  .         .   10    
one_class_svm = OneClassSVM(nu=10 / histograms.shape[0], gamma='auto')
one_class_svm.fit(histograms[~histograms.index.isin(bad_images)])
#  
svm_outliers = one_class_svm.predict(histograms)
svm_outliers = np.array([1 if label == -1 else 0 for label in svm_outliers])
#   
uncommon_images = paintings_links[(svm_outliers ==1) & (~histograms.index.isin(bad_images))].index.values
for i in uncommon_images:
    plot_image_with_hist_by_index(i,4)


دعونا نرى ما هو غير طبيعي نجد في صناديق معرضنا.

العمل بالقلم الرصاص:


العمل بألوان داكنة للغاية:

سيدة باللون الأحمر:

شيء سطحي:

صورة داكنة جدًا:


ابحث عن وظائف مماثلة


حسنًا ، يجد نموذجنا شيئًا غير عادي ، بعيدًا عن أي شيء آخر.

ولكن هل يمكننا صنع أداة تساعد في إيجاد عمل مماثل في اللون؟

الآن تتميز كل صورة بمتجه 153 قيمة (لأنه عند بناء الرسم البياني ، تصل إلى 5 وحدات كثافة لكل صندوق ، إجمالي 255/5 = 51 تردد لكل قناة).

يمكننا تحديد "درجة التشابه" عن طريق حساب المسافات بين المتجهات التي تهمنا. إن المسافة الإقليدية المألوفة من المدرسة هنا ستولي الكثير من الاهتمام لطول مكونات المتجه ، ونود أن نولي المزيد من الاهتمام لمجموعات الظلال التي تشكل الصورة. سنجد هنا مقياس جيب التمام للمسافة المستخدمة على نطاق واسع ، على سبيل المثال ، في مشاكل تحليل النص. دعنا نحاول تطبيقه لهذه المهمة. نأخذ التنفيذ من مكتبة scipy.

مصدر
#  ,        
from scipy import spatial
def find_closest(target_id,n=5):
    distance_vector = np.apply_along_axis(spatial.distance.cosine,
                        arr=histograms,
                       axis=1,
                       v=histograms.values[target_id])
    return np.argsort(distance_vector)[:n]


دعونا نرى ما يشبه الموجة التاسعة من Aivazovsky.

الأصل:



أعمال مشابهة:





ما يشبه "فانوس اللوز" الخاص بـ فان جوخ.

الأصل:



أعمال مشابهة:





وما الذي يشبه سيدة شاذة تم العثور عليها باللون الأحمر في وقت سابق؟

الأصل:



أعمال مماثلة:






فراغات اللون


حتى تلك اللحظة ، عملنا في مساحة ألوان RGB. إنه مناسب جدًا للفهم ، ولكنه بعيد عن أن يكون مثاليًا لمهامنا.

انظر ، على سبيل المثال ، على حافة مكعب الألوان RGB



بالعين المجردة ، يمكنك أن ترى أن هناك مناطق كبيرة على الوجوه حيث لا ترى أعيننا تغييرات ، ومناطق صغيرة نسبيًا حيث يتغير إدراك اللون لدينا بشكل حاد للغاية. يمنع هذا الإدراك غير الخطي الآلة من تقييم الألوان بالطريقة التي يفعلها الشخص.

لحسن الحظ ، هناك العديد من مساحات الألوان ، ربما يناسب بعضها مهامنا.

سنختار مساحة اللون المفضلة لدينا من خلال مقارنة فائدتها في حل مشكلة بشرية. دعونا ، على سبيل المثال ، نحسب الفنان بمحتويات اللوحة!

خذ كل مساحات الألوان المتاحة من مكتبة opencv ، ودرب xgboost على كل منها ، وشاهد المقاييس على الاختيار المؤجل.

مصدر
# ,        
def get_hist_data_by_index_and_colorspace(bgr_img, colorspace):
    bin_div = 5
    img_cvt = cv2.cvtColor(bgr_img, getattr(cv2, colorspace))
    c1, bins =  np.histogram(img_cvt[:,:,0], bins=255//bin_div, range=(0,255), density=True)
    c2 = np.histogram(img_cvt[:,:,1], bins=255//bin_div, range=(0,255), density=True)[0]
    c3 = np.histogram(img_cvt[:,:,2], bins=255//bin_div, range=(0,255), density=True)[0]
    return bins, c1, c2, c3
#        
all_res = {}
all_errors = {}
for colorspace in list_of_color_spaces:
    all_res[colorspace] =[]
    all_errors[colorspace] =[]
for img_index in tqdm(range(paintings_links.shape[0]) ):
    
    for colorspace in list_of_color_spaces:
        try:
            bgr_img = load_image_by_index(img_index)
            bins, c1, c2, c3 = get_hist_data_by_index_and_colorspace(bgr_img, colorspace)
            all_res[colorspace].append(np.hstack([c1, c2, c3]))
        except:
            all_res[colorspace].append(np.zeros(153,))
            all_errors[colorspace].append(img_index)
all_res_np = {}
for colorspace in list_of_color_spaces:  
    all_res_np[colorspace] = np.vstack(all_res[colorspace])
res = []
#        
for colorspace in tqdm(list_of_color_spaces):
    temp_df = pd.DataFrame(all_res_np.get(colorspace))
    temp_x_train =   temp_df[temp_df.index.isin(X_train.index.values)]
    temp_x_test =   temp_df[temp_df.index.isin(X_test.index.values)]

    xgb=XGBClassifier()
    xgb.fit(temp_x_train, y_train)
    current_res = classification_report(y_test, xgb.predict(temp_x_test), labels=None, target_names=None, output_dict=True).get("macro avg")
    current_res["colorspace"] = colorspace
    res.append(current_res)
pd.DataFrame(res).sort_values(by="f1-score")


الاحكاماعد الاتصالدرجة f1مساحة الألوان
0.0013290.0036630.001059COLOR_BGR2YUV
0.0032290.0046890.001849COLOR_BGR2RGB
0.0030260.0041310.001868COLOR_BGR2HSV
0.0029090.0045780.001934COLOR_BGR2XYZ
0.0035450.0044340.001941COLOR_BGR2HLS
0.0039220.0047840.002098COLOR_BGR2LAB
0.0051180.0048360.002434COLOR_BGR2LUV

تم تحقيق زيادة ملموسة في الجودة باستخدام مساحة اللون LUV.

حاول مبدعو هذا المقياس جعل تصور تغيرات الألوان على طول محور المقياس موحدة قدر الإمكان. وبفضل هذا ، سيكون تغير اللون المدرك وتقييمه الرياضي أقرب ما يمكن.

هذه هي الطريقة التي تبدو بها شريحة من مساحة لون معينة عند إصلاح أحد المحاور:



دعونا نلقي نظرة على النموذج


بعد الخطوة السابقة ، لا يزال لدينا نموذج يمكنه التنبؤ بشيء ما.
دعونا نرى من نتعلم عمله بدقة أكبر.
الاحكاماعد الاتصالدرجة f1فنان
0.0425530.0194170.026667إيليا إيفيموفيتش ريبين
0.0555560.0200000.029412وليام ميريت تشيس
0.0714290.0222220.033898بونار بيير
0.0354610.0352110.035336جيل الفرين
0.1000000.0217390.035714جان أوغست دومينيك إنجرس
0.0228140.2240660.041411بيير أوغست رينوار
0.1000000.0285710.044444ألبرت بيرشتات
0.2500000.0322580.057143هانز زاتسكا
0.0303960.5187970.057428كلود أوسكار مونيه
0.2500000.0370370.064516جيروتو والتر

المقاييس نفسها ليست بعيدة عن المثالية ، ولكن عليك أن تتذكر أن نظام الألوان هو جزء صغير من المعلومات حول العمل. يستخدم الفنان العديد من الوسائل التعبيرية. حقيقة أننا وجدنا في هذه البيانات "خطًا" معينًا للفنان هو بالفعل انتصار.

سنختار أحد الفنانين لتحليل أعمق. فليكن كلود أوسكار مونيه (سأجعل زوجتي لطيفة ، تحب الانطباعيين).

دعنا نأخذ عمله ، اطلب من النموذج أن يخبرنا المؤلف ويحسب الترددات
مؤلف متوقععدد التنبؤات
كلود أوسكار مونيه186
بيير أوغست رينوار171
فنسنت فان غوغ25
بيتر بول روبنزتسعة عشر
غوستاف دوري17

يميل الكثير من الناس إلى الخلط بين مونيه ومانيه ، ويفضل نموذجنا الخلط بينه وبين رينوار وفان جوخ. دعونا نرى ما يشبه فان جوخ ، وفقًا للنموذج.










والآن سنستخدم وظيفة البحث لأعمال مماثلة ونعثر على لوحات فان جوخ مشابهة للأعمال المذكورة أعلاه (هذه المرة سنقيس المسافات في مساحة LUV).

الأصل:



أعمال مماثلة:



الأصل:



أعمال مماثلة:






الأصل:



أعمال مماثلة:





راضيا عن نفسي ، عرضت النتائج على صديق واكتشفت أن نهج المدرج التكراري غير مهذب في الواقع ، لأنه يحلل توزيع اللون نفسه ، ولكن مكوناته بشكل منفصل. بالإضافة إلى ذلك ، ليس تواتر الألوان هو المهم مثل تكوينها. اتضح أن الفنانين المعاصرين قد أثبتوا مناهج لاختيار مخططات الألوان. لذلك اكتشفت عن يوهانس إيتن وعجلة ألوانه.

عجلة الألوان Itten




يوهانس إيتن هو فنان ومنظر ومعلم فني ومؤلف كتب شهيرة عن الشكل واللون. عجلة الألوان هي واحدة من أكثر الأدوات المعروفة التي تساعد على الجمع بين الألوان لإرضاء العين.

نوضح أكثر طرق اختيار الألوان شيوعًا:



  1. ألوان تكميلية - تقع على أجزاء متقابلة من الدائرة
  2. الألوان المجاورة - المجاورة للدائرة
  3. الثالوث الكلاسيكي - الألوان في قمم مثلث متساوي الأضلاع
  4. ثالوث على النقيض - الألوان في قمم مثلث متساوي الساقين
  5. مسطرة المستطيل - الألوان عند رؤوس المستطيل
  6. مسطرة المربع - الألوان على قمم المربع

نحن نحلل كفنانين


دعونا نحاول تطبيق المعرفة المكتسبة. للبدء ، نحصل على مجموعة من الألوان ملقاة على عجلة الألوان ، ونتعرف عليها من الصورة.



الآن يمكننا المقارنة في أزواج كل بكسل من لوحاتنا مع مجموعة من ألوان الدائرة Itten. نستبدل البيكسل الأصلي بأقرب بكسل في عجلة الألوان ونحسب تكرار الألوان في الصورة الناتجة

مصدر
#          
def plot_composition_analysis(image_index):
    img = load_image_by_index(image_index)
    luv_img = cv2.cvtColor(load_image_by_index(image_index), cv2.COLOR_BGR2LUV)
    closest_colors = np.argmin(euclidean_distances(luv_img.reshape(-1,3),wheel_colors_luv),axis=1)
    wheel_colors2[closest_colors].reshape(luv_img.shape)
    color_areas_img = wheel_colors2[closest_colors].reshape(img.shape)

    v, c = get_image_colors(image_index)
    #         
    c_int = (c*img.shape[1]).astype(int)
    c_int_delta = img.shape[1] - sum(c_int)
    c_int[np.argmax(c_int)] = c_int[np.argmax(c_int)] + c_int_delta

    _ = []

    for i, vi in enumerate(v):
        bar_width = c_int[i]
        _.append(np.tile(wheel_colors2[vi], (150,bar_width,1)))
    color_bar_img = np.hstack(_)
    final_image = np.hstack([
                                np.vstack([img,
                                           np.tile(np.array([254,254,254]),(160,img.shape[1],1))]),

                                np.tile(np.array([254,254,254]),(img.shape[0]+160,10,1)),
                                np.vstack([color_areas_img,
                                           np.tile(np.array([254,254,254]),(10,img.shape[1],1)),
                                           color_bar_img])
                            ])
    h = 12
    w = h / final_image.shape[1] * final_image.shape[0]
    fig = plt.figure(figsize=(h,w))
    plt.imshow(cv2.cvtColor(final_image.astype(np.uint8), cv2.COLOR_BGR2RGB),interpolation='nearest', aspect='auto')
    plt.title("{} - {}".format(paintings_links.iloc[image_index].artist_name, 
                             paintings_links.iloc[image_index].picture_name),ha= "center")
    plt.axis('off');












هل لاحظت كيف أن "Arlesian الشباب" قد تغير قليلاً بعد تحولنا؟ ربما يجدر قياس ليس فقط تواتر الألوان الجديدة ، ولكن أيضًا إحصاءات أخطاء التحويل - وهذا يمكن أن يساعدنا في التحليل.

لكن هذا لا يكفي للتحليل الحالي. دعونا نبحث عن تركيبات متناغمة في دائرة؟

جميع الأزواج التكميلية:



جميع الثلاثيات الكلاسيكية:



وجميع المربعات: سنبحث



عن هذه المجموعات في لوحاتنا ، ونجد أهمها (في التردد)
ونرى ما سيحدث.

لبدء الزوج:











ثم الثلاثيات:













وأخيرًا ، المربعات:















ليس سيئًا بالعين ، ولكن هل ستساعد المقاييس الجديدة في تحديد مؤلف العمل؟

سنقوم بتدريب النموذج باستخدام ترددات ألوان Itten فقط ، وخصائص الخطأ والمجموعات المتناغمة الموجودة.

هذه المرة ، تغيرت قائمة أكثر الفنانين "المتوقعين" قليلاً ، مما يعني أن نهجًا مختلفًا للتحليل سمح لنا باستخراج مزيد من المعلومات من محتويات الصورة.
الاحكاماعد الاتصالدرجة f1فنان
0.0434780.0121950.019048مارتن ، هنري جان غيوم
0.0326800.0290700.030769كاميل بيسارو
0.1666670.0196080.035088جان ليون جيروم
0.0769230.0277780.040816تورنر ، جوزيف مالورد وليام
0.1333330.0243900.041237Poortvliet ، ريان
0.1000000.0263160.041667ماكس كلينجر
0.0267250.2282160.047847بيير أوغست رينوار
0.2000000.0285710.050000برازيلير ، أندريه
0.0287450.6390980.055016كلود أوسكار مونيه

استنتاج


الأعمال الفنية فريدة من نوعها. يستخدم الفنانون العديد من التقنيات التركيبية التي تجعلنا معجبون بعملهم مرارًا وتكرارًا.
مخطط الألوان مهم ، ولكنه بعيد عن المكون الوحيد في تحليل أعمال الفنانين.

سوف يضحك العديد من نقاد الفن الحقيقي على سذاجة التحليل ، لكنني ما زلت راضيًا عن العمل المنجز. هناك العديد من الأفكار التي لم تصل بعد إلى أيدي:

  1. تطبيق خوارزميات التجميع على تحليل مخططات ألوان الفنانين. بالتأكيد يمكننا تسليط الضوء على مجموعات مثيرة للاهتمام هناك ، والتمييز بين الاتجاهات المختلفة في الرسم
  2. تطبيق خوارزميات التجميع على اللوحات الفردية. ابحث عن "قطع الأرض" التي يمكن تحديدها من خلال نظام الألوان. على سبيل المثال ، في مجموعات مختلفة تحصل على مناظر طبيعية وصور ولا تزال الحياة
  3. لا تبحث فقط عن أزواج وثلاثية ومربعات ، ولكن أيضًا عن مجموعات أخرى من دائرة Itten
  4. الانتقال من تحليل التردد إلى تحليل نقاط الألوان من خلال تجميع وحدات البكسل حسب الموقع
  5. ابحث عن الأعمال التي يكون التأليف فيها موضع شك وانظر لمن سيصوت له النموذج

ملاحظة


كانت هذه المقالة في الأصل مشروع تخرج لدورة تعلم الآلة ، لكن العديد من الأشخاص أوصوا بتحويلها إلى مادة لـ Habr.

اتمنى انك كنت مهتم

كل الكود المستخدم في العمل متاح على جيثب .

All Articles